From 6b016a712f2fdf717267cb9f79c0c924eddb1eee Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 22 Jun 2025 21:40:42 +0200 Subject: [PATCH] Adding upstream version 1.4.2. Signed-off-by: Daniel Baumann --- .codespell-ignore | 20 + .editorconfig | 27 + .gitattributes | 1 + .gitignore | 45 + .gitlab-ci.yml | 590 ++ .gitlab/ci/check_missing_headers.sh | 28 + .gitlab/issue_templates/.gitkeep | 0 .gitlab/issue_templates/bluetooth issue.md | 43 + .gitlab/issue_templates/issue.md | 29 + CODE_OF_CONDUCT.md | 77 + COPYING | 26 + INSTALL.md | 237 + LICENSE | 11 + Makefile.in | 78 + NEWS | 7690 ++++++++++++++++ README.md | 217 + autogen.sh | 18 + doc/Doxyfile.in | 76 + doc/DoxygenLayout.xml | 269 + doc/custom.css | 60 + doc/dox/api/index.dox | 90 + doc/dox/api/spa-buffer.dox | 71 + doc/dox/api/spa-design.dox | 35 + doc/dox/api/spa-index.dox | 88 + doc/dox/api/spa-plugins.dox | 360 + doc/dox/api/spa-pod.dox | 1034 +++ doc/dox/config/index.md | 53 + doc/dox/config/libpipewire-modules.7.md | 44 + doc/dox/config/pipewire-client.conf.5.md | 235 + .../config/pipewire-filter-chain.conf.5.md | 42 + doc/dox/config/pipewire-jack.conf.5.md | 342 + doc/dox/config/pipewire-props.7.md | 1214 +++ doc/dox/config/pipewire-pulse-modules.7.md | 20 + doc/dox/config/pipewire-pulse.conf.5.md | 195 + doc/dox/config/pipewire.conf.5.md | 696 ++ doc/dox/config/xref.md | 51 + doc/dox/index.dox | 52 + doc/dox/internals/access.dox | 124 + doc/dox/internals/audio.dox | 127 + doc/dox/internals/daemon.dox | 177 + doc/dox/internals/design.dox | 70 + doc/dox/internals/dma-buf.dox | 305 + doc/dox/internals/index.dox | 26 + doc/dox/internals/library.dox | 240 + doc/dox/internals/midi.dox | 115 + doc/dox/internals/objects.dox | 347 + doc/dox/internals/portal.dox | 215 + doc/dox/internals/protocol.dox | 1691 ++++ doc/dox/internals/pulseaudio.dox | 69 + doc/dox/internals/scheduling.dox | 348 + doc/dox/internals/session-manager.dox | 46 + doc/dox/modules.dox | 100 + doc/dox/overview.dox | 149 + doc/dox/programs/index.md | 27 + doc/dox/programs/pipewire-pulse.1.md | 58 + doc/dox/programs/pipewire.1.md | 251 + doc/dox/programs/pw-cat.1.md | 163 + doc/dox/programs/pw-cli.1.md | 192 + doc/dox/programs/pw-config.1.md | 97 + doc/dox/programs/pw-container.1.md | 71 + doc/dox/programs/pw-dot.1.md | 56 + doc/dox/programs/pw-dump.1.md | 43 + doc/dox/programs/pw-jack.1.md | 45 + doc/dox/programs/pw-link.1.md | 135 + doc/dox/programs/pw-loopback.1.md | 67 + doc/dox/programs/pw-metadata.1.md | 73 + doc/dox/programs/pw-mididump.1.md | 38 + doc/dox/programs/pw-mon.1.md | 36 + doc/dox/programs/pw-profiler.1.md | 46 + doc/dox/programs/pw-reserve.1.md | 79 + doc/dox/programs/pw-top.1.md | 209 + doc/dox/programs/pw-v4l2.1.md | 40 + doc/dox/programs/spa-acp-tool.1.md | 94 + doc/dox/programs/spa-inspect.1.md | 28 + doc/dox/programs/spa-json-dump.1.md | 24 + doc/dox/programs/spa-monitor.1.md | 26 + doc/dox/programs/spa-resample.1.md | 47 + doc/dox/pulse-modules.dox | 42 + doc/dox/pulse-modules.inc | 113 + doc/dox/tutorial/index.dox | 21 + doc/dox/tutorial/tutorial1.dox | 47 + doc/dox/tutorial/tutorial2.dox | 129 + doc/dox/tutorial/tutorial3.dox | 119 + doc/dox/tutorial/tutorial4.dox | 158 + doc/dox/tutorial/tutorial5.dox | 223 + doc/dox/tutorial/tutorial6.dox | 69 + doc/doxygen-awesome.css | 1364 +++ doc/examples.dox.in | 9 + doc/examples/tutorial1.c | 19 + doc/examples/tutorial2.c | 56 + doc/examples/tutorial3.c | 91 + doc/examples/tutorial4.c | 120 + doc/examples/tutorial5.c | 141 + doc/examples/tutorial6.c | 97 + doc/input-filter-h.sh | 37 + doc/input-filter-md.py | 167 + doc/input-filter.py | 53 + doc/man-fixup.py | 100 + doc/meson.build | 288 + doc/tree.dox | 134 + include/valgrind/memcheck.h | 302 + include/valgrind/valgrind.h | 6647 ++++++++++++++ meson.build | 626 ++ meson_options.txt | 381 + pipewire-alsa/alsa-plugins/ctl_pipewire.c | 1452 +++ pipewire-alsa/alsa-plugins/meson.build | 27 + pipewire-alsa/alsa-plugins/pcm_pipewire.c | 1515 ++++ pipewire-alsa/conf/50-pipewire.conf | 106 + pipewire-alsa/conf/99-pipewire-default.conf | 13 + pipewire-alsa/conf/meson.build | 5 + pipewire-alsa/tests/meson.build | 23 + .../tests/test-pipewire-alsa-stress.c | 131 + pipewire-jack/examples/ump-source.c | 114 + pipewire-jack/examples/video-dsp-play.c | 185 + pipewire-jack/jack/control.h | 658 ++ pipewire-jack/jack/intclient.h | 130 + pipewire-jack/jack/jack.h | 1475 ++++ pipewire-jack/jack/jslist.h | 293 + pipewire-jack/jack/metadata.h | 322 + pipewire-jack/jack/midiport.h | 197 + pipewire-jack/jack/net.h | 429 + pipewire-jack/jack/ringbuffer.h | 243 + pipewire-jack/jack/session.h | 302 + pipewire-jack/jack/statistics.h | 57 + pipewire-jack/jack/systemdeps.h | 141 + pipewire-jack/jack/thread.h | 160 + pipewire-jack/jack/transport.h | 247 + pipewire-jack/jack/types.h | 775 ++ pipewire-jack/jack/uuid.h | 50 + pipewire-jack/jack/weakjack.h | 125 + pipewire-jack/jack/weakmacros.h | 97 + pipewire-jack/meson.build | 5 + pipewire-jack/src/control.c | 452 + pipewire-jack/src/dummy.c | 19 + pipewire-jack/src/export.c | 18 + pipewire-jack/src/meson.build | 120 + pipewire-jack/src/metadata.c | 416 + pipewire-jack/src/net.c | 149 + pipewire-jack/src/pipewire-jack-extensions.h | 39 + pipewire-jack/src/pipewire-jack.c | 7779 +++++++++++++++++ pipewire-jack/src/pw-jack.in | 60 + pipewire-jack/src/ringbuffer.c | 282 + pipewire-jack/src/statistics.c | 46 + pipewire-jack/src/uuid.c | 91 + pipewire-v4l2/meson.build | 1 + pipewire-v4l2/src/meson.build | 42 + pipewire-v4l2/src/pipewire-v4l2.c | 2580 ++++++ pipewire-v4l2/src/pipewire-v4l2.h | 18 + pipewire-v4l2/src/pw-v4l2.in | 52 + pipewire-v4l2/src/v4l2-func.c | 139 + po/LINGUAS | 54 + po/POTFILES.in | 25 + po/POTFILES.skip | 2 + po/af.po | 577 ++ po/as.po | 619 ++ po/be.po | 669 ++ po/bg.po | 686 ++ po/bn_IN.po | 618 ++ po/ca.po | 748 ++ po/cs.po | 731 ++ po/da.po | 647 ++ po/de.po | 739 ++ po/de_CH.po | 638 ++ po/el.po | 613 ++ po/eo.po | 573 ++ po/es.po | 621 ++ po/fi.po | 713 ++ po/fr.po | 623 ++ po/gl.po | 707 ++ po/gu.po | 622 ++ po/he.po | 573 ++ po/hi.po | 627 ++ po/hr.po | 727 ++ po/hu.po | 715 ++ po/id.po | 718 ++ po/it.po | 600 ++ po/ja.po | 602 ++ po/ka.po | 718 ++ po/kk.po | 578 ++ po/kn.po | 618 ++ po/ko.po | 596 ++ po/lt.po | 623 ++ po/meson.build | 12 + po/ml.po | 608 ++ po/mr.po | 618 ++ po/my.po | 595 ++ po/nl.po | 605 ++ po/nn.po | 638 ++ po/oc.po | 731 ++ po/or.po | 641 ++ po/pa.po | 614 ++ po/pipewire.pot | 637 ++ po/pl.po | 742 ++ po/pt.po | 677 ++ po/pt_BR.po | 720 ++ po/ro.po | 679 ++ po/ru.po | 746 ++ po/si.po | 571 ++ po/sk.po | 587 ++ po/sl.po | 766 ++ po/sr.po | 641 ++ po/sr@latin.po | 641 ++ po/sv.po | 733 ++ po/ta.po | 647 ++ po/te.po | 624 ++ po/tr.po | 706 ++ po/uk.po | 779 ++ po/zh_CN.po | 704 ++ po/zh_TW.po | 585 ++ pw-uninstalled.sh | 66 + spa/examples/adapter-control.c | 1066 +++ spa/examples/example-control.c | 540 ++ spa/examples/local-libcamera.c | 516 ++ spa/examples/local-v4l2.c | 512 ++ spa/examples/local-videotestsrc.c | 535 ++ spa/examples/meson.build | 38 + .../spa-private/dbus-helpers.h | 72 + spa/include/meson.build | 18 + spa/include/spa/buffer/alloc.h | 338 + spa/include/spa/buffer/buffer.h | 129 + spa/include/spa/buffer/meta.h | 203 + spa/include/spa/buffer/type-info.h | 92 + spa/include/spa/control/control.h | 45 + spa/include/spa/control/type-info.h | 42 + spa/include/spa/control/ump-utils.h | 230 + spa/include/spa/debug/buffer.h | 121 + spa/include/spa/debug/context.h | 71 + spa/include/spa/debug/dict.h | 50 + spa/include/spa/debug/file.h | 70 + spa/include/spa/debug/format.h | 223 + spa/include/spa/debug/log.h | 111 + spa/include/spa/debug/mem.h | 59 + spa/include/spa/debug/node.h | 55 + spa/include/spa/debug/pod.h | 215 + spa/include/spa/debug/types.h | 113 + spa/include/spa/filter-graph/filter-graph.h | 150 + spa/include/spa/graph/graph.h | 355 + spa/include/spa/interfaces/audio/aec.h | 147 + spa/include/spa/monitor/device.h | 310 + spa/include/spa/monitor/event.h | 43 + spa/include/spa/monitor/type-info.h | 47 + spa/include/spa/monitor/utils.h | 94 + spa/include/spa/node/command.h | 53 + spa/include/spa/node/event.h | 44 + spa/include/spa/node/io.h | 368 + spa/include/spa/node/keys.h | 48 + spa/include/spa/node/node.h | 767 ++ spa/include/spa/node/type-info.h | 88 + spa/include/spa/node/utils.h | 146 + spa/include/spa/param/audio/aac-types.h | 43 + spa/include/spa/param/audio/aac-utils.h | 78 + spa/include/spa/param/audio/aac.h | 56 + spa/include/spa/param/audio/alac-utils.h | 70 + spa/include/spa/param/audio/alac.h | 34 + spa/include/spa/param/audio/amr-types.h | 37 + spa/include/spa/param/audio/amr-utils.h | 74 + spa/include/spa/param/audio/amr.h | 41 + spa/include/spa/param/audio/ape-utils.h | 69 + spa/include/spa/param/audio/ape.h | 34 + spa/include/spa/param/audio/compressed.h | 19 + spa/include/spa/param/audio/dsd-utils.h | 89 + spa/include/spa/param/audio/dsd.h | 61 + spa/include/spa/param/audio/dsp-utils.h | 64 + spa/include/spa/param/audio/dsp.h | 33 + spa/include/spa/param/audio/flac-utils.h | 69 + spa/include/spa/param/audio/flac.h | 34 + spa/include/spa/param/audio/format-utils.h | 130 + spa/include/spa/param/audio/format.h | 62 + spa/include/spa/param/audio/iec958-types.h | 60 + spa/include/spa/param/audio/iec958-utils.h | 68 + spa/include/spa/param/audio/iec958.h | 49 + spa/include/spa/param/audio/layout.h | 170 + spa/include/spa/param/audio/mp3-types.h | 39 + spa/include/spa/param/audio/mp3-utils.h | 69 + spa/include/spa/param/audio/mp3.h | 42 + spa/include/spa/param/audio/opus.h | 34 + spa/include/spa/param/audio/ra-utils.h | 69 + spa/include/spa/param/audio/ra.h | 34 + spa/include/spa/param/audio/raw-json.h | 105 + spa/include/spa/param/audio/raw-types.h | 286 + spa/include/spa/param/audio/raw-utils.h | 85 + spa/include/spa/param/audio/raw.h | 301 + spa/include/spa/param/audio/type-info.h | 15 + spa/include/spa/param/audio/vorbis-utils.h | 69 + spa/include/spa/param/audio/vorbis.h | 34 + spa/include/spa/param/audio/wma-types.h | 42 + spa/include/spa/param/audio/wma-utils.h | 82 + spa/include/spa/param/audio/wma.h | 52 + spa/include/spa/param/bluetooth/audio.h | 60 + spa/include/spa/param/bluetooth/type-info.h | 63 + spa/include/spa/param/buffers-types.h | 71 + spa/include/spa/param/buffers.h | 53 + spa/include/spa/param/format-types.h | 178 + spa/include/spa/param/format-utils.h | 46 + spa/include/spa/param/format.h | 159 + spa/include/spa/param/latency-types.h | 55 + spa/include/spa/param/latency-utils.h | 182 + spa/include/spa/param/latency.h | 91 + spa/include/spa/param/param-types.h | 101 + spa/include/spa/param/param.h | 88 + spa/include/spa/param/port-config-types.h | 53 + spa/include/spa/param/port-config.h | 46 + spa/include/spa/param/profile-types.h | 45 + spa/include/spa/param/profile.h | 52 + spa/include/spa/param/profiler-types.h | 41 + spa/include/spa/param/profiler.h | 93 + spa/include/spa/param/props-types.h | 104 + spa/include/spa/param/props.h | 127 + spa/include/spa/param/route-types.h | 51 + spa/include/spa/param/route.h | 49 + spa/include/spa/param/tag-types.h | 39 + spa/include/spa/param/tag-utils.h | 151 + spa/include/spa/param/tag.h | 46 + spa/include/spa/param/type-info.h | 19 + spa/include/spa/param/video/chroma.h | 44 + spa/include/spa/param/video/color.h | 105 + spa/include/spa/param/video/dsp-utils.h | 76 + spa/include/spa/param/video/dsp.h | 35 + spa/include/spa/param/video/encoded.h | 11 + spa/include/spa/param/video/format-utils.h | 73 + spa/include/spa/param/video/format.h | 41 + spa/include/spa/param/video/h264-utils.h | 78 + spa/include/spa/param/video/h264.h | 48 + spa/include/spa/param/video/mjpg-utils.h | 70 + spa/include/spa/param/video/mjpg.h | 33 + spa/include/spa/param/video/multiview.h | 111 + spa/include/spa/param/video/raw-types.h | 162 + spa/include/spa/param/video/raw-utils.h | 128 + spa/include/spa/param/video/raw.h | 202 + spa/include/spa/param/video/type-info.h | 10 + spa/include/spa/pod/builder.h | 698 ++ spa/include/spa/pod/command.h | 49 + spa/include/spa/pod/compare.h | 174 + spa/include/spa/pod/dynamic.h | 75 + spa/include/spa/pod/event.h | 48 + spa/include/spa/pod/filter.h | 473 + spa/include/spa/pod/iter.h | 477 + spa/include/spa/pod/parser.h | 582 ++ spa/include/spa/pod/pod.h | 226 + spa/include/spa/pod/vararg.h | 93 + spa/include/spa/support/cpu.h | 209 + spa/include/spa/support/dbus.h | 150 + spa/include/spa/support/i18n.h | 87 + spa/include/spa/support/log-impl.h | 124 + spa/include/spa/support/log.h | 394 + spa/include/spa/support/loop.h | 441 + spa/include/spa/support/plugin-loader.h | 81 + spa/include/spa/support/plugin.h | 247 + spa/include/spa/support/system.h | 218 + spa/include/spa/support/thread.h | 129 + spa/include/spa/utils/ansi.h | 94 + spa/include/spa/utils/atomic.h | 35 + spa/include/spa/utils/cleanup.h | 124 + spa/include/spa/utils/defs.h | 466 + spa/include/spa/utils/dict.h | 113 + spa/include/spa/utils/dll.h | 61 + spa/include/spa/utils/endian.h | 26 + spa/include/spa/utils/enum-types.h | 49 + spa/include/spa/utils/hook.h | 546 ++ spa/include/spa/utils/json-core.h | 635 ++ spa/include/spa/utils/json-pod.h | 182 + spa/include/spa/utils/json.h | 222 + spa/include/spa/utils/keys.h | 151 + spa/include/spa/utils/list.h | 156 + spa/include/spa/utils/names.h | 152 + spa/include/spa/utils/ratelimit.h | 53 + spa/include/spa/utils/result.h | 62 + spa/include/spa/utils/ringbuffer.h | 176 + spa/include/spa/utils/string.h | 404 + spa/include/spa/utils/type-info.h | 95 + spa/include/spa/utils/type.h | 184 + spa/lib/lib.c | 161 + spa/lib/meson.build | 6 + spa/meson.build | 137 + spa/plugins/aec/aec-null.c | 157 + spa/plugins/aec/aec-webrtc.cpp | 451 + spa/plugins/aec/meson.build | 16 + spa/plugins/alsa/90-pipewire-alsa.rules | 216 + spa/plugins/alsa/acp-tool.c | 776 ++ spa/plugins/alsa/acp/acp.c | 2215 +++++ spa/plugins/alsa/acp/acp.h | 310 + spa/plugins/alsa/acp/alsa-mixer.c | 5360 ++++++++++++ spa/plugins/alsa/acp/alsa-mixer.h | 464 + spa/plugins/alsa/acp/alsa-ucm.c | 2966 +++++++ spa/plugins/alsa/acp/alsa-ucm.h | 318 + spa/plugins/alsa/acp/alsa-util.c | 2065 +++++ spa/plugins/alsa/acp/alsa-util.h | 183 + spa/plugins/alsa/acp/array.h | 130 + spa/plugins/alsa/acp/card.h | 77 + spa/plugins/alsa/acp/channelmap.h | 476 + spa/plugins/alsa/acp/compat.c | 287 + spa/plugins/alsa/acp/compat.h | 718 ++ spa/plugins/alsa/acp/conf-parser.c | 387 + spa/plugins/alsa/acp/conf-parser.h | 88 + spa/plugins/alsa/acp/device-port.h | 119 + spa/plugins/alsa/acp/dynarray.h | 139 + spa/plugins/alsa/acp/hashmap.h | 199 + spa/plugins/alsa/acp/idxset.h | 262 + spa/plugins/alsa/acp/llist.h | 109 + spa/plugins/alsa/acp/meson.build | 22 + spa/plugins/alsa/acp/proplist.h | 191 + spa/plugins/alsa/acp/volume.h | 207 + spa/plugins/alsa/alsa-acp-device.c | 1231 +++ .../alsa/alsa-compress-offload-device.c | 620 ++ spa/plugins/alsa/alsa-compress-offload-sink.c | 2024 +++++ spa/plugins/alsa/alsa-pcm-device.c | 611 ++ spa/plugins/alsa/alsa-pcm-sink.c | 990 +++ spa/plugins/alsa/alsa-pcm-source.c | 933 ++ spa/plugins/alsa/alsa-pcm.c | 3902 +++++++++ spa/plugins/alsa/alsa-pcm.h | 365 + spa/plugins/alsa/alsa-seq-bridge.c | 1011 +++ spa/plugins/alsa/alsa-seq.c | 1212 +++ spa/plugins/alsa/alsa-seq.h | 191 + spa/plugins/alsa/alsa-udev.c | 1166 +++ spa/plugins/alsa/alsa.c | 72 + spa/plugins/alsa/alsa.h | 19 + spa/plugins/alsa/compress-offload-api-util.c | 36 + spa/plugins/alsa/compress-offload-api-util.h | 30 + spa/plugins/alsa/compress-offload-api.c | 273 + spa/plugins/alsa/compress-offload-api.h | 66 + spa/plugins/alsa/meson.build | 66 + spa/plugins/alsa/mixer/meson.build | 7 + .../alsa/mixer/paths/analog-input-aux.conf | 65 + .../mixer/paths/analog-input-dock-mic.conf | 104 + .../alsa/mixer/paths/analog-input-fm.conf | 65 + .../mixer/paths/analog-input-front-mic.conf | 104 + .../paths/analog-input-headphone-mic.conf | 102 + .../mixer/paths/analog-input-headset-mic.conf | 114 + .../analog-input-internal-mic-always.conf | 133 + .../paths/analog-input-internal-mic.conf | 154 + .../alsa/mixer/paths/analog-input-linein.conf | 144 + .../mixer/paths/analog-input-mic-line.conf | 66 + .../alsa/mixer/paths/analog-input-mic.conf | 141 + .../mixer/paths/analog-input-mic.conf.common | 60 + .../mixer/paths/analog-input-rear-mic.conf | 107 + .../mixer/paths/analog-input-tvtuner.conf | 65 + .../alsa/mixer/paths/analog-input-video.conf | 64 + .../alsa/mixer/paths/analog-input.conf | 102 + .../alsa/mixer/paths/analog-input.conf.common | 289 + .../alsa/mixer/paths/analog-output-chat.conf | 5 + .../paths/analog-output-headphones-2.conf | 118 + .../mixer/paths/analog-output-headphones.conf | 180 + .../mixer/paths/analog-output-lineout.conf | 214 + .../alsa/mixer/paths/analog-output-mono.conf | 99 + .../paths/analog-output-speaker-always.conf | 187 + .../mixer/paths/analog-output-speaker.conf | 246 + .../alsa/mixer/paths/analog-output.conf | 88 + .../mixer/paths/analog-output.conf.common | 199 + .../paths/audigy-analog-output-mirror.conf | 56 + .../mixer/paths/audigy-analog-output.conf | 44 + .../alsa/mixer/paths/hdmi-output-0.conf | 16 + .../alsa/mixer/paths/hdmi-output-1.conf | 16 + .../alsa/mixer/paths/hdmi-output-10.conf | 16 + .../alsa/mixer/paths/hdmi-output-2.conf | 16 + .../alsa/mixer/paths/hdmi-output-3.conf | 16 + .../alsa/mixer/paths/hdmi-output-4.conf | 16 + .../alsa/mixer/paths/hdmi-output-5.conf | 16 + .../alsa/mixer/paths/hdmi-output-6.conf | 16 + .../alsa/mixer/paths/hdmi-output-7.conf | 16 + .../alsa/mixer/paths/hdmi-output-8.conf | 16 + .../alsa/mixer/paths/hdmi-output-9.conf | 16 + .../alsa/mixer/paths/iec958-stereo-input.conf | 20 + .../mixer/paths/iec958-stereo-output.conf | 18 + ...steelseries-arctis-output-chat-common.conf | 27 + ...steelseries-arctis-output-game-common.conf | 27 + .../mixer/paths/usb-gaming-headset-input.conf | 34 + .../paths/usb-gaming-headset-output-mono.conf | 34 + .../usb-gaming-headset-output-stereo.conf | 34 + .../mixer/paths/virtual-surround-7.1.conf | 5 + .../alsa/mixer/profile-sets/9999-custom.conf | 22 + .../alsa/mixer/profile-sets/analog-only.conf | 102 + .../mixer/profile-sets/asus-xonar-se.conf | 91 + .../alsa/mixer/profile-sets/audigy.conf | 94 + .../cmedia-high-speed-true-hdaudio.conf | 66 + .../alsa/mixer/profile-sets/default.conf | 576 ++ .../dell-dock-tb16-usb-audio.conf | 55 + .../force-speaker-and-int-mic.conf | 153 + .../mixer/profile-sets/force-speaker.conf | 152 + .../alsa/mixer/profile-sets/hdmi-ac3.conf | 110 + .../profile-sets/hp-tbt-dock-120w-g2.conf | 35 + .../hp-tbt-dock-audio-module.conf | 36 + .../alsa/mixer/profile-sets/kinect-audio.conf | 38 + .../profile-sets/maudio-fasttrack-pro.conf | 86 + .../native-instruments-audio4dj.conf | 90 + .../native-instruments-audio8dj.conf | 161 + .../native-instruments-komplete-audio6.conf | 110 + .../native-instruments-korecontroller.conf | 84 + .../native-instruments-traktor-audio10.conf | 130 + .../native-instruments-traktor-audio2.conf | 53 + .../native-instruments-traktor-audio6.conf | 91 + .../native-instruments-traktorkontrol-s4.conf | 80 + .../profile-sets/sb-omni-surround-5.1.conf | 112 + .../mixer/profile-sets/sennheiser-gsx.conf | 58 + .../profile-sets/simple-headphones-mic.conf | 42 + .../steelseries-arctis-common-usb-audio.conf | 23 + .../texas-instruments-pcm2902.conf | 75 + .../profile-sets/usb-gaming-headset.conf | 64 + .../samples/ATI IXP--Realtek ALC655 rev 0 | 150 + .../alsa/mixer/samples/Brooktree Bt878--Bt87x | 24 + ...soniq AudioPCI--Cirrus Logic CS4297A rev 3 | 135 + .../mixer/samples/HDA ATI HDMI--ATI R6xx HDMI | 4 + .../samples/HDA Intel--Analog Devices AD1981 | 62 + .../mixer/samples/HDA Intel--Realtek ALC889A | 113 + ...Intel 82801CA-ICH3--Analog Devices AD1881A | 128 + .../samples/Logitech USB Speaker--USB Mixer | 27 + .../alsa/mixer/samples/USB Audio--USB Mixer | 37 + .../samples/USB Device 0x46d_0x9a4--USB Mixer | 5 + .../samples/VIA 8237--Analog Devices AD1888 | 211 + .../VIA 8237--C-Media Electronics CMI9761A+ | 160 + spa/plugins/alsa/test-hw-params.c | 153 + spa/plugins/alsa/test-timer.c | 290 + spa/plugins/audioconvert/audioadapter.c | 2208 +++++ spa/plugins/audioconvert/audioconvert.c | 4142 +++++++++ spa/plugins/audioconvert/benchmark-fmt-ops.c | 325 + spa/plugins/audioconvert/benchmark-resample.c | 184 + spa/plugins/audioconvert/biquad.c | 374 + spa/plugins/audioconvert/biquad.h | 58 + spa/plugins/audioconvert/channelmix-ops-c.c | 584 ++ spa/plugins/audioconvert/channelmix-ops-sse.c | 691 ++ spa/plugins/audioconvert/channelmix-ops.c | 799 ++ spa/plugins/audioconvert/channelmix-ops.h | 141 + spa/plugins/audioconvert/crossover.c | 19 + spa/plugins/audioconvert/crossover.h | 28 + spa/plugins/audioconvert/fmt-ops-avx2.c | 1187 +++ spa/plugins/audioconvert/fmt-ops-c.c | 413 + spa/plugins/audioconvert/fmt-ops-neon.c | 467 + spa/plugins/audioconvert/fmt-ops-rvv.c | 259 + spa/plugins/audioconvert/fmt-ops-sse2.c | 1641 ++++ spa/plugins/audioconvert/fmt-ops-sse41.c | 73 + spa/plugins/audioconvert/fmt-ops-ssse3.c | 91 + spa/plugins/audioconvert/fmt-ops.c | 578 ++ spa/plugins/audioconvert/fmt-ops.h | 492 ++ spa/plugins/audioconvert/hilbert.h | 57 + spa/plugins/audioconvert/law.h | 2143 +++++ spa/plugins/audioconvert/meson.build | 257 + spa/plugins/audioconvert/peaks-ops-c.c | 30 + spa/plugins/audioconvert/peaks-ops-sse.c | 102 + spa/plugins/audioconvert/peaks-ops.c | 69 + spa/plugins/audioconvert/peaks-ops.h | 52 + spa/plugins/audioconvert/plugin.c | 33 + .../audioconvert/resample-native-avx.c | 74 + spa/plugins/audioconvert/resample-native-c.c | 45 + .../audioconvert/resample-native-impl.h | 160 + .../audioconvert/resample-native-neon.c | 198 + .../audioconvert/resample-native-sse.c | 74 + .../audioconvert/resample-native-ssse3.c | 95 + spa/plugins/audioconvert/resample-native.c | 440 + spa/plugins/audioconvert/resample-peaks.c | 141 + spa/plugins/audioconvert/resample.h | 54 + .../audioconvert/spa-resample-dump-coeffs.c | 200 + spa/plugins/audioconvert/spa-resample.c | 347 + spa/plugins/audioconvert/test-audioadapter.c | 282 + spa/plugins/audioconvert/test-audioconvert.c | 1146 +++ spa/plugins/audioconvert/test-channelmix.c | 396 + spa/plugins/audioconvert/test-fmt-ops.c | 905 ++ spa/plugins/audioconvert/test-peaks.c | 108 + .../audioconvert/test-resample-delay.c | 456 + spa/plugins/audioconvert/test-resample.c | 157 + spa/plugins/audioconvert/test-source.c | 913 ++ spa/plugins/audioconvert/volume-ops-c.c | 25 + spa/plugins/audioconvert/volume-ops-sse.c | 46 + spa/plugins/audioconvert/volume-ops.c | 64 + spa/plugins/audioconvert/volume-ops.h | 48 + spa/plugins/audioconvert/wavfile.c | 250 + spa/plugins/audioconvert/wavfile.h | 39 + spa/plugins/audiomixer/audiomixer.c | 1040 +++ spa/plugins/audiomixer/benchmark-mix-ops.c | 202 + spa/plugins/audiomixer/meson.build | 126 + spa/plugins/audiomixer/mix-ops-avx.c | 68 + spa/plugins/audiomixer/mix-ops-c.c | 48 + spa/plugins/audiomixer/mix-ops-sse.c | 67 + spa/plugins/audiomixer/mix-ops-sse2.c | 67 + spa/plugins/audiomixer/mix-ops.c | 116 + spa/plugins/audiomixer/mix-ops.h | 149 + spa/plugins/audiomixer/mixer-dsp.c | 977 +++ spa/plugins/audiomixer/plugin.c | 33 + spa/plugins/audiomixer/test-mix-ops.c | 273 + spa/plugins/audiotestsrc/audiotestsrc.c | 1161 +++ spa/plugins/audiotestsrc/meson.build | 7 + spa/plugins/audiotestsrc/plugin.c | 29 + spa/plugins/audiotestsrc/render.c | 44 + spa/plugins/avb/avb-pcm-sink.c | 892 ++ spa/plugins/avb/avb-pcm-source.c | 892 ++ spa/plugins/avb/avb-pcm.c | 1216 +++ spa/plugins/avb/avb-pcm.h | 287 + spa/plugins/avb/avb.c | 37 + spa/plugins/avb/avb.h | 19 + spa/plugins/avb/avbtp/packets.h | 200 + spa/plugins/avb/meson.build | 14 + spa/plugins/bluez5/README-MIDI.md | 24 + spa/plugins/bluez5/README-OPUS-A2DP.md | 335 + spa/plugins/bluez5/README-SBC-XQ.md | 54 + spa/plugins/bluez5/README-Telephony.md | 345 + spa/plugins/bluez5/a2dp-codec-aac.c | 736 ++ spa/plugins/bluez5/a2dp-codec-aptx.c | 756 ++ spa/plugins/bluez5/a2dp-codec-caps.h | 491 ++ spa/plugins/bluez5/a2dp-codec-faststream.c | 627 ++ spa/plugins/bluez5/a2dp-codec-lc3plus.c | 768 ++ spa/plugins/bluez5/a2dp-codec-ldac.c | 606 ++ spa/plugins/bluez5/a2dp-codec-opus-g.c | 542 ++ spa/plugins/bluez5/a2dp-codec-opus.c | 1437 +++ spa/plugins/bluez5/a2dp-codec-sbc.c | 686 ++ spa/plugins/bluez5/asha-codec-g722.c | 176 + spa/plugins/bluez5/backend-hsphfpd.c | 1521 ++++ spa/plugins/bluez5/backend-native.c | 4053 +++++++++ spa/plugins/bluez5/backend-ofono.c | 901 ++ spa/plugins/bluez5/bap-codec-caps.h | 178 + spa/plugins/bluez5/bap-codec-lc3.c | 1394 +++ spa/plugins/bluez5/bluez-hardware.conf | 109 + spa/plugins/bluez5/bluez5-dbus.c | 6786 ++++++++++++++ spa/plugins/bluez5/bluez5-device.c | 3084 +++++++ spa/plugins/bluez5/bt-latency.h | 175 + spa/plugins/bluez5/codec-loader.c | 219 + spa/plugins/bluez5/codec-loader.h | 19 + spa/plugins/bluez5/dbus-monitor.c | 246 + spa/plugins/bluez5/dbus-monitor.h | 64 + spa/plugins/bluez5/decode-buffer.h | 311 + spa/plugins/bluez5/defs.h | 883 ++ spa/plugins/bluez5/g722/g722_enc_dec.h | 148 + spa/plugins/bluez5/g722/g722_encode.c | 387 + spa/plugins/bluez5/hci.c | 67 + spa/plugins/bluez5/iso-io.c | 558 ++ spa/plugins/bluez5/iso-io.h | 49 + spa/plugins/bluez5/media-codecs.c | 227 + spa/plugins/bluez5/media-codecs.h | 232 + spa/plugins/bluez5/media-sink.c | 2309 +++++ spa/plugins/bluez5/media-source.c | 1978 +++++ spa/plugins/bluez5/meson.build | 219 + spa/plugins/bluez5/midi-enum.c | 869 ++ spa/plugins/bluez5/midi-node.c | 2150 +++++ spa/plugins/bluez5/midi-parser.c | 275 + spa/plugins/bluez5/midi-server.c | 560 ++ spa/plugins/bluez5/midi.h | 124 + spa/plugins/bluez5/modemmanager.c | 1120 +++ spa/plugins/bluez5/modemmanager.h | 141 + spa/plugins/bluez5/org.bluez.xml | 71 + spa/plugins/bluez5/player.c | 389 + spa/plugins/bluez5/player.h | 31 + spa/plugins/bluez5/plugin.c | 66 + spa/plugins/bluez5/quirks.c | 407 + spa/plugins/bluez5/rate-control.h | 194 + spa/plugins/bluez5/rtp.h | 74 + spa/plugins/bluez5/sco-io.c | 296 + spa/plugins/bluez5/sco-sink.c | 1763 ++++ spa/plugins/bluez5/sco-source.c | 1779 ++++ spa/plugins/bluez5/telephony.c | 1870 ++++ spa/plugins/bluez5/telephony.h | 132 + spa/plugins/bluez5/test-midi.c | 299 + spa/plugins/bluez5/upower.c | 240 + spa/plugins/bluez5/upower.h | 16 + spa/plugins/control/meson.build | 10 + spa/plugins/control/mixer.c | 997 +++ spa/plugins/control/plugin.c | 29 + spa/plugins/ffmpeg/ffmpeg-dec.c | 513 ++ spa/plugins/ffmpeg/ffmpeg-enc.c | 491 ++ spa/plugins/ffmpeg/ffmpeg.c | 143 + spa/plugins/ffmpeg/ffmpeg.h | 20 + spa/plugins/ffmpeg/meson.build | 9 + spa/plugins/filter-graph/audio-dsp-avx.c | 326 + spa/plugins/filter-graph/audio-dsp-c.c | 345 + spa/plugins/filter-graph/audio-dsp-impl.h | 94 + spa/plugins/filter-graph/audio-dsp-sse.c | 744 ++ spa/plugins/filter-graph/audio-dsp.c | 124 + spa/plugins/filter-graph/audio-dsp.h | 168 + spa/plugins/filter-graph/audio-plugin.h | 95 + spa/plugins/filter-graph/biquad.h | 58 + spa/plugins/filter-graph/builtin_plugin.c | 2647 ++++++ spa/plugins/filter-graph/convolver.c | 396 + spa/plugins/filter-graph/convolver.h | 14 + spa/plugins/filter-graph/ebur128_plugin.c | 638 ++ spa/plugins/filter-graph/filter-graph.c | 2170 +++++ spa/plugins/filter-graph/ladspa.h | 603 ++ spa/plugins/filter-graph/ladspa_plugin.c | 385 + spa/plugins/filter-graph/lv2_plugin.c | 604 ++ spa/plugins/filter-graph/meson.build | 117 + spa/plugins/filter-graph/pffft.c | 2381 +++++ spa/plugins/filter-graph/pffft.h | 181 + spa/plugins/filter-graph/sofa_plugin.c | 555 ++ spa/plugins/jack/jack-client.c | 101 + spa/plugins/jack/jack-client.h | 64 + spa/plugins/jack/jack-device.c | 440 + spa/plugins/jack/jack-sink.c | 914 ++ spa/plugins/jack/jack-source.c | 939 ++ spa/plugins/jack/meson.build | 12 + spa/plugins/jack/plugin.c | 37 + spa/plugins/libcamera/libcamera-device.cpp | 355 + spa/plugins/libcamera/libcamera-manager.cpp | 466 + spa/plugins/libcamera/libcamera-manager.hpp | 9 + spa/plugins/libcamera/libcamera-source.cpp | 1067 +++ spa/plugins/libcamera/libcamera-utils.cpp | 1071 +++ spa/plugins/libcamera/libcamera.c | 38 + spa/plugins/libcamera/libcamera.h | 31 + spa/plugins/libcamera/meson.build | 13 + spa/plugins/meson.build | 58 + spa/plugins/support/cpu-arm.c | 117 + spa/plugins/support/cpu-riscv.c | 29 + spa/plugins/support/cpu-x86.c | 184 + spa/plugins/support/cpu.c | 300 + spa/plugins/support/dbus.c | 578 ++ spa/plugins/support/evl-plugin.c | 30 + spa/plugins/support/evl-system.c | 506 ++ spa/plugins/support/journal.c | 329 + spa/plugins/support/logger.c | 428 + spa/plugins/support/loop.c | 1337 +++ spa/plugins/support/meson.build | 72 + spa/plugins/support/node-driver.c | 778 ++ spa/plugins/support/null-audio-sink.c | 1003 +++ spa/plugins/support/plugin.c | 50 + spa/plugins/support/system.c | 366 + spa/plugins/test/fakesink.c | 828 ++ spa/plugins/test/fakesrc.c | 858 ++ spa/plugins/test/meson.build | 7 + spa/plugins/test/plugin.c | 33 + spa/plugins/test/test-helper.h | 97 + spa/plugins/v4l2/meson.build | 19 + spa/plugins/v4l2/v4l2-device.c | 285 + spa/plugins/v4l2/v4l2-source.c | 1129 +++ spa/plugins/v4l2/v4l2-udev.c | 825 ++ spa/plugins/v4l2/v4l2-utils.c | 1946 +++++ spa/plugins/v4l2/v4l2.c | 48 + spa/plugins/v4l2/v4l2.h | 31 + spa/plugins/videoconvert/meson.build | 26 + spa/plugins/videoconvert/plugin.c | 41 + spa/plugins/videoconvert/videoadapter.c | 2043 +++++ spa/plugins/videoconvert/videoconvert-dummy.c | 720 ++ .../videoconvert/videoconvert-ffmpeg.c | 2082 +++++ spa/plugins/videotestsrc/draw.c | 268 + spa/plugins/videotestsrc/meson.build | 7 + spa/plugins/videotestsrc/plugin.c | 29 + spa/plugins/videotestsrc/videotestsrc.c | 972 ++ spa/plugins/volume/meson.build | 7 + spa/plugins/volume/plugin.c | 29 + spa/plugins/volume/volume.c | 876 ++ spa/plugins/vulkan/dmabuf.h | 62 + spa/plugins/vulkan/dmabuf_fallback.c | 42 + spa/plugins/vulkan/dmabuf_linux.c | 127 + spa/plugins/vulkan/meson.build | 26 + spa/plugins/vulkan/pixel-formats.c | 33 + spa/plugins/vulkan/pixel-formats.h | 12 + spa/plugins/vulkan/plugin.c | 41 + .../vulkan/shaders/disk-intersection.comp | 143 + spa/plugins/vulkan/shaders/filter-color.comp | 39 + spa/plugins/vulkan/shaders/filter.comp | 44 + spa/plugins/vulkan/shaders/filter.spv | Bin 0 -> 4884 bytes spa/plugins/vulkan/shaders/main.comp | 50 + spa/plugins/vulkan/shaders/main.spv | Bin 0 -> 9980 bytes spa/plugins/vulkan/utils.c | 91 + spa/plugins/vulkan/utils.h | 11 + spa/plugins/vulkan/vulkan-blit-dsp-filter.c | 954 ++ spa/plugins/vulkan/vulkan-blit-filter.c | 1056 +++ spa/plugins/vulkan/vulkan-blit-utils.c | 683 ++ spa/plugins/vulkan/vulkan-blit-utils.h | 96 + spa/plugins/vulkan/vulkan-compute-filter.c | 863 ++ spa/plugins/vulkan/vulkan-compute-source.c | 1070 +++ spa/plugins/vulkan/vulkan-compute-utils.c | 751 ++ spa/plugins/vulkan/vulkan-compute-utils.h | 100 + spa/plugins/vulkan/vulkan-types.h | 66 + spa/plugins/vulkan/vulkan-utils.c | 996 +++ spa/plugins/vulkan/vulkan-utils.h | 126 + spa/tests/benchmark-dict.c | 125 + spa/tests/benchmark-pod.c | 274 + spa/tests/meson.build | 58 + spa/tests/spa-include-test-template.c | 5 + spa/tests/stress-ringbuffer.c | 148 + spa/tools/meson.build | 11 + spa/tools/spa-inspect.c | 299 + spa/tools/spa-json-dump.c | 226 + spa/tools/spa-monitor.c | 209 + src/daemon/client.conf.avail/20-upmix.conf.in | 8 + src/daemon/client.conf.avail/meson.build | 12 + src/daemon/client.conf.in | 148 + src/daemon/filter-chain.conf.in | 63 + src/daemon/filter-chain/35-ebur128.conf | 63 + src/daemon/filter-chain/36-dcblock.conf | 59 + src/daemon/filter-chain/demonic.conf | 64 + src/daemon/filter-chain/meson.build | 21 + .../filter-chain/sink-dolby-surround.conf | 47 + src/daemon/filter-chain/sink-eq6.conf | 70 + src/daemon/filter-chain/sink-make-LFE.conf | 56 + .../filter-chain/sink-matrix-spatialiser.conf | 42 + src/daemon/filter-chain/sink-mix-FL-FR.conf | 40 + .../filter-chain/sink-upmix-5.1-filter.conf | 151 + .../sink-virtual-surround-5.1-kemar.conf | 180 + .../sink-virtual-surround-7.1-hesuvi.conf | 104 + .../filter-chain/source-duplicate-FL.conf | 52 + src/daemon/filter-chain/source-rnnoise.conf | 44 + src/daemon/filter-chain/spatializer-7.1.conf | 160 + .../filter-chain/spatializer-single.conf | 48 + src/daemon/jack.conf.in | 140 + src/daemon/meson.build | 166 + src/daemon/minimal.conf.in | 476 + src/daemon/pipewire-aes67.conf.in | 157 + src/daemon/pipewire-avb.conf.in | 80 + .../20-upmix.conf.in | 8 + .../pipewire-pulse.conf.avail/meson.build | 12 + src/daemon/pipewire-pulse.conf.in | 187 + src/daemon/pipewire-vulkan.conf.in | 99 + src/daemon/pipewire.c | 142 + .../pipewire.conf.avail/10-rates.conf.in | 4 + .../pipewire.conf.avail/20-upmix.conf.in | 8 + src/daemon/pipewire.conf.avail/meson.build | 13 + src/daemon/pipewire.conf.in | 371 + src/daemon/pipewire.desktop.in | 9 + src/daemon/systemd/meson.build | 6 + src/daemon/systemd/system/meson.build | 21 + .../systemd/system/pipewire-manager.socket | 13 + .../systemd/system/pipewire-pulse.service.in | 24 + .../systemd/system/pipewire-pulse.socket | 12 + src/daemon/systemd/system/pipewire.service.in | 35 + src/daemon/systemd/system/pipewire.socket | 12 + .../systemd/user/filter-chain.service.in | 21 + src/daemon/systemd/user/meson.build | 33 + .../systemd/user/pipewire-pulse.service.in | 36 + src/daemon/systemd/user/pipewire-pulse.socket | 11 + src/daemon/systemd/user/pipewire.service.in | 33 + src/daemon/systemd/user/pipewire.socket | 11 + src/examples/audio-capture.c | 189 + src/examples/audio-dsp-filter.c | 160 + src/examples/audio-dsp-src.c | 145 + src/examples/audio-src-ring.c | 226 + src/examples/audio-src-ring2.c | 269 + src/examples/audio-src.c | 168 + src/examples/bluez-session.c | 381 + src/examples/export-sink.c | 564 ++ src/examples/export-source.c | 545 ++ src/examples/export-spa-device.c | 124 + src/examples/export-spa.c | 163 + src/examples/gmain.c | 101 + src/examples/internal.c | 128 + src/examples/local-v4l2.c | 453 + src/examples/meson.build | 62 + src/examples/midi-src.c | 264 + src/examples/sdl.h | 178 + src/examples/video-dsp-play.c | 295 + src/examples/video-dsp-src.c | 374 + src/examples/video-play-fixate.c | 496 ++ src/examples/video-play-pull.c | 571 ++ src/examples/video-play-reneg.c | 418 + src/examples/video-play.c | 539 ++ src/examples/video-src-alloc.c | 442 + src/examples/video-src-fixate.c | 580 ++ src/examples/video-src-reneg.c | 487 ++ src/examples/video-src.c | 357 + src/gst/.editorconfig | 7 + src/gst/gstpipewire.c | 49 + src/gst/gstpipewireclock.c | 114 + src/gst/gstpipewireclock.h | 35 + src/gst/gstpipewirecore.c | 200 + src/gst/gstpipewirecore.h | 40 + src/gst/gstpipewiredeviceprovider.c | 973 +++ src/gst/gstpipewiredeviceprovider.h | 72 + src/gst/gstpipewireformat.c | 1326 +++ src/gst/gstpipewireformat.h | 24 + src/gst/gstpipewirepool.c | 359 + src/gst/gstpipewirepool.h | 73 + src/gst/gstpipewiresink.c | 1119 +++ src/gst/gstpipewiresink.h | 82 + src/gst/gstpipewiresrc.c | 1573 ++++ src/gst/gstpipewiresrc.h | 72 + src/gst/gstpipewirestream.c | 172 + src/gst/gstpipewirestream.h | 61 + src/gst/meson.build | 35 + src/meson.build | 13 + src/modules/flatpak-utils.h | 140 + src/modules/meson.build | 759 ++ src/modules/module-access.c | 405 + src/modules/module-adapter.c | 400 + src/modules/module-adapter/adapter.c | 242 + src/modules/module-adapter/adapter.h | 28 + src/modules/module-avb.c | 113 + src/modules/module-avb/aaf.h | 82 + src/modules/module-avb/acmp.c | 456 + src/modules/module-avb/acmp.h | 80 + src/modules/module-avb/adp.c | 357 + src/modules/module-avb/adp.h | 86 + src/modules/module-avb/aecp-aem-descriptors.h | 227 + src/modules/module-avb/aecp-aem.c | 265 + src/modules/module-avb/aecp-aem.h | 325 + src/modules/module-avb/aecp.c | 148 + src/modules/module-avb/aecp.h | 40 + src/modules/module-avb/avb.c | 89 + src/modules/module-avb/avb.h | 26 + src/modules/module-avb/avdecc.c | 317 + src/modules/module-avb/descriptors.h | 255 + src/modules/module-avb/iec61883.h | 90 + src/modules/module-avb/internal.h | 146 + src/modules/module-avb/maap.c | 441 + src/modules/module-avb/maap.h | 50 + src/modules/module-avb/mmrp.c | 213 + src/modules/module-avb/mmrp.h | 48 + src/modules/module-avb/mrp.c | 592 ++ src/modules/module-avb/mrp.h | 161 + src/modules/module-avb/msrp.c | 439 + src/modules/module-avb/msrp.h | 114 + src/modules/module-avb/mvrp.c | 277 + src/modules/module-avb/mvrp.h | 42 + src/modules/module-avb/packets.h | 81 + src/modules/module-avb/srp.c | 39 + src/modules/module-avb/srp.h | 12 + src/modules/module-avb/stream.c | 569 ++ src/modules/module-avb/stream.h | 84 + src/modules/module-avb/utils.h | 66 + src/modules/module-client-device.c | 269 + .../module-client-device/client-device.h | 24 + .../module-client-device/protocol-native.c | 536 ++ .../module-client-device/proxy-device.c | 70 + .../module-client-device/resource-device.c | 137 + src/modules/module-client-node.c | 281 + src/modules/module-client-node/client-node.c | 1846 ++++ src/modules/module-client-node/client-node.h | 40 + .../module-client-node/protocol-native.c | 1248 +++ src/modules/module-client-node/remote-node.c | 1295 +++ .../module-client-node/v0/client-node.c | 1429 +++ .../module-client-node/v0/client-node.h | 91 + .../module-client-node/v0/ext-client-node.h | 394 + .../module-client-node/v0/protocol-native.c | 514 ++ src/modules/module-client-node/v0/transport.c | 241 + src/modules/module-client-node/v0/transport.h | 39 + src/modules/module-combine-stream.c | 1655 ++++ src/modules/module-echo-cancel.c | 1539 ++++ src/modules/module-example-filter.c | 629 ++ src/modules/module-example-sink.c | 442 + src/modules/module-example-source.c | 448 + src/modules/module-fallback-sink.c | 462 + src/modules/module-ffado-driver.c | 1616 ++++ src/modules/module-filter-chain.c | 1531 ++++ src/modules/module-jack-tunnel.c | 1187 +++ src/modules/module-jack-tunnel/weakjack.h | 197 + src/modules/module-jackdbus-detect.c | 395 + src/modules/module-link-factory.c | 669 ++ src/modules/module-loopback.c | 1034 +++ src/modules/module-metadata.c | 366 + src/modules/module-metadata/metadata.c | 301 + src/modules/module-metadata/protocol-native.c | 334 + src/modules/module-metadata/proxy-metadata.c | 72 + src/modules/module-netjack2-driver.c | 1326 +++ src/modules/module-netjack2-manager.c | 1368 +++ src/modules/module-netjack2/packets.h | 194 + src/modules/module-netjack2/peer.c | 1042 +++ src/modules/module-parametric-equalizer.c | 384 + src/modules/module-pipe-tunnel.c | 966 ++ src/modules/module-portal.c | 320 + src/modules/module-profiler.c | 576 ++ src/modules/module-profiler/protocol-native.c | 108 + src/modules/module-protocol-native.c | 1879 ++++ .../module-protocol-native/connection.c | 860 ++ .../module-protocol-native/connection.h | 92 + src/modules/module-protocol-native/defs.h | 35 + .../module-protocol-native/local-socket.c | 189 + .../portal-screencast.c | 21 + .../module-protocol-native/protocol-footer.c | 132 + .../module-protocol-native/protocol-footer.h | 39 + .../module-protocol-native/protocol-native.c | 2360 +++++ .../module-protocol-native/security-context.c | 183 + .../module-protocol-native/test-connection.c | 205 + .../module-protocol-native/v0/interfaces.h | 514 ++ .../v0/protocol-native.c | 1352 +++ .../module-protocol-native/v0/typemap.h | 292 + src/modules/module-protocol-pulse.c | 482 + src/modules/module-protocol-pulse/client.c | 403 + src/modules/module-protocol-pulse/client.h | 118 + src/modules/module-protocol-pulse/cmd.c | 126 + src/modules/module-protocol-pulse/cmd.h | 12 + src/modules/module-protocol-pulse/collect.c | 607 ++ src/modules/module-protocol-pulse/collect.h | 152 + src/modules/module-protocol-pulse/commands.h | 188 + src/modules/module-protocol-pulse/dbus-name.c | 67 + src/modules/module-protocol-pulse/dbus-name.h | 13 + src/modules/module-protocol-pulse/defs.h | 327 + src/modules/module-protocol-pulse/extension.c | 49 + src/modules/module-protocol-pulse/extension.h | 23 + src/modules/module-protocol-pulse/format.c | 928 ++ src/modules/module-protocol-pulse/format.h | 227 + src/modules/module-protocol-pulse/internal.h | 88 + src/modules/module-protocol-pulse/log.h | 13 + src/modules/module-protocol-pulse/manager.c | 1047 +++ src/modules/module-protocol-pulse/manager.h | 131 + .../module-protocol-pulse/message-handler.c | 174 + .../module-protocol-pulse/message-handler.h | 12 + src/modules/module-protocol-pulse/message.c | 877 ++ src/modules/module-protocol-pulse/message.h | 67 + src/modules/module-protocol-pulse/module.c | 393 + src/modules/module-protocol-pulse/module.h | 90 + .../modules/module-alsa-sink.c | 252 + .../modules/module-alsa-source.c | 252 + .../modules/module-always-sink.c | 115 + .../modules/module-combine-sink.c | 347 + .../modules/module-device-manager.c | 63 + .../modules/module-device-restore.c | 493 ++ .../modules/module-echo-cancel.c | 375 + .../modules/module-gsettings.c | 325 + .../modules/module-jackdbus-detect.c | 223 + .../modules/module-ladspa-sink.c | 242 + .../modules/module-ladspa-source.c | 250 + .../modules/module-loopback.c | 237 + .../modules/module-native-protocol-tcp.c | 121 + .../modules/module-null-sink.c | 212 + .../modules/module-pipe-sink.c | 199 + .../modules/module-pipe-source.c | 194 + .../modules/module-raop-discover.c | 127 + .../modules/module-remap-sink.c | 230 + .../modules/module-remap-source.c | 237 + .../modules/module-roc-sink-input.c | 206 + .../modules/module-roc-sink.c | 214 + .../modules/module-roc-source.c | 225 + .../modules/module-rtp-recv.c | 164 + .../modules/module-rtp-send.c | 260 + .../modules/module-simple-protocol-tcp.c | 193 + .../modules/module-stream-restore.c | 467 + .../modules/module-switch-on-connect.c | 285 + .../modules/module-tunnel-sink.c | 210 + .../modules/module-tunnel-source.c | 207 + .../modules/module-virtual-sink.c | 197 + .../modules/module-virtual-source.c | 206 + .../modules/module-x11-bell.c | 128 + .../modules/module-zeroconf-discover.c | 130 + .../modules/module-zeroconf-publish.c | 733 ++ .../org.freedesktop.pulseaudio.gschema.xml | 115 + src/modules/module-protocol-pulse/operation.c | 72 + src/modules/module-protocol-pulse/operation.h | 37 + .../module-protocol-pulse/pending-sample.c | 138 + .../module-protocol-pulse/pending-sample.h | 32 + .../module-protocol-pulse/pulse-server.c | 5588 ++++++++++++ .../module-protocol-pulse/pulse-server.h | 36 + src/modules/module-protocol-pulse/quirks.c | 59 + src/modules/module-protocol-pulse/quirks.h | 20 + src/modules/module-protocol-pulse/remap.c | 38 + src/modules/module-protocol-pulse/remap.h | 32 + src/modules/module-protocol-pulse/reply.c | 65 + src/modules/module-protocol-pulse/reply.h | 22 + .../module-protocol-pulse/sample-play.c | 191 + .../module-protocol-pulse/sample-play.h | 56 + src/modules/module-protocol-pulse/sample.c | 30 + src/modules/module-protocol-pulse/sample.h | 41 + src/modules/module-protocol-pulse/server.c | 1107 +++ src/modules/module-protocol-pulse/server.h | 40 + .../module-protocol-pulse/snap-policy.c | 203 + .../module-protocol-pulse/snap-policy.h | 22 + src/modules/module-protocol-pulse/stream.c | 460 + src/modules/module-protocol-pulse/stream.h | 122 + src/modules/module-protocol-pulse/utils.c | 228 + src/modules/module-protocol-pulse/utils.h | 21 + src/modules/module-protocol-pulse/volume.c | 94 + src/modules/module-protocol-pulse/volume.h | 65 + src/modules/module-protocol-simple.c | 1023 +++ src/modules/module-pulse-tunnel.c | 1248 +++ src/modules/module-raop-discover.c | 615 ++ src/modules/module-raop-sink.c | 2030 +++++ src/modules/module-raop/rtsp-client.c | 628 ++ src/modules/module-raop/rtsp-client.h | 72 + src/modules/module-roc-sink.c | 499 ++ src/modules/module-roc-source.c | 573 ++ src/modules/module-roc/common.h | 131 + src/modules/module-rt.c | 1211 +++ src/modules/module-rt/20-pw-defaults.conf.in | 20 + src/modules/module-rt/25-pw-rlimits.conf.in | 14 + src/modules/module-rt/meson.build | 23 + src/modules/module-rtp-sap.c | 1953 +++++ src/modules/module-rtp-session.c | 1841 ++++ src/modules/module-rtp-sink.c | 642 ++ src/modules/module-rtp-source.c | 733 ++ src/modules/module-rtp/apple-midi.h | 52 + src/modules/module-rtp/audio.c | 625 ++ src/modules/module-rtp/midi.c | 515 ++ src/modules/module-rtp/opus.c | 373 + src/modules/module-rtp/ptp.h | 61 + src/modules/module-rtp/rtp.h | 93 + src/modules/module-rtp/sap.h | 38 + src/modules/module-rtp/stream.c | 723 ++ src/modules/module-rtp/stream.h | 79 + src/modules/module-session-manager.c | 58 + .../client-endpoint/client-endpoint.c | 276 + .../client-endpoint/client-endpoint.h | 42 + .../client-endpoint/endpoint-stream.c | 334 + .../client-endpoint/endpoint-stream.h | 44 + .../client-endpoint/endpoint.c | 366 + .../client-endpoint/endpoint.h | 41 + .../client-session/client-session.c | 275 + .../client-session/client-session.h | 42 + .../client-session/endpoint-link.c | 351 + .../client-session/endpoint-link.h | 45 + .../client-session/session.c | 325 + .../client-session/session.h | 42 + .../module-session-manager/endpoint-link.c | 559 ++ .../module-session-manager/endpoint-stream.c | 550 ++ src/modules/module-session-manager/endpoint.c | 559 ++ .../module-session-manager/protocol-native.c | 3063 +++++++ .../proxy-session-manager.c | 168 + src/modules/module-session-manager/session.c | 547 ++ src/modules/module-snapcast-discover.c | 907 ++ src/modules/module-spa-device-factory.c | 265 + src/modules/module-spa-device.c | 101 + src/modules/module-spa-node-factory.c | 373 + src/modules/module-spa-node.c | 104 + src/modules/module-vban-recv.c | 777 ++ src/modules/module-vban-send.c | 517 ++ src/modules/module-vban/audio.c | 266 + src/modules/module-vban/midi.c | 326 + src/modules/module-vban/stream.c | 473 + src/modules/module-vban/stream.h | 54 + src/modules/module-vban/vban.h | 150 + src/modules/module-x11-bell.c | 359 + src/modules/module-zeroconf-discover.c | 543 ++ .../module-zeroconf-discover/avahi-poll.c | 181 + .../module-zeroconf-discover/avahi-poll.h | 11 + src/modules/network-utils.h | 135 + src/modules/spa/spa-device.c | 163 + src/modules/spa/spa-device.h | 42 + src/modules/spa/spa-node.c | 219 + src/modules/spa/spa-node.h | 43 + src/pipewire/array.h | 164 + src/pipewire/buffers.c | 356 + src/pipewire/buffers.h | 56 + src/pipewire/client.h | 206 + src/pipewire/conf.c | 1435 +++ src/pipewire/conf.h | 55 + src/pipewire/context.c | 2069 +++++ src/pipewire/context.h | 216 + src/pipewire/control.c | 247 + src/pipewire/control.h | 63 + src/pipewire/core.c | 519 ++ src/pipewire/core.h | 697 ++ src/pipewire/data-loop.c | 338 + src/pipewire/data-loop.h | 123 + src/pipewire/device.h | 195 + src/pipewire/extensions/client-node.h | 367 + src/pipewire/extensions/meson.build | 22 + src/pipewire/extensions/metadata.h | 141 + src/pipewire/extensions/profiler.h | 82 + src/pipewire/extensions/protocol-native.h | 85 + src/pipewire/extensions/security-context.h | 131 + src/pipewire/extensions/session-manager.h | 27 + .../session-manager/impl-interfaces.h | 278 + .../extensions/session-manager/interfaces.h | 497 ++ .../session-manager/introspect-funcs.h | 303 + .../extensions/session-manager/introspect.h | 110 + .../extensions/session-manager/keys.h | 47 + src/pipewire/factory.h | 110 + src/pipewire/filter.c | 2143 +++++ src/pipewire/filter.h | 280 + src/pipewire/global.c | 424 + src/pipewire/global.h | 146 + src/pipewire/i18n.h | 36 + src/pipewire/impl-client.c | 818 ++ src/pipewire/impl-client.h | 170 + src/pipewire/impl-core.c | 644 ++ src/pipewire/impl-core.h | 84 + src/pipewire/impl-device.c | 1041 +++ src/pipewire/impl-device.h | 96 + src/pipewire/impl-factory.c | 283 + src/pipewire/impl-factory.h | 111 + src/pipewire/impl-link.c | 1639 ++++ src/pipewire/impl-link.h | 106 + src/pipewire/impl-metadata.c | 610 ++ src/pipewire/impl-metadata.h | 94 + src/pipewire/impl-module.c | 404 + src/pipewire/impl-module.h | 102 + src/pipewire/impl-node.c | 2868 ++++++ src/pipewire/impl-node.h | 195 + src/pipewire/impl-port.c | 2011 +++++ src/pipewire/impl-port.h | 129 + src/pipewire/impl.h | 42 + src/pipewire/introspect.c | 573 ++ src/pipewire/keys.h | 398 + src/pipewire/link.h | 136 + src/pipewire/log.c | 506 ++ src/pipewire/log.h | 198 + src/pipewire/loop.c | 192 + src/pipewire/loop.h | 155 + src/pipewire/main-loop.c | 142 + src/pipewire/main-loop.h | 69 + src/pipewire/map.h | 236 + src/pipewire/mem.c | 931 ++ src/pipewire/mem.h | 197 + src/pipewire/meson.build | 138 + src/pipewire/module.h | 108 + src/pipewire/node.h | 241 + src/pipewire/permission.h | 72 + src/pipewire/pipewire.c | 793 ++ src/pipewire/pipewire.h | 101 + src/pipewire/port.h | 186 + src/pipewire/private.h | 1431 +++ src/pipewire/properties.c | 954 ++ src/pipewire/properties.h | 200 + src/pipewire/protocol.c | 168 + src/pipewire/protocol.h | 147 + src/pipewire/proxy.c | 351 + src/pipewire/proxy.h | 203 + src/pipewire/resource.c | 341 + src/pipewire/resource.h | 154 + src/pipewire/settings.c | 322 + src/pipewire/stream.c | 2685 ++++++ src/pipewire/stream.h | 701 ++ src/pipewire/thread-loop.c | 522 ++ src/pipewire/thread-loop.h | 162 + src/pipewire/thread.c | 181 + src/pipewire/thread.h | 66 + src/pipewire/type.h | 45 + src/pipewire/utils.c | 373 + src/pipewire/utils.h | 111 + src/pipewire/version.h.in | 54 + src/pipewire/work-queue.c | 249 + src/pipewire/work-queue.h | 51 + src/tests/meson.build | 53 + src/tests/test-cpp.cpp | 15 + src/tests/test-endpoint.c | 449 + src/tests/test-filter.c | 355 + src/tests/test-interfaces.c | 364 + src/tests/test-security-context.c | 174 + src/tests/test-stream.c | 237 + src/tools/dfffile.c | 343 + src/tools/dfffile.h | 31 + src/tools/dsffile.c | 254 + src/tools/dsffile.h | 31 + src/tools/meson.build | 101 + src/tools/midifile.c | 1014 +++ src/tools/midifile.h | 47 + src/tools/pw-cat.c | 2120 +++++ src/tools/pw-cli.c | 2453 ++++++ src/tools/pw-config.c | 253 + src/tools/pw-container.c | 303 + src/tools/pw-dot.c | 1440 +++ src/tools/pw-dump.c | 1642 ++++ src/tools/pw-link.c | 1107 +++ src/tools/pw-loopback.c | 280 + src/tools/pw-metadata.c | 295 + src/tools/pw-mididump.c | 216 + src/tools/pw-mon.c | 917 ++ src/tools/pw-profiler.c | 801 ++ src/tools/pw-reserve.c | 238 + src/tools/pw-top.c | 928 ++ src/tools/reserve.c | 547 ++ src/tools/reserve.h | 66 + subprojects/libcamera.wrap | 3 + subprojects/media-session.wrap | 4 + subprojects/webrtc-audio-processing.wrap | 8 + subprojects/wireplumber.wrap | 3 + template.test.in | 3 + test/data/test-spa-json.txt | 1703 ++++ test/meson.build | 159 + test/pwtest-compat.c | 35 + test/pwtest-implementation.h | 117 + test/pwtest.c | 1467 ++++ test/pwtest.h | 557 ++ test/test-array.c | 125 + test/test-client.c | 50 + test/test-config.c | 82 + test/test-context.c | 270 + test/test-example.c | 247 + test/test-functional.c | 39 + test/test-lib.c | 48 + test/test-logger.c | 636 ++ test/test-loop.c | 448 + test/test-map.c | 222 + test/test-properties.c | 608 ++ test/test-pwtest.c | 35 + test/test-spa-buffer.c | 132 + test/test-spa-control.c | 173 + test/test-spa-json.c | 1115 +++ test/test-spa-log.c | 194 + test/test-spa-node.c | 232 + test/test-spa-pod.c | 1687 ++++ test/test-spa-utils.c | 1033 +++ test/test-support.c | 68 + test/test-utils.c | 257 + 1265 files changed, 465320 insertions(+) create mode 100644 .codespell-ignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100755 .gitlab/ci/check_missing_headers.sh create mode 100644 .gitlab/issue_templates/.gitkeep create mode 100644 .gitlab/issue_templates/bluetooth issue.md create mode 100644 .gitlab/issue_templates/issue.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 COPYING create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 Makefile.in create mode 100644 NEWS create mode 100644 README.md create mode 100755 autogen.sh create mode 100644 doc/Doxyfile.in create mode 100644 doc/DoxygenLayout.xml create mode 100644 doc/custom.css create mode 100644 doc/dox/api/index.dox create mode 100644 doc/dox/api/spa-buffer.dox create mode 100644 doc/dox/api/spa-design.dox create mode 100644 doc/dox/api/spa-index.dox create mode 100644 doc/dox/api/spa-plugins.dox create mode 100644 doc/dox/api/spa-pod.dox create mode 100644 doc/dox/config/index.md create mode 100644 doc/dox/config/libpipewire-modules.7.md create mode 100644 doc/dox/config/pipewire-client.conf.5.md create mode 100644 doc/dox/config/pipewire-filter-chain.conf.5.md create mode 100644 doc/dox/config/pipewire-jack.conf.5.md create mode 100644 doc/dox/config/pipewire-props.7.md create mode 100644 doc/dox/config/pipewire-pulse-modules.7.md create mode 100644 doc/dox/config/pipewire-pulse.conf.5.md create mode 100644 doc/dox/config/pipewire.conf.5.md create mode 100644 doc/dox/config/xref.md create mode 100644 doc/dox/index.dox create mode 100644 doc/dox/internals/access.dox create mode 100644 doc/dox/internals/audio.dox create mode 100644 doc/dox/internals/daemon.dox create mode 100644 doc/dox/internals/design.dox create mode 100644 doc/dox/internals/dma-buf.dox create mode 100644 doc/dox/internals/index.dox create mode 100644 doc/dox/internals/library.dox create mode 100644 doc/dox/internals/midi.dox create mode 100644 doc/dox/internals/objects.dox create mode 100644 doc/dox/internals/portal.dox create mode 100644 doc/dox/internals/protocol.dox create mode 100644 doc/dox/internals/pulseaudio.dox create mode 100644 doc/dox/internals/scheduling.dox create mode 100644 doc/dox/internals/session-manager.dox create mode 100644 doc/dox/modules.dox create mode 100644 doc/dox/overview.dox create mode 100644 doc/dox/programs/index.md create mode 100644 doc/dox/programs/pipewire-pulse.1.md create mode 100644 doc/dox/programs/pipewire.1.md create mode 100644 doc/dox/programs/pw-cat.1.md create mode 100644 doc/dox/programs/pw-cli.1.md create mode 100644 doc/dox/programs/pw-config.1.md create mode 100644 doc/dox/programs/pw-container.1.md create mode 100644 doc/dox/programs/pw-dot.1.md create mode 100644 doc/dox/programs/pw-dump.1.md create mode 100644 doc/dox/programs/pw-jack.1.md create mode 100644 doc/dox/programs/pw-link.1.md create mode 100644 doc/dox/programs/pw-loopback.1.md create mode 100644 doc/dox/programs/pw-metadata.1.md create mode 100644 doc/dox/programs/pw-mididump.1.md create mode 100644 doc/dox/programs/pw-mon.1.md create mode 100644 doc/dox/programs/pw-profiler.1.md create mode 100644 doc/dox/programs/pw-reserve.1.md create mode 100644 doc/dox/programs/pw-top.1.md create mode 100644 doc/dox/programs/pw-v4l2.1.md create mode 100644 doc/dox/programs/spa-acp-tool.1.md create mode 100644 doc/dox/programs/spa-inspect.1.md create mode 100644 doc/dox/programs/spa-json-dump.1.md create mode 100644 doc/dox/programs/spa-monitor.1.md create mode 100644 doc/dox/programs/spa-resample.1.md create mode 100644 doc/dox/pulse-modules.dox create mode 100644 doc/dox/pulse-modules.inc create mode 100644 doc/dox/tutorial/index.dox create mode 100644 doc/dox/tutorial/tutorial1.dox create mode 100644 doc/dox/tutorial/tutorial2.dox create mode 100644 doc/dox/tutorial/tutorial3.dox create mode 100644 doc/dox/tutorial/tutorial4.dox create mode 100644 doc/dox/tutorial/tutorial5.dox create mode 100644 doc/dox/tutorial/tutorial6.dox create mode 100644 doc/doxygen-awesome.css create mode 100644 doc/examples.dox.in create mode 100644 doc/examples/tutorial1.c create mode 100644 doc/examples/tutorial2.c create mode 100644 doc/examples/tutorial3.c create mode 100644 doc/examples/tutorial4.c create mode 100644 doc/examples/tutorial5.c create mode 100644 doc/examples/tutorial6.c create mode 100755 doc/input-filter-h.sh create mode 100755 doc/input-filter-md.py create mode 100755 doc/input-filter.py create mode 100755 doc/man-fixup.py create mode 100644 doc/meson.build create mode 100644 doc/tree.dox create mode 100644 include/valgrind/memcheck.h create mode 100644 include/valgrind/valgrind.h create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 pipewire-alsa/alsa-plugins/ctl_pipewire.c create mode 100644 pipewire-alsa/alsa-plugins/meson.build create mode 100644 pipewire-alsa/alsa-plugins/pcm_pipewire.c create mode 100644 pipewire-alsa/conf/50-pipewire.conf create mode 100644 pipewire-alsa/conf/99-pipewire-default.conf create mode 100644 pipewire-alsa/conf/meson.build create mode 100644 pipewire-alsa/tests/meson.build create mode 100644 pipewire-alsa/tests/test-pipewire-alsa-stress.c create mode 100644 pipewire-jack/examples/ump-source.c create mode 100644 pipewire-jack/examples/video-dsp-play.c create mode 100644 pipewire-jack/jack/control.h create mode 100644 pipewire-jack/jack/intclient.h create mode 100644 pipewire-jack/jack/jack.h create mode 100644 pipewire-jack/jack/jslist.h create mode 100644 pipewire-jack/jack/metadata.h create mode 100644 pipewire-jack/jack/midiport.h create mode 100644 pipewire-jack/jack/net.h create mode 100644 pipewire-jack/jack/ringbuffer.h create mode 100644 pipewire-jack/jack/session.h create mode 100644 pipewire-jack/jack/statistics.h create mode 100644 pipewire-jack/jack/systemdeps.h create mode 100644 pipewire-jack/jack/thread.h create mode 100644 pipewire-jack/jack/transport.h create mode 100644 pipewire-jack/jack/types.h create mode 100644 pipewire-jack/jack/uuid.h create mode 100644 pipewire-jack/jack/weakjack.h create mode 100644 pipewire-jack/jack/weakmacros.h create mode 100644 pipewire-jack/meson.build create mode 100644 pipewire-jack/src/control.c create mode 100644 pipewire-jack/src/dummy.c create mode 100644 pipewire-jack/src/export.c create mode 100644 pipewire-jack/src/meson.build create mode 100644 pipewire-jack/src/metadata.c create mode 100644 pipewire-jack/src/net.c create mode 100644 pipewire-jack/src/pipewire-jack-extensions.h create mode 100644 pipewire-jack/src/pipewire-jack.c create mode 100755 pipewire-jack/src/pw-jack.in create mode 100644 pipewire-jack/src/ringbuffer.c create mode 100644 pipewire-jack/src/statistics.c create mode 100644 pipewire-jack/src/uuid.c create mode 100644 pipewire-v4l2/meson.build create mode 100644 pipewire-v4l2/src/meson.build create mode 100644 pipewire-v4l2/src/pipewire-v4l2.c create mode 100644 pipewire-v4l2/src/pipewire-v4l2.h create mode 100755 pipewire-v4l2/src/pw-v4l2.in create mode 100644 pipewire-v4l2/src/v4l2-func.c create mode 100644 po/LINGUAS create mode 100644 po/POTFILES.in create mode 100644 po/POTFILES.skip create mode 100644 po/af.po create mode 100644 po/as.po create mode 100644 po/be.po create mode 100644 po/bg.po create mode 100644 po/bn_IN.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/da.po create mode 100644 po/de.po create mode 100644 po/de_CH.po create mode 100644 po/el.po create mode 100644 po/eo.po create mode 100644 po/es.po create mode 100644 po/fi.po create mode 100644 po/fr.po create mode 100644 po/gl.po create mode 100644 po/gu.po create mode 100644 po/he.po create mode 100644 po/hi.po create mode 100644 po/hr.po create mode 100644 po/hu.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/ja.po create mode 100644 po/ka.po create mode 100644 po/kk.po create mode 100644 po/kn.po create mode 100644 po/ko.po create mode 100644 po/lt.po create mode 100644 po/meson.build create mode 100644 po/ml.po create mode 100644 po/mr.po create mode 100644 po/my.po create mode 100644 po/nl.po create mode 100644 po/nn.po create mode 100644 po/oc.po create mode 100644 po/or.po create mode 100644 po/pa.po create mode 100644 po/pipewire.pot create mode 100644 po/pl.po create mode 100644 po/pt.po create mode 100644 po/pt_BR.po create mode 100644 po/ro.po create mode 100644 po/ru.po create mode 100644 po/si.po create mode 100644 po/sk.po create mode 100644 po/sl.po create mode 100644 po/sr.po create mode 100644 po/sr@latin.po create mode 100644 po/sv.po create mode 100644 po/ta.po create mode 100644 po/te.po create mode 100644 po/tr.po create mode 100644 po/uk.po create mode 100644 po/zh_CN.po create mode 100644 po/zh_TW.po create mode 100755 pw-uninstalled.sh create mode 100644 spa/examples/adapter-control.c create mode 100644 spa/examples/example-control.c create mode 100644 spa/examples/local-libcamera.c create mode 100644 spa/examples/local-v4l2.c create mode 100644 spa/examples/local-videotestsrc.c create mode 100644 spa/examples/meson.build create mode 100644 spa/include-private/spa-private/dbus-helpers.h create mode 100644 spa/include/meson.build create mode 100644 spa/include/spa/buffer/alloc.h create mode 100644 spa/include/spa/buffer/buffer.h create mode 100644 spa/include/spa/buffer/meta.h create mode 100644 spa/include/spa/buffer/type-info.h create mode 100644 spa/include/spa/control/control.h create mode 100644 spa/include/spa/control/type-info.h create mode 100644 spa/include/spa/control/ump-utils.h create mode 100644 spa/include/spa/debug/buffer.h create mode 100644 spa/include/spa/debug/context.h create mode 100644 spa/include/spa/debug/dict.h create mode 100644 spa/include/spa/debug/file.h create mode 100644 spa/include/spa/debug/format.h create mode 100644 spa/include/spa/debug/log.h create mode 100644 spa/include/spa/debug/mem.h create mode 100644 spa/include/spa/debug/node.h create mode 100644 spa/include/spa/debug/pod.h create mode 100644 spa/include/spa/debug/types.h create mode 100644 spa/include/spa/filter-graph/filter-graph.h create mode 100644 spa/include/spa/graph/graph.h create mode 100644 spa/include/spa/interfaces/audio/aec.h create mode 100644 spa/include/spa/monitor/device.h create mode 100644 spa/include/spa/monitor/event.h create mode 100644 spa/include/spa/monitor/type-info.h create mode 100644 spa/include/spa/monitor/utils.h create mode 100644 spa/include/spa/node/command.h create mode 100644 spa/include/spa/node/event.h create mode 100644 spa/include/spa/node/io.h create mode 100644 spa/include/spa/node/keys.h create mode 100644 spa/include/spa/node/node.h create mode 100644 spa/include/spa/node/type-info.h create mode 100644 spa/include/spa/node/utils.h create mode 100644 spa/include/spa/param/audio/aac-types.h create mode 100644 spa/include/spa/param/audio/aac-utils.h create mode 100644 spa/include/spa/param/audio/aac.h create mode 100644 spa/include/spa/param/audio/alac-utils.h create mode 100644 spa/include/spa/param/audio/alac.h create mode 100644 spa/include/spa/param/audio/amr-types.h create mode 100644 spa/include/spa/param/audio/amr-utils.h create mode 100644 spa/include/spa/param/audio/amr.h create mode 100644 spa/include/spa/param/audio/ape-utils.h create mode 100644 spa/include/spa/param/audio/ape.h create mode 100644 spa/include/spa/param/audio/compressed.h create mode 100644 spa/include/spa/param/audio/dsd-utils.h create mode 100644 spa/include/spa/param/audio/dsd.h create mode 100644 spa/include/spa/param/audio/dsp-utils.h create mode 100644 spa/include/spa/param/audio/dsp.h create mode 100644 spa/include/spa/param/audio/flac-utils.h create mode 100644 spa/include/spa/param/audio/flac.h create mode 100644 spa/include/spa/param/audio/format-utils.h create mode 100644 spa/include/spa/param/audio/format.h create mode 100644 spa/include/spa/param/audio/iec958-types.h create mode 100644 spa/include/spa/param/audio/iec958-utils.h create mode 100644 spa/include/spa/param/audio/iec958.h create mode 100644 spa/include/spa/param/audio/layout.h create mode 100644 spa/include/spa/param/audio/mp3-types.h create mode 100644 spa/include/spa/param/audio/mp3-utils.h create mode 100644 spa/include/spa/param/audio/mp3.h create mode 100644 spa/include/spa/param/audio/opus.h create mode 100644 spa/include/spa/param/audio/ra-utils.h create mode 100644 spa/include/spa/param/audio/ra.h create mode 100644 spa/include/spa/param/audio/raw-json.h create mode 100644 spa/include/spa/param/audio/raw-types.h create mode 100644 spa/include/spa/param/audio/raw-utils.h create mode 100644 spa/include/spa/param/audio/raw.h create mode 100644 spa/include/spa/param/audio/type-info.h create mode 100644 spa/include/spa/param/audio/vorbis-utils.h create mode 100644 spa/include/spa/param/audio/vorbis.h create mode 100644 spa/include/spa/param/audio/wma-types.h create mode 100644 spa/include/spa/param/audio/wma-utils.h create mode 100644 spa/include/spa/param/audio/wma.h create mode 100644 spa/include/spa/param/bluetooth/audio.h create mode 100644 spa/include/spa/param/bluetooth/type-info.h create mode 100644 spa/include/spa/param/buffers-types.h create mode 100644 spa/include/spa/param/buffers.h create mode 100644 spa/include/spa/param/format-types.h create mode 100644 spa/include/spa/param/format-utils.h create mode 100644 spa/include/spa/param/format.h create mode 100644 spa/include/spa/param/latency-types.h create mode 100644 spa/include/spa/param/latency-utils.h create mode 100644 spa/include/spa/param/latency.h create mode 100644 spa/include/spa/param/param-types.h create mode 100644 spa/include/spa/param/param.h create mode 100644 spa/include/spa/param/port-config-types.h create mode 100644 spa/include/spa/param/port-config.h create mode 100644 spa/include/spa/param/profile-types.h create mode 100644 spa/include/spa/param/profile.h create mode 100644 spa/include/spa/param/profiler-types.h create mode 100644 spa/include/spa/param/profiler.h create mode 100644 spa/include/spa/param/props-types.h create mode 100644 spa/include/spa/param/props.h create mode 100644 spa/include/spa/param/route-types.h create mode 100644 spa/include/spa/param/route.h create mode 100644 spa/include/spa/param/tag-types.h create mode 100644 spa/include/spa/param/tag-utils.h create mode 100644 spa/include/spa/param/tag.h create mode 100644 spa/include/spa/param/type-info.h create mode 100644 spa/include/spa/param/video/chroma.h create mode 100644 spa/include/spa/param/video/color.h create mode 100644 spa/include/spa/param/video/dsp-utils.h create mode 100644 spa/include/spa/param/video/dsp.h create mode 100644 spa/include/spa/param/video/encoded.h create mode 100644 spa/include/spa/param/video/format-utils.h create mode 100644 spa/include/spa/param/video/format.h create mode 100644 spa/include/spa/param/video/h264-utils.h create mode 100644 spa/include/spa/param/video/h264.h create mode 100644 spa/include/spa/param/video/mjpg-utils.h create mode 100644 spa/include/spa/param/video/mjpg.h create mode 100644 spa/include/spa/param/video/multiview.h create mode 100644 spa/include/spa/param/video/raw-types.h create mode 100644 spa/include/spa/param/video/raw-utils.h create mode 100644 spa/include/spa/param/video/raw.h create mode 100644 spa/include/spa/param/video/type-info.h create mode 100644 spa/include/spa/pod/builder.h create mode 100644 spa/include/spa/pod/command.h create mode 100644 spa/include/spa/pod/compare.h create mode 100644 spa/include/spa/pod/dynamic.h create mode 100644 spa/include/spa/pod/event.h create mode 100644 spa/include/spa/pod/filter.h create mode 100644 spa/include/spa/pod/iter.h create mode 100644 spa/include/spa/pod/parser.h create mode 100644 spa/include/spa/pod/pod.h create mode 100644 spa/include/spa/pod/vararg.h create mode 100644 spa/include/spa/support/cpu.h create mode 100644 spa/include/spa/support/dbus.h create mode 100644 spa/include/spa/support/i18n.h create mode 100644 spa/include/spa/support/log-impl.h create mode 100644 spa/include/spa/support/log.h create mode 100644 spa/include/spa/support/loop.h create mode 100644 spa/include/spa/support/plugin-loader.h create mode 100644 spa/include/spa/support/plugin.h create mode 100644 spa/include/spa/support/system.h create mode 100644 spa/include/spa/support/thread.h create mode 100644 spa/include/spa/utils/ansi.h create mode 100644 spa/include/spa/utils/atomic.h create mode 100644 spa/include/spa/utils/cleanup.h create mode 100644 spa/include/spa/utils/defs.h create mode 100644 spa/include/spa/utils/dict.h create mode 100644 spa/include/spa/utils/dll.h create mode 100644 spa/include/spa/utils/endian.h create mode 100644 spa/include/spa/utils/enum-types.h create mode 100644 spa/include/spa/utils/hook.h create mode 100644 spa/include/spa/utils/json-core.h create mode 100644 spa/include/spa/utils/json-pod.h create mode 100644 spa/include/spa/utils/json.h create mode 100644 spa/include/spa/utils/keys.h create mode 100644 spa/include/spa/utils/list.h create mode 100644 spa/include/spa/utils/names.h create mode 100644 spa/include/spa/utils/ratelimit.h create mode 100644 spa/include/spa/utils/result.h create mode 100644 spa/include/spa/utils/ringbuffer.h create mode 100644 spa/include/spa/utils/string.h create mode 100644 spa/include/spa/utils/type-info.h create mode 100644 spa/include/spa/utils/type.h create mode 100644 spa/lib/lib.c create mode 100644 spa/lib/meson.build create mode 100644 spa/meson.build create mode 100644 spa/plugins/aec/aec-null.c create mode 100644 spa/plugins/aec/aec-webrtc.cpp create mode 100644 spa/plugins/aec/meson.build create mode 100644 spa/plugins/alsa/90-pipewire-alsa.rules create mode 100644 spa/plugins/alsa/acp-tool.c create mode 100644 spa/plugins/alsa/acp/acp.c create mode 100644 spa/plugins/alsa/acp/acp.h create mode 100644 spa/plugins/alsa/acp/alsa-mixer.c create mode 100644 spa/plugins/alsa/acp/alsa-mixer.h create mode 100644 spa/plugins/alsa/acp/alsa-ucm.c create mode 100644 spa/plugins/alsa/acp/alsa-ucm.h create mode 100644 spa/plugins/alsa/acp/alsa-util.c create mode 100644 spa/plugins/alsa/acp/alsa-util.h create mode 100644 spa/plugins/alsa/acp/array.h create mode 100644 spa/plugins/alsa/acp/card.h create mode 100644 spa/plugins/alsa/acp/channelmap.h create mode 100644 spa/plugins/alsa/acp/compat.c create mode 100644 spa/plugins/alsa/acp/compat.h create mode 100644 spa/plugins/alsa/acp/conf-parser.c create mode 100644 spa/plugins/alsa/acp/conf-parser.h create mode 100644 spa/plugins/alsa/acp/device-port.h create mode 100644 spa/plugins/alsa/acp/dynarray.h create mode 100644 spa/plugins/alsa/acp/hashmap.h create mode 100644 spa/plugins/alsa/acp/idxset.h create mode 100644 spa/plugins/alsa/acp/llist.h create mode 100644 spa/plugins/alsa/acp/meson.build create mode 100644 spa/plugins/alsa/acp/proplist.h create mode 100644 spa/plugins/alsa/acp/volume.h create mode 100644 spa/plugins/alsa/alsa-acp-device.c create mode 100644 spa/plugins/alsa/alsa-compress-offload-device.c create mode 100644 spa/plugins/alsa/alsa-compress-offload-sink.c create mode 100644 spa/plugins/alsa/alsa-pcm-device.c create mode 100644 spa/plugins/alsa/alsa-pcm-sink.c create mode 100644 spa/plugins/alsa/alsa-pcm-source.c create mode 100644 spa/plugins/alsa/alsa-pcm.c create mode 100644 spa/plugins/alsa/alsa-pcm.h create mode 100644 spa/plugins/alsa/alsa-seq-bridge.c create mode 100644 spa/plugins/alsa/alsa-seq.c create mode 100644 spa/plugins/alsa/alsa-seq.h create mode 100644 spa/plugins/alsa/alsa-udev.c create mode 100644 spa/plugins/alsa/alsa.c create mode 100644 spa/plugins/alsa/alsa.h create mode 100644 spa/plugins/alsa/compress-offload-api-util.c create mode 100644 spa/plugins/alsa/compress-offload-api-util.h create mode 100644 spa/plugins/alsa/compress-offload-api.c create mode 100644 spa/plugins/alsa/compress-offload-api.h create mode 100644 spa/plugins/alsa/meson.build create mode 100644 spa/plugins/alsa/mixer/meson.build create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-aux.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-fm.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-linein.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input-video.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-input.conf.common create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-chat.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-headphones.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-lineout.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-mono.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output-speaker.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output.conf create mode 100644 spa/plugins/alsa/mixer/paths/analog-output.conf.common create mode 100644 spa/plugins/alsa/mixer/paths/audigy-analog-output-mirror.conf create mode 100644 spa/plugins/alsa/mixer/paths/audigy-analog-output.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-0.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-1.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-10.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-2.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-3.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-4.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-5.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-6.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-7.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-8.conf create mode 100644 spa/plugins/alsa/mixer/paths/hdmi-output-9.conf create mode 100644 spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf create mode 100644 spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf create mode 100644 spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf create mode 100644 spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf create mode 100644 spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf create mode 100644 spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf create mode 100644 spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf create mode 100644 spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/9999-custom.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/analog-only.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/audigy.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/default.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/force-speaker.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf create mode 100644 spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf create mode 100644 spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0 create mode 100644 spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x create mode 100644 spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3 create mode 100644 spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI create mode 100644 spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981 create mode 100644 spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A create mode 100644 spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A create mode 100644 spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer create mode 100644 spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer create mode 100644 spa/plugins/alsa/mixer/samples/USB Device 0x46d_0x9a4--USB Mixer create mode 100644 spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888 create mode 100644 spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+ create mode 100644 spa/plugins/alsa/test-hw-params.c create mode 100644 spa/plugins/alsa/test-timer.c create mode 100644 spa/plugins/audioconvert/audioadapter.c create mode 100644 spa/plugins/audioconvert/audioconvert.c create mode 100644 spa/plugins/audioconvert/benchmark-fmt-ops.c create mode 100644 spa/plugins/audioconvert/benchmark-resample.c create mode 100644 spa/plugins/audioconvert/biquad.c create mode 100644 spa/plugins/audioconvert/biquad.h create mode 100644 spa/plugins/audioconvert/channelmix-ops-c.c create mode 100644 spa/plugins/audioconvert/channelmix-ops-sse.c create mode 100644 spa/plugins/audioconvert/channelmix-ops.c create mode 100644 spa/plugins/audioconvert/channelmix-ops.h create mode 100644 spa/plugins/audioconvert/crossover.c create mode 100644 spa/plugins/audioconvert/crossover.h create mode 100644 spa/plugins/audioconvert/fmt-ops-avx2.c create mode 100644 spa/plugins/audioconvert/fmt-ops-c.c create mode 100644 spa/plugins/audioconvert/fmt-ops-neon.c create mode 100644 spa/plugins/audioconvert/fmt-ops-rvv.c create mode 100644 spa/plugins/audioconvert/fmt-ops-sse2.c create mode 100644 spa/plugins/audioconvert/fmt-ops-sse41.c create mode 100644 spa/plugins/audioconvert/fmt-ops-ssse3.c create mode 100644 spa/plugins/audioconvert/fmt-ops.c create mode 100644 spa/plugins/audioconvert/fmt-ops.h create mode 100644 spa/plugins/audioconvert/hilbert.h create mode 100644 spa/plugins/audioconvert/law.h create mode 100644 spa/plugins/audioconvert/meson.build create mode 100644 spa/plugins/audioconvert/peaks-ops-c.c create mode 100644 spa/plugins/audioconvert/peaks-ops-sse.c create mode 100644 spa/plugins/audioconvert/peaks-ops.c create mode 100644 spa/plugins/audioconvert/peaks-ops.h create mode 100644 spa/plugins/audioconvert/plugin.c create mode 100644 spa/plugins/audioconvert/resample-native-avx.c create mode 100644 spa/plugins/audioconvert/resample-native-c.c create mode 100644 spa/plugins/audioconvert/resample-native-impl.h create mode 100644 spa/plugins/audioconvert/resample-native-neon.c create mode 100644 spa/plugins/audioconvert/resample-native-sse.c create mode 100644 spa/plugins/audioconvert/resample-native-ssse3.c create mode 100644 spa/plugins/audioconvert/resample-native.c create mode 100644 spa/plugins/audioconvert/resample-peaks.c create mode 100644 spa/plugins/audioconvert/resample.h create mode 100644 spa/plugins/audioconvert/spa-resample-dump-coeffs.c create mode 100644 spa/plugins/audioconvert/spa-resample.c create mode 100644 spa/plugins/audioconvert/test-audioadapter.c create mode 100644 spa/plugins/audioconvert/test-audioconvert.c create mode 100644 spa/plugins/audioconvert/test-channelmix.c create mode 100644 spa/plugins/audioconvert/test-fmt-ops.c create mode 100644 spa/plugins/audioconvert/test-peaks.c create mode 100644 spa/plugins/audioconvert/test-resample-delay.c create mode 100644 spa/plugins/audioconvert/test-resample.c create mode 100644 spa/plugins/audioconvert/test-source.c create mode 100644 spa/plugins/audioconvert/volume-ops-c.c create mode 100644 spa/plugins/audioconvert/volume-ops-sse.c create mode 100644 spa/plugins/audioconvert/volume-ops.c create mode 100644 spa/plugins/audioconvert/volume-ops.h create mode 100644 spa/plugins/audioconvert/wavfile.c create mode 100644 spa/plugins/audioconvert/wavfile.h create mode 100644 spa/plugins/audiomixer/audiomixer.c create mode 100644 spa/plugins/audiomixer/benchmark-mix-ops.c create mode 100644 spa/plugins/audiomixer/meson.build create mode 100644 spa/plugins/audiomixer/mix-ops-avx.c create mode 100644 spa/plugins/audiomixer/mix-ops-c.c create mode 100644 spa/plugins/audiomixer/mix-ops-sse.c create mode 100644 spa/plugins/audiomixer/mix-ops-sse2.c create mode 100644 spa/plugins/audiomixer/mix-ops.c create mode 100644 spa/plugins/audiomixer/mix-ops.h create mode 100644 spa/plugins/audiomixer/mixer-dsp.c create mode 100644 spa/plugins/audiomixer/plugin.c create mode 100644 spa/plugins/audiomixer/test-mix-ops.c create mode 100644 spa/plugins/audiotestsrc/audiotestsrc.c create mode 100644 spa/plugins/audiotestsrc/meson.build create mode 100644 spa/plugins/audiotestsrc/plugin.c create mode 100644 spa/plugins/audiotestsrc/render.c create mode 100644 spa/plugins/avb/avb-pcm-sink.c create mode 100644 spa/plugins/avb/avb-pcm-source.c create mode 100644 spa/plugins/avb/avb-pcm.c create mode 100644 spa/plugins/avb/avb-pcm.h create mode 100644 spa/plugins/avb/avb.c create mode 100644 spa/plugins/avb/avb.h create mode 100644 spa/plugins/avb/avbtp/packets.h create mode 100644 spa/plugins/avb/meson.build create mode 100644 spa/plugins/bluez5/README-MIDI.md create mode 100644 spa/plugins/bluez5/README-OPUS-A2DP.md create mode 100644 spa/plugins/bluez5/README-SBC-XQ.md create mode 100644 spa/plugins/bluez5/README-Telephony.md create mode 100644 spa/plugins/bluez5/a2dp-codec-aac.c create mode 100644 spa/plugins/bluez5/a2dp-codec-aptx.c create mode 100644 spa/plugins/bluez5/a2dp-codec-caps.h create mode 100644 spa/plugins/bluez5/a2dp-codec-faststream.c create mode 100644 spa/plugins/bluez5/a2dp-codec-lc3plus.c create mode 100644 spa/plugins/bluez5/a2dp-codec-ldac.c create mode 100644 spa/plugins/bluez5/a2dp-codec-opus-g.c create mode 100644 spa/plugins/bluez5/a2dp-codec-opus.c create mode 100644 spa/plugins/bluez5/a2dp-codec-sbc.c create mode 100644 spa/plugins/bluez5/asha-codec-g722.c create mode 100644 spa/plugins/bluez5/backend-hsphfpd.c create mode 100644 spa/plugins/bluez5/backend-native.c create mode 100644 spa/plugins/bluez5/backend-ofono.c create mode 100644 spa/plugins/bluez5/bap-codec-caps.h create mode 100644 spa/plugins/bluez5/bap-codec-lc3.c create mode 100644 spa/plugins/bluez5/bluez-hardware.conf create mode 100644 spa/plugins/bluez5/bluez5-dbus.c create mode 100644 spa/plugins/bluez5/bluez5-device.c create mode 100644 spa/plugins/bluez5/bt-latency.h create mode 100644 spa/plugins/bluez5/codec-loader.c create mode 100644 spa/plugins/bluez5/codec-loader.h create mode 100644 spa/plugins/bluez5/dbus-monitor.c create mode 100644 spa/plugins/bluez5/dbus-monitor.h create mode 100644 spa/plugins/bluez5/decode-buffer.h create mode 100644 spa/plugins/bluez5/defs.h create mode 100644 spa/plugins/bluez5/g722/g722_enc_dec.h create mode 100644 spa/plugins/bluez5/g722/g722_encode.c create mode 100644 spa/plugins/bluez5/hci.c create mode 100644 spa/plugins/bluez5/iso-io.c create mode 100644 spa/plugins/bluez5/iso-io.h create mode 100644 spa/plugins/bluez5/media-codecs.c create mode 100644 spa/plugins/bluez5/media-codecs.h create mode 100644 spa/plugins/bluez5/media-sink.c create mode 100644 spa/plugins/bluez5/media-source.c create mode 100644 spa/plugins/bluez5/meson.build create mode 100644 spa/plugins/bluez5/midi-enum.c create mode 100644 spa/plugins/bluez5/midi-node.c create mode 100644 spa/plugins/bluez5/midi-parser.c create mode 100644 spa/plugins/bluez5/midi-server.c create mode 100644 spa/plugins/bluez5/midi.h create mode 100644 spa/plugins/bluez5/modemmanager.c create mode 100644 spa/plugins/bluez5/modemmanager.h create mode 100644 spa/plugins/bluez5/org.bluez.xml create mode 100644 spa/plugins/bluez5/player.c create mode 100644 spa/plugins/bluez5/player.h create mode 100644 spa/plugins/bluez5/plugin.c create mode 100644 spa/plugins/bluez5/quirks.c create mode 100644 spa/plugins/bluez5/rate-control.h create mode 100644 spa/plugins/bluez5/rtp.h create mode 100644 spa/plugins/bluez5/sco-io.c create mode 100644 spa/plugins/bluez5/sco-sink.c create mode 100644 spa/plugins/bluez5/sco-source.c create mode 100644 spa/plugins/bluez5/telephony.c create mode 100644 spa/plugins/bluez5/telephony.h create mode 100644 spa/plugins/bluez5/test-midi.c create mode 100644 spa/plugins/bluez5/upower.c create mode 100644 spa/plugins/bluez5/upower.h create mode 100644 spa/plugins/control/meson.build create mode 100644 spa/plugins/control/mixer.c create mode 100644 spa/plugins/control/plugin.c create mode 100644 spa/plugins/ffmpeg/ffmpeg-dec.c create mode 100644 spa/plugins/ffmpeg/ffmpeg-enc.c create mode 100644 spa/plugins/ffmpeg/ffmpeg.c create mode 100644 spa/plugins/ffmpeg/ffmpeg.h create mode 100644 spa/plugins/ffmpeg/meson.build create mode 100644 spa/plugins/filter-graph/audio-dsp-avx.c create mode 100644 spa/plugins/filter-graph/audio-dsp-c.c create mode 100644 spa/plugins/filter-graph/audio-dsp-impl.h create mode 100644 spa/plugins/filter-graph/audio-dsp-sse.c create mode 100644 spa/plugins/filter-graph/audio-dsp.c create mode 100644 spa/plugins/filter-graph/audio-dsp.h create mode 100644 spa/plugins/filter-graph/audio-plugin.h create mode 100644 spa/plugins/filter-graph/biquad.h create mode 100644 spa/plugins/filter-graph/builtin_plugin.c create mode 100644 spa/plugins/filter-graph/convolver.c create mode 100644 spa/plugins/filter-graph/convolver.h create mode 100644 spa/plugins/filter-graph/ebur128_plugin.c create mode 100644 spa/plugins/filter-graph/filter-graph.c create mode 100644 spa/plugins/filter-graph/ladspa.h create mode 100644 spa/plugins/filter-graph/ladspa_plugin.c create mode 100644 spa/plugins/filter-graph/lv2_plugin.c create mode 100644 spa/plugins/filter-graph/meson.build create mode 100644 spa/plugins/filter-graph/pffft.c create mode 100644 spa/plugins/filter-graph/pffft.h create mode 100644 spa/plugins/filter-graph/sofa_plugin.c create mode 100644 spa/plugins/jack/jack-client.c create mode 100644 spa/plugins/jack/jack-client.h create mode 100644 spa/plugins/jack/jack-device.c create mode 100644 spa/plugins/jack/jack-sink.c create mode 100644 spa/plugins/jack/jack-source.c create mode 100644 spa/plugins/jack/meson.build create mode 100644 spa/plugins/jack/plugin.c create mode 100644 spa/plugins/libcamera/libcamera-device.cpp create mode 100644 spa/plugins/libcamera/libcamera-manager.cpp create mode 100644 spa/plugins/libcamera/libcamera-manager.hpp create mode 100644 spa/plugins/libcamera/libcamera-source.cpp create mode 100644 spa/plugins/libcamera/libcamera-utils.cpp create mode 100644 spa/plugins/libcamera/libcamera.c create mode 100644 spa/plugins/libcamera/libcamera.h create mode 100644 spa/plugins/libcamera/meson.build create mode 100644 spa/plugins/meson.build create mode 100644 spa/plugins/support/cpu-arm.c create mode 100644 spa/plugins/support/cpu-riscv.c create mode 100644 spa/plugins/support/cpu-x86.c create mode 100644 spa/plugins/support/cpu.c create mode 100644 spa/plugins/support/dbus.c create mode 100644 spa/plugins/support/evl-plugin.c create mode 100644 spa/plugins/support/evl-system.c create mode 100644 spa/plugins/support/journal.c create mode 100644 spa/plugins/support/logger.c create mode 100644 spa/plugins/support/loop.c create mode 100644 spa/plugins/support/meson.build create mode 100644 spa/plugins/support/node-driver.c create mode 100644 spa/plugins/support/null-audio-sink.c create mode 100644 spa/plugins/support/plugin.c create mode 100644 spa/plugins/support/system.c create mode 100644 spa/plugins/test/fakesink.c create mode 100644 spa/plugins/test/fakesrc.c create mode 100644 spa/plugins/test/meson.build create mode 100644 spa/plugins/test/plugin.c create mode 100644 spa/plugins/test/test-helper.h create mode 100644 spa/plugins/v4l2/meson.build create mode 100644 spa/plugins/v4l2/v4l2-device.c create mode 100644 spa/plugins/v4l2/v4l2-source.c create mode 100644 spa/plugins/v4l2/v4l2-udev.c create mode 100644 spa/plugins/v4l2/v4l2-utils.c create mode 100644 spa/plugins/v4l2/v4l2.c create mode 100644 spa/plugins/v4l2/v4l2.h create mode 100644 spa/plugins/videoconvert/meson.build create mode 100644 spa/plugins/videoconvert/plugin.c create mode 100644 spa/plugins/videoconvert/videoadapter.c create mode 100644 spa/plugins/videoconvert/videoconvert-dummy.c create mode 100644 spa/plugins/videoconvert/videoconvert-ffmpeg.c create mode 100644 spa/plugins/videotestsrc/draw.c create mode 100644 spa/plugins/videotestsrc/meson.build create mode 100644 spa/plugins/videotestsrc/plugin.c create mode 100644 spa/plugins/videotestsrc/videotestsrc.c create mode 100644 spa/plugins/volume/meson.build create mode 100644 spa/plugins/volume/plugin.c create mode 100644 spa/plugins/volume/volume.c create mode 100644 spa/plugins/vulkan/dmabuf.h create mode 100644 spa/plugins/vulkan/dmabuf_fallback.c create mode 100644 spa/plugins/vulkan/dmabuf_linux.c create mode 100644 spa/plugins/vulkan/meson.build create mode 100644 spa/plugins/vulkan/pixel-formats.c create mode 100644 spa/plugins/vulkan/pixel-formats.h create mode 100644 spa/plugins/vulkan/plugin.c create mode 100644 spa/plugins/vulkan/shaders/disk-intersection.comp create mode 100644 spa/plugins/vulkan/shaders/filter-color.comp create mode 100644 spa/plugins/vulkan/shaders/filter.comp create mode 100644 spa/plugins/vulkan/shaders/filter.spv create mode 100644 spa/plugins/vulkan/shaders/main.comp create mode 100644 spa/plugins/vulkan/shaders/main.spv create mode 100644 spa/plugins/vulkan/utils.c create mode 100644 spa/plugins/vulkan/utils.h create mode 100644 spa/plugins/vulkan/vulkan-blit-dsp-filter.c create mode 100644 spa/plugins/vulkan/vulkan-blit-filter.c create mode 100644 spa/plugins/vulkan/vulkan-blit-utils.c create mode 100644 spa/plugins/vulkan/vulkan-blit-utils.h create mode 100644 spa/plugins/vulkan/vulkan-compute-filter.c create mode 100644 spa/plugins/vulkan/vulkan-compute-source.c create mode 100644 spa/plugins/vulkan/vulkan-compute-utils.c create mode 100644 spa/plugins/vulkan/vulkan-compute-utils.h create mode 100644 spa/plugins/vulkan/vulkan-types.h create mode 100644 spa/plugins/vulkan/vulkan-utils.c create mode 100644 spa/plugins/vulkan/vulkan-utils.h create mode 100644 spa/tests/benchmark-dict.c create mode 100644 spa/tests/benchmark-pod.c create mode 100644 spa/tests/meson.build create mode 100644 spa/tests/spa-include-test-template.c create mode 100644 spa/tests/stress-ringbuffer.c create mode 100644 spa/tools/meson.build create mode 100644 spa/tools/spa-inspect.c create mode 100644 spa/tools/spa-json-dump.c create mode 100644 spa/tools/spa-monitor.c create mode 100644 src/daemon/client.conf.avail/20-upmix.conf.in create mode 100644 src/daemon/client.conf.avail/meson.build create mode 100644 src/daemon/client.conf.in create mode 100644 src/daemon/filter-chain.conf.in create mode 100644 src/daemon/filter-chain/35-ebur128.conf create mode 100644 src/daemon/filter-chain/36-dcblock.conf create mode 100644 src/daemon/filter-chain/demonic.conf create mode 100644 src/daemon/filter-chain/meson.build create mode 100644 src/daemon/filter-chain/sink-dolby-surround.conf create mode 100644 src/daemon/filter-chain/sink-eq6.conf create mode 100644 src/daemon/filter-chain/sink-make-LFE.conf create mode 100644 src/daemon/filter-chain/sink-matrix-spatialiser.conf create mode 100644 src/daemon/filter-chain/sink-mix-FL-FR.conf create mode 100644 src/daemon/filter-chain/sink-upmix-5.1-filter.conf create mode 100644 src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf create mode 100644 src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf create mode 100644 src/daemon/filter-chain/source-duplicate-FL.conf create mode 100644 src/daemon/filter-chain/source-rnnoise.conf create mode 100644 src/daemon/filter-chain/spatializer-7.1.conf create mode 100644 src/daemon/filter-chain/spatializer-single.conf create mode 100644 src/daemon/jack.conf.in create mode 100644 src/daemon/meson.build create mode 100644 src/daemon/minimal.conf.in create mode 100644 src/daemon/pipewire-aes67.conf.in create mode 100644 src/daemon/pipewire-avb.conf.in create mode 100644 src/daemon/pipewire-pulse.conf.avail/20-upmix.conf.in create mode 100644 src/daemon/pipewire-pulse.conf.avail/meson.build create mode 100644 src/daemon/pipewire-pulse.conf.in create mode 100644 src/daemon/pipewire-vulkan.conf.in create mode 100644 src/daemon/pipewire.c create mode 100644 src/daemon/pipewire.conf.avail/10-rates.conf.in create mode 100644 src/daemon/pipewire.conf.avail/20-upmix.conf.in create mode 100644 src/daemon/pipewire.conf.avail/meson.build create mode 100644 src/daemon/pipewire.conf.in create mode 100644 src/daemon/pipewire.desktop.in create mode 100644 src/daemon/systemd/meson.build create mode 100644 src/daemon/systemd/system/meson.build create mode 100644 src/daemon/systemd/system/pipewire-manager.socket create mode 100644 src/daemon/systemd/system/pipewire-pulse.service.in create mode 100644 src/daemon/systemd/system/pipewire-pulse.socket create mode 100644 src/daemon/systemd/system/pipewire.service.in create mode 100644 src/daemon/systemd/system/pipewire.socket create mode 100644 src/daemon/systemd/user/filter-chain.service.in create mode 100644 src/daemon/systemd/user/meson.build create mode 100644 src/daemon/systemd/user/pipewire-pulse.service.in create mode 100644 src/daemon/systemd/user/pipewire-pulse.socket create mode 100644 src/daemon/systemd/user/pipewire.service.in create mode 100644 src/daemon/systemd/user/pipewire.socket create mode 100644 src/examples/audio-capture.c create mode 100644 src/examples/audio-dsp-filter.c create mode 100644 src/examples/audio-dsp-src.c create mode 100644 src/examples/audio-src-ring.c create mode 100644 src/examples/audio-src-ring2.c create mode 100644 src/examples/audio-src.c create mode 100644 src/examples/bluez-session.c create mode 100644 src/examples/export-sink.c create mode 100644 src/examples/export-source.c create mode 100644 src/examples/export-spa-device.c create mode 100644 src/examples/export-spa.c create mode 100644 src/examples/gmain.c create mode 100644 src/examples/internal.c create mode 100644 src/examples/local-v4l2.c create mode 100644 src/examples/meson.build create mode 100644 src/examples/midi-src.c create mode 100644 src/examples/sdl.h create mode 100644 src/examples/video-dsp-play.c create mode 100644 src/examples/video-dsp-src.c create mode 100644 src/examples/video-play-fixate.c create mode 100644 src/examples/video-play-pull.c create mode 100644 src/examples/video-play-reneg.c create mode 100644 src/examples/video-play.c create mode 100644 src/examples/video-src-alloc.c create mode 100644 src/examples/video-src-fixate.c create mode 100644 src/examples/video-src-reneg.c create mode 100644 src/examples/video-src.c create mode 100644 src/gst/.editorconfig create mode 100644 src/gst/gstpipewire.c create mode 100644 src/gst/gstpipewireclock.c create mode 100644 src/gst/gstpipewireclock.h create mode 100644 src/gst/gstpipewirecore.c create mode 100644 src/gst/gstpipewirecore.h create mode 100644 src/gst/gstpipewiredeviceprovider.c create mode 100644 src/gst/gstpipewiredeviceprovider.h create mode 100644 src/gst/gstpipewireformat.c create mode 100644 src/gst/gstpipewireformat.h create mode 100644 src/gst/gstpipewirepool.c create mode 100644 src/gst/gstpipewirepool.h create mode 100644 src/gst/gstpipewiresink.c create mode 100644 src/gst/gstpipewiresink.h create mode 100644 src/gst/gstpipewiresrc.c create mode 100644 src/gst/gstpipewiresrc.h create mode 100644 src/gst/gstpipewirestream.c create mode 100644 src/gst/gstpipewirestream.h create mode 100644 src/gst/meson.build create mode 100644 src/meson.build create mode 100644 src/modules/flatpak-utils.h create mode 100644 src/modules/meson.build create mode 100644 src/modules/module-access.c create mode 100644 src/modules/module-adapter.c create mode 100644 src/modules/module-adapter/adapter.c create mode 100644 src/modules/module-adapter/adapter.h create mode 100644 src/modules/module-avb.c create mode 100644 src/modules/module-avb/aaf.h create mode 100644 src/modules/module-avb/acmp.c create mode 100644 src/modules/module-avb/acmp.h create mode 100644 src/modules/module-avb/adp.c create mode 100644 src/modules/module-avb/adp.h create mode 100644 src/modules/module-avb/aecp-aem-descriptors.h create mode 100644 src/modules/module-avb/aecp-aem.c create mode 100644 src/modules/module-avb/aecp-aem.h create mode 100644 src/modules/module-avb/aecp.c create mode 100644 src/modules/module-avb/aecp.h create mode 100644 src/modules/module-avb/avb.c create mode 100644 src/modules/module-avb/avb.h create mode 100644 src/modules/module-avb/avdecc.c create mode 100644 src/modules/module-avb/descriptors.h create mode 100644 src/modules/module-avb/iec61883.h create mode 100644 src/modules/module-avb/internal.h create mode 100644 src/modules/module-avb/maap.c create mode 100644 src/modules/module-avb/maap.h create mode 100644 src/modules/module-avb/mmrp.c create mode 100644 src/modules/module-avb/mmrp.h create mode 100644 src/modules/module-avb/mrp.c create mode 100644 src/modules/module-avb/mrp.h create mode 100644 src/modules/module-avb/msrp.c create mode 100644 src/modules/module-avb/msrp.h create mode 100644 src/modules/module-avb/mvrp.c create mode 100644 src/modules/module-avb/mvrp.h create mode 100644 src/modules/module-avb/packets.h create mode 100644 src/modules/module-avb/srp.c create mode 100644 src/modules/module-avb/srp.h create mode 100644 src/modules/module-avb/stream.c create mode 100644 src/modules/module-avb/stream.h create mode 100644 src/modules/module-avb/utils.h create mode 100644 src/modules/module-client-device.c create mode 100644 src/modules/module-client-device/client-device.h create mode 100644 src/modules/module-client-device/protocol-native.c create mode 100644 src/modules/module-client-device/proxy-device.c create mode 100644 src/modules/module-client-device/resource-device.c create mode 100644 src/modules/module-client-node.c create mode 100644 src/modules/module-client-node/client-node.c create mode 100644 src/modules/module-client-node/client-node.h create mode 100644 src/modules/module-client-node/protocol-native.c create mode 100644 src/modules/module-client-node/remote-node.c create mode 100644 src/modules/module-client-node/v0/client-node.c create mode 100644 src/modules/module-client-node/v0/client-node.h create mode 100644 src/modules/module-client-node/v0/ext-client-node.h create mode 100644 src/modules/module-client-node/v0/protocol-native.c create mode 100644 src/modules/module-client-node/v0/transport.c create mode 100644 src/modules/module-client-node/v0/transport.h create mode 100644 src/modules/module-combine-stream.c create mode 100644 src/modules/module-echo-cancel.c create mode 100644 src/modules/module-example-filter.c create mode 100644 src/modules/module-example-sink.c create mode 100644 src/modules/module-example-source.c create mode 100644 src/modules/module-fallback-sink.c create mode 100644 src/modules/module-ffado-driver.c create mode 100644 src/modules/module-filter-chain.c create mode 100644 src/modules/module-jack-tunnel.c create mode 100644 src/modules/module-jack-tunnel/weakjack.h create mode 100644 src/modules/module-jackdbus-detect.c create mode 100644 src/modules/module-link-factory.c create mode 100644 src/modules/module-loopback.c create mode 100644 src/modules/module-metadata.c create mode 100644 src/modules/module-metadata/metadata.c create mode 100644 src/modules/module-metadata/protocol-native.c create mode 100644 src/modules/module-metadata/proxy-metadata.c create mode 100644 src/modules/module-netjack2-driver.c create mode 100644 src/modules/module-netjack2-manager.c create mode 100644 src/modules/module-netjack2/packets.h create mode 100644 src/modules/module-netjack2/peer.c create mode 100644 src/modules/module-parametric-equalizer.c create mode 100644 src/modules/module-pipe-tunnel.c create mode 100644 src/modules/module-portal.c create mode 100644 src/modules/module-profiler.c create mode 100644 src/modules/module-profiler/protocol-native.c create mode 100644 src/modules/module-protocol-native.c create mode 100644 src/modules/module-protocol-native/connection.c create mode 100644 src/modules/module-protocol-native/connection.h create mode 100644 src/modules/module-protocol-native/defs.h create mode 100644 src/modules/module-protocol-native/local-socket.c create mode 100644 src/modules/module-protocol-native/portal-screencast.c create mode 100644 src/modules/module-protocol-native/protocol-footer.c create mode 100644 src/modules/module-protocol-native/protocol-footer.h create mode 100644 src/modules/module-protocol-native/protocol-native.c create mode 100644 src/modules/module-protocol-native/security-context.c create mode 100644 src/modules/module-protocol-native/test-connection.c create mode 100644 src/modules/module-protocol-native/v0/interfaces.h create mode 100644 src/modules/module-protocol-native/v0/protocol-native.c create mode 100644 src/modules/module-protocol-native/v0/typemap.h create mode 100644 src/modules/module-protocol-pulse.c create mode 100644 src/modules/module-protocol-pulse/client.c create mode 100644 src/modules/module-protocol-pulse/client.h create mode 100644 src/modules/module-protocol-pulse/cmd.c create mode 100644 src/modules/module-protocol-pulse/cmd.h create mode 100644 src/modules/module-protocol-pulse/collect.c create mode 100644 src/modules/module-protocol-pulse/collect.h create mode 100644 src/modules/module-protocol-pulse/commands.h create mode 100644 src/modules/module-protocol-pulse/dbus-name.c create mode 100644 src/modules/module-protocol-pulse/dbus-name.h create mode 100644 src/modules/module-protocol-pulse/defs.h create mode 100644 src/modules/module-protocol-pulse/extension.c create mode 100644 src/modules/module-protocol-pulse/extension.h create mode 100644 src/modules/module-protocol-pulse/format.c create mode 100644 src/modules/module-protocol-pulse/format.h create mode 100644 src/modules/module-protocol-pulse/internal.h create mode 100644 src/modules/module-protocol-pulse/log.h create mode 100644 src/modules/module-protocol-pulse/manager.c create mode 100644 src/modules/module-protocol-pulse/manager.h create mode 100644 src/modules/module-protocol-pulse/message-handler.c create mode 100644 src/modules/module-protocol-pulse/message-handler.h create mode 100644 src/modules/module-protocol-pulse/message.c create mode 100644 src/modules/module-protocol-pulse/message.h create mode 100644 src/modules/module-protocol-pulse/module.c create mode 100644 src/modules/module-protocol-pulse/module.h create mode 100644 src/modules/module-protocol-pulse/modules/module-alsa-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-alsa-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-always-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-combine-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-device-manager.c create mode 100644 src/modules/module-protocol-pulse/modules/module-device-restore.c create mode 100644 src/modules/module-protocol-pulse/modules/module-echo-cancel.c create mode 100644 src/modules/module-protocol-pulse/modules/module-gsettings.c create mode 100644 src/modules/module-protocol-pulse/modules/module-jackdbus-detect.c create mode 100644 src/modules/module-protocol-pulse/modules/module-ladspa-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-ladspa-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-loopback.c create mode 100644 src/modules/module-protocol-pulse/modules/module-native-protocol-tcp.c create mode 100644 src/modules/module-protocol-pulse/modules/module-null-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-pipe-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-pipe-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-raop-discover.c create mode 100644 src/modules/module-protocol-pulse/modules/module-remap-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-remap-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-roc-sink-input.c create mode 100644 src/modules/module-protocol-pulse/modules/module-roc-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-roc-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-rtp-recv.c create mode 100644 src/modules/module-protocol-pulse/modules/module-rtp-send.c create mode 100644 src/modules/module-protocol-pulse/modules/module-simple-protocol-tcp.c create mode 100644 src/modules/module-protocol-pulse/modules/module-stream-restore.c create mode 100644 src/modules/module-protocol-pulse/modules/module-switch-on-connect.c create mode 100644 src/modules/module-protocol-pulse/modules/module-tunnel-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-tunnel-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-virtual-sink.c create mode 100644 src/modules/module-protocol-pulse/modules/module-virtual-source.c create mode 100644 src/modules/module-protocol-pulse/modules/module-x11-bell.c create mode 100644 src/modules/module-protocol-pulse/modules/module-zeroconf-discover.c create mode 100644 src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c create mode 100644 src/modules/module-protocol-pulse/modules/org.freedesktop.pulseaudio.gschema.xml create mode 100644 src/modules/module-protocol-pulse/operation.c create mode 100644 src/modules/module-protocol-pulse/operation.h create mode 100644 src/modules/module-protocol-pulse/pending-sample.c create mode 100644 src/modules/module-protocol-pulse/pending-sample.h create mode 100644 src/modules/module-protocol-pulse/pulse-server.c create mode 100644 src/modules/module-protocol-pulse/pulse-server.h create mode 100644 src/modules/module-protocol-pulse/quirks.c create mode 100644 src/modules/module-protocol-pulse/quirks.h create mode 100644 src/modules/module-protocol-pulse/remap.c create mode 100644 src/modules/module-protocol-pulse/remap.h create mode 100644 src/modules/module-protocol-pulse/reply.c create mode 100644 src/modules/module-protocol-pulse/reply.h create mode 100644 src/modules/module-protocol-pulse/sample-play.c create mode 100644 src/modules/module-protocol-pulse/sample-play.h create mode 100644 src/modules/module-protocol-pulse/sample.c create mode 100644 src/modules/module-protocol-pulse/sample.h create mode 100644 src/modules/module-protocol-pulse/server.c create mode 100644 src/modules/module-protocol-pulse/server.h create mode 100644 src/modules/module-protocol-pulse/snap-policy.c create mode 100644 src/modules/module-protocol-pulse/snap-policy.h create mode 100644 src/modules/module-protocol-pulse/stream.c create mode 100644 src/modules/module-protocol-pulse/stream.h create mode 100644 src/modules/module-protocol-pulse/utils.c create mode 100644 src/modules/module-protocol-pulse/utils.h create mode 100644 src/modules/module-protocol-pulse/volume.c create mode 100644 src/modules/module-protocol-pulse/volume.h create mode 100644 src/modules/module-protocol-simple.c create mode 100644 src/modules/module-pulse-tunnel.c create mode 100644 src/modules/module-raop-discover.c create mode 100644 src/modules/module-raop-sink.c create mode 100644 src/modules/module-raop/rtsp-client.c create mode 100644 src/modules/module-raop/rtsp-client.h create mode 100644 src/modules/module-roc-sink.c create mode 100644 src/modules/module-roc-source.c create mode 100644 src/modules/module-roc/common.h create mode 100644 src/modules/module-rt.c create mode 100644 src/modules/module-rt/20-pw-defaults.conf.in create mode 100644 src/modules/module-rt/25-pw-rlimits.conf.in create mode 100644 src/modules/module-rt/meson.build create mode 100644 src/modules/module-rtp-sap.c create mode 100644 src/modules/module-rtp-session.c create mode 100644 src/modules/module-rtp-sink.c create mode 100644 src/modules/module-rtp-source.c create mode 100644 src/modules/module-rtp/apple-midi.h create mode 100644 src/modules/module-rtp/audio.c create mode 100644 src/modules/module-rtp/midi.c create mode 100644 src/modules/module-rtp/opus.c create mode 100644 src/modules/module-rtp/ptp.h create mode 100644 src/modules/module-rtp/rtp.h create mode 100644 src/modules/module-rtp/sap.h create mode 100644 src/modules/module-rtp/stream.c create mode 100644 src/modules/module-rtp/stream.h create mode 100644 src/modules/module-session-manager.c create mode 100644 src/modules/module-session-manager/client-endpoint/client-endpoint.c create mode 100644 src/modules/module-session-manager/client-endpoint/client-endpoint.h create mode 100644 src/modules/module-session-manager/client-endpoint/endpoint-stream.c create mode 100644 src/modules/module-session-manager/client-endpoint/endpoint-stream.h create mode 100644 src/modules/module-session-manager/client-endpoint/endpoint.c create mode 100644 src/modules/module-session-manager/client-endpoint/endpoint.h create mode 100644 src/modules/module-session-manager/client-session/client-session.c create mode 100644 src/modules/module-session-manager/client-session/client-session.h create mode 100644 src/modules/module-session-manager/client-session/endpoint-link.c create mode 100644 src/modules/module-session-manager/client-session/endpoint-link.h create mode 100644 src/modules/module-session-manager/client-session/session.c create mode 100644 src/modules/module-session-manager/client-session/session.h create mode 100644 src/modules/module-session-manager/endpoint-link.c create mode 100644 src/modules/module-session-manager/endpoint-stream.c create mode 100644 src/modules/module-session-manager/endpoint.c create mode 100644 src/modules/module-session-manager/protocol-native.c create mode 100644 src/modules/module-session-manager/proxy-session-manager.c create mode 100644 src/modules/module-session-manager/session.c create mode 100644 src/modules/module-snapcast-discover.c create mode 100644 src/modules/module-spa-device-factory.c create mode 100644 src/modules/module-spa-device.c create mode 100644 src/modules/module-spa-node-factory.c create mode 100644 src/modules/module-spa-node.c create mode 100644 src/modules/module-vban-recv.c create mode 100644 src/modules/module-vban-send.c create mode 100644 src/modules/module-vban/audio.c create mode 100644 src/modules/module-vban/midi.c create mode 100644 src/modules/module-vban/stream.c create mode 100644 src/modules/module-vban/stream.h create mode 100644 src/modules/module-vban/vban.h create mode 100644 src/modules/module-x11-bell.c create mode 100644 src/modules/module-zeroconf-discover.c create mode 100644 src/modules/module-zeroconf-discover/avahi-poll.c create mode 100644 src/modules/module-zeroconf-discover/avahi-poll.h create mode 100644 src/modules/network-utils.h create mode 100644 src/modules/spa/spa-device.c create mode 100644 src/modules/spa/spa-device.h create mode 100644 src/modules/spa/spa-node.c create mode 100644 src/modules/spa/spa-node.h create mode 100644 src/pipewire/array.h create mode 100644 src/pipewire/buffers.c create mode 100644 src/pipewire/buffers.h create mode 100644 src/pipewire/client.h create mode 100644 src/pipewire/conf.c create mode 100644 src/pipewire/conf.h create mode 100644 src/pipewire/context.c create mode 100644 src/pipewire/context.h create mode 100644 src/pipewire/control.c create mode 100644 src/pipewire/control.h create mode 100644 src/pipewire/core.c create mode 100644 src/pipewire/core.h create mode 100644 src/pipewire/data-loop.c create mode 100644 src/pipewire/data-loop.h create mode 100644 src/pipewire/device.h create mode 100644 src/pipewire/extensions/client-node.h create mode 100644 src/pipewire/extensions/meson.build create mode 100644 src/pipewire/extensions/metadata.h create mode 100644 src/pipewire/extensions/profiler.h create mode 100644 src/pipewire/extensions/protocol-native.h create mode 100644 src/pipewire/extensions/security-context.h create mode 100644 src/pipewire/extensions/session-manager.h create mode 100644 src/pipewire/extensions/session-manager/impl-interfaces.h create mode 100644 src/pipewire/extensions/session-manager/interfaces.h create mode 100644 src/pipewire/extensions/session-manager/introspect-funcs.h create mode 100644 src/pipewire/extensions/session-manager/introspect.h create mode 100644 src/pipewire/extensions/session-manager/keys.h create mode 100644 src/pipewire/factory.h create mode 100644 src/pipewire/filter.c create mode 100644 src/pipewire/filter.h create mode 100644 src/pipewire/global.c create mode 100644 src/pipewire/global.h create mode 100644 src/pipewire/i18n.h create mode 100644 src/pipewire/impl-client.c create mode 100644 src/pipewire/impl-client.h create mode 100644 src/pipewire/impl-core.c create mode 100644 src/pipewire/impl-core.h create mode 100644 src/pipewire/impl-device.c create mode 100644 src/pipewire/impl-device.h create mode 100644 src/pipewire/impl-factory.c create mode 100644 src/pipewire/impl-factory.h create mode 100644 src/pipewire/impl-link.c create mode 100644 src/pipewire/impl-link.h create mode 100644 src/pipewire/impl-metadata.c create mode 100644 src/pipewire/impl-metadata.h create mode 100644 src/pipewire/impl-module.c create mode 100644 src/pipewire/impl-module.h create mode 100644 src/pipewire/impl-node.c create mode 100644 src/pipewire/impl-node.h create mode 100644 src/pipewire/impl-port.c create mode 100644 src/pipewire/impl-port.h create mode 100644 src/pipewire/impl.h create mode 100644 src/pipewire/introspect.c create mode 100644 src/pipewire/keys.h create mode 100644 src/pipewire/link.h create mode 100644 src/pipewire/log.c create mode 100644 src/pipewire/log.h create mode 100644 src/pipewire/loop.c create mode 100644 src/pipewire/loop.h create mode 100644 src/pipewire/main-loop.c create mode 100644 src/pipewire/main-loop.h create mode 100644 src/pipewire/map.h create mode 100644 src/pipewire/mem.c create mode 100644 src/pipewire/mem.h create mode 100644 src/pipewire/meson.build create mode 100644 src/pipewire/module.h create mode 100644 src/pipewire/node.h create mode 100644 src/pipewire/permission.h create mode 100644 src/pipewire/pipewire.c create mode 100644 src/pipewire/pipewire.h create mode 100644 src/pipewire/port.h create mode 100644 src/pipewire/private.h create mode 100644 src/pipewire/properties.c create mode 100644 src/pipewire/properties.h create mode 100644 src/pipewire/protocol.c create mode 100644 src/pipewire/protocol.h create mode 100644 src/pipewire/proxy.c create mode 100644 src/pipewire/proxy.h create mode 100644 src/pipewire/resource.c create mode 100644 src/pipewire/resource.h create mode 100644 src/pipewire/settings.c create mode 100644 src/pipewire/stream.c create mode 100644 src/pipewire/stream.h create mode 100644 src/pipewire/thread-loop.c create mode 100644 src/pipewire/thread-loop.h create mode 100644 src/pipewire/thread.c create mode 100644 src/pipewire/thread.h create mode 100644 src/pipewire/type.h create mode 100644 src/pipewire/utils.c create mode 100644 src/pipewire/utils.h create mode 100644 src/pipewire/version.h.in create mode 100644 src/pipewire/work-queue.c create mode 100644 src/pipewire/work-queue.h create mode 100644 src/tests/meson.build create mode 100644 src/tests/test-cpp.cpp create mode 100644 src/tests/test-endpoint.c create mode 100644 src/tests/test-filter.c create mode 100644 src/tests/test-interfaces.c create mode 100644 src/tests/test-security-context.c create mode 100644 src/tests/test-stream.c create mode 100644 src/tools/dfffile.c create mode 100644 src/tools/dfffile.h create mode 100644 src/tools/dsffile.c create mode 100644 src/tools/dsffile.h create mode 100644 src/tools/meson.build create mode 100644 src/tools/midifile.c create mode 100644 src/tools/midifile.h create mode 100644 src/tools/pw-cat.c create mode 100644 src/tools/pw-cli.c create mode 100644 src/tools/pw-config.c create mode 100644 src/tools/pw-container.c create mode 100644 src/tools/pw-dot.c create mode 100644 src/tools/pw-dump.c create mode 100644 src/tools/pw-link.c create mode 100644 src/tools/pw-loopback.c create mode 100644 src/tools/pw-metadata.c create mode 100644 src/tools/pw-mididump.c create mode 100644 src/tools/pw-mon.c create mode 100644 src/tools/pw-profiler.c create mode 100644 src/tools/pw-reserve.c create mode 100644 src/tools/pw-top.c create mode 100644 src/tools/reserve.c create mode 100644 src/tools/reserve.h create mode 100644 subprojects/libcamera.wrap create mode 100644 subprojects/media-session.wrap create mode 100644 subprojects/webrtc-audio-processing.wrap create mode 100644 subprojects/wireplumber.wrap create mode 100644 template.test.in create mode 100644 test/data/test-spa-json.txt create mode 100644 test/meson.build create mode 100644 test/pwtest-compat.c create mode 100644 test/pwtest-implementation.h create mode 100644 test/pwtest.c create mode 100644 test/pwtest.h create mode 100644 test/test-array.c create mode 100644 test/test-client.c create mode 100644 test/test-config.c create mode 100644 test/test-context.c create mode 100644 test/test-example.c create mode 100644 test/test-functional.c create mode 100644 test/test-lib.c create mode 100644 test/test-logger.c create mode 100644 test/test-loop.c create mode 100644 test/test-map.c create mode 100644 test/test-properties.c create mode 100644 test/test-pwtest.c create mode 100644 test/test-spa-buffer.c create mode 100644 test/test-spa-control.c create mode 100644 test/test-spa-json.c create mode 100644 test/test-spa-log.c create mode 100644 test/test-spa-node.c create mode 100644 test/test-spa-pod.c create mode 100644 test/test-spa-utils.c create mode 100644 test/test-support.c create mode 100644 test/test-utils.c 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/.gitattributes b/.gitattributes new file mode 100644 index 0000000..648f3f5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +test/data/*.txt diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ebb460 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +.* +!.gitlab +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 +subprojects/googletest* +subprojects/gtest.wrap +subprojects/libyaml.wrap +subprojects/libyaml +subprojects/libcamera +subprojects/webrtc-audio-processing + +# 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..3953445 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,590 @@ +# Create merge request pipelines for open merge requests, branch pipelines +# otherwise. This allows MRs for new users to run CI, and prevents duplicate +# pipelines for branches with open MRs. +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - if: $CI_COMMIT_BRANCH + +stages: + - container + - container_coverity + - 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: '2024-12-10.0' + FDO_DISTRIBUTION_VERSION: '40' + 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 + libasan + libcanberra-devel + libebur128-devel + libffado-devel + libldac-devel + libmysofa-devel + libsndfile-devel + libubsan + libusb1-devel + lilv-devel + libv4l-devel + libva-devel + libX11-devel + ModemManager-devel + meson + openssl-devel + pulseaudio-libs-devel + python3-docutils + python3-pip + sbc-devel + ShellCheck + SDL2-devel + systemd-devel + vulkan-loader-devel + webrtc-audio-processing-devel + which + valgrind + ninja-build + pkgconf + pulseaudio-utils + openal-soft + readline-devel + pandoc +# Uncommenting the following two lines and disabling the meson entry above +# will re-enable use of Meson via pip but please consider using a newer distro +# image first or making the build system compatible instead! This is because +# using pip or another 3rd party repo defeats the point testing the particular +# distro for regressions. +# NOTE: If you do end up using pip3 for meson, be sure to also update the +# build_meson_prerelease and build_meson_exact_release build instructions +# to uninstall the pip3 version again and probably to not call dnf remove +# FDO_DISTRIBUTION_EXEC: >- +# pip3 install meson + +.ubuntu: + variables: + # Update this tag when you want to trigger a rebuild + FDO_DISTRIBUTION_TAG: '2024-01-12.0' + FDO_DISTRIBUTION_VERSION: '22.04' + FDO_DISTRIBUTION_PACKAGES: >- + debhelper-compat + findutils + git + libapparmor-dev + libasound2-dev + libavcodec-dev + libavfilter-dev + libavformat-dev + libdbus-1-dev + libbluetooth-dev + libglib2.0-dev + libgstreamer1.0-dev + libgstreamer-plugins-base1.0-dev + libsbc-dev + libsdl2-dev + libsnapd-glib-dev + libudev-dev + libva-dev + libv4l-dev + libx11-dev + meson + ninja-build + pkg-config + python3-docutils + systemd +# Uncommenting the following three lines and disabling the meson entry above +# will re-enable use of Meson via pip but please consider using a newer distro +# image first or making the build system compatible instead! This is because +# using pip or another 3rd party repo defeats the point testing the particular +# distro for regressions. +# python3-pip +# FDO_DISTRIBUTION_EXEC: >- +# pip3 install meson + +.alpine: + variables: + # Update this tag when you want to trigger a rebuild + FDO_DISTRIBUTION_TAG: '2024-09-20.0' + FDO_DISTRIBUTION_VERSION: '3.20' + FDO_DISTRIBUTION_PACKAGES: >- + alsa-lib-dev + avahi-dev + bash + bluez-dev + gcc + g++ + dbus-dev + doxygen + elogind-dev + 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 + rules: + - if: $COVERITY != null + +.not_coverity: + rules: + - if: $COVERITY == null + +.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 + COMPILE_ARGS="-j$FDO_CI_CONCURRENT" + export COMPILE_ARGS + fi + script: + - echo "Building with meson options $MESON_OPTIONS" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - meson test -C "$BUILD_DIR" --no-rebuild + - meson install -C "$BUILD_DIR" --no-rebuild + 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=[] -Dsnap=enabled" + +.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 + -Dman=enabled + -Ddoc-prefix-value=/usr + -Ddoc-sysconfdir-value=/etc + -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=[] + -Dsnap=disabled + artifacts: + name: pipewire-$CI_COMMIT_SHA + when: always + paths: + - build-*/meson-logs + - prefix-* + +build_on_fedora_html_docs: + extends: + - .build_on_fedora + variables: + MESON_OPTIONS: >- + -Ddocs=enabled + -Dman=enabled + -Ddoc-prefix-value=/usr + -Ddoc-sysconfdir-value=/etc + -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=[] + before_script: + - git fetch origin 1.0 1.2 master + - git branch -f 1.0 origin/1.0 + - git clone -b 1.0 . branch-1.0 + - git branch -f 1.2 origin/1.2 + - git clone -b 1.2 . branch-1.2 + - git branch -f master origin/master + - git clone -b master . branch-master + - !reference [.build, before_script] + script: + - cd branch-1.0 + - meson setup builddir $MESON_OPTIONS + - meson compile -C builddir doc/pipewire-docs + - cd .. + - cd branch-1.2 + - meson setup builddir $MESON_OPTIONS + - meson compile -C builddir doc/pipewire-docs + - cd .. + - cd branch-master + - meson setup builddir $MESON_OPTIONS + - meson compile -C builddir doc/pipewire-docs + artifacts: + name: pipewire-$CI_COMMIT_SHA + when: always + paths: + - branch-*/builddir/meson-logs + - branch-*/builddir/doc/html + rules: + - !reference [pages, rules] + +build_on_alpine: + extends: + - .alpine + - .not_coverity + - .fdo.distribution-image@alpine + - .build + stage: build + variables: + MESON_OPTIONS: "-Dsession-managers=[] -Dsnap=disabled -Dlogind=enabled -Dlogind-provider=libelogind" + +# 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=[] + -Dc_args=['-UFASTPATH'] + -Dcpp_args=['-UFASTPATH'] + -Dsnap=disabled + 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=[] -Dsnap=disabled" + 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 setup "$BUILD_DIR" --prefix="$PREFIX" "-D$MESON_OPTION=$MESON_OPTION_VALUE" -Dsession-managers=[] + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - meson test -C "$BUILD_DIR" --no-rebuild + +build_with_asan_ubsan: + extends: + - .build_on_fedora + script: + - echo "Building with ASan and UBSan" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" -D debug=true -D optimization=g -D b_sanitize=address,undefined -D session-managers=[] + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - env UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1 ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1 meson test -C "$BUILD_DIR" --no-rebuild + +# 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=[] -Dsnap=disabled" + parallel: + matrix: + - CC: [gcc, clang] + +build_session_managers: + extends: + - .build_on_fedora + script: + - echo "Building with meson options $MESON_OPTIONS" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - meson install -C "$BUILD_DIR" --no-rebuild + variables: + MESON_OPTIONS: "-Dsession-managers=$SESSION_MANAGERS -Dsnap=disabled" + parallel: + matrix: + - SESSION_MANAGERS: ["[]", "wireplumber", "media-session", "media-session,wireplumber", "wireplumber,media-session" ] + allow_failure: true + +build_meson_prerelease: + extends: + - .build_on_fedora + script: + - dnf remove --assumeyes meson + - pip3 install --upgrade --pre meson + - echo "Building with meson options $MESON_OPTIONS" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - meson install -C "$BUILD_DIR" --no-rebuild + variables: + MESON_OPTIONS: "-Dsession-managers=wireplumber,media-session -Dsnap=disabled" + 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) + - dnf remove --assumeyes meson +# - pip3 uninstall --yes meson + - pip3 install "meson==$meson_version" + - echo "Building with meson options $MESON_OPTIONS" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS + - meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - meson install -C "$BUILD_DIR" --no-rebuild + variables: + MESON_OPTIONS: "-Dsession-managers=[] -Dsnap=disabled" + +valgrind: + extends: + - .build_on_fedora + script: + - echo "Building with meson options $MESON_OPTIONS" + - meson setup "$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 setup "$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 meson compile -C "$BUILD_DIR" $COMPILE_ARGS + - 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 + variables: + MESON_OPTIONS: >- + -Dpipewire-v4l2=enabled + -Dpipewire-jack=enabled + script: + - echo "Configuring with meson options $MESON_OPTIONS" + - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS + - shellcheck $(git ls-files '*.sh') + - shellcheck $(grep -rl "#\!/.*bin/.*sh" "$BUILD_DIR") + +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_html_docs + script: + - mkdir public public/1.0 public/1.2 public/devel + - cp -R branch-1.0/builddir/doc/html/* public/1.0/ + - cp -R branch-1.2/builddir/doc/html/* public/1.2/ + - cp -R branch-master/builddir/doc/html/* public/devel/ + - (cd public && ln -s 1.2/* .) + artifacts: + paths: + - public + rules: + - if: $CI_COMMIT_BRANCH == 'master' + - if: $CI_COMMIT_BRANCH == '1.0' + - if: $CI_COMMIT_BRANCH == '1.2' diff --git a/.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 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 @@ + + + + +- 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 @@ + + +- 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..7a0d7f2 --- /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` and +`WIREPLUMBER_DEBUG` environment variables like so: + +``` +cd builddir/ +PIPEWIRE_DEBUG="D" WIREPLUMBER_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..1046193 --- /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 --leak-check=full" + +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..ea4317e --- /dev/null +++ b/NEWS @@ -0,0 +1,7690 @@ +# PipeWire 1.4.2 (2025-04-14) + +This is a bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Do extra checks for MIDI to avoid 100% CPU usage on older kernels. + - Fix some potential crashes in POD builder. + - pw-cat streaming improvements on stdout/stdin. + - Small fixes and improvements. + + +## PipeWire + - Make the service files depend on DBus to avoid startup races. + +## SPA + - Do extra checks for MIDI to avoid 100% CPU usage on older kernels. + - Use Header metadata by default in videoadapter. + - Handle set_format result from v4l2 better. + - Handle crash when POD builder overflows in the filter. + - Work around a libebur128 bug. (#4646) + +## Tools + - pw-cat prefers AU format when streaming on stdout/stdin. (#4629) + - Improve pw-cat verbose sndfile format debug. + - Add the missing --channel-map long option to pw-loopback. + +## GStreamer + - Fix a leak in the deviceprovider. (#4616) + - Fix negotiation and make renegotiation better. + + +Older versions: + +# PipeWire 1.4.1 (2025-03-14) + +This is a quick bugfix release that is API and ABI compatible with +previous 1.x releases. + +## Highlights + - Handle SplitPCM wrong channels specifications. This fixes some + problems with disappearing devices. + - Add backwards compatibility support for when the kernel does not + support UMP. Also fix UMP output. This restores MIDI support for + older kernels/ALSA. + - Fix a crash in audioconvert because the resampler was not using the + right number of channels. + - Some compilation fixes and small improvements. + + +## PipeWire + - Don't emit events when disconnecting a stream. (#3314) + - Fix some compilation problems. (#4603) + +## Modules + - Bump the ROC requirement to version 0.4.0 + +## SPA + - Handle SplitPCM too few or too many channels. Add an error string + to the device names when the UCM config has an error. + - Add backwards compatibility support for when the kernel does not + support UMP. + - Configure the channels in the resampler correctly in all + cases. (#4595) + - Fix UMP output. + - Use the right samplerate of the filter-graph in audioconvert in + all cases. + +## Bluetooth + - Fix a crash with an incomming call. + +# PipeWire 1.4.0 (2025-03-06) + +This is the 1.4 release that is API and ABI compatible with previous +1.2.x and 1.0.x releases. + +This release contains some of the bigger changes that happened since +the 1.2 release last year, including: + + * client-rt.conf was removed, all clients now use client.conf and + are given RT priority in the data threads. + * UMP (aka MIDI2) support was added and is now the default format + to carry MIDI1 and MIDI2 around in PipeWire. There are helper + functions to convert between legacy MIDI and UMP. + * The resampler can now precompute (at compile time) some common + conversion filters. Delay reporting in the resampler was fixed and + improved. + * Bluetooth support for BAP broadcast links and support for hearing aids + using ASHA was added. A new G722 codec was also added. + Delay reporting and configuration in Bluetooth was improved. + * The ALSA plugin now supports DSD playback when explicitly allowed + with the alsa.formats property. + * A PipeWire JACK control API was added. + * A system service was added for pipewire-pulse. + * Many documentation and translation updates. + * Many of the SPA macros are converted to inline functions. All SPA + inline functions are now also compiled into a libspa.so library to + make it easier to access them from bindings. + * The module-filter-chain graph code was moved to a separate + filter-graph SPA plugin so that it becomes usable in more places. + EBUR128, param_eq and dcblock plugins were added to filter-graph. + The filter graph can now also use fftw for doing convolutions. + The audioconvert plugin was optimized and support was added to + audioconvert to insert extra filter-graphs in the processing pipeline. + * New helper functions were added to parse JSON format descriptions. + * The profiler now also includes the clock of the followers. + * RISCV CPU support and assembler optimisations were added. + * The clock used for logging timestamps can be configured now. + * The JSON parser was split into core functions and helper. + * Support for UCM split PCMs was added. Instead of alsa-lib splitting + up PCMs, PipeWire can mark the PCMs with the correct metadata so that + the session manager can use native PipeWire features to do this. + * Support for webrtc2 was added to echo-cancel. + * IEC958 codecs are now detected from the HDMI ELD data. + * Conversion between floating point and 32 bits now preserve 25 bits of + precision instead of 24 bits. + * A new Telephony D-BUS API compatible with ofono was added. + * The invoke queues are now more efficient and can be called from multiple + threads concurrently. + * Clock information in v4l2 was improved. + * An ffmpeg based videoconvert plugin was added that can be used with the + videoadapter. + * The GStreamer elements have improved buffer pool handling and rate + matching. + * The combine-stream module can now also mix streams. + * link-factory now checks that the port and node belong together. + * The netjack-manager module has support for autoconnecting streams. + * The native-protocol has support for abstract sockets. + * The pulse server has support for blocking playback and capture in + pulse.rules. + * The corked state of stream is now reported correctly in pulse-server. + * Fix backwards jumps in pulse-server. + * Latency configuration support was added in loopback and raop-sink. + * The ROC module has more configuration options. + * The SAP module now only send updated SDP when something changed. + * RTP source now has a standby mode where it idles when there is no + data received. + * Support for PTP clocking was added the RTP streams. + * The VBAN receiver can now dynamically create streams when they are + detected. + * Error reporting when making links was improved. + * Support for returning (canceling) a dequeued buffer in pw-stream. + * Support for emiting events in pw-stream was added. + * pw-cat now support stdin and stdout. + + +## Highlights (since the previous 1.3.83 release) + - Small fixes and improvements. + +## PipeWire + - Fix some missing includes in metadata.h + - Pass the current error in errno when a stream is in error (#4574) + + +## modules + - Evaluate node rules before loading adapter follower to ensure + properties are set correctly. (#4562) + +## SPA + - Avoid a use after free when building PODs. (#4445) + - Take headroom into account when calculating resync. + +## Bluetooth + - Fix +CLCC parsing. + +## GStreamer + - Notify about default device changes in deviceprovider. + - Copy frames between pools and avoid splitting video buffers. + +## JACK + - Add an option to disable the MIDI2 port flags. (#4584) + +# PipeWire 1.3.83 (2025-02-20) + +This is the third and hopefully last 1.4 release candidate that +is (almost) API and (entirely) ABI compatible with previous 1.2.x +and 1.0.x releases. + +We note that in the 1.3.x series, the API is slighty not backwards +compatible because some methods previously used to accept void* as +a parameter while they now require the correct type. We think this +is however a good kind of API breakage and expect projects to patch +their code to get things compiled with newer version (which will also +compile for older versions). Note also that this is not an ABI break. + + +## Highlights + - Handle JACK transport updates in a better way. + - Fix a SAP regression when starting. + - Fix regression in rate scaling. + - Improve bluetooth source rate handling. + - More small bugfixes and improvements. + + +## PipeWire + - Handle JACK transport updates in a better way. (#4543) + +## Modules + - Check that the link factory port and nodes match. Deprecate the + port.id when making links. + - Improve profiler output by scaling the quantum with the node + rate so that we don't end up with confusing information. (#4555) + - Fix sending of the SAP SDP. Handle some SDP parsing errors. + - Add some more options to the ROC source module. (#4516) + +## SPA + - Fix firewire quirks in udev rules. (#4528) + - Fix a bug in the rate scaling in some cases that would make things + run with the wrong samplerate. + - Improve introspection of control types. + +## Bluetooth + - Use the G722 codec from Android instead of FFmpeg for ASHA. + - Use the A2DP source rate as the graph rate. (#4555) + - Specify the bluetooth source latency property in the rate of the + stream to avoid conversions and rounding errors. + +# PipeWire 1.3.82 (2025-02-06) + +This is the second 1.4 release candidate that is API and ABI +compatible with previous 1.2.x and 1.0.x releases. + +## Highlights + - Various pw-stream improvements: timing information fixes, + avoid locking buffers in some cases and an improved drain + event. + - A new Telephony D-BUS API compatible with ofono. + - Documentation fixes and updates. + - More small fixes and improvements. + + +## PipeWire + - Improve timing information when rate is unknown. + - Avoid locked buffers in pw_stream in some cases. + - Improve pw_stream drain event emission. + - Improve manager socket handling. Applications can avoid hardcoding + the sockets so that they will respect the config settings. + +## modules + - Fix header size calculation when using ipv6. (#4524) + +## SPA + - Optimize byteswapped s16 conversions. + - Improve event handling for internal events. + - Optimize negiotiation when in convert mode, prefer the format + of the follower in adapter. + - Fix EnumPortConfig for videoadapter without converter. + - Fix libcamera property buffer size. + +## Pulse-server + - Add systemwide systemd files. + +## JACK + - Add a UMP example. + - Use the new JackPortMIDI2 flag to mark UMP ports to JACK. + +## Bluetooth + - Support BAP hardware volume. + - Add a Telephony DBUS API. + +## GStreamer + - Disable buffer pools for audio by default. + +## Docs + - Improve the module documentation. + +# PipeWire 1.3.81 (2025-01-23) + +This is the first 1.4 release candidate that is API and ABI +compatible with previous 1.2.x and 1.0.x releases. + +In addition to all the changes backported to 1.2.x, this release +also contains some new features: + +## Highlights + - UMP support was added with MIDI 1.0 and MIDI 2.0 support in the ALSA + sequencer plugin. By default PipeWire will now use MIDI 2.0 in UMP + messages to transport MIDI in the graph, with conversions to/from legacy + MIDI where required. This requires UMP support in the kernel. + - client-rt.conf is no longer supported. Custom changes made to this + config should be moved to client.conf. Clients that try to load the + client-rt.conf will emit a warning and be directed to client.conf + automatically for backwards compatibility. + - The module-filter-chain code was moved to a new filter-graph plugin. This + made it possible to add filter-graph support directly in audioconvert. It + is now possible to run up to 8 run-time swappable filter-graphs inside + streams and nodes. This should make it easier to add effects to streams + and device nodes. + - Bluetooth support for BAP broadcast links and support for hearing aids + using ASHA was added. + - Many more bugfixes and improvements. + +## PipeWire + - Nodes are now only scheduled when ready to signal the driver. + - Add slovenian translation. (#4156) + - Link errors are handled better. + - The videoadapter is now enabled by default but no videoconverter + is loaded yet by default. + - Streams now have support for ProcessLatency. + - Streams now have a method to emit events. + - The RequestProcess event and command can now pass around extra + properties. + - Local timestamps are now used for logging. + - client-rt.conf is no longer supported. Custom changes made to this + config should be moved to client.conf. Clients that try to load the + client-rt.conf will emit a warning and be directed to cliert.conf + automatically to preserve backwards compatibility. + - pw_stream now has an API to return unused buffers. + +## modules + - module-combine-stream can now mix streams. + - Links in error are now destroyed by link-factory. + - The netjack2 driver can now also create streams that autoconnect when + specified. (#4125) + - Many updates and bugfixes to the RTP modules. + - The netjack2 driver can now bind to a custom IP and port pair. (#4144) + - The loopback module and module-raop have support for ProcessLatency, which + can be used to query and update the latency. + - The profiler module can now reduce the sampling rate. + - The filter-chain was optimized some more. + - The filter-chain gained some more plugins: param_eq, ebur128, dcblock. + - Support for fftw based convolver was added. + - Some module arguments can now be overridden. + - The VBAN receiver now creates new streams per stream name. (#4400) + - The RTP SAP module is now smarter with generating new SAP messages. + - The RTP source can now be paused when no data is received. (#4456) + +## tools + - pw-cat can now stream most formats from stdin/stdout. + - pw-profiler has a JSON dump option to dump the raw profiler data. + - pw-cli now supports unload-module. (#4276) + +## SPA + - The resampler can precompute some common coeficients now at compile + time. + - UMP support was added with MIDI 1.0 and MIDI 2.0 support in the ALSA + sequencer plugin. By default PipeWire will now use MIDI 2.0 in UMP + messages to transport MIDI in the graph, with conversions to/from legacy + MIDI where required. + - Control types can now be negotiated. + - Support for writing ALSA bind controls was added. + - The ALSA sequencer now has better names for the ports. + - The F32 to S32 conversion now uses 25 bits for an extra bit of + precision. + - libcamera controls can now be set in all cases. + - The videoadapter has been improved and a dummy and ffmpeg based + videoconverter plugin was added. + - Negotiation was improved in audioadapter. First a passthrough format + is tried. + - Some JSON helper functions were added and some duplicate code removed + or simplified. + - Add support for RISC V CPU detection and add many optimizations in + the audio converters. + - Add an option to disable ALSA mixer path select. (#4311) + - Fix a potential bug with the cleanup of the loop queues. + - ALSA nodes now dynamically adjust the DLL bandwidth based on average + measured variance. + - The loop invoke queue was made more efficient and make it possible to + invoke from multiple threads. + - The filter-chain code was moved to a new filter-graph plugin. + - Most function macros are now static inlined and can also be built into a + libspa.so file. This should improve language bindings. + - V4l2 clock information was improved. + - Supported IEC958 codecs are now autodetected via ELD info. + - Audioconvert was optimized some more. + - Audioconvert can now include filter-graphs in its processing. + - webrtc-audio-processing-2 is now supported in AEC. + - The resampler now reports the delay and subsample delay. Also the + delay is reported in the samplerate of the input. + - The ALSA sequencer now handle kernels without UMP support. (#4507) + +## Pulse-server + - Add quirk to block clients from making record and playback streams. + - The corked state is now set on stream to always report this state + correctly to other clients. + - Readiness notification was added to the pulse server with the + PIPEWIRE_PULSE_NOTIFICATION_FD environment variable. (#4347) + - The pulse.cmd config now supports conditions. + - A bug in clearing the ringbuffer was fixed. (#4464) + +## GStreamer + - Support for the default devices was added to the deviceprovider. (#4268) + - The graph clock is now used as the source for the GStreamer clock. + - The sink now does some rate control. + +## ALSA + - The ALSA plugin now supports DSD when explicitly enabled. + +## JACK + - JACK now supports 2 new extension formats for OSC and UMP. + - JACK clients can receive UMP MIDI1 or MIDI2 messages when using + the new UMP port format extension. + - JACK now reports the PipeWire version in the minor/micro/proto. + - Implement more jackserver functions. + +## Bluetooth + - Support BAP broadcast links. + - Support for ASHA was added. + - Delay reporting in A2DP sources was improved. + +## Examples + - 2 new examples of pw-stream using spa_ringbuffer were added. + +## Docs + - Many updates to the man pages. + - More documentation about thread safety of functions in stream + and filters. (#4521) + +# PipeWire 1.2.7 (2024-11-26) + +This is a bugfix release that is API and ABI compatible with the previous +1.2.x and 1.0.x releases. + +## Highlights + - Backport support for lazy scheduling. + - Handle the case where processing would stop when an ALSA driver is + destroyed. + - Add support for v4l2loopback in the v4l2 plugin. + - Small bug fixes and improvements. + +## PipeWire + - Invalidate the proxy ID when removed. + - Backport support for lazy scheduling. + - Fix profiler stats for async nodes. + - Fix EARLY_PROCESS again in pw-stream. (#3480) + +## Modules + - Fix a crasher issue when nodes are created in the wrong order in + module-filter-chain. + - Fix unmap bug in lv2 uri tables. + - Add ratelimit to jack-tunnel xruns. + - Remove hardcoded limit in filter-chain sofa plugin. + - Handle the MTU size correctly in module-rtp and handle large MTUs. + (#4396) + - Fix JSON float parsing errors in equalizer module. (#4418) + +## SPA + - Fix crash in audiotestsrc when using spa-inspect (#4365). + - Improve JSON float infinity checks. + - Improve resampler performace a little. + - Make audioconvert only output when there is something to output. + - Fix regression in v4l2 port flags which would disable support for + EXPBUF. + - Handle the case where an ALSA driver is destroyed and the follower + becomes a driver. Processing would stop. (#4401) + - Add support for v4l2loopback in the v4l2 plugin. + +## Pulse-server + - Give a better error message when running out of fds. + - Ensure positive latency reporting. + +## GStreamer + - Fix memory leak in deviceprovider. + - Fix locking when emitting an error. + +## Tools + - Fix pw-dot link labels. + +# PipeWire 1.2.6 (2024-10-23) + +This is a bugfix release that is API and ABI compatible with the previous +1.2.x and 1.0.x releases. + +## Highlights + - The filter-chain param changes were not aggregated correctly, causing some + param changes to be ignored. (#4331) + - Clear the JACK io ports correctly when stopping to avoid crashes. (#4337) + - Some more small fixes and improvements. + + +## PipeWire + - Stream states are now updated based on the underlying node state. + - Exported nodes now have their state change done synchronously so that the + server can immediately start the driver and avoid some initial xruns. + - Improve stream flush handling and improve the docs. + - Don't send mix_info to destroyed ports to avoid some errors in the + JACK clients. + +## Modules + - The filter-chain param changes were not aggregated correctly, causing some + param changes to be ignored. (#4331) + - The filter-chain now correctly optimizes unlinked nodes in all cases. + +## SPA + - ALSA PCM node properties are now no longer overwritten with card properties. + (#4135) + - Increase the adapter retry count to avoid xruns in some cases. (#4334) + - Fix potential crash in cleanup of ALSA nodes. + +## Bluetooth + - Fix a crash with broadcast sinks. + - Improve compatibility with Phonak hearing aids. + - Don't exit when DBus goes down. + +## JACK + - Clear the io ports correctly when stopping to avoid crashes. (#4337) + +## Docs + - Backport docs from master. + +# PipeWire 1.0.9 (2024-10-22) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Fix an fd leak and confusion in the protocol that would cause leaks and + wrong memory to be used. + - Fix bug where the mixer would not be synced correctly after selecting + a port, leaving the audio muted. (#4084) + - Backport v4l2 systemd-logind support to avoid races when starting. + (#3539 and #3960). + - Other small fixed and improvements. + + +## PipeWire + - Fix a bug where renegotiation would sometimes fail to deactivate a link. + - Fix an fd leaks and confusion in the protocol. + +## modules + - Fix a use-after-free in the rt module when stopping a thread. + +## SPA + - Fix bug where the mixer would not be synced correctly after selecting + a port, leaving the audio muted. (#4084) + - Fix a compilation issue with empty initializers. (#4317) + - Backport v4l2 systemd-logind support to avoid races when starting. + (#3539 and #3960). + - Fix a potential crash when cleaning ALSA nodes. + +## JACK + - align buffers to the max cpu alignment in order to allow more + optimizations. + +# PipeWire 1.2.5 (2024-09-27) + +This is an important bugfix release that is API and ABI compatible with the +previous 1.2.x and 1.0.x releases. + +## Highlights + - Fix an fd mismatch in the protocol in some cases that could lead to + fd leaks and crashes. + - Fix a bug where the mixer was not updated after setting the port, which would + cause muted audio at boot or resume from suspend. + - Fix a potential use-after-free in module-rt when stopping a thread. + - Cached objects are now freed in the JACK API to avoid memory leaks. + - Some more fixes and improvements. + + +## PipeWire + - RequestProcess commands are now only sent after the node completes + the state change to RUNNING. + - More FreeBSD fixes. + - Handle ACTIVE links going to < PAUSED as well. This improves + renegotiation in some cases. + - Fix an fd mismatch in the protocol in some cases that could lead to + fd leaks and crashes. + +## Modules + - Many of the network modules can now also accept hostnames instead of + IP addresses. + - Fix a potential use-after-free in module-rt when stopping the thread. + +## SPA + - Support for elogind was added. + - Some more errors are checked when converting JSON to POD. (#4313) + - Fix a bug where the mixer was not updated after setting the port, which would + cause muted audio at boot or resume from suspend. (#4084) + +## JACK + - The BBT transport handling was improved. Some fields were added to be able + to handle the JACK semantics correctly. (#4314) + - Buffers are now aligned according to the maximum CPU alignment instead of + the hardcoded 16 bytes alignment. + - Cached objects are now freed correctly. + +## Doc + - Some small doc updates. (#4272) + +# PipeWire 1.2.4 (2024-09-19) + +This is a bugfix release that is API and ABI compatible with the +previous 1.2.x and 1.0.x releases. + +## Highlights + - Avoid a crash in cleanup of globals. (#4250) + - Use systemd-logind to scan for new devices in v4l2. + - Some more bugfixes and improvements. + + +## PipeWire + - Avoid a crash in cleanup of globals. (#4250) + - Improve RequestProcess dispatch. + +## Tools + - Improve float parsing. (#4234) + +## SPA + - Clear the ringbuffer when stopping in libcamera. + - Use systemd-logind to scan for new devices in v4l2. (#3539, #3960) + - Queue dropped first buffer in v4l2. + - Unlink pcm devices when moving drivers to avoid broken pipe. + +## JACK + - Emit buffer_size callback in jack_activate() to improve + compatibility with GStreamer. (#4260) + +# PipeWire 1.0.8 (2024-09-19) + +This is a small bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Backport support for explicit sync. + - FFADO backport fixes. + - Fix some races in JACK. + - More small fixes and improvements. + + +## PipeWire + - Add support for mandatory metadata and explicit sync metadata. + - Fix RequestProcess again. + - Include config.h to use malloc_trim() when cleaning nodes. + - Avoid crash when destroying a global. (#4250) + +## Modules + - FFADO fixes: improve timing reporting, avoid some xruns, improve + samplerate and period size handling, implement freewheeling. + - Decrease memory usage of the profiler. + +## Tools + - Fix pw-dump metadata changes fix. (#4053) + - Support large params in pw-cli. (#4166) + +## SPA + - Improve libcamera devices reporting to properly filter out duplicates in + all cases. + - Improve property reporting in v4l2. + - Fix lost buffer in v4l2. + +## Bluetooth + - Improve compatibility with some devices. + +## JACK + - Fix some races when shutting down. + - Fix rt-priority on the main thread when using custom thread create + function. (#4099) + +## ALSA + - Handle format renegotiation. (#3856) + +# PipeWire 1.2.3 (2024-08-22) + +This is a bugfix release that is API and ABI compatible with the +previous 1.2.x and 1.0.x releases. + +## Highlights + - Implement freewheeling support in the FFADO driver. Also improve + buffersize and samplerate handling. + - Improve some locking on spa_loop. Remove a possible deadlock when + the queue was full. + - Allocate more space for the libcamera devices string to properly + deduplicate libcamera and v4l2 devices. + - Some more bugfixes and improvements. + + +## PipeWire + - Improve activation state changes and xrun detection some more. + (#4182) + - Avoid a memory leak when a link in error is destroyed. + +## Modules + - Improve samplerate and buffersize handling in FFADO driver so that + it is possible to force a rate and buffer size. + - Implement freewheeling support in the ffado driver. + - Always set the server side clock.quantum-limit on nodes. This fixes + a buffer size problem in Midi-bridge. (#4005) + +## SPA + - Improve some locking on spa_loop. Remove a possible deadlock when + the queue was full. (#4114) + - Allocate more space for the libcamera devices string to properly + deduplicate libcamera and v4l2 devices. + - Fix a potential race when enumerating v4l2 udev devices. (#3960) + +## Bluetooth + - Improve compatibility with some devices (Soundcore Motion 300). + +## Tools + - pw-cli can now handle arbitrarily large input and params. (#4166) + - Avoid some compiler warnings in pw-top. + +# PipeWire 1.2.2 (2024-07-31) + +This is a bugfix release that is API and ABI compatible with the +previous 1.2.x and 1.0.x releases. + +## Highlights + - Fix some more fallout of the async nodes rewrite. Fixes some + crackling, xruns and possibly also some crashes in some cases. + - Fix freewheeling timeouts in case of xruns. This fixes ardour export. + - Fix event mixdown in JACK. Fixes qsynth and possibly other apps. + - Some more small fixes and improvements. + + +## PipeWire + - Add a new SPA_IO_CLOCK_FLAG_XRUN_RECOVER flag when the process function + is called because of xrun recovery. + - Properly stop nodes in all cases, this avoids spurious xruns and + scheduling errors. (#4122) + - Make sure async nodes receive an async link in all cases. Do the + processing of source output ports slightly differently to make sure we + don't cause latency for sources. (#4138) (#4133) + - Fix some races when negotiating and starting nodes. (#4094) + - Actually include the config.h header to use malloc_trim() to reduce + memory usage in pulse-server. + +## Modules + - Avoid unloading some modules on stream errors because it is possible to + recover from the error. (#4121) + - Fix a (harmless) warning in module-rtp because of comparing samples and + time. (#4095) + +## SPA + - Let the freewheel driver detect xrun recovery and handle the timeouts + correctly. This fixes an issue with ardour export. + - Remove the HDMI/AC3 profiles. they turn out to fail on some hardware + with no way to detect this. + - Signal the eventfd when the loop is full to make sure the other thread + is woken up to process the queue. + +## JACK + - Don't check timestamps when mixing down events. The timestamps are only + checked when writing new events with the public API. This fixes an + issue where qsynth would not receive midi events anymore. + - Fix the jack_get_time() function, it was returning nano instead of micro + seconds. + +# PipeWire 1.2.1 (2024-07-12) + +This is a bugfix release that is API and ABI compatible with previous +the previous 1.2.0 release and the 1.0.x releases. + +## Highlights + - Fix a regression in the node activation counters that would break audio + when using KODI. + - Fix a regression in ardour export because of mishandling of sync groups. + - Fix a regression in KDE screen preview because of the new async + scheduling. + - Fix a regression in context.exec argument parsing that would break some + existing scripts. + - More small bug fixes and improvements. + + +## PipeWire + - Fix a regression in the node activation counters that would break audio + when using KODI. (#4087) + - Fix a regression in ardour export because of mishandling of sync groups. + (#4083) + - Fix a regression in KDE screen preview because of the new async + scheduling. Disable async for driver nodes. (#4092) + - Slightly improve node shutdown to cause less xruns. + - Fix a regression in context.exec argument parsing that would break some + existing scripts. + - Support custom thread create functions. + +## Modules + - Improve snapcast address parsing. (#4093) + +## SPA + - Fix multiple %f parsing in ACP for the new plug+a52 profiles. + - Improve v4l2 param generation. Improve recovery when framesize or rates + are unknown, support vivid. (#4063) + +## JACK + - Use the custom thread create function to correctly let module-rt kit + manage threads so that we don't end up with priorities on the wrong + threads. (#4099) + +## GStreamer + - Fix a crash when destroying a stream. + +# PipeWire 1.2.0 (2024-06-27) + +This is the 1.2 release that is API and ABI compatible with previous +1.1.x and 1.0.x releases. + +This release contains some of the bigger changes that happened since +the 1.0 release last year, including: + + * Support for asynchronous processing has been implemented. Nodes can choose + (or be forced) to be scheduled asynchronously. The graph will not wait for + the output of the node to continue processing but it will use the output + of the previous cycle (or silence) instead. This adds one cycle of latency + but it can avoid having some nodes blocking the processing graph. Non realtime + streams and filters now also use this asynchronous processing instead of + their own slightly broken version. + * The concept of node.sync-group was added. This groups nodes with overlapping + sync-group together when one of them sets the node.sync = true. This is now + used to make sure all nodes are scheduled together when JACK transport is + started so that they all see the same transport time. + * Config parsing errors are reported earlier and much better with line and + column numbers where the parsing started to fail. + * Add support for mandatory metadata when negotiating buffer parameters. This + can be used to only negotiate extra buffer planes when certain metadata is + negotiated. One use case is the explicit sync support that requires 2 + extra fds for the timelines. + * Explicit sync metadata and support was added. + * Support was added for making and using multiple data-loops in the server + and clients. Support for CPU affinity and priorities was added to the + data-loops as well. + * The log topic debug levels can now be changed at runtime with metadata. + The log levels in the pulse server can be dynamically changed with a + /core message. + * The UCM conflicting devices patches were merged. + * Add snapcast-discover module to stream to snapcast servers. + * Rework how peers are linked and the counters are updated. Resume the + peers when a node is unlinked and not yet processed. This should cause + less occasional dropouts in the graph when reconnecting things. + * Many GStreamer element updates. + * Many more fixes and improvements. + +Enjoy the summer vacation! + + +## Highlights (since the previous 1.1.83 release) + - Small fixes here and there. + +## PipeWire + - Compilation fixes after enabling -Werror=float-conversion + +## Modules + - The module-rtp-sap now propagates the cleanup.sec property to the + rtp-source and the rtp-source now sets a property with the receiving + status. + - Fix for ROC 0.3, explicitly specify sender encoding. (#4070) + - Some fixes to the RAOP sink module, including a format fix for 32 bit + machines. + +## Tools + - Fix pw-cli monitoring code. + +## SPA + - Revert peer_enum_params again because it was not used and flawed. + - Fix multichannel processing in webrtc AEC. + +## GStreamer + - Logging improvements. + - Fix a race in the bufferpool activation. + +## Bluetooth + - Improvements to BAP broadcast code parsing. + +# PipeWire 1.1.83 (2024-06-17) + +This is the third and hopefully the last 1.2 release candidate that is +API and ABI compatible with previous 1.0.x releases. + +Some last minute changes went in to clean up the node activation and +scheduling that justify another pre-release. + +## Highlights + - Rework how peers are linked and the counters are updated. Resume the + peers when a node is unlinked and not yet processed. This should cause + less occasional dropouts in the graph when reconnecting things. + - Improve xruns in module-ffado. + - Many GStreamer element updates. + - More fixes and improvements. + + +## PipeWire + - Rework how peers are linked and the counters are updated. Resume the + peers when a node is unlinked and not yet processed. This should cause + less dropouts in the graph when reconnecting. (#4026) + - Improve debug of xruns. + - Evaluate node.rules and device.rules before loading the plugin so that + extra properties can be passed to the plugin init function. + +## Modules + - Improve timing reporting in module-ffado some more. + - Prealloc less memory in the profiler by default. + - Improve xrun handling in module-ffado. + +## Tools + - Fix a crash in pw-link when a link fails. + - Fix pw-dump update for metadata. (#4053) + +## SPA + - Improve handling of controls. (#4028) + - Fix the string size in v4l2 to hold the device and vendor id. + - Support meta_videotransform on buffers in v4l2. This can be used to + signal that the buffer was rotated for example. + - Add HDMI/AC3 profile to ALSA when supported. + - Make it possible to disable the webrtc dependency + +## GStreamer + - Improve caps handling in the elements. + - Set buffer duration when we can. + - Post an element error when all the elements buffers are removed. + (#1980) + - Improve DMA_DRM caps selection. + - Some refactoring work. + - Improve state handling in the elements. + +## JACK + - Improve how links are activated. + - Fix some races when freeing memory. + +## Bluetooth + - Support multiple BIS in the broadcast source. + +# PipeWire 1.1.82 (2024-05-24) + +This is the second 1.2 release candidate that is API and ABI +compatible with previous 1.0.x releases. + +Not so many things needed to be fixed so this might already be the +last prerelease if everything goes well... + +## Highlights + - Fix problem when moving nodes that could cause nodes to be scheduled + wrongly and cause errors. (#4017) + - Add snapcast-discover module to stream to snapcast servers. + - Work around wrong kernel provided MTU for USB controllers. + - Fix some spelling mistakes all over the codebase. + - More small fixes and improvements. + + +## PipeWire + - Remove the private cleanup.h header and use the public SPA version. + - Fix problem when moving nodes that could cause nodes to be scheduled + wrongly and cause errors. (#4017) + +## Modules + - Handle IPv6 in module-protocol-simple and support port allocation. + - Add snapcast-discover module to stream to snapcast servers. + +## Bluetooth + - Work around wrong kernel provided MTU for USB controllers. + +# PipeWire 1.0.7 (2024-05-24) + +This is a small bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Fix a potential race/crash. + - Fix some problems with negotiation of large integers and floats. + - Fix JACK sysex MIDI event handling. + - Some more smaller fixes and improvements. + +## PipeWire + - Fix a potential race when adding/removing a port to be scheduled. + +## Modules + - Fix FFADO default device handling. (#4023) + +## SPA + - Fix in integer overflow and float/double compare in POD. + +## JACK + - Copy larger MIDI events correctly. + +# PipeWire 1.1.81 (2024-05-16) + +This is the first 1.2 release candidate that is API and ABI +compatible with previous 1.0.x releases. + +In addition to all the changes backported to 1.0.x, this release +also contains some new features: + +## Highlights + - Support for asynchronous processing has been implemented. Nodes can choose + (or be forced) to be scheduled asynchronously. The graph will not wait for + the output of the node to continue processing but it will use the output + of the previous cycle (or silence) instead. This adds one cycle of latency + but it can avoid having some nodes blocking the processing graph. Non realtime + streams and filters now also use this asynchronous processing instead of + their own slightly broken version. + - The concept of node.sync-group was added. This groups nodes with overlapping + sync-group together when one of them sets the node.sync = true. This is now + used to make sure all nodes are scheduled together when JACK transport is + started so that they all see the same time. + - Config parsing errors are reported earlier and much better with line and + column numbers where the parsing started to fail. + - Add support for mandatory metadata when negotiation buffer parameters. This + can be used to only negotiate extra buffer planes when certain metadata is + negotiated. One use case is the explicit sync support that requires 2 + extra fds for the timelines. + - Support was added for making and using multiple data-loops in the server + and clients. Support for CPU affinity and priorities was added to the + data-loops as well. + - The log topic debug levels can now be changed at runtime with metadata. + The log levels in the pulse server can be dynamically changed with a + /core message. + - The UCM conflicting devices patches were merged. + + +## PipeWire + - snap support has been added. + - Implement async processing. (#3509) + - Support for explicit sync was added. + - Config parsing errors are reported earlier and much better. + - A -P option was added to provide extra properties to the context. This can be + used to enable some features that use rules. + - properties.rules was added to enhance properties based on some rules. + This deprecates the vm.overrides. + - Support was added for security-context. This makes it possible for a flatpak + to request a socket with specific properties from pipewire to mount in the + flatpak. The session manager can then assign permissions based on the connection + properties. + - Support for fixed arrays in pw_array was improved. + - PipeWire server and clients can now use multiple threads to process the nodes + in parallel. + - device.rules and node.rules were added to update device and node properties + based on rules. + - device.param and node.param can now be used to configure params when devices + and nodes are created. + - Memory will now try to use MFD_NOEXEC_SEAL. + - The driver id of a node is now placed in the properties. + - A potential race was fixed when adding and removing ports to the scheduling + lists. + +## Modules + - Priorities for the FFADO threads can be configured now. + - The loopback module now has support for up and downmixing. + - Extra properties can now be configured per native-connection socket. + - The pulse-tunnel can now automatically reconnect when the connection is + broken. + - The RTP module now supports the PTP management protocol. + - The RTP sender can now use a timer to send out multiple packets per + quantum. + - A new module was added for loading Parametric EQ. + - The simple-protocol module now has per stream configurable properties + and can also be used to interface with a snapcast server. + - Support for local services was added to raop, rtp and pulse avahi + discoverers. Support for IPv6 on local services was added to RAOP. + +## SPA + - Support for reporting JSON parsing errors has been added. + - Some extra checks are added when iterating POD structures. + - Port and profiles can now be hidden from ALSA nodes with + api.acp.hidden-ports and api.acp.hidden-profiles properties. + - The UCM conflicting devices patches were merged. + - Profiles and Routes can now also be set by name. + - Hires timestamps are now used when possible in IRQ based scheduling to + get more accurate wakeup times. + - udev can now be an optional dependency. + - audioadapter now has an option to automatically configure its ports. + - Camera rotation was added to the libcamera node. + - invoke on loops can now be done from multiple threads at the same time. + - Make sure we use CLOCK_MONOTONIC everywhere in the io_clock. + - Vulkan bit and convert filters were added. + - ALSA will now always read the HW ringbuffer pointer when followers are + not on the same card. + - Support for larger MIDI sysex messages was improved. Configuration of + the client input and output pool was added. (#4005) + +## Bluetooth + - Support Google OPUS codec. + - Support the LC3-SWB codec. + - Support the AAC-ELD codec. + - Broadcast source configuration support was added. + +## pulseaudio-server + - The GSettings schemas are now optionally installed. + - Extensions were moved to the modules. + - The log level of the pulse server can dynamically be changed with + a core object message. + - snap access control was added to pulse-server. + - The old pacmd describe-module functionality is now implemented with + a core message pipewire-pulse:describe-module. + - An option was added to disable module loading and unloading. + +## JACK + - OSC messages can now also be placed in JACK MIDI and the translation + layer will detect and tag the right PipeWire control message types. + - A jack.other-connect-mode was added to limit the connections that an + app can do to ports it doesn't own. + - The way the transport is started and how the nodes are grouped together + in the transport was improved using the new sync groups. (#3850) + - Fix large MIDI messages handling. (#4005) + +## ALSA + - Fix format renegotiation. (#3858) + - Handle period events better. (#3676) + - Improve handling of the eventfd wakeups. + +## GStreamer + - The GStreamer elements can now negotiate and use DMABUF. + +## Tools + - The T flag is used in pw-top when the transport is running. + - A new pw-container tool was added to start a new security context and + run an application in it. + - pw-dot handles properties with quotes better. Nodes are grouped with the + node.link-group. + - pw-link has a --wait option to wait for all links to be created. + +# PipeWire 1.0.6 (2024-05-09) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - A bitfield race was fixed that could cause some crashes or undefined + behaviour when moving nodes between drivers. + - Fix to some invalid memory access in the pw-mon and pw-dump. + - A regression in kodi with IEC958 formats playback was fixed. + - A race in the ALSA plugin was fixed when updating the eventfd. + - Improvements and fixes to module-combine-stream. + - Negotiation was improved in pipewiresrc. + - Some more small fixes and improvements. + +## PipeWire + - Context properties are now set early so that client properties can be + matched with rules. + - A bitfield race was fixed that could cause some crashes or undefined + behaviour when moving nodes between drivers. + +## Tools + - Fix failure to hide properties in pw-mon. (#3997) + - Fix some memleaks and a crash in pw-dump. (#4001) + +## Modules + - The combine-stream module now prevents resampling to avoid broken + audio because of different samplerates. + - Fix a potential double free in module-loopback when calculating the + delay. (#3748) + - The FFADO module now only starts when ports are negotiated to avoid + startup races. (#3968) + - The combine-stream module will now forward tags. + +## SPA + - Monitor volumes are now also clamped to the min/max volumes. (#3962) + - V4l2 and libcamera now encodes the device ids into a JSON array. This + is part of the deduplication code of devices. + - A regression in kodi with IEC958 formats playback was fixed. + +## Bluetooth + - Improved buffer handling and queued data when stopping. + +## ALSA + - A race was fixed when updating the eventfd. (#3711) + +## GStreamer + - Handle some errors better instead of crashing. (#3994) + - Fix a memleak in the stream params handling. + - Negotiation was improved in pipewiresrc. + +# PipeWire 1.0.5 (2024-04-15) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - pw_stream can now report timestamps on buffers and the expected + amount of samples for the resampler. + - The GStreamer element now has more correct timestamps using the new + pw_stream timestamps as a fallback. + - The FFADO module now handles suspend and resume better. + - A regression in v4l2 was fixed when parsing malformed filters. + - A potential memory/fd leak was fixed in client-node. + - Many more small bugfixes and improvements. + + +## PipeWire + - pw_stream now reports the expected resampler input or output size in + the pw_time structure. (#3750) + - pw_stream now also adds a time field to the buffer, which contains the + time of the graph when the buffer was received in the stream. + - Fix a compiler error when compiling with -Werror=shadow. (#3915) + - The config parser will warn when invalid config is detected. + +## Modules + - The FFADO module now opens and closes when suspending. This fixes some + problems when FFADO properties are changed while suspended. (#3558) + - Filter-chain will now warn when invalid config is detected. + - Echo-cancel will now handle manage the state of the echo-cancel plugin + better, making sure run() is not called after deactivate(). + - Fix some potential memory/fd leaks in client-node. + +## SPA + - Improve reading the bound ALSA controls. + - The resampler can now also report the number of expected output samples. + - The ALSA ACP device objects have some more properties like the card.id + and alsa.components. (#3912) + - Fix a potential string corruption when parsing JSON strings. + - V4l2 now sets the latency on the port. (#3910) + - alsa-udev now has an option to expose the device even if busy. (#3914) + - Improve null-audio-sink channel handling. (#3931) + - v4l2 will now drop the first frame because it often contains wrong + timestamps or garbage. (#3910) + - A regression in v4l2 was fixed where invalid/empty properties in the + filter would make it error early. (#3959) + +## GStreamer + - The source now falls back to the new pw_buffer time for the timestamps. + +## Docs + - Sync with the master branch. + +# PipeWire 1.0.4 (2024-03-13) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + + - Track memfd better to avoid inconsistent memory. Also make sure the + mixer info is removed correctly in all cases on destroyed ports. + - Correctly handle removed objects in the metadata. + - Add an option to set the server and client priorities instead of using + a hardcoded value of 88. + - The FFADO module has been fixed. Audio and MIDI now works with + the same latency as the JACK driver. This has now also been + tested with a Focusrite Saffire Pro 14. + - The JACK library has seen some important fixes. Some ardour crackling + has been fixed when looping and multiple MIDI ports on a client should + now work. + - Small bugfixes and improvements. + + +## PipeWire + - Track memfd better to avoid inconsistent memory. Also make sure the + mixer info is removed correctly in all cases on destroyed ports. + - Fix Props param emission again in pw_stream. (#3833) + - Add MAPPABLE flag to buffer data to indicate that the fd can be + mmapped directly. Use this on DMABUF from v4l2. (#3840) + - Correctly handle removed object in the metadata. + - FreeBSD build and compatibility fixes. + - Add an option to set the server and client priorities instead of using + a hardcoded value of 88. + - Read config overrides in the right order. + - Fix PIPEWIRE_QUANTUM rate handling in pw_stream and pw_filter. + - Fix pw_context_parse_conf_section(), actually use the conf argument. + - A new pw_stream_get_nsec() and pw_filter_get_nsec() function was added + to get the current time of the stream/filter without having to assume a + particular clock. + - A new default.clock.quantum-floor property was added to configure the + absolute lowest buffer-size. (#3908) + +## docs + - Many doc updates. + +## tools + - Make sure we always quit pw-cli when the server stops. (#3837) + - pw-top now prints all drivers in batch mode. (#3899) + +## modules + - Don't destroy the client in protocol-simple on EAGAIN. + - Handle IPv6 better in the RTP modules. Fix IPv6 SAP header + parsing. (#3851) + - The FFADO module has been fixed. Audio and MIDI now works with + the same latency as the JACK driver. This has now also been + tested with a Focusrite Saffire Pro 14. (#3558) + +## pulse-server + - Make sure the peer_name is filled to avoid protocol errors. + +## SPA + - Small resampler tweaks to improve stability of adaptive resampler. + - Add ALSA option to control htimestamp autodisable. + - Avoid some potential crashes in audioconvert when ports are removed. + - Improve HDMI jack detection on some SOCs. + - The audioconvert now has a monitor.passthrough option to pass the + latency information on the monitor ports. (#3888) + +## GStreamer + - Don't use timeouts when autoconnect=false in pipewiresrc. (#3884) + - pipewiresrc and pipewiresink can now be automatically selected as + audio source and sink. + - An invalid memory access was fixed when destroying the device + provider. + +## JACK + - Remove properties correctly with the object id, not serial. + - Improve sync with the data thread by pausing the core. Also improve + handling of port io to avoid invalid buffer access. + - Fix PIPEWIRE_QUANTUM rate handling. + - Support multiple MIDI input ports per client. (#3901) + - The output buffer size is now always correctly set. (#3892) + +## ALSA + - Handle errors from eventfd_create correctly. + +# PipeWire 1.0.3 (2024-02-02) + +This is a quick bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Fix ALSA version check. This should allow the alsa plugin to work again. + - Some small fixes and improvements. + +## PipeWire + - Escape @DEFAULT_SINK@ in the conf files. + +## Modules + - Improve logging in module-pipe-tunnel. + +## SPA + - Always recheck rate matching in ALSA when moving drivers. This fixes a + potential issue where the adaptive resampler would not be activated in + some cases. + +## ALSA + - Fix version check. This should allow the alsa plugin to work again + with version 1.0.2. + +# PipeWire 1.0.2 (2024-01-31) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Fix v4l2 enumeration with filter. This should fix negotiation in some + GStreamer pipelines with capsfilter. Also probe for EXPBUF support + before using it. + - Fix max-latency property and Buffer param when dealing with small + ALSA device buffers. This should fix stuttering with some AMD + based soundcards. + - More small cleanups an improvements. + +## Modules + - Improve netjack2 channel positions. + - Improve RAOP module state after suspend/resume. (#3778) + - Avoid crash in some LV2 plugins by configuring the Atom ports. (#3815) + +## SPA + - Bump libcamera requirements to 0.2.0. + - Try to avoid unaligned load exceptions. (#3790) + - Fix v4l2 enumeration with filter. (#1793) + - Fix max-latency property and Buffer param when dealing with small + ALSA device buffers. This should fix stuttering with some AMD + based soundcards. (#3744,#3622) + - Add a resync.ms option to node.driver to make it possible to resync + fast to clock jumps. + - Probe for EXPBUF support in v4l2 before using it. (#3821) + +## pulse-server + - Also emit change events when the port list change. + +## Bluetooth + - Log a more verbose explanation when other soundservers seem to be + interfering with bluetooth. + - Add quirks for Rockbox Brick. (#3786) + - Add quirks for SoundCore mini2. (#2927) + +## JACK + - Improve check for the running state of clients. (#3794) + +# PipeWire 1.0.1 (2024-01-11) + +This is a bugfix release that is API and ABI compatible with previous +1.0.x releases. + +## Highlights + - Work around the buggy ALSA backend in libcanberra by forcing the pulse + backend in module-x11-bell. + - Fix a race in the device info updates in pulse-server. + - Fix timing and rate matching in ALSA sequencer. + - Improve timing information in JACK and from the ALSA driver. + - More small fixes and improvements. + + +## PipeWire + - Fix a build issue when examples where disabled. + - Avoid some compiler warnings. + - Avoid some bitfield data races. (#3706) + +## Modules + - Bump the PTP driver priority. (#3217) + - Support the previous "allowed" permission in the access module. + - Fix filename leak in module-filter-chain. + - Work around the buggy ALSA backend in libcanberra by forcing the pulse + backend in module-x11-bell. (#3688) + - Fix a race in the device info updates in pulse-server. + - Fix compatibility in RAOP. (#3698) + +## SPA + - Handle ALSA picth control errors correctly + - Clamp buffer-frames correctly. (#3000) + - Fix timing and rate matching in ALSA sequencer. (#3657) + - Revert a commit that could result in current time in the future in the + timing updates. + - Improve adapter state checks. + - Remove the timer from the ALSA pcm. + - Fix timeout in freewheel driver. + +## Pulse-server + - Also handle active ports for monitor sources. + - Fix zeroconf-publish format properties. + +## JACK + - Improve timing and transport calculations. + - Handle -ENOENT from the core and don't error out. + +## GStreamer + - Handle node port removal in the device provider. (#3708) + - Improve error handling while connecting. + - Fix dts_offset. + +# PipeWire 1.0.0 (2023-11-26) + +The PipeWire project is immensely proud to announce the 1.0 release +of PipeWire. + +It is API and ABI compatible with previous 0.3.x releases. + + "PipeWire represents the next evolution of audio handling for Linux, taking + the best of both pro-audio (JACK) and desktop audio servers (PulseAudio) and + linking them into a single, seamless, powerful new system." + - Paul Davis, JACK and Ardour author + + "What exciting times! PipeWire 1.0 is the culmination of 15 years of + Linux audio expertise, blending lessons from PulseAudio into a high-performance, + flexible, and user-friendly foundation for audio and multimedia on Linux. + I'm looking forward to the next decade of progress in the free software + consumer and professional audio space!." + - Arun Raghavan, PulseAudio developer/maintainer. + + "I'm thrilled to witness the first stable release of PipeWire after five years + of collaboration with its remarkable community, pushing the boundaries of + multimedia integration in the Linux ecosystem one step further.” + - George Kiagiadakis, WirePlumber author + + "From the beginning of the libcamera project, we have always seen + PipeWire as the solution to handle desktop and mobile integration and + give a seamless multimedia integration to users while providing security + features and resource sharing between applications." + - Kieran Bingham, libcamera author + +Happy Holidays! + + +## Highlights + - Fix a memfd/dmabuf leak when uploading buffers while shutting down. + - Handle concurrent jack_port_get_buffer() calls because ardour seems to + be doing this. + - Improve time reporting (less jitter) in ALSA when using IRQ. + - Many doc improvements. + +## PipeWire + - Respect PIPEWIRE_DLCLOSE everywhere, remove pw_in_valgrind(). + - Remove a warning when a client tries to change ignored properties. + +## Modules + - Fix a memfd/dmabuf leak when uploading buffers while shutting down. + - Fix a potential segfault when copying mix structures. (#3658) + - Avoid races in setrlimit in module-rt. + - Fix a memory leak in filter-chain. + - Set rtp.ptime on senders, not receivers. + - The ROC modules were ported to ROC 0.3 + +## SPA + - Improve time reporting (less jitter) in ALSA when using IRQ. (#3657) + - Add latency param query in libcamera. + - Fix some compiler warnings. + - The EVL plugin was updated. + +## Bluetooth + - LC3 codec and compatibility improvements. + +## Pulse server + - Fix emission of events when a sink/source state changes. (#3660) + +## JACK + - Improve transport and time handling. Use unique ids to make consistent + snapshots of the current time and transport. + - Avoid enumerating port params that we are not going to use. + - Optimize buffer reuse. + - Handle concurrent jack_port_get_buffer() calls because ardour seems to + be doing this. (#3632) + +## Docs + - Many doc improvements. + - Add man pages for pw-dump, pw-loopback, modules, pipewire-pulse. + - Manpages are now made with Doxygen. + - Add docs for pulse-modules + +# PipeWire 0.3.85 (2023-11-16) + +This is the fifth (and last) 1.0 release candidate that is API and ABI +compatible with previous 0.3.x releases. + +## Highlights + - Fix an issue where a link could end up paused while not negotiated. + - Fix an infinite recursion issue when finding runnable nodes. + - Support XDG base directories when loading ACP config. + - Fix MIDI event recording preview in Ardour. + - Many more small fixes, cleanups and improvements. + + +## PipeWire + - Fix an issue where a link could end up paused while not negotiated. + (#3619) + - Fix an infinite recursion issue when finding runnable nodes by stopping + the scan on feedback links around the driver. (#3621) + - The system service now has better socket permissions. + +## Modules + - Add support for uclamp. This allows the scheduler to make better informed + decisions about where tasks should be placed, and what pstate to set + for the CPU it is running on. + - Emit warnings when applications are not doing the right locking instead + of crashing. + - Improve media.name for RAOP sinks. (#3801) + - Support pause/resume in pipe-tunnel. (#3197) + - Remove time rlimit when probing for realtime to avoid SIGXCPU. + +## SPA + - Fix a bug where the resampler would be activated even when there is an + ALSA pitch element. (#3628) + - Improve resume from suspend in ALSA. (#3646) + - Add option to expose ALSA controls as prop params. + - Support XDG base directories when loading ACP config. This makes it possible + to override the ACP config files. + +## Bluetooth + - Schedule nodes in the same ISO group together. + - More BAP fixes and cleanups. + +## JACK + - Fix MIDI events from peer ports. This makes the MIDI event recording preview + of Ardour work correctly. + +## GStreamer + - Fix some error handling in the source and sink. + +## ALSA plugin + - Improve poll descriptor handling. (#3648) + +## Docs + - Many improvements to the layout and organization. + +# PipeWire 0.3.84 (2023-11-02) + +This is the fourth 1.0 release candidate that is API and ABI compatible +with previous 0.3.x releases. + +## Highlights + - Fix a regression with openal because the queued buffers in the stream + were not reported correctly. + - Fix a bug in port busy counters that could cause random silent links. + - Fix a regression in echo-cancel because it was not reporting its + streams as ASYNC. + - Fix a JACK regression where not all ports were enumerated in all cases. + - Many more fixes and improvements. + + +## PipeWire + - pw_stream now reports the queued buffers more accurately. This fixes + a regression when using openal. (#3592) + - The port busy counters were not updated correctly in some cases. This + could lead to negotiation errors and silent links. (#3547) + - Ignore latency maximum when forcing rate/quantum. (#3613) + - Nodes can now be added to multiple groups and link-groups. (#3612) + +## Modules + - The filter-chain now also handles notify port dependencies + correctly. (#3596) + - Filter-chain has support for new linear, clamp, recip, exp, log, mult, + sine builtin plugins. + - The echo-cancel module now correctly reports its playback and capture + streams as ASYNC to avoid running out of buffers. (#3593) + - It is now possible to specify an array of remote names to connect to + with the native protocol. + - module-rtp-sap and module-rtp-sink now try to bind to the specified + interface. + +## SPA + - The alsa plugin now removes the runtime properties such as period-num, + period-size and max-latency when suspended. (#3613) + +## Bluetooth + - BAP Locations/Context is now set on endpoints as required by new bluez. + - Improve selection of BAP leader. + +## JACK + - Add a jack_set_sample_rate() extension function. + - Make sure we get the info of all nodes/ports before completing the + jack_client_open() operation so that we can enumerate the ports + correctly in all cases. (#3618) + +## GStreamer + - Fix types of metadata in pipewiresink. + - Also copy metadata in buffers in all cases. + - Fix size allocation in bufferpool for compressed formats. + - Don't stop streaming thread when unlinked. (#3620) + +## ALSA + - The ALSA plugin now handles NULL values from mmap_areas. (#3600) + +# PipeWire 0.3.83 (2023-10-19) + +This is the third 1.0 release candidate that is API and ABI compatible +with previous 0.3.x releases. + +## Highlights + - A quantum change regression was fixed. + - Use a 2 socket server now for the manager and the applications + with (when wireplumber is updated) different permissions. + - Reduce memory usage a little in audioconvert and use fewer buffers. + - Some JACK deadlocks were fixed. + - More bugfixes and improvements. + + +## PipeWire + - Fix quantum change regression. (#3574) + - Use a 2 socket server by default. One for the session-manager and one + for applications. + - Fix a potential use-after-free in node and device cleanup. (#3588) + +## modules + - Some hardcoded buffer size limits were removed. + - Fix ASYNC flag on combined-streams. + - Add support for on-demand combined-streams using metadata. + +## SPA + - alsa-udev will now ignore PCMs with the ACP_IGNORE udev environment + variable. (#3570) + - The audioadapter now uses at least 2 buffers when the follower is + async. + - The number of buffers used by plugins was tweaked a little. Most + plugins now only ask 1 buffer. + - Memory usage in audioconvert was reduced. + - Fix some unaligned reads and writes and undefined left shifts reported + by ASAN. (#3572) + - Rework vulkan dependency checking. + - Don't try to link ALSA devices when prepare fails. This fixes some + crashes. + - Fix a stall when the allowed codecs are changed in ALSA. + - Improve ALSA rate control for sources to avoid xruns. (#3584) + - Try to fix IEC958 TrueHD and DTS playback. (#2284) + +## Bluetooth + - Improve fallback SCO mtu when the kernel doesn't tell us. + +## JACK + - The fixed buffer size limit was removed. + - Add an option to make input buffers writable (default true). + - A potential deadlock was fixed when applications lock the process + function. (#3585) + - Use a separate thread to dispatch notifications to avoid deadlocks. + (#3585) + - Potentially fix silent export in ardour in some cases. (#3514) + +# PipeWire 0.3.82 (2023-10-13) + +This is the second 1.0 release candidate that is API and ABI compatible +with previous 0.3.x releases. + +## Highlights + - Fix a regression in some devices when the Pro-Audio profile was selected. + Only enable the IRQ based scheduling and device linking in specific + safe cases. (#3556) + - Improve rate switching. In some cases the graph rate would not switch + correctly. (#2929) + - Fix regression in alsa wakeups that would cause silence in VMs. + - Fix a leak in the SBC codecs for SCO. + - More improvements to the RAOP module. + - Other small improvements and fixes. + + +## PipeWire + - Improve client property checks. + - Allow non-power-of-2 quantums when forced. + - Improve rate switching. In some cases the graph rate would not switch + correctly. (#2929) + - The PIPEWIRE_QUANTUM env variable now forces the size and rate in the + graph for the duration of the application. The softer PIPEWIRE_LATENCY + and PIPEWIRE_RATE can still be used to merely suggest a maximum latency + and a rate. + +## modules + - Remove the RTSP FLUSH request in RAOP because it does not seem necessary. + - The RAOP module now uses the common RTP stream functions. + - Add sockets option to protocol-native to make pipewire listen on multiple + sockets. + +## SPA + - Clean up some of the log functions. + - Add an option in ALSA to disable linking devices together. + - Only link pcms together when 1 capture and 1 playback pcm. For more complex + devices we can't be sure which ones can be linked. (#3556) + - disable tsched only when using linked devices. + - Add some extra checks in ALSA to avoid segfaults. (#3554) + - Add Tag support to alsa-sink and alsa-source. + - Use dynamic pod builder when we can. + - Set priority.driver on midi-bridge to allow it as a fallback driver. (#3562) + - Fix regression in alsa wakeups. (#3565) + - The PTP clock can now be found from the interface in node-driver. + +## pulse-server + - Some small cleanups and internal improvements. + - Add some memory debugging messages. + - Add Tag messages to streams. + +## Bluetooth + - Fix a leak in the SBC codecs for SCO. + +## JACK + - Patch up midi events in the destination buffer instead of writing to the + source buffer. (#3580) + - Group all jack clients together to avoid transport issues. (#3562) + +## ALSA-plugins + - Add also.deny option to block alsa clients from opening the PCM. + +# PipeWire 0.3.81 (2023-10-06) + +This is the first 1.0 release candidate that is API and ABI compatible +with previous 0.3.x releases. + +## Highlights + - jackdbus support is now enabled by default. + - IRQ based scheduling in ALSA was improved and enabled by default for + Pro-Audio profile. It will also link the pcms together to get lower + latency. This now matches what JACK does and gives equal latency to + PipeWire for Pro-Audio profiles. + - Support both old and new versions of webrtc-audio-processing to make + the transition easier. + - Forced quantum changes by nodes or metadata will now also force a + suspend and resume of the graph, like the rate changes to make sure all + nodes adapt to the new quantum. This is important for Pro-Audio nodes + that need to reconfigure the hardware to a new period in IRQ based + scheduling. + - Fix a regression in regex parsing. + - Many bugfixes and improvements. + + +## PipeWire + - jackdbus is by default enabled now. The idea is that when jackdbus is + installed, the real libjack.so is in the path and we can become a + real JACK client. + - Forces quantum changes by nodes or metadata will now also force a + suspend and resume in the graph, like the rate changes to make sure all + nodes adapt to the new quantum. This is important for Pro-Audio nodes + that need to reconfigure the hardware to a new period. + - The stream now has an EARLY_PROCESS option that can be used to implement + custom buffer fill levels. (#3480) + - Fix a regression in regex parsing. (#3528) + - Fix a bug in position reporting in the driver node. (#3189) (#3544) + - Destroying a link will now recalculate the graph correctly. + - Fix the rate comparison for finding the best rate in the graph. + - Use malloc_trim() when available to release memory. (#1840) + +## Tools + - pw-cat now supports DFF DSD files. + - pw-cli avoid some NULL derefs in some cases. + +## Modules + - The RAOP sink has seen some cleanups and improvements. It will now ask + for feedback every 2 seconds to keep some devices alive. + - A bug in filter-chain was fixed where it would fail to apply the gain + when mixing just one source. + - The filter-chain can now pass the stream volume to a control in the + filter-chain graph. (#3434) + - Improve volume handling in RAOP sink. + +## Pulse-server + - Some cleanup in the pending_stream handling. + - Fix a regression in the event emission code where it failed to emit + a changed event when a node was linked. (#3522) + - Lower the realtime priority of pulseaudio clients. + - Set pulse.module.id on the echo-cancel streams. (#3541) + +## SPA + - Support both old and new versions of webrtc-audio-processing to make + the transition easier. + - The ALSA driver now does the sync of all followers directly from the + wakeup event. This results in more stable rate matching. + - IRQ based scheduling in ALSA was improved and enabled by default for + Pro-Audio profile. It will also link the pcms together to get lower + latency. This now matches what JACK does and gives equal latency to + PipeWire for Pro-Audio profiles. + - GNU/Hurd support was added. + - Some improvements to passthrough handling. + +## Bluetooth + - Improvements to the codec handling when PipeWire is used as Audio + Gateway. + - Adapt to new Bluez API for BAP devices. + +## JACK + - When the jack library is set in the default library path, avoid using + LD_LIBRARY_PATH because this can cause confusion. + - Handle clearing the latency on a port. + - jack_property now always manages to actually change the metadata because + it waits for a roundtrip before exiting. + +# PipeWire 0.3.80 (2023-09-14) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - A new Tag param was added that allows arbitrary metadata to be transported + out-of-band in the graph. + - Vulkan DMA buf support was merged. + - The echo-canceller was ported to webrtc-audio-processing-1. + - Fix a regression in locating monitor sources by id in pulse-server. + - Mixer io areas updates are now synchronized correctly with the data + thread to avoid potential crashes. + - Many more bugfixes and improvements. + + +## PipeWire + - Handle driver nodes that refuse to change the quantum or rate. + - A new Tag param was added that allows arbitrary metadata to be transported + out-of-band in the graph. + +## Modules + - The pipe-tunnel source has been reworked to use a ringbuffer and rate + adaption to keep the latency constant. It can now also function as a + driver to reduce resampling. (#3478) + +## Tools + - pw-cat will now place media properties in Tag params. + - pw-mon can now filter props and params. + +## SPA + - ALSA refuses to change quantum and rate when in IRQ mode. + - ALSA will now be smarter in selecting the period size for batch devices + and will make it depend on the samplerate. (#3444) + - Vulkan DMA buf support was merged. + - ALSA latency will now be reported in the time domain of the graph. + - Add udev based autodetection for compress-offload devices. + - The echo-canceller was ported to webrtc-audio-processing-1. + - The v4l2 inotify code was rewritten to avoid a use-after-free and by + using a separate watch (but same fd) for each device. (#3439) + - The tag and latency handling was improved in audioadpter. + - Don't use -Ofast on alpha because it can crash on denormalized + values. (#3489) + - The mixers now synchronize spa_io_buffers updates with the data + thread to avoid crashes. + - Handle NULL param updates. (#3504) + +## Pulse-server + - Fix a regression in locating monitor sources by id. (#3476) + - Add support for use_system_clock_for_timing in module-pipe-sink. + - Add support for checking module arguments. + - Avoid some useless change events. + +## Bluetooth + - Ports are now marked as physical, which makes the bluetooth devices show + up as hardware devices in Ardour and other JACK apps. (#3418) + - Some fixes for LE audio support (#3479) + +## JACK + - Also emit unregister notify even when suppressed when creating the + client. + - The notify callbacks now match JACK2 behaviour more. + - The mixer io areas are updated and handled safely now to avoid + crashes. (#3506) + +# PipeWire 0.3.79 (2023-08-29) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a regression in suspend that could cause silence. + - Fix a regression in JACK port registration that could cause all kinds of + JACK problems. (#3485) + - Fix a typo in the neon sample conversion functions that could cause + distortion. + - Add BAP broadcast source and sink support. + - pw-top now has a batch mode to dump the output to stdout. + - Many more bugfixes and improvements. + + +## PipeWire + - Fix a regression in shutdown where a node might not first suspend + properly. This cause loss of sound in some cases. (#3378) + - Failure to compile a regular expression in the config file will now + be reported and ! can be used to negate the match. (#3460) + - Fix a regression where some nodes might not set running in some + cases. + - Nodes are now suspended before the format is cleared, which might + fix some crashes. + +## Tools + - pw-top now has a batch mode to dump the output to stdout. + +## SPA + - The queued samples in audioconvert are now correctly reported in the + delay. (#3454) + - Make it easier to add a custom profile in ACP. + - Fix a typo in the neon sample conversion functions that could cause + distortion. (#3463) + - device.profile.pro=true is added for pro audio nodes. + - An xrun counter was added to spa_io_clock to detect and track skipped + data because of xruns. + +## Pulse-server + - Add alsa-sink and alsa-source modules. (#3456) + +## Bluetooth + - Fix a regression where only the BAP off profile is shown. + - Add BAP broadcast source and sink support. + +## JACK + - Also emit a latency notify when the buffer size changes. + - Fix a regression in JACK port registration. (#3485) + - jack_port_tie() is now supported. + +## ALSA + - Improve property handling, support lists and ranges in addition to + fixed values. (#3451) + +# PipeWire 0.3.78 (2023-08-22) + +This is a small bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - An old regression was fixed with where some nodes would not run. + - A regression was fixed where removed events would not be shown in some + cases. This would result in duplicate entries in audio clients. + - Fix an off-by-one in the vban audio receiver. Tweak the rate adaption + a little. + - ACP will now set a UCM verb before probing the pro-audio devices. + - More bugfixes and improvements. + + +## PipeWire + - An old regression was fixed with where some nodes would not run. (#3405) + - Suspend was improved a little to avoid races when the session manager would + suspend right when a driver was starting. + +## Modules + - module-rtp-sap does not use the deprecated inet_aton anymore. + - Fix an off-by-one in the vban audio receiver. Tweak the rate adaption + a little. (#3380) + +## SPA + - ACP will now set a UCM verb before probing the pro-audio devices. (#3407) + - The mandatory flag will be set now on the video modifiers. + - EVL was updated to Xenomai4 r46 and xbuf creation was improved. + - An option was added to force colors in the log even when logging to !tty. + - The return type of spa_pod_builder_control() was fixed. + - inotify errors are handled better now. (#3439) + +## pulse-server + - A regression was fixed where removed events would not be shown in some + cases. (#3414) + +## Bluetooth + - Improve compatibility with more devices, avoid reusing the same transport + for different media-sink instances to avoid encoder resets. + - Improve enumeration of codec profiles for BAP and A2DP. + +## JACK + - Ensure we can't iterate ports from a deactivated client. Also make sure + the JACK clients with the node.always-process=false always show their + ports. (#3416) + +## GStreamer + - A potential crash was fixed in the device provider when stopping. + +# PipeWire 0.3.77 (2023-08-04) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a bug in ALSA source where the available number of samples was miscaluclated + and resulted in xruns in some cases. + - A new L permission was added to make it possible to force a link between + nodes even when the nodes can't see each other. + - The VBAN module now supports midi send and receive as well. + - Many cleanups and small fixes. + + +## PipeWire + - Global objects now only show permissions that apply to them. The permissions + required to perform various API calls are documented. + - A new L permission was added to make it possible to force a link between + nodes even when the nodes can't see each other. + - Config files need to end with .conf. + - The client.api is added the to global properties of a node. + +## modules + - The VBAN module now supports midi send and receive as well. + - Fix module-profiler alignment and make sure we don't overrun our buffers with + many nodes. + - Protect libcanberra calls with a mutex because it is not thread safe. (#2834) + +## SPA + - Support older compilers for spa_clear_ptr(). + - Fix a bug in ALSA source where the available number of samples was miscaluclated + and resulted in xruns. (#3395) + - Don't set inotify on /dev but on the videoX devices directly. Setting inotify + on /dev would cause a lot of spurious wakeups and lock contention in the + fsnotify subsystem on some benchmarks. + - Audioconvert now rate limits the warnings when it runs out of buffers. (#3384) + +## pulse-server + - Some bugs and inconsistencies were fixed in device lookup. + - Improve subscribe event emission, detect changes to the sink or the monitor + and send the right sink/source event. (#3388) + +## JACK + - The libjack.so now has a minor version of 3 and a micro version of the pipewire + version. + - JACK clients will now see portregistration from other jack clients when they + activate/deactivate like real JACK. (#3260) + +## bluetooth + - Use some more autoptr cleanups, fix some leaks. + +# PipeWire 0.3.76 (2023-07-28) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a regression that would cause the MPV pipewire backend to fail because + of a spurious thread-loop signal. + - Fix a crash when DBus is not found. + - ALSA hires timestamps are now disabled by default. + - Some more fixes and improvements. + +## PipeWire + - A new option was added to pw-thread-loop to signal when the thread starts. + This is only used in module-rt to avoid regressions in mpv. (#3374) + - Fix a compilation problem. + - Stream flags now only set the properties when not already set. This fixes + a regression with node autoconnect. (#3382) + +## Tools + - pw-cat will now stop when the stream is disconnected. (#2731) + - Improve the pw-cat man page, mention that stdin/stdout handling is only + on raw data. + +## modules + - module-rt will now not crash when dbus is not available but error out as + before. + - A new VBAN (vb-audio.com) sender and receiver was added. (#3380) + +## SPA + - Add an option in audioconvert to disable volume updates. (#3361) + - ALSA hires timestamps are disabled by default because many drivers seem to + give wrong timestamps and cause extra delay. + +## bluetooth + - LE Audio support is now enabled by default when liblc3 is available now that + bluez has support for detecting the hardware features. + +# PipeWire 0.3.75 (2023-07-21) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Link permissions between nodes are now enforced. This avoids potential portal + managed screencast nodes to link to the camera even though it was not assigned + permissions to do so by the session manager. + - Libcamera and v4l2 devices now have properties so that duplicates can be + filtered out by the session manager. + - A bug with draining was fixed where a buffer would be marked EMPTY and would not + play when it contained drained samples. (#3365) + - Many fixes and improvements. + + +## PipeWire + - Permissions for links between nodes are now enforced. The link will now check + that the owner clients of the nodes can see each other before allowing the link. + This avoids screensharing clients to accidentally being linked to the camera + nodes by the session manager. A side effect is that patchbay tools will no longer + be able to link portal managed screencast nodes to the camera, for this we need + a new permission for those patchbay clients. (wireplumber#218) + - The stream.rules/filter.rules are now evaluated when connecting the stream/filter + so that more properties can be matched. (#3355) + - Move some internal events from the context to the nodes to better handle per-node + threads in the future. + - The thread-loop will now signal when the thread is started. + +## modules + - A timestamp workaround in module-raop was reverted because it does not work + in all cases. Instead latency was increased to 1.5 seconds, which also makes + the problematic device in question work. (#3247) + - The profiler module was reworked a bit to use the new node realtime events. It + should now also handle dynamically added and removed drivers. + - The module-rt now does the rtkit calls from a separate thread so that it does + not block the main thread. This could cause deadlocks during startup in some + cases. (#3357) + +## SPA + - Atomic operation macros were move from internal pipewire API to public API. + - The video-info structure now has a new SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED + flag to instruct the application to fixate the modifiers. This simplifies some + logic in applications a lot. + - The libcamera and v4l2 nodes now have properties to enumerate the device id + they are using. This can be used to match v4l2 devices and libcamera devices + and filter out duplicates. + - A bug with draining was fixed where a buffer would be marked EMPTY and would not + play when it contained drained samples. (#3365) + +# PipeWire 0.3.74 (2023-07-12) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a critical bug where audio to bluetooth devices would cut out + randomly. (#3316) + - Improve RAOP compatibility. + - Avoid crashes after an update. + - Small fixes and improvements. + + +## PipeWire + - Mix info on port is now created explicitly. + - Remove the node as a driver peer when stopping. This caused some problem + with playback on and other remote bluetooth devices. (#3316) + - Work on avoiding crashes when loading new modules that use internal API + with old libpipewire. This is typical after an update where the old library is + still loaded by an application but when a new stream is created, updated + modules are loaded. (#3243) + +## Modules + - The RTP source module now has an option to ignore the SSRC, which is + useful to continue to receive the stream when the sender is restarted. + - The native protocol will refuse to load twice now instead of silently + ignoring the error. + - module-raop is compatible with more devices. (#3247) + +## SPA + - plugins will now warn when running out of buffers. This is always a bad + thing. + - Merge scope based cleanup macros. + - Add ratelimit function. + +# PipeWire 0.3.73 (2023-07-06) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fixes an ALSA resume after suspend error. + - Handle and disable seemingly wrong hires timestamps from ALSA. + - Filter-chain now has loadable plugin modules. The LV2 and sofa plugins are + moved to a separate .so file to make things more modular. + - Rate changes in the graph should now be handled more gracefully by loopback + and filter-chain. + - A regression in the rtp-sap module was fixed where it would in some cases + fail to start. + - A potential crash in the peaks resampler was fixed. + - Many cleanups and other small bug fixes. + + +## PipeWire + - Fix a potential segfault when no fallback driver was set in the config. + - Improve OPUS detection. + - Add ASYNC flag to pw-filter and pw-stream when queue/dequeue is not called + from the process function. This ensure we allocate an extra buffer. + - Discard pending process callbacks when disconnecting. (#3314) + - Cleanups and improvements to the debug environment variable parsing. + - The graph rate was tweaked to better handle very low rates such as those + requested by pavucontrol when it does the signal monitoring. + +## Modules + - An example filter module was added. + - Filter-chain and loopback now disable the resamplers if no rate is specified + and will always follow the graph rate. + - Improve setup of filter-chain. The graph is now created when starting + because this ensure the target graph rate is known. + - Filter-chain can now link notify ports to control ports in the graph. + - Filter-chain now has loadable plugin modules. The LV2 and sofa plugins are + moved to a separate .so file. + - A regression in the rtp-sap module was fixed where it would in some cases + fail to start. + - Module-rt now has options to disable rlimits, portal and rtkit. + - module-raop-discover now has an options to set the latency. (#3247) + +## Tools + - pw-cat now supports overriding all stream properties. + +## SPA + - Disable rate negotiation when the resampler is disabled. We will always + follow the graph rate. + - Set device.icon property for UCM ports as well. + - Improve ALSA recover when using hires timestamps. This fixes some problems + after resume from suspend. (#3315) + - ALSA will now warn and disable hires timestamp when they seem wrong. + They can also be disabled manually with a property. + - V4l2 will now gracefully handle ENOTTY when enumerating frame sizes and + frame rates. (#3325) + - A potential crash in the peaks resampler was fixed. (#3320) + +## pulse-server + - A client crash in pavucontrol is avoided by always setting a card name. + - The graph rate is now taken correctly when using the FIX flags. (#3317) + - An option was added to ignore the FIX flags of a stream. Also the + documentation for those options was updated. (#3317) + - module-raop-discover now support latency_msec. (#3247) + +## Bluetooth + - Remove an assert and issue a warning/recover instead when a buffer is too + small. + +## GStreamer + - The device provider does locking when destroying the registry. + +# PipeWire 0.3.72 (2023-06-26) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a critical bug that would refuse to update the samplerate or + buffersize in JACK clients. (#3226) + - A new module-netjack2-driver and module-netjack2-manager were added + that are compatible with NETJACK2. This allows PipeWire to become + a NETJACK2 manager or a driver between JACK2 or PipeWire servers. + - Support was added for firewire devices with FFADO. This is untested + for now and MIDI is not implemented yet. + - The node scheduling was optimized some more. External drivers are now + as efficient as in-server ones. This should improve performance of + various drivers such as bluetooth and JACK based drivers. + - Many, many bug fixes and a ton of improvements. + + +## PipeWire + - pw-filter can now be used to write sinks and sources. + - The node activation for drivers was changed. The driver now does not + need to go to the server to start the processing cycle. This makes + out-of-server drivers as efficient as in-server drivers. + - Don't try to use drivers with 0 priority as fallback drivers. This + avoids making the screencast driver a driver for audio. (#3219) + - Improve xrun count reporting in pw-top and the profiler. Now each + node has their own xrun counter updated when it fails to complete + processing during the cycle. + - pw-filter now also has support for TRIGGER. + - A potential fd leak was found when fds were send to a zombie client. + (#1840) + - Fix a bug where monitor or capture streams were logged twice in the + profiler. (#3278) + - Remove stream hooks safely. (#3251) + - A bug in serialization of container properties was fixed. This could + result in truncated property values. (#3290) + - The PIPEWIRE_AUTOCONNECT environment variable now always overrides the + autoconnect settings of streams. (#3299) + - Node, port and link destroy now avoids some useless work. + - Port will now try to renegotiate a new format when idle. (#3266) + +## Modules + - The module-sap now is more compatible with AES67. + - A new FFADO driver module was added. This is completely untested because + of lack of hardware. Please test and report issues. + - A new NETJACK2 driver and a NETJACK2 manager module were added. These + should be drop in replacements for the JACK2 parts. + - The RAOP discover module now tries harder to only list devices once. + - The zeroconf discover module now tries harder to only list devices once. + - The RAOP sink module now handles latency better and is compatible with + some more devices. (#3247, #3282) + - The loopback and filter-chain modules now always dequeue the last input + buffer to avoid stuttering in some cases. (#3276) + - The SPA node factory module can now also export nodes. This is used to + export the PTP clock from the AES67 config file. + - A bug in module-jack-tunnel was fixed that would cause stuttering and + corrupted output in some cases. (#3255) + - The resampler is now disabled in module-loopback and filter-chain when + the samplerate is set to follow the graph rate. (#2969) + - The way the mixer peer is sent to clients was improved. It is now also + possible to let a remote node know about mixer port removes, which + can avoid memory leaks and some code simplifications. + +## SPA + - Monitor ports now report latency correctly. + - The ALSA plugin now uses htimestamp to get a more accurate ringbuffer + position to estimate the clock skew. + - The channelmixer now has min/max-volume settings to limit or fix the + volume. + - The ALSA plugin can now control the playback and capture rate of USB + gadgets. This can avoid resampling and instead use the USB feedback + to control the rate. + - The ALSA output to multiple devices has been improved, some lockups + are avoided when the device ringbuffer is full. + - The compress-offload sink has improved negotiation. + +## pulse-server + - Only try to use GSettings when the schema exists. + - @DEFAULT_SOURCE@, @DEFAULT_SINK@ and @DEFAULT_MONITOR@ are now correctly + handled as targets in playback and capture streams. (#3284) + - 2 new quirks are added to disable volume updates on sinks/sources. + (#1517) + - The virtual-sink and virtual-source modules were added. These are really + example modules but actually also work and are useful on PulseAudio so + implement them as well. + - Fix initial stream volumes. (#3306) + +## Bluetooth + - Only register A2DP or BAP when we have codecs. + - Include codec into the media.name + +## JACK + - Fix a critical bug that would refuse to update the samplerate or + buffersize. (#3226) + - Improve updates of samplerate/buffersize, delay the updates until the + client is activated. (#3297) + - Use the new mix-info updates to simplify the mixer setup and peer + detection. + +## GStreamer + - Fill default strides instead of 0 on pipewire video buffers. (#3236) + +# PipeWire 0.3.71 (2023-05-17) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - A new zero-latency jackdbus bridge was added. This works similar to what + PulseAudio has to offer and creates a sink/source when jackdbus is + started. It is however much more efficient and runs the complete PipeWire + graph as a synchronous JACK client with no added latency. + - Many performance improvements. Activation of remote nodes is more + efficient, fewer eventfds are required on the clients, less callback + overhead in performance critical paths and an optimized poll function + was added. This was mainly driven by the jackdbus module to get the lowest + possible overhead when running the graph. + - The JACK notify callback implementation was reworked to emulate better what + JACK does, improving compatibility with ardour7 and the JACK stress test. + - More work on BAP devices. Device latency is now passed on to + applications also for multi-device headsets, and channel allocation + is handled better. + - Many more improvements and bugfixes. + + +## PipeWire + - Remove the hardcoded limit on io_areas. This is used to link nodes together + and exchange buffers, it was limited to 2048 but now dynamically scales + based on requirements. + - Rate and quantum changes are now applied correctly in more cases. (#3159) + - Updates to client-node to more efficiently process the driver. + - The profiler information was improved to be more accurate. It should + now work better for remote drivers. + - Some potential memory map errors were fixed in the protocol because in some + case with large messages, some fds were closed too soon. + - pw-filter now implements the pw_filter_set_active() method. + - A potential out-of-buffers case was fixed in capture pw-streams where buffers + were not moved to the recycle queue when the node suspended. + - Nodes are now always woken up with the eventfd. Previously there were + some optimizations in the server to directly call into the node process + function but that optimization is not necessary. Without this optimization + it is now possible to run nodes in different threads. + - pw-stream trigger is now implemented correctly in all cases. + - Remote nodes now use one eventfd less because they get triggered with the + node eventfd directly. + - Monitor ports are now ignored in latency updates. + - A potential race when reporting an error to a client was fixed. (#3192) + - Fix a bug where always_process nodes would sometimes IDLE. (#3189) + - Optimize peer activation. Nodes are now activated more efficiently and + independent of the number of links. It also reduces the number of eventfds + and memory in remote clients. + - A bug in property serialization was fixed. Values with spaces would only + serialize the first part of the value. + +## Modules + - Correctly handle the echo-canceler plugin init method fallback. The + samplerate was not correctly configured. This is only a regression for people + that have external echo-canceler plugins. + - RAOP sink now only sets the volume on the remote end when the stream is + recording. (#3175) + - RAOP discover now tries to deduplicate entries from the same host. + - A new zero-latency jackdbus bridge was added. This works similar to what + pulseaudio has to offer and creates a sink/source when jackdbus is + started. It is however much more efficient and runs the complete PipeWire + graph as a synchronous JACK client. + - The access module uses a more secure way to check the application + executable. + - module-combine-stream now has configurable delay and latency for each + stream. This can be used to align sinks/sources with different latencies. + - A potential crash in module-pulse-tunnel was fixed when shutting down. + (#3199) + - Module-rt will now clamp the nice value to the min allowed value to avoid + errors from rtkit. (#3186) + - Fix a bug with the session counters in module-rtp-sap. Also use the right + format for L24. Improve the AES67 example config. + - Improve some warning and info messages in module-rt. (#3194) + - module-rtp-session should now do something when started without arguments. + - A potential crash in module-rtp-session was fixed. (#3217) + - module-filter-chain has better error reporting when a convolver fails to + load. (#3223) + +## SPA + - Move some things around to avoid compiler warnings. (#3171) + - Increase mixer ports. Reorganize some things and bump mixer input ports + from 128 to 512. + - Fix a potential crash when a node is scheduled before it completes + the setup. + - The JACK sink and source SPA plugins have seen some improvements. + - Allow the peaks resampler still if we disabled resampling. + - Perform more cleanup in audioadapter when in error. + - An optimized non-cancellable loop implementation was added. + - Callbacks were optimized with a _fast() varsion that doesn't check the + version and method. When this check is performed earlier, it can + be skipped in performance critical places. + - Some of the callbacks and system methods are now using the fast function + calls in critical paths. + - A potential division by zero was fixed in the ALSA plugins. + - Improve rate and quantum when starting audioconvert. + - Make it possible to override node.driver in the SPA null-audio-driver. + (#3220) + +## pulse-server + - The audio info parameter parsing was refactored and improved. + - Fix some races with clients exiting when playing samples. + - An option was added to change or disable the dbus name registration. + (#2987) + +## Bluetooth + - Implement battery reporting using AT+XEVENT. + - Disable hardware volume for 3M WorkTunes. + - Implement BAP audio locations (channel positions) by using the new + bluez properties. + +## JACK + - Fix some errors reported by JACK test.cpp. (#2638) + - Add jack.show-midi option to show/hide midi ports. + - Add jack.max-client-ports option. JACK also has a port limit and so + PipeWire needs it as well to make the tests happy. + - Call the shutdown callback only when the server stopped, not when there + is a random error. (#3070) + - Avoid registering the same port name twice. + - Call port registration callbacks in activate/deactivate. + - Improve jack_port_connected(). + - Improve some error reporting. + - The JACK headers were updated to a newer version. + - JACK callbacks are now managed with an event queue to simulate + more what JACK does. This avoids emitting callbacks when a method is blocking + for a reply and causing deadlocks. (#3183) + - Assign unique names to JACK clients. (#2833) + - Fix a potential crash when the thread_utils was used after free. + - Aliases are now not filled in by default to improve JACK compatibility. + (#3154) + +# ALSA + - The ALSA plugin will now wait for negotiation to complete or an error + before _prepare() completes. This makes more applications deal correctly + with the potential errors. + +# Docs + - A new document about how scheduling is implemented was added. + - Update the pw-cli man page. (#2988) + - Document the SPA Pod serialization. + - Document the PipeWire native protocol. + +# PipeWire 0.3.70 (2023-04-20) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a regression in the scheduler that could keep some nodes IDLE. + - Fix a regression in the biquad filters in filter-chain. + - Fix a regression and potential crash in the ALSA mixer probing. + - Fix a regression in pipewiresrc with timestamps that could cause cheese + to record video with wrong timestamps. + - Beamforming support was enabled in the echo-canceler. + - pulse-tunnel and raop-sink will now proxy local volume changes to the + remote end. + - More bugfixes and improvements. + + +## PipeWire + - Fix a bug in the graph scheduler where some nodes might stay IDLE in + some cases (like when connecting the source of the echo-canceler to the + sink). + - pw-metadata can now be created from the factory with initial values for + the metadata. (#3076) + - Conditions were added to the pipewire config file to make it possible to + configure the access module and the exec sections. + - Support was added in pw-stream to intercept and override properties for + the adapter. This can be used to implement custom volume control, for + example. + +## Tools + - pw-metadata can now list all available metadata objects with the -l + option. + - A new pw-config tool was added to debug configuration file loading and + parsing. + +## Modules + - The webrtc echo canceler now supports beamforming. You can provide the + coordinates of the microphones and let webrtc perform beamforming on + the captured samples to improve quality and remove noise. + - Fix a regression in the filter-chain with biquad filters. (#3161) and + improve error reporting. + - The pulse-tunnel will now proxy the volume changes to the remote end. + - The RAOP sink will now send volume parameters to control the volume + remotely. (#2061) + +## SPA + - One ALSA commit was not correctly reverted and might cause crashes. + - The ALSA sink and source now calculate the ALSA ringbuffer memory + location more correctly which might improve compatibility with some + hardware. + - v4l2 now sets the values of the controls in the Props param. + +## Pulse-server + - The echo-canceler aec_args are now parsed like they would be under + pulseaudio. + +## Bluetooth + - More work on synchronizing BAP devices. + +## GStreamer + - The GStreamer source can now renegotiate the format when it changes. + - The GStreamer source now uses the BaseSrc clocking code to implement + the clock and timing code. + +# PipeWire 0.3.69 (2023-04-13) + +This is a quick bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Reverted the UCM changes, they seem to cause regressions causing audio + to be muted in some cases. + - Fix a regression in the scheduler where a driver node might not be marked + runnable in some cases, like when echo-cancel is used. (#3145) + - Handle links from the driver to itself. This makes the midi bridge work + again. (#3153) + - ALSA rate matching for sources was fixed. It would previously wait too + long for rate matching and then cause drift. This should reduce + crackling and stuttering when capturing in low latency. + - Fix the GStreamer clock to make cheese video recording work again. (#3149) + - More fixes and improvements. + +## PipeWire + - Fix a regression in the scheduler where a driver node might not be marked + runnable in some cases, like when echo-cancel is used. (#3145) + - Handle links from the driver to itself. This makes the midi bridge work + again. (#3153) + - Some man pages were improved. + - Fix a potential crash when thread-loop is destroyed before the loop. + (#3150) + +## Modules + - A new raw biquad filter was added to filter-chain. You can manually set the + 6 parameters and you can use this to create custom filters per sample rate. + (#3139) + - The echo-canceler now supports different channels for the capture and playback + streams. + +## SPA + - A SB Audigy specific profile set was added to make better use of the + controls. (#2934) + - More ALSA IRQ based scheduling improvements. + - ALSA rate matching for sources was fixed. It would previously wait too + long for rate matching and then cause drift. This should reduce + crackling and stuttering when capturing in low latency. + - The echo-cancel plugin API has a new method to make it possible to have + different channels for capture, source and playback. + - Reverted the UCM changes, they seem to cause regressions causing audio + to be muted in some cases. + +## Bluetooth + - Many more BAP fixes and improvements. Devices are now created as a set + and can be combined into one device by the session manager. + +## GStreamer + - Fix the GStreamer clock to make cheese video recording work again. (#3149) + +# PipeWire 0.3.68 (2023-04-06) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +This release contains a huge number of changes, some of which might cause +regressions. Please report anything that seems to fail after the upgrade. +UCM devices in particular might have changed names, profiles and ports that +might require changes in custom scripts. + +## Highlights + - Symbolic links to the pipewire binary are now used instead of recompiling + the same binary multiple times. + - Changes to the graph scheduler related to quantum/rate updates and + calculation of the node states. Things should start and switch between + quantums and rates more smoothly now and especially virtual devices should + now only run when required. + - A new RTP session module was added. This uses the Apple MIDI protocol + to configure low-latency bidirectional MIDI (and with a PipeWire specific + extension, also audio) between machines. OPUS encoding was added to the + RTP formats. The SAP module was separated from the rtp-sink/source module + to make it more usable. + - A new runtime debug property was added to all streams and nodes to trigger + a save of the raw samples to a wav file. Support for this has also been + added to the echo-canceler to debug potential issues. + - Module pulse-tunnel has improved rate matching and synchronization + support. It should also not drift anymore for capture devices. + - The link-factory now ignores by default the link.passive property. This means + that tools like pw-link or jack clients and wireplumber can't make passive + links anymore. The reason is that there is now much more advanced logic in + PipeWire itself to handle passive links based on node and port properties. + - The RAOP sink was ported to new OpenSSL functions. Digest passwords are + handled correctly now and support for more devices was added. + - The ACP code was updated with new PulseAudio UCM code: "Create multiple + profiles per verb for conflicting devices". This might change the names + of devices, profiles and ports so scripts might need to be updated. + - Upmixing is disabled again by default. We now ship config files that + distros can install to enable upmixing again. The reason being that PipeWire + should not apply fancy DSP processing to audio by default. + - Many cleanups and bugfixes, including some crashes and memory corruption + bugs. + +## PipeWire + - Various FreeBSD compilation fixes. + - Don't crash when calling _connect twice in stream/filter. (#3091) + - Links are now installed instead of compiling the pipewire binary + multiple times. + - There is now a new core event bound_props that augments the bound_id event + with the global properties. This can be used to get the global.serial among + other global properties. It also makes it possible in the future to let the + server allocate unique names or uuids. + - Fix a bug where the server could go into an infinite reconfigure loop when + the samplerate of a driver would change. + - When a samplerate was forced, restore the previous best samplerate when the + samplerate is no longer forced. (#2133) + - Rework how the states of the nodes in the graph are calculated. A more + refined algorithm is now used that only runs nodes that need to run. + - Rework how the quantum change is applied to the graph. Drivers are now + responsible for using the new updated rate/quantum before starting a new + cycle. This avoids starting a cycle with an old quantum first. + - pw-stream and pw-filter will now ensure that the Trigger event is called + from the main thread. + - node.force-rate=0 will now force the node.rate on the graph, forcefully + switching the hardware into the new rate if possible. (#3026) + - Additional checks were added to the thread-loop to check locking order. + - Additional checks were added to pw-stream and pw-filter to check if methods + are called from the right thread context. + +## modules + - A new RTP session module was added. This uses the Apple MIDI protocol + to configure bidirectional MIDI (or audio) between machines. + - SAP support was removed from module-rtp-source and module-rtp-sink and + moved to a separate module. This makes it possible to use the RTP modules + without SAP support as well. + - The echo-cancel module now has support to save the signals to a wav + file for debugging purposes. + - The RTP modules now have support for the OPUS codec. + - The RAOP module was ported to new openssl encryption functions and handles + digest passwords correctly now. + - module-raop-discover now has match rules to be able to select the streams + and set properties. + - Module pulse-tunnel has improved rate matching and synchronization + support. (#3093) + - Fix potential memory corruption and infinite loops because + module-pulse-tunnel was unloaded from the wrong thread. + - The link-factory now ignores by default the link.passive property. This means + that tools like pw-link or jack clients and wireplumber can't make passive + links anymore. The reason is that there is now much more advanced logic in + PipeWire itself to handle passive links based on node and port properties. + - module-echo-cancel will now clear its buffers after a suspend to avoid + playing stray samples. + - module-raop-sink will now handle 0 timing_port replies. (#3133) + +## SPA + - The adapter module now has support for saving the raw audio to a wav + file for debugging purposes. + - The ACP code was updated with new PulseAudio UCM code: "Create multiple + profiles per verb for conflicting devices". This might change the names + of devices, profiles and ports so scripts might need to be updated. + - Upmixing was disabled again by default. We now ship config files that + distros can install to enable upmixing again. (#3081) + - audioadapter and audioconvert have seen improvements in the experimental + non-DSP/passthrough mode. + - Fix a potential race where the dummy drivers could fail to stop a timer + and cause endless warnings in the logs. + - The ALSA plugin has experimental support for IRQ based scheduling. This + should decrease latency for some (mostly USB) drivers. This should bring + latency within JACK latency. More work on this will be done before the + 1.0 release later this year. + - Audioconvert now has support for volume ramping. (#3046) + - A new loop method was added the check if a thread is currently running the + loop. + - channelmix.disable and resample.disable now generate an error when true + and channelmixing or resampling is required in the converter. + +## Bluetooth + - Fix a crash in some cases when a device was disconnected. + - Support async transport state changes. This avoids some lockups when the + bluetooth backend is having issues. (#3023) + - Align BAP sinks. This improves synchronization between earpieces. + +## ALSA + - Improve properties in pw-top and pavucontrol. + +## pulse-server + - Improve error handling from pulse-tunnel. + - Generate silence correctly for unsigned formats as well. + - Review buffer params. The streams should now just work with 1 or 2 + buffers. + - module-rtp-send and module-rtp-recv now have support for the OPUS codec. + +# JACK + - Make sure we don't call any callbacks anymore when deactivating. (#2781) + +## GStreamer + - Sort the device by priority in deviceprovider. (#3072) + +# PipeWire 0.3.67 (2023-03-09) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - The loopback module and other couples streams will now not randomly + fail in some cases. (#3028) + - The RTP module now has support for sending and receiving MIDI as + well. + - The compress offload sink has seen many improvements. It now uses + ioctls directly to bypass limitations of tinycompress (to be able + to detect the available codecs, for example). + - Pulse server compatibility was improved for some apps by improving + the format parsing and FIX_ stream flag handling. + - The min quantum in the pulse server was changed from 256/48000 to + 128/48000 to fix some issues with games that expect 5ms or less of + latency. + - The Bluetooth plugin has seen many improvements in packet scheduling + to attempt to reduce stuttering on some devices. + - The ALSA plugin now handles some impossible cases better. This fixes + recording in QEMU again. (#2971) + +## PipeWire + - SPDX tags were added to the code for copyright information. + - The random number functions were made more usable. + - The port property code was moved from the adapter to the port + implementation itself to make it more useful and unified for the + cases where no adapter is used (midi and video). + - Fix a potential overflow in mixer areas. + - Improve runnable state calculations of nodes. This is part of + ongoing work to avoid running nodes that should not need to run. + - The stream will now always call the process function when using + trigger, even if there are no buffers. This avoids stalls of the + processing graph in some cases. (#3028) + - Links are now marked as passive by PipeWire itself so that + the right thing happens in all cases. + - Implement the in/out/true values for the node.passive property. + Place a passive state on ports to make passive links on a port + by port basis. + +## Tools + - pw-cat has seen improvements in the encoded file playback case. + +## Modules + - The rtp module has support for MIDI now. + - DSCP is now configurable in the RTP module. + - The loopback module doesn't randomly fail to work anymore. (#3028) + +## SPA + - The null-audio sink can now be given a format and it will return this + instead of the default float ones. This makes it possible to make a + null-sink that has a given format. + - The compress offload sink has seen many improvements. It now no longer + uses tinycompress to be able to detect the available codecs. + - The ALSA plugin now handles some impossible cases better. (#2971) + - Fix compilation on older compilers. (#3050) + +## Pulse-server + - The FIX_ flags are now implemented more correctly by fixating the + stream to the format of the sink/source they ask to be connected to. + There is now also an option to override the fixation based on rules. + - Format parsing was improved and should now support all format strings + supported by pulseaudio including upper and lower case variants + and shortcuts. + - Channelmap parsing was improved and should now reject invalid + channelmaps as well as support the shortcuts supported by pulseaudio. + - Escape codes in module arguments now work as it does in pulseaudio. (#3071) + - The min quantum was changed from 256/48000 to 128/48000 to fix some + issues with games that expect 5ms or less of latency. + +## JACK + - jack.passive-links can now be used to have a JACK client make passive + links and the node.passive property is no longer used for this because + it has a different function. + - The qsynth rule was updated to the new node.passive features. It is now + only passive on the output side. + +## Bluetooth + - BAP delay and transport latency are handled now. + - A2DP and SCO can now use bigger buffers to improve quality when the + reception is jittery. + - The AT+BCC command is now implemented. + - Packet encoding now happens ahead of time when possible to avoid delays + before sending it. + - Source should now always produce complete (padded) buffers to avoid sync + problems. + - Don't set unnecessary socket options. + +## GStreamer + - The pipewiresrc now has an autoconnect argument. + - The metadata plane count is now handled correctly in more cases. + - Stream errors are now handled correctly to stop the GStreamer elements. + +# PipeWire 0.3.66 (2023-02-16) + +This is a bugfix release that is API and ABI compatible with previous +0.3.x releases. + +## Highlights + - Fix a regression in the pulseaudio module-combine-stream because the new + module-combine-stream was not installed. + - PipeWire can now generate a limits.d config file with our recommended + settings for priorities and memlock. + - Modules, exec and objects can now be loaded depending on conditions. One + example is the X11-bell module that can now be disabled with a custom + property override. + - Filter-chain has a new mysofa based spacializer plugin. + - Support was added for different clocks that allow the RTP modules to work + with a PTP clock, for example. + - Many bugfixes and improvements. + + +## PipeWire + - Avoid rate switches when the graph is idle. + - The rate selection algorithm was improved. This ensures minimal performance + and quality loss when resampling. + - The default min.quantum was set to 32 again after it got erroneously changed + to (the too low) 16 in version 0.3.45. + - Fix compilation issues with rust bindings because of macros in defines. + Work around it for now. (#2952) + - Invalid file mappings are now refused (#2617 #2914 #3007) + - Modules, exec and objects can now be loaded depending on conditions. One + example is the X11-bell module that can now be disabled with a custom + property override. + - Filter now also supports _trigger_process() to drive the graph. + - TID is now added to the journald log. + - PipeWire generates and installs */etc/security/limits.d/25-pw-rlimits.conf* + that by default contains project's recommended settings. Creation of the + pipewire group is left to the distro or user ( `groupadd -r pipewire` ). + See the rlimits-* Meson options for controlling this behavior. + - Additionally there is now by default disabled Meson option that will + install */etc/security/limits.d/20-pw-defaults.conf* with the current Linux + default memlock value. Distros with only kernels >=5.16 or always using + systemd v251 or newer do not need this. But all other builds should set the + `-Dpam-defaults-install=true` Meson option to ensure that the memlock value + is always large enough. Thanks to Rickie Schroeder for pointing out that + the default Linux memlock value has been somewhat recently increased. + +## modules + - Install module-combine-stream. + - RTP source now has support for custom channel names. + - RTP source will now stop when inactive. + - Filter-chain has a new mysofa based spacializer plugin. + - The RTP modules can now use direct clock timestamps to send and receive + packets. This makes it possible to synchronize sender and receiver with + a PTP clock, for example. + - Filter-chain now has an invert plugin to invert the polarity of a + signal. (#3008) + +## SPA + - There is now an option to set the channels used for probing Pro Audio + devices. This could unlock more samplerates for some devices when they are + probed with fewer channels. (#2990) + - Support was added for other clocks than the MONOTONIC clock in the + driver nodes. This can be used to synchronize the graph to a PTP clock, + for example. + - The ALSA source has some more headroom when rate matching to avoid + stuttering when following another driver. + - libcamera controls are now mapped to standard PipeWire property values. + - The channelmixer has seen some improvements. MONO and undefined channel + layouts are now upmixed and downmixed more correctly. (#3010) + +## Bluetooth + - Many BAP support fixes. + +## GStreamer + - The gstreamer elements now support buffer video metadata so that strides + are correctly handled. + - pipewiresrc will now error out correctly in more cases. (#2935) + +## JACK + - The frame to/from time functions are improved to also work with negative + time and frame offsets. + +# 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 advertise DMABUF support if the pipeline supports + this. + - pipewiresrc will now always be a live source unless told otherwise. + +# 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 battery 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 adjustment 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 receive 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 adjustment 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 receive 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 reasonably 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 adjustments 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 + awful 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 frequencies + 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 specialization. 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 default 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 emitted. + +## 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 applications. + - 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 minimum 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..6282e8f --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# 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=` to increase the debug level (or use one of + `XEWIDT` for none, error, warnings, info, + debug, or trace, respectively). +* `PIPEWIRE_LOG=` to redirect log to filename +* `PIPEWIRE_LOG_SYSTEMD=false` to disable logging to systemd journal +* `PIPEWIRE_LATENCY=` 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=` to configure a rate for the graph. +* `PIPEWIRE_QUANTUM=` to configure latency as a fraction and a + samplerate. This function will force the graph samplerate to + `denom` and force the specified `num` as the buffer size. +* `PIPEWIRE_NODE=` 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 +``` + +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..4d9dc63 --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,76 @@ +PROJECT_NAME = PipeWire +PROJECT_NUMBER = @PACKAGE_VERSION@ +OUTPUT_DIRECTORY = "@output_directory@" +FULL_PATH_NAMES = YES +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 +GENERATE_DEPRECATEDLIST= NO +QUIET = YES +WARN_NO_PARAMDOC = YES +HAVE_DOT = @HAVE_DOT@ +INPUT = @inputs@ +FILTER_PATTERNS = "*.c=@c_input_filter@" "*.h=@h_input_filter@" "*.md=@md_input_filter@" +FILE_PATTERNS = "*.h" "*.c" +RECURSIVE = YES +EXAMPLE_PATH = "@top_srcdir@/src/examples" \ + "@top_srcdir@/spa/examples" \ + "@top_srcdir@/doc/examples" \ + "@top_srcdir@/doc/dox" +EXAMPLE_PATTERNS = "*.c" "*.inc" + +GENERATE_MAN = YES +MAN_EXTENSION = 3 + +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +IGNORE_PREFIX = pw_ \ + PW_ \ + spa_ \ + SPA_ +GENERATE_TREEVIEW = YES +SEARCHENGINE = YES +GENERATE_LATEX = NO + +TOC_INCLUDE_HEADINGS = 0 +LAYOUT_FILE = @layout@ + +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = PA_C_DECL_BEGIN= \ + 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/DoxygenLayout.xml b/doc/DoxygenLayout.xml new file mode 100644 index 0000000..b5d353a --- /dev/null +++ b/doc/DoxygenLayout.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/custom.css b/doc/custom.css new file mode 100644 index 0000000..46e58dd --- /dev/null +++ b/doc/custom.css @@ -0,0 +1,60 @@ +: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; + } +} + +#nav-tree .arrow { + opacity: 1; + padding-right: 0.25em; +} + +.textblock h1 { + font-size: 150%; + border-bottom: 1px solid var(--page-foreground-color); + margin-top: 1.5em; +} +.textblock h2 { + font-size: 120%; + margin-top: 1.5em; +} +.textblock h3, .textblock h4, .textblock h5, .textblock h6 { + font-size: 100%; + font-style: italic; + font-size: medium; + margin-top: 1.5em; +} + +.textblock dl.section dd { + margin-left: 2rem; +} + +ul.multicol li { + word-break: break-word; + padding-left: 3em; + text-indent: -3em; +} + +ul.multicol li a.el { + font-weight: normal; +} + +div.contents div.toc li { + word-break: break-word; + padding-left: 2em; + text-indent: -2em; +} diff --git a/doc/dox/api/index.dox b/doc/dox/api/index.dox new file mode 100644 index 0000000..9626866 --- /dev/null +++ b/doc/dox/api/index.dox @@ -0,0 +1,90 @@ +/** \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_spa +- \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/dox/api/spa-buffer.dox b/doc/dox/api/spa-buffer.dox new file mode 100644 index 0000000..ddd0935 --- /dev/null +++ b/doc/dox/api/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 + * || | ... | 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 + * ||| | ... | 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 + * || | ... | + * || +------------------------------+ + * +->| struct spa_chunk | memory for n_datas chunks + * | | uint32_t offset | + * | | uint32_t size | + * | | int32_t stride | + * | | int32_t dummy | + * | | ... chunks | + * | +------------------------------+ + * +>| data | memory for n_datas data, aligned + * | ... 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/dox/api/spa-design.dox b/doc/dox/api/spa-design.dox new file mode 100644 index 0000000..f4a8a35 --- /dev/null +++ b/doc/dox/api/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 static 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 `` and a +number of utility functions, see \ref spa_utils. + +*/ diff --git a/doc/dox/api/spa-index.dox b/doc/dox/api/spa-index.dox new file mode 100644 index 0000000..f7f5ffe --- /dev/null +++ b/doc/dox/api/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 +#include + +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/dox/api/spa-plugins.dox b/doc/dox/api/spa-plugins.dox new file mode 100644 index 0000000..af14d5e --- /dev/null +++ b/doc/dox/api/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: # + continue + + interface_info = get_next_interface_info(factory) + if info->type != SPA_TYPE_INTERFACE_Log: # + 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. `` 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 +`` 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/dox/api/spa-pod.dox b/doc/dox/api/spa-pod.dox new file mode 100644 index 0000000..0bba2a4 --- /dev/null +++ b/doc/dox/api/spa-pod.dox @@ -0,0 +1,1034 @@ +/** \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(4, // 4 values follow + 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 id, type, subtype, format, rate, channels; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, &id, + 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 id, type, subtype, format, rate = 0, channels = 0; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, &id, + 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 id, type, subtype; +struct spa_pod *format; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, &id, + 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(4, // 4 values follow + 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(3, // 3 values follow + 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 + +A POD always starts with a size/type pair of uint32_t in native endianness, +followed by size in bytes of the payload data and padding. See +\ref page_spa_pod for more details. + +The payload is always padded to 8 bytes so that a complete pod is always +a multiple of 8 bytes. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | payload ... | + . | ... padding . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The total size of the POD is thus ROUND_UP_8(8 + size). + +# POD Types + +Here follows the layout of the POD types. + +## None (1) + +Type 1 is the None type or the null pointer. It has a size of 0 and thus +no payload. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Bool (2) + +Type 2 is the Bool type. I contains a true or false value. The value is +stored in a int32, a value of 0 is false, any other value is true. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 2 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value (int32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Id (3) + +An id is stored as a uint32. The id refers to an index in a table where more +information about the value can be found. This is typically a type table +containing some well known ids. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 3 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | id (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Int (4) + +A 32 bit signed integer. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value (int32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Long (5) + +A 64 bit signed integer. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 5 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value (int64) | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Float (6) + +A 32 bit float value. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 4 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 6 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value (float32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Double (7) + +A 64 bit float value. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 7 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | value (float64) | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## String (8) + +A string. This does not have to be valid UTF8 but it is 0 terminated. +The size field is set to the length of the string, including the 0 +byte. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | chars .... | + . . + | ... 0 | padding.. | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + + +## Bytes (9) + +A byte array. The size field is set to the number of bytes. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 9 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | bytes .... | + . . + | | padding.. | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Rectangle (10) + +A Rectangle. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 10 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | width (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | height (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Fraction (11) + +A Fraction. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 11 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | num (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | denom (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Bitmap (12) + +A bitmap. Stored as bits in uint8. size is the number of bytes with +bits. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 12 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | bits (uint8) ... | + . . + | | padding.. | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Array (13) + +An array is an array of (basic) types. In principle the array can contain +any type as long as each item in the array has the same child_size and +child_type. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 13 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child_size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child_type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child1 (child_size bytes) ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | childN (child_size bytes) ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +We describe Array types with a shortcut like: + +``` + Array[Int](,,...) +``` + +## Struct (14) + +Multiple PODs can be combined into a struct: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 14 (Struct) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | type1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | payload 1... | + . | ... padding . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | sizeN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | typeN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | payloadN ... | + . | ... padding . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +We describe Struct types with a shortcut like: + +``` + Struct( + : , + : , + ...) +``` + +The type of a struct is 14 and the size the total sum in bytes of all +PODs (with padding) inside the struct. + +## Object (15) + +An object contains a set of of properties. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 15 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | object_type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | object_id | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | property1 | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | propertyN | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +object_type is one of the well defined object types. +object_id is extra information about the context of the object. + +Each property is as follows: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | key (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | flags (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | POD value ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +Object are written with a shortcut as: + +``` + Object[type,id]( + key1: , + key2: , + ...) +``` + +## Sequence (16) + +A sequence is a series of times events. It is usually used for transporting +MIDI and control updates. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 16 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unit | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | pad | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | control1 | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | controlN | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The unit field and pad is currently set to 0. + +Each control look like: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | offset (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | type (uint32) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | POD value ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- offset: the offset relative to the current graph clock time. +- type: the type of control, see enum spa_control_type + + +## Pointer (17) + +A generic pointer to some memory region. Pointer types are usually not serialized. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 16 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 17 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | padding (must be 0) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | native pointer value ... | + . | .. padding . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + + +## Fd (18) + +A file descriptor stored as int64. When serializing, the file descriptor +is modified to contain the index of the fd in the message. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 8 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 18 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | fd (int64) ... | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +## Choice (19) + +A choice contains an array of possible values. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 19 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | flags | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child_size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child_type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | child1 (child_size bytes) ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | childN (child_size bytes) ... | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- type: one of possible values, see enum spa_choice_type + - None (0) : only child1 is an valid option + - Range (1) : child1 is a default value, options are between + child2 and child3 in the value array. + - Step (2) : child1 is a default value, options are between + child2 and child3, in steps of child4 in the value array. + - Enum (3) : child1 is a default value, options are any value from + the value array, preferred values come first. + - Flags (4) : child1 is a default value, options are any value from + the value array, preferred values come first. +- flags: must be 0 + +## Pod (20) + +The value id the POD itself. + +\addtogroup spa_pod + +See: \ref page_spa_pod + +*/ diff --git a/doc/dox/config/index.md b/doc/dox/config/index.md new file mode 100644 index 0000000..8617534 --- /dev/null +++ b/doc/dox/config/index.md @@ -0,0 +1,53 @@ +\page page_config Configuration + +One of the design goals of PipeWire is to be able to closely control +and configure all aspects of the processing graph. + +A fully configured PipeWire setup runs various pieces, each with their +configuration options and files: + +- **pipewire**: The PipeWire main daemon that runs and coordinates the processing. + +- **pipewire-pulse**: The PipeWire PulseAudio replacement server. It also configures + the properties of the PulseAudio clients connecting to it. + +- **wireplumber**: Most configuration of devices is performed by the session manager. + It typically loads ALSA and other devices and configures the profiles, port volumes and more. + The session manager also configures new clients and links them to the targets, as configured + in the session manager policy. + +- **PipeWire clients**: Each native PipeWire client also loads a configuration file. + Emulated JACK client also have separate configuration. + +# Configuration Settings + +Configuration of daemons: + +- \ref page_man_pipewire_conf_5 "PipeWire daemon configuration reference" +- \ref page_man_pipewire-pulse_conf_5 "PipeWire Pulseaudio daemon configuration reference" +- [WirePlumber daemon configuration](https://pipewire.pages.freedesktop.org/wireplumber/) + +Configuration of devices: + +- [WirePlumber configuration](https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html) +- \ref page_man_pipewire-props_7 "Object property reference" +- \subpage page_config_xref "Configuration Index" + +Configuration for client applications, either connecting via the +native PipeWire interface, or the emulated ALSA, JACK, or PulseAudio +interfaces: + +- \ref page_man_pipewire-client_conf_5 "PipeWire native and ALSA client configuration reference" +- \ref page_man_pipewire-jack_conf_5 "PipeWire JACK client configuration reference" +- \ref page_man_pipewire-pulse_conf_5 "PipeWire Pulseaudio client configuration reference" + +# Manual Pages + +- \subpage page_man_pipewire_conf_5 +- \subpage page_man_pipewire-client_conf_5 +- \subpage page_man_pipewire-pulse_conf_5 +- \subpage page_man_pipewire-jack_conf_5 +- \subpage page_man_pipewire-filter-chain_conf_5 +- \subpage page_man_pipewire-props_7 +- \subpage page_man_pipewire-pulse-modules_7 +- \subpage page_man_libpipewire-modules_7 diff --git a/doc/dox/config/libpipewire-modules.7.md b/doc/dox/config/libpipewire-modules.7.md new file mode 100644 index 0000000..ae3f88b --- /dev/null +++ b/doc/dox/config/libpipewire-modules.7.md @@ -0,0 +1,44 @@ +\page page_man_libpipewire-modules_7 libpipewire-modules + +PipeWire modules + +# DESCRIPTION + +A PipeWire module is effectively a PipeWire client running inside +`pipewire(1)` which can host multiple modules. Usually modules are +loaded when they are listed in the configuration files. For example the +default configuration file loads several modules: + + context.modules = [ + ... + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # The profile module. Allows application to access profiler + # and performance data. It provides an interface that is used + # by pw-top and pw-profiler. + { name = libpipewire-module-profiler } + + # Allows applications to create metadata objects. It creates + # a factory for Metadata objects. + { name = libpipewire-module-metadata } + + # Creates a factory for making devices that run in the + # context of the PipeWire server. + { name = libpipewire-module-spa-device-factory } + ... + ] + +# KNOWN MODULES + +$(LIBPIPEWIRE_MODULES) + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pipewire_conf_5 "pipewire.conf(5)" diff --git a/doc/dox/config/pipewire-client.conf.5.md b/doc/dox/config/pipewire-client.conf.5.md new file mode 100644 index 0000000..66bf3d2 --- /dev/null +++ b/doc/dox/config/pipewire-client.conf.5.md @@ -0,0 +1,235 @@ +\page page_man_pipewire-client_conf_5 client.conf + +The PipeWire client configuration file. + +\tableofcontents + +# SYNOPSIS + +*$XDG_CONFIG_HOME/pipewire/client.conf* + +*$(PIPEWIRE_CONFIG_DIR)/client.conf* + +*$(PIPEWIRE_CONFDATADIR)/client.conf* + +*$(PIPEWIRE_CONFDATADIR)/client.conf.d/* + +*$(PIPEWIRE_CONFIG_DIR)/client.conf.d/* + +*$XDG_CONFIG_HOME/pipewire/client.conf.d/* + +# DESCRIPTION + +Configuration for PipeWire native clients, and for PipeWire's ALSA +plugin. + +A PipeWire native client program selects the default config to load, +and if nothing is specified, it usually loads `client.conf`. + +The configuration file format and lookup logic is the same as for \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +Drop-in configuration files `client.conf.d/*.conf` can be used, and are recommended. +See \ref pipewire_conf__drop-in_configuration_files "pipewire.conf(5)". + +# CONFIGURATION FILE SECTIONS @IDX@ client.conf + +\par stream.properties +Configures options for native client streams. + +\par stream.rules +Configures rules for native client streams. + +\par alsa.properties +ALSA client configuration. + +\par alsa.rules +ALSA client match rules. + +In addition, the PipeWire context configuration sections +may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +# STREAM PROPERTIES @IDX@ client.conf + +The client configuration files contain a stream.properties section that configures the options for client streams: +```css +# ~/.config/pipewire/client.conf.d/custom.conf + +stream.properties = { + #node.latency = 1024/48000 + #node.autoconnect = true + #resample.disable = false + #resample.quality = 4 + #monitor.channel-volumes = false + #channelmix.disable = false + #channelmix.min-volume = 0.0 + #channelmix.max-volume = 10.0 + #channelmix.normalize = false + #channelmix.lock-volume = false + #channelmix.mix-lfe = true + #channelmix.upmix = true + #channelmix.upmix-method = psd # none, simple + #channelmix.lfe-cutoff = 150.0 + #channelmix.fc-cutoff = 12000.0 + #channelmix.rear-delay = 12.0 + #channelmix.stereo-widen = 0.0 + #channelmix.hilbert-taps = 0 + #dither.noise = 0 + #dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5 + #debug.wav-path = "" +} +``` + +Some of the properties refer to different aspects of the stream: + +* General stream properties to identify the stream. +* General stream properties to classify the stream. +* How it is going to be scheduled by the graph. +* How it is going to be linked by the session manager. +* How the internal processing will be done. +* Properties to configure the media format. + +A list of object properties that can be applied to streams can be found in +\ref props__common_node_properties "pipewire-props(7) Common Node Properties" +and +\ref props__audio_converter_properties "pipewire-props(7) Audio Adapter Properties" + +# STREAM RULES @IDX@ client.conf + +You can add \ref pipewire_conf__match_rules "match rules, see pipewire(1)" +to set properties for certain streams and filters. + +`stream.rules` and `filter.rules` provides an `update-props` action +that takes an object with properties that are updated on the node +object of the stream and filter. + +Add a `stream.rules` or `filter.rules` section in the config file like +this: + +```css +# ~/.config/pipewire/client.conf.d/custom.conf + +stream.rules = [ + { + matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + application.process.binary = "firefox" + } + ] + actions = { + update-props = { + node.name = "My Name" + } + } + } +] +``` + +Will set the node.name of Firefox to "My Name". + +# ALSA CLIENT PROPERTIES @IDX@ client.conf + +An `alsa.properties` section can be added to configure client applications +that connect via the PipeWire ALSA plugin. + +```css +# ~/.config/pipewire/client.conf.d/custom.conf + +alsa.properties = { + #alsa.deny = false + #alsa.format = 0 + #alsa.rate = 0 + #alsa.channels = 0 + #alsa.period-bytes = 0 + #alsa.buffer-bytes = 0 + #alsa.volume-method = cubic # linear, cubic +} +``` + +@PAR@ client.conf alsa.deny +Denies ALSA access for the client. Useful in rules or PIPEWIRE_ALSA environment variable. + +@PAR@ client.conf alsa.format +The ALSA format to use for the client. This is an ALSA format name. default 0, which is to +allow all formats. + +@PAR@ client.conf alsa.rate +The samplerate to use for the client. The default is 0, which is to allow all rates. + +@PAR@ client.conf alsa.channels +The number of channels for the client. The default is 0, which is to allow any number of channels. + +@PAR@ client.conf alsa.period-bytes +The number of bytes per period. The default is 0 which is to allow any number of period bytes. + +@PAR@ client.conf alsa.buffer-bytes +The number of bytes in the alsa buffer. The default is 0, which is to allow any number of bytes. + +@PAR@ client.conf alsa.volume-method = cubic | linear +This controls the volume curve used on the ALSA mixer. Possible values are `cubic` and +`linear`. The default is to use `cubic`. + +# ALSA CLIENT RULES @IDX@ client.conf + +It is possible to set ALSA client specific properties by using +\ref pipewire_conf__match_rules "Match rules, see pipewire(1)". You can +set any of the above ALSA properties or any of the `stream.properties`. + +### Example + +```css +# ~/.config/pipewire/client.conf.d/custom.conf + +alsa.rules = [ + { matches = [ { application.process.binary = "resolve" } ] + actions = { + update-props = { + alsa.buffer-bytes = 131072 + } + } + } +] +``` + +# ENVIRONMENT VARIABLES @IDX@ client-env + +See \ref page_man_pipewire_1 "pipewire(1)" for common environment +variables. Many of these also apply to client applications. + +The environment variables also influence ALSA applications that are +using PipeWire's ALSA plugin. + +@PAR@ client-env PIPEWIRE_ALSA +\parblock +This can be an object with properties from `alsa.properties` or `stream.properties` that will +be used to construct the client and streams. + +For example: +``` +PIPEWIRE_ALSA='{ alsa.buffer-bytes=16384 node.name=foo }' aplay ... +``` +Starts aplay with custom properties. +\endparblock + +@PAR@ client-env PIPEWIRE_NODE +\parblock +Instructs the ALSA client to link to a particular sink or source `object.serial` or `node.name`. + +For example: +``` +PIPEWIRE_NODE=alsa_output.pci-0000_00_1b.0.analog-stereo aplay ... +``` +Makes aplay play on the give audio sink. +\endparblock + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)", +\ref page_man_pipewire_conf_5 "pipewire.conf(5)", +\ref page_man_pipewire-pulse_1 "pipewire-pulse(1)", +\ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)" diff --git a/doc/dox/config/pipewire-filter-chain.conf.5.md b/doc/dox/config/pipewire-filter-chain.conf.5.md new file mode 100644 index 0000000..bde3fff --- /dev/null +++ b/doc/dox/config/pipewire-filter-chain.conf.5.md @@ -0,0 +1,42 @@ +\page page_man_pipewire-filter-chain_conf_5 filter-chain.conf + +PipeWire example configuration for running audio filters. + +\tableofcontents + +# SYNOPSIS + +*$XDG_CONFIG_HOME/pipewire/filter-chain.conf* + +*$(PIPEWIRE_CONFIG_DIR)/filter-chain.conf* + +*$(PIPEWIRE_CONFDATADIR)/filter-chain.conf* + +*$(PIPEWIRE_CONFDATADIR)/filter-chain.conf.d/* + +*$(PIPEWIRE_CONFIG_DIR)/filter-chain.conf.d/* + +*$XDG_CONFIG_HOME/pipewire/filter-chain.conf.d/* + +# DESCRIPTION + +When \ref page_man_pipewire_1 "pipewire(1)" is run using +this configuration file, `pipewire -c filter-chain.conf`, +it starts a PipeWire client application that publishes +nodes that apply various audio filters to their input. + +It is a normal PipeWire client application in all respects. + +Drop-in configuration files `filter-chain.conf.d/*.conf` can be used +to modify the filter configuration, see \ref pipewire_conf__drop-in_configuration_files "pipewire.conf(5)". +Some examples are in *$(PIPEWIRE_CONFDATADIR)/filter-chain/* + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pipewire_conf_5 "pipewire.conf(5)" diff --git a/doc/dox/config/pipewire-jack.conf.5.md b/doc/dox/config/pipewire-jack.conf.5.md new file mode 100644 index 0000000..b8a4a5c --- /dev/null +++ b/doc/dox/config/pipewire-jack.conf.5.md @@ -0,0 +1,342 @@ +\page page_man_pipewire-jack_conf_5 jack.conf + +The PipeWire JACK client configuration file. + +\tableofcontents + +# SYNOPSIS + +*$XDG_CONFIG_HOME/pipewire/jack.conf* + +*$(PIPEWIRE_CONFIG_DIR)/jack.conf* + +*$(PIPEWIRE_CONFDATADIR)/jack.conf* + +*$(PIPEWIRE_CONFDATADIR)/jack.conf.d/* + +*$(PIPEWIRE_CONFIG_DIR)/jack.conf.d/* + +*$XDG_CONFIG_HOME/pipewire/jack.conf.d/* + +# DESCRIPTION + +Configuration for PipeWire JACK clients. + +The configuration file format and lookup logic is the same as for \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +Drop-in configuration files `jack.conf.d/*.conf` can be used, and are recommended. +See \ref pipewire_conf__drop-in_configuration_files "pipewire.conf(5)". + +# CONFIGURATION FILE SECTIONS @IDX@ jack.conf + +\par jack.properties +JACK client configuration. + +\par jack.rules +JACK client match rules. + +In addition, the PipeWire context configuration sections +may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +# JACK PROPERTIES @IDX@ jack.conf + +The configuration file can contain an extra JACK specific section called `jack.properties` like this: +```css +# ~/.config/pipewire/jack.conf.d/custom.conf + +jack.properties = { + #rt.prio = 88 + #node.latency = 1024/48000 + #node.lock-quantum = true + #node.force-quantum = 0 + #jack.show-monitor = true + #jack.merge-monitor = true + #jack.show-midi = 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 + #jack.passive-links = false + #jack.max-client-ports = 768 + #jack.fill-aliases = false + #jack.writable-input = false + #jack.flag-midi2 = false +} +``` + +See `stream.properties` in +\ref client_conf__stream_properties "pipewire-client.conf(5)" for +an explanation of the generic node properties. + +It is also possible to have per-client settings, see Match Rules below. + +@PAR@ jack.conf rt.prio +To limit the realtime priority that jack clients can acquire. + +@PAR@ jack.conf node.latency +To force a specific minimum buffer size for the JACK applications, configure: +``` +node.latency = 1024/48000 +``` +This configures a buffer-size of 1024 samples at 48KHz. If the graph is running at a different sample rate, the buffer-size will be adjusted accordingly. + +@PAR@ jack.conf node.lock-quantum +To make sure that no automatic quantum is changes while JACK applications are running, configure: +``` +node.lock-quantum = true +``` +The quantum can then only be changed by metadata or when an application is started with node.force-quantum. JACK Applications will also be able to use jack_set_buffersize() to override the quantum. + +@PAR@ jack.conf node.force-quantum +To force the quantum to a certain value and avoid changes to it: +``` + node.force-quantum = 1024 +``` +The quantum can then only be changed by metadata or when an application is started with node.force-quantum (or JACK applications that use jack_set_buffersize() to override the quantum). + +@PAR@ jack.conf jack.show-monitor +Show the Monitor client and its ports. + +@PAR@ jack.conf jack.merge-monitor +\parblock +Exposes the capture ports and monitor ports on the same JACK device client. This is how JACK presents monitor ports to the clients. The default is however *not* to merge them together because this results in more user friendly user interfaces, usually. An extra client with a `Monitor` suffix is created that contains the monitor ports. + +For example, this is (part of) the output of `jack_lsp` with the default setting (`jack.merge-monitor = false`): + +Compare: + +| `jack.merge-monitor = true` | `jack.merge-monitor = false` | +|:--|:--| +| Built-in Audio Analog Stereo:playback_FL | Built-in Audio Analog Stereo:playback_FL +| Built-in Audio Analog Stereo:monitor_FL | Built-in Audio Analog Stereo Monitor:monitor_FL +| Built-in Audio Analog Stereo:playback_FR | Built-in Audio Analog Stereo:playback_FR +| Built-in Audio Analog Stereo:monitor_FR |Built-in Audio Analog Stereo Monitor:monitor_FR +\endparblock + +@PAR@ jack.conf jack.show-midi +Show the MIDI clients and their ports. + +@PAR@ jack.conf jack.short-name +\parblock +To use shorter names for the device client names use `jack.short-name = true`. Compare: + +| `jack.short-name = true` | `jack.short-name = false` | +|:--|:--| +| HDA Intel PCH:playback_FL | Built-in Audio Analog Stereo:playback_FL +| HDA Intel PCH Monitor:monitor_FL | Built-in Audio Analog Stereo Monitor:monitor_FL +| HDA Intel PCH:playback_FR | Built-in Audio Analog Stereo:playback_FR +| HDA Intel PCH Monitor:monitor_FR |Built-in Audio Analog Stereo Monitor:monitor_FR +\endparblock + +@PAR@ jack.conf jack.filter-name +@PAR@ jack.conf jack.filter-char +Will replace all special characters with `jack.filter-char`. For clients the special characters are ` ()[].:*$` and for ports they are ` ()[].*$`. Use this option when a client is not able to deal with the special characters. (and older version of PortAudio was known to use the client and port names as a regex, and thus failing when there are regex special characters in the name). + +@PAR@ jack.conf jack.self-connect-mode +\parblock +Restrict a client from making connections to and from itself. Possible values and their meaning are summarized as: + +| Value | Behavior +|:--|:--| +| `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. +\endparblock + +@PAR@ jack.conf jack.locked-process +Make sure the process and callbacks can not be called at the same time. This is the +normal operation but it can be disabled in case a specific client can handle this. + +@PAR@ jack.conf jack.default-as-system +\parblock +Name the default source and sink as `system` and number the ports to maximize +compatibility with JACK programs. + +| `jack.default-as-system = false` | `jack.default-as-system = true` | +|:--|:--| +| HDA Intel PCH:playback_FL | system:playback_1 +| HDA Intel PCH Monitor:monitor_FL | system:monitor_1 +| HDA Intel PCH:playback_FR | system:playback_2 +| HDA Intel PCH Monitor:monitor_FR | system:monitor_2 +\endparblock + +@PAR@ jack.conf jack.fix-midi-events +Fix NoteOn events with a 0 velocity to NoteOff. This is standard behaviour in JACK and is thus +enabled by default to maximize compatibility. Especially LV2 plugins do not allow NoteOn +with 0 velocity. + +@PAR@ jack.conf jack.global-buffer-size +When a client has this option, buffersize changes will be applied globally and permanently for all PipeWire clients using the metadata. + +@PAR@ jack.conf jack.passive-links +Makes JACK clients make passive links. This option only works when the server link-factory was configured with the `allow.link.passive` option. + +@PAR@ jack.conf jack.max-client-ports +Limit the number of allowed ports per client to this value. + +@PAR@ jack.conf jack.fill-aliases +Automatically set the port alias1 and alias2 on the ports. + +@PAR@ jack.conf jack.writable-input +\parblock +Makes the input buffers writable. This is the default because some JACK clients write to the +input buffer. This however can cause corruption in other clients when they are also reading +from the buffer. + +Set this to true to avoid buffer corruption if you are only dealing with non-buggy clients. +\endparblock + +@PAR@ jack.conf jack.flag-midi2 +\parblock +Use the new JACK MIDI2 port flag on MIDI2 (UMP) ports. This is disabled by default because most +JACK apps don't know about this flag yet and refuse to show the port. + +Set this to true for applications that know how to handle MIDI2 ports. +\endparblock + +# MATCH RULES @IDX@ jack.conf + +`jack.rules` provides an `update-props` action that takes an object with properties that are updated +on the client and node object of the jack client. + +Add a `jack.rules` section in the config file like this: + +```css +# ~/.config/pipewire/jack.conf.d/custom.conf + +jack.rules = [ + { + matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + application.process.binary = "jack_simple_client" + } + ] + actions = { + update-props = { + node.latency = 512/48000 + } + } + } + { + matches = [ + { + client.name = "catia" + } + ] + actions = { + update-props = { + jack.merge-monitor = true + } + } + } +] +``` +Will set the latency of jack_simple_client to 512/48000 and makes Catia see the monitor client merged with the playback client. + +# ENVIRONMENT VARIABLES @IDX@ jack-env + +See \ref page_man_pipewire_1 "pipewire(1)" for common environment +variables. Many of these also apply to JACK client applications. + +Environment variables can be used to control the behavior of the PipeWire JACK client library. + +@PAR@ jack-env PIPEWIRE_NOJACK +@PAR@ jack-env PIPEWIRE_INTERNAL +When any of these variables is set, the JACK client library will refuse to open a client. The `PIPEWIRE_INTERNAL` variable is set by the PipeWire main daemon to avoid self connections. + +@PAR@ jack-env PIPEWIRE_PROPS +Adds/overrides the properties specified in the `jack.conf` file. Check out the output of this: +``` +> PIPEWIRE_PROPS='{ jack.short-name=true jack.merge-monitor=true }' jack_lsp +... +HDA Intel PCH:playback_FL +HDA Intel PCH:monitor_FL +HDA Intel PCH:playback_FR +HDA Intel PCH:monitor_FR +... +``` + +@PAR@ jack-env PIPEWIRE_LATENCY +\parblock +``` +PIPEWIRE_LATENCY=/ +``` +A quick way to configure the maximum buffer-size for a client. It will run this client with the specified buffer-size (or smaller). + +`PIPEWIRE_LATENCY=256/48000 jack_lsp` is equivalent to `PIPEWIRE_PROPS='{ node.latency=256/48000 }' jack_lsp` + +A better way to start a jack session in a specific buffer-size is to force it with: +``` +pw-metadata -n settings 0 clock.force-quantum +``` +This always works immediately and the buffer size will not change until the quantum is changed back to 0. +\endparblock + +@PAR@ jack-env PIPEWIRE_RATE +\parblock +``` +PIPEWIRE_RATE=1/ +``` + +A quick way to configure the rate of the graph. It will try to switch the samplerate of the graph. This can usually only be done with the graph is idle and the rate is part of the allowed sample rates. + +`PIPEWIRE_RATE=1/48000 jack_lsp` is equivalent to `PIPEWIRE_PROPS='{ node.rate=1/48000 }' jack_lsp` + +A better way to start a jack session in a specific rate is to force the rate with: +``` +pw-metadata -n settings 0 clock.force-rate +``` +This always works and the samplerate does not need to be in the allowed rates. The rate will also not change until it is set back to 0. +\endparblock + +@PAR@ jack-env PIPEWIRE_QUANTUM +\parblock +``` +PIPEWIRE_QUANTUM=/ +``` + +Is similar to using `PIPEWIRE_LATENCY=/` and `PIPEWIRE_RATE=1/` (see above), except that it is not just a suggestion but it actively *forces* the graph to change the rate and quantum. It can be used to set both a buffersize and samplerate at the same time. + +When 2 applications force a quantum, the last one wins. When the winning app is stopped, the quantum of the previous app is restored. +\endparblock + +@PAR@ jack-env PIPEWIRE_LINK_PASSIVE +\parblock +``` +PIPEWIRE_LINK_PASSIVE=true qjackctl +``` +Make this client create passive links only. All links created by the client will be marked passive and will not keep the sink/source busy. + +You can use this to link filters to devices. When there is no client connected to the filter, only passive links remain between the filter and the device and the device will become idle and suspended. +\endparblock + +@PAR@ jack-env PIPEWIRE_NODE +\parblock +``` +PIPEWIRE_NODE= +``` +Will sort the ports so that only the ports of the node with are listed. You can use this to force an application to only deal with the ports of a certain node, for example when auto connecting. +\endparblock + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pw-jack_1 "pw-jack(1)", +\ref page_man_pipewire_conf_5 "pipewire.conf(5)" diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md new file mode 100644 index 0000000..d7e6af8 --- /dev/null +++ b/doc/dox/config/pipewire-props.7.md @@ -0,0 +1,1214 @@ +\page page_man_pipewire-props_7 pipewire-props + +PipeWire object property reference. + +\tableofcontents + +# DESCRIPTION + +PipeWire describes and configures audio and video elements with +objects of the following main types: + +\par Node +Audio or video sink/source endpoint + +\par Device +Sound cards, bluetooth devices, cameras, etc. May have multiple nodes. + +\par Monitor +Finding devices and handling hotplugging + +\par Port +Audio/video endpoint in a node + +\par Link +Connection between ports, that transporting audio/video between them. + +\par Client +Application connected to PipeWire. + +All objects have *properties* ("props"), most of which can be set in +configuration files or at runtime when the object is created. + +Some of the properties are "common properties" (for example +`node.description`) and can be set on all objects of the given +type. Other properties control settings of a specific kinds of device +or node (ALSA, Bluetooth, ...), and have meaning only for those +objects. + +Usually, all the properties are configured in the session manager +configuration. For how to configure them, see the session manager +documentation. In minimal PipeWire setups without a session manager, +they can be configured via +\ref pipewire_conf__context_objects "context.objects in pipewire.conf(5)". + +\see [WirePlumber configuration](https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html) + +# COMMON DEVICE PROPERTIES @IDX@ props + +These are common properties for devices. + +@PAR@ device-prop device.name # string +A (unique) name for the device. It can be used by command-line and other tools to identify the device. + +@PAR@ device-prop device.param.PARAM = { ... } # JSON +\parblock +Set value of a device \ref spa_param_type "Param" to a JSON value when the device is loaded. +This works similarly as \ref page_man_pw-cli_1 "pw-cli(1)" `set-param` command. +The `PARAM` should be replaced with the name of the Param to set, +ie. for example `device.Param.Props = { ... }` to set `Props`. +\endparblock + +@PAR@ device-prop device.plugged # integer +\parblock +\copydoc PW_KEY_DEVICE_PLUGGED +\endparblock + +@PAR@ device-prop device.nick # string +\parblock +\copydoc PW_KEY_DEVICE_NICK +\endparblock + +@PAR@ device-prop device.description # string +\parblock +\copydoc PW_KEY_DEVICE_DESCRIPTION +\endparblock + +@PAR@ device-prop device.serial # string +\parblock +\copydoc PW_KEY_DEVICE_SERIAL +\endparblock + +@PAR@ device-prop device.vendor.id # integer +\parblock +\copydoc PW_KEY_DEVICE_VENDOR_ID +\endparblock + +@PAR@ device-prop device.vendor.name # string +\parblock +\copydoc PW_KEY_DEVICE_VENDOR_NAME +\endparblock + +@PAR@ device-prop device.product.id # integer +\parblock +\copydoc PW_KEY_DEVICE_PRODUCT_NAME +\endparblock + +@PAR@ device-prop device.product.name # string +\parblock +\copydoc PW_KEY_DEVICE_PRODUCT_ID +\endparblock + +@PAR@ device-prop device.class # string +\parblock +\copydoc PW_KEY_DEVICE_CLASS +\endparblock + +@PAR@ device-prop device.form-factor # string +\parblock +\copydoc PW_KEY_DEVICE_FORM_FACTOR +\endparblock + +@PAR@ device-prop device.icon # string +\parblock +\copydoc PW_KEY_DEVICE_ICON +\endparblock + +@PAR@ device-prop device.icon-name # string +\parblock +\copydoc PW_KEY_DEVICE_ICON_NAME +\endparblock + +@PAR@ device-prop device.intended-roles # string +\parblock +\copydoc PW_KEY_DEVICE_INTENDED_ROLES +\endparblock + +@PAR@ device-prop device.disabled = false # boolean +Disable the creation of this device in session manager. + + +There are other common `device.*` properties for technical purposes +and not usually user-configurable. + +\see pw_keys in the API documentation for a full list. + +# COMMON NODE PROPERTIES @IDX@ props + +The properties here apply to general audio or video input/output +streams, and other nodes such as sinks or sources corresponding to +real or virtual devices. + +## Identifying Properties @IDX@ props + +These contain properties to identify the node or to display the node in a GUI application. + +@PAR@ node-prop node.name +A (unique) name for the node. This is usually set on sink and sources to identify them +as targets for linking by the session manager. + +@PAR@ node-prop node.description +A human readable description of the node or stream. + +@PAR@ node-prop media.name +A user readable media name, usually the artist and title. +These are usually shown in user facing applications +to inform the user about the current playing media. + +@PAR@ node-prop media.title +A user readable stream title. + +@PAR@ node-prop media.artist +A user readable stream artist + +@PAR@ node-prop media.copyright +User readable stream copyright information + +@PAR@ node-prop media.software +User readable stream generator software information + +@PAR@ node-prop media.language +Stream language in POSIX format. Ex: `en_GB` + +@PAR@ node-prop media.filename +File name for the stream + +@PAR@ node-prop media.icon +Icon for the media, a base64 blob with PNG image data + +@PAR@ node-prop media.icon-name +An XDG icon name for the media. Ex: `audio-x-mp3` + +@PAR@ node-prop media.comment +Extra stream comment + +@PAR@ node-prop media.date +Date of the media + +@PAR@ node-prop media.format +User readable stream format information + +@PAR@ node-prop object.linger = false +If the object should outlive its creator. + +@PAR@ node-prop device.id +ID of the device the node belongs to. + +## Classifying Properties @IDX@ props + +The classifying properties of a node are use for routing the signal to its destination and +for configuring the settings. + +@PAR@ node-prop media.type +The media type contains a broad category of the media that is being processed by the node. +Possible values include "Audio", "Video", "Midi" + +@PAR@ node-prop media.category +\parblock +What kind of processing is done with the media. Possible values include: + +* Playback: media playback. +* Capture: media capture. +* Duplex: media capture and playback or media processing in general. +* Monitor: a media monitor application. Does not actively change media data but monitors + activity. +* Manager: Will manage the media graph. +\endparblock + +@PAR@ node-prop media.role +\parblock +The Use case of the media. Possible values include: + +* Movie: Movie playback with audio and video. +* Music: Music listening. +* Camera: Recording video from a camera. +* Screen: Recording or sharing the desktop screen. +* Communication: VOIP or other video chat application. +* Game: Game. +* Notification: System notification sounds. +* DSP: Audio or Video filters and effect processing. +* Production: Professional audio processing and production. +* Accessibility: Audio and Visual aid for accessibility. +* Test: Test program. +\endparblock + +@PAR@ node-prop media.class +\parblock +The media class is to classify the stream function. Possible values include: + +* Video/Source: a producer of video, like a webcam. +* Video/Sink: a consumer of video, like a display window. +* Audio/Source: a source of audio samples like a microphone. +* Audio/Sink: a sink for audio samples, like an audio card. +* Audio/Duplex: a node that is both a sink and a source. +* Stream/Output/Audio: a playback stream. +* Stream/Input/Audio: a capture stream. + +The session manager assigns special meaning to the nodes based on the media.class. Sink or Source +classes are used as targets for Stream classes, etc.. +\endparblock + +## Scheduling Properties @IDX@ props + +@PAR@ node-prop node.latency = 1024/48000 +Sets a suggested latency on the node as a fraction. This is just a suggestion, the graph will try to configure this latency or less for the graph. It is however possible that the graph is forced to a higher latency. + +@PAR@ node-prop node.lock-quantum = false +\parblock +When this node is active, the quantum of the graph is locked and not allowed to change automatically. +It can still be changed forcibly with metadata or when a node forces a quantum. + +JACK clients use this property to avoid unexpected quantum changes. +\endparblock + +@PAR@ node-prop node.force-quantum = INTEGER +\parblock +While the node is active, force a quantum in the graph. The last node to be activated with this property wins. + +A value of 0 unforces the quantum. +\endparblock + +@PAR@ node-prop node.rate = RATE +Suggest a rate (samplerate) for the graph. The suggested rate will only be applied when doing so would not cause +interruptions (devices are idle) and when the rate is in the list of allowed rates in the server. + +@PAR@ node-prop node.lock-rate = false +When the node is active, the rate of the graph will not change automatically. It is still possible to force a rate change with metadata or with a node property. + +@PAR@ node-prop node.force-rate = RATE +\parblock +When the node is active, force a specific sample rate on the graph. The last node to activate with this property wins. + +A RATE of 0 means to force the rate in `node.rate` denominator. +\endparblock + +@PAR@ node-prop node.always-process = false +\parblock +When the node is active, it will always be joined with a driver node, even when nothing is linked to the node. +Setting this property to true also implies node.want-driver = true. + +This is the default for JACK nodes, that always need their process callback called. +\endparblock + +@PAR@ node-prop node.want-driver = true +The node wants to be linked to a driver so that it can start processing. This is the default for streams +and filters since 0.3.51. Nodes that are not linked to anything will still be set to the idle state, +unless node.always-process is set to true. + +@PAR@ node-prop node.pause-on-idle = false +@PAR@ node-prop node.suspend-on-idle = false +\parblock +When the node is not linked anymore, it becomes idle. Normally idle nodes keep processing and are suspended by the session manager after some timeout. It is possible to immediately pause a node when idle with this property. + +When the session manager does not suspend nodes (or when there is no session manager), the node.suspend-on-idle property can be used instead. +\endparblock + +@PAR@ node-prop node.loop.name = null +@PAR@ node-prop node.loop.class = data.rt +\parblock +Add the node to a specific loop name or loop class. By default the node is added to the +data.rt loop class. You can make more specific data loops and then assign the nodes to those. + +Other well known names are main-loop.0 and the main node.loop.class which runs the node data processing +in the main loop. +\endparblock + +@PAR@ node-prop priority.driver # integer +\parblock +The priority of choosing this device as the driver in the graph. The driver is selected from all linked devices by selecting the device with the highest priority. + +Normally, the session manager assigns higher priority to sources so that they become the driver in the graph. The reason for this is that adaptive resampling should be done on the sinks rather than the source to avoid signal distortion when capturing audio. +\endparblock + +@PAR@ node-prop clock.name # string +\parblock +The name of the clock. This name is auto generated from the card index and stream direction. Devices with the same clock name will not use a resampler to align the clocks. This can be used to link devices together with a shared word clock. + +In Pro Audio mode, nodes from the same device are assumed to have the same clock and no resampling will happen when linked together. So, linking a capture port to a playback port will not use any adaptive resampling in Pro Audio mode. + +In Non Pro Audio profile, no such assumption is made and adaptive resampling is done in all cases by default. This can also be disabled by setting the same clock.name on the nodes. +\endparblock + +## Session Manager Properties @IDX@ props + +@PAR@ node-prop node.autoconnect = true +Instructs the session manager to automatically connect this node to some other node, usually +a sink or source. + +@PAR@ node-prop node.exclusive = false +If this node wants to be linked exclusively to the sink/source. + +@PAR@ node-prop node.target = +Where this node should be linked to. This can be a node.name or an object.id of a node. This property is +deprecated, the target.object property should be used instead, which uses the more unique object.serial as +a possible target. + +@PAR@ node-prop target.object = +Where the node should link to, this can be a node.name or an object.serial. + +@PAR@ node-prop node.dont-reconnect = false +\parblock +When the node has a target configured and the target is destroyed, destroy the node as well. +This property also inhibits that the node is moved to another sink/source. + +Note that if a stream should appear/disappear in sync with the target, a session manager (WirePlumber) script +should be written instead. +\endparblock + +@PAR@ node-prop node.passive = false +\parblock +This is a passive node and so it should not keep sinks/sources busy. This property makes the session manager create passive links to the sink/sources. If the node is not otherwise linked (via a non-passive link), the node and the sink it is linked to are idle (and eventually suspended). + +This is used for filter nodes that sit in front of sinks/sources and need to suspend together with the sink/source. +\endparblock + +@PAR@ node-prop node.link-group = ID +Add the node to a certain link group. Nodes from the same link group are not automatically linked to each other by the session manager. And example is a coupled stream where you don't want the output to link to the input streams, making a useless loop. + +@PAR@ node-prop stream.dont-remix = false +Instruct the session manager to not remix the channels of a stream. Normally the stream channel configuration is changed to match the sink/source it is connected to. With this property set to true, the stream will keep its original channel layout and the session manager will link matching channels with the sink. + +@PAR@ node-prop priority.session # integer +The priority for selecting this node as the default source or sink. + +## Format Properties + +Streams and also most device nodes can be configured in a certain format with properties. + +@PAR@ node-prop audio.rate = RATE +Forces a samplerate on the node. + +@PAR@ node-prop audio.channels = INTEGER +The number of audio channels to use. Must be a value between 1 and 64. + +@PAR@ node-prop audio.format = FORMAT +\parblock +Forces an audio format on the node. This is the format used internally in the node because the graph processing format is always float 32. + +Valid formats include: S16, S32, F32, F64, S16LE, S16BE, ... +\endparblock + +@PAR@ node-prop audio.allowed-rates +An array of allowed samplerates for the node. ex. "[ 44100 48000 ]" + +## Other Properties + +@PAR@ node-prop node.param.PARAM = { ... } # JSON +\parblock +Set value of a node \ref spa_param_type "Param" to a JSON value when the device is loaded. +This works similarly as \ref page_man_pw-cli_1 "pw-cli(1)" `set-param` command. +The `PARAM` should be replaced with the name of the Param to set, +ie. for example `node.param.Props = { ... }` to set `Props`. +\endparblock + +@PAR@ node-prop node.disabled = false # boolean +Disable the creation of this node in session manager. + + +# AUDIO ADAPTER PROPERTIES @IDX@ props + +Most audio nodes (ALSA, Bluetooth, audio streams from applications, +...) have common properties for the audio adapter. The adapter +performs sample format, sample rate and channel mixing operations. + +All properties listed below are node properties. + +## Merger Parameters + +The merger is used as the input for a sink device node or a capture stream. It takes the various channels and merges them into a single stream for further processing. + +The merger will also provide the monitor ports of the input channels and can +apply a software volume on the monitor signal. + +@PAR@ node-prop monitor.channel-volumes = false +The volume of the input channels is applied to the volume of the monitor ports. Normally +the monitor ports expose the raw unmodified signal on the input ports. + +## Resampler Parameters + +Source, sinks, capture and playback streams contain a high quality adaptive resampler. +It uses [sinc](https://ccrma.stanford.edu/~jos/resample/resample.pdf) based resampling +with linear interpolation of filter banks to perform arbitrary +resample factors. The resampler is activated in the following cases: + +* The hardware of a device node does not support the graph samplerate. Resampling will occur + from the graph samplerate to the hardware samplerate. +* The hardware clock of a device does not run at the same speed as the graph clock and adaptive + resampling is required to match the clocks. +* A stream does not have the same samplerate as the graph and needs to be resampled. +* An application wants to activate adaptive resampling in a stream to make it match some other + clock. + +PipeWire performs most of the sample conversions and resampling in the client (Or in the case of the PulseAudio server, in the pipewire-pulse server that creates the streams). This ensures all the conversions are offloaded to the clients and the server can deal with one single format for performance reasons. + +Below is an explanation of the options that can be tuned in the sample converter. + +@PAR@ node-prop resample.quality = 4 +\parblock +The quality of the resampler. from 0 to 14, the default is 4. + +Increasing the quality will result in better cutoff and less aliasing at the expense of +(much) more CPU consumption. The default quality of 4 has been selected as a good compromise +between quality and performance with no artifacts that are well below the audible range. + +See [Infinite Wave](https://src.infinitewave.ca/) for a comparison of the performance. +\endparblock + +@PAR@ node-prop resample.disable = false +Disable the resampler entirely. The node will only be able to negotiate with the graph +when the samplerates are compatible. + +## Channel Mixer Parameters + +Source, sinks, capture and playback streams can apply channel mixing on the incoming signal. + +Normally the channel mixer is not used for devices, the device channels are usually exposed as they are. This policy is usually enforced by the session manager, so we refer to its documentation there. + +Playback and capture streams are usually configured to the channel layout of the sink/source +they connect to and will thus perform channel mixing. + +The channel mixer also implements a software volume. This volume adjustment is performed on the original +channel layout. ex: A stereo playback stream that is up-mixed to 5.1 has 2 a left an right volume control. + +@PAR@ node-prop channelmix.disable = false +Disables the channel mixer completely. The stream will only be able to link to compatible +sources/sinks with the exact same channel layout. + +@PAR@ node-prop channelmix.min-volume = 0.0 +@PAR@ node-prop channelmix.max-volume = 10.0 +Gives the min and max volume values allowed. Any volume that is set will be clamped to these +values. + +@PAR@ node-prop channelmix.normalize = false +\parblock +Makes sure that during such mixing & resampling original 0 dB level is preserved, so nothing sounds wildly quieter/louder. + +While this options prevents clipping, it can in some cases produce too low volume. Increase the +volume in that case or disable normalization. +\endparblock + +@PAR@ node-prop channelmix.lock-volumes = false +Completely disable volume or mute changes. Defaults to false. + +@PAR@ node-prop channelmix.mix-lfe = true +Mixes the low frequency effect channel into the front center or stereo pair. This might enhance the dynamic range of the signal if there is no subwoofer and the speakers can reproduce the low frequency signal. + +@PAR@ node-prop channelmix.upmix = true +\parblock +Enables up-mixing of the front center (FC) when the target has a FC channel. +The sum of the stereo channels is used and an optional lowpass filter can be used +(see `channelmix.fc-cutoff`). + +Also enabled up-mixing of LFE when `channelmix.lfe-cutoff` is set to something else than 0 and +the target has an LFE channel. The LFE channel is produced by adding the stereo channels. + +If `channelmix.upmix` is true, the up-mixing of the rear channels is also enabled and controlled +with the `channelmix-upmix-method` property. +\endparblock + +@PAR@ node-prop channelmix.upmix-method = psd +\parblock +3 methods are provided to produce the rear channels in a surround sound: + +1. none. No rear channels are produced. + +2. simple. Front channels are copied to the rear. This is fast but can produce phasing effects. + +3. psd. The rear channels as produced from the front left and right ambient sound (the +difference between the channels). A delay and optional phase shift are added to the rear signal +to make the sound bigger. +\endparblock + +@PAR@ node-prop channelmix.lfe-cutoff = 150 +Apply a lowpass filter to the low frequency effects. The value is expressed in Hz. Typical subwoofers have a cutoff at around 150 and 200. The default value of 0 disables the feature. + +@PAR@ node-prop channelmix.fc-cutoff = 12000 +\parblock +Apply a lowpass filter to the front center frequency. The value is expressed in Hz. + +Since the front center contains the dialogs, a typical cutoff frequency is 12000 Hz. + +This option is only active when the up-mix is enabled. +\endparblock + +@PAR@ node-prop channelmix.rear-delay = 12.0 +\parblock +Apply a delay in milliseconds when up-mixing the rear channels. This improves +specialization of the sound. A typical delay of 12 milliseconds is the default. + +This is only active when the `psd` up-mix method is used. +\endparblock + +@PAR@ node-prop channelmix.stereo-widen = 0.0 +\parblock +Subtracts some of the front center signal from the stereo channels. This moves the dialogs +more to the center speaker and leaves the ambient sound in the stereo channels. + +This is only active when up-mix is enabled and a Front Center channel is mixed. +\endparblock + +@PAR@ node-prop channelmix.hilbert-taps = 0 +\parblock +This option will apply a 90 degree phase shift to the rear channels to improve specialization. +Taps needs to be between 15 and 255 with more accurate results (and more CPU consumption) +for higher values. + +This is only active when the `psd` up-mix method is used. +\endparblock + +@PAR@ node-prop dither.noise = 0 +\parblock +This option will add N bits of random data to the signal. When no dither.method is +specified, the random data will flip between [-(1<<(N-1)), 0] every 1024 samples. With +a dither.method, the dither noise is amplified with 1<<(N-1) bits. + +This can be used to keep some amplifiers alive during silent periods. One or two bits of noise is +usually enough, otherwise the noise will become audible. This is usually used together with +`session.suspend-timeout-seconds` to disable suspend in the session manager. + +Note that PipeWire uses floating point operations with 24 bits precission for all of the audio +processing. Conversion to 24 bits integer sample formats is lossless and conversion to 32 bits +integer sample formats are simply padded with 0 bits at the end. This means that the dither noise +is always only in the 24 most significant bits. +\endparblock + +@PAR@ node-prop dither.method = none +\parblock +Optional [dithering](https://en.wikipedia.org/wiki/Dither) can be done on the quantized +output signal. + +There are 6 modes available: + +1. none No dithering is done. +2. rectangular Dithering with a rectangular noise distribution. This adds random + bits in the [-0.5, 0.5] range to the signal with even distribution. +3. triangular Dithering with a triangular noise distribution. This add random + bits in the [-1.0, 1.0] range to the signal with triangular distribution + around 0.0. +4. triangular-hf Dithering with a sloped triangular noise distribution. +5. wannamaker3 Additional noise shaping is performed on the sloped triangular + dithering to move the noise to the more inaudible range. This is using + the "F-Weighted" noise filter described by Wannamaker. +6. shaped5 Additional noise shaping is performed on the triangular dithering + to move the noise to the more inaudible range. This is using the + Lipshitz filter. + +Dithering is only useful for conversion to a format with less than 24 bits and will be +disabled otherwise. +\endparblock + +## Debug Parameters + +@PAR@ node-prop debug.wav-path = "" +Make the stream to also write the raw samples to a WAV file for debugging purposes. + +## Other Parameters + +These control low-level technical features: + +@PAR@ node-prop clock.quantum-limit +\ref pipewire_conf__default_clock_quantum-limit "See pipewire.conf(5)" + +@PAR@ node-prop resample.peaks = false # boolean +Instead of actually resampling, produce peak amplitude values as output. +This is used for volume monitoring, where it is set as a property +of the "recording" stream. + +@PAR@ node-prop resample.prefill = false # boolean +Prefill resampler buffers with silence. This affects the initial +samples produced by the resampler. + +@PAR@ node-prop adapter.auto-port-config = null # JSON +\parblock +If specified, configure the ports of the node when it is created, instead of +leaving that to the session manager to do. This is useful (only) for minimal +configurations without a session manager. + +Value is SPA JSON of the form: +```json +{ + mode = "none", # "none", "passthrough", "convert", "dsp" + monitor = false, # boolean + control = false, # boolean + position = "preserve" # "unknown", "aux", "preserve" +} +``` +See \ref spa_param_port_config for the meaning. +\endparblock + +# ALSA PROPERTIES @IDX@ props + +## Monitor properties + +@PAR@ monitor-prop alsa.use-acp # boolean +Use \ref monitor-prop__alsa_card_profiles "ALSA Card Profiles" (ACP) for device configuration. + +@PAR@ monitor-prop alsa.udev.expose-busy # boolean +Expose the ALSA card even if it is busy/in use. Default false. This can be useful when some +of the PCMs are in use by other applications but the other free PCMs should still be exposed. + +## Device properties + +@PAR@ device-prop api.alsa.path # string +ALSA device path as can be used in snd_pcm_open() and snd_ctl_open(). + +@PAR@ device-prop api.alsa.use-ucm = true # boolean +\parblock +When ACP is enabled and a UCM configuration is available for a device, by +default it is used instead of the ACP profiles. This option allows you to +disable this and use the ACP profiles instead. + +This option does nothing if `api.alsa.use-acp` is set to `false`. +\endparblock + +@PAR@ device-prop api.alsa.soft-mixer = false # boolean +Setting this option to `true` will disable the hardware mixer for volume +control and mute. All volume handling will then use software volume and mute, +leaving the hardware mixer untouched. This can be interesting to work around +bugs in the mixer detection or decibel reporting. The hardware mixer will still +be used to mute unused audio paths in the device. Use `api.alsa.disable-mixer-path` +to also disable mixer path selection. + +@PAR@ device-prop api.alsa.disable-mixer-path = false # boolean +Setting this option to `true` will disable the hardware mixer path selection. +The hardware mixer path is the configuration of the mixer depending on the +jacks that are inserted in the card. If this is disabled, you will have to +manually enable and disable mixer controls but it can be used to work around +bugs in the mixer. The hardware mixer will still be used for +volume and mute. Use `api.alsa.soft-mixer` to also disable hardware volume +and mute. + +@PAR@ device-prop api.alsa.ignore-dB = false # boolean +Setting this option to `true` will ignore the decibel setting configured by +the driver. Use this when the driver reports wrong settings. + +@PAR@ device-prop device.profile-set # string +This option can be used to select a custom ACP profile-set name for the +device. This can be configured in UDev rules, but it can also be specified +here. The default is to use "default.conf" unless there is a matching udev rule. + +@PAR@ device-prop device.profile # string +The initial active profile name. The default is to start from the "Off" +profile and then let session manager select the best profile based on its +policy. + +@PAR@ device-prop api.acp.auto-profile = true # boolean +Automatically select the best profile for the device. The session manager +usually disables this, as it handles this task instead. This can be +enabled in custom configurations without the session manager handling this. + +@PAR@ device-prop api.acp.auto-port = true # boolean +Automatically select the highest priority port that is available ("port" is a +PulseAudio/ACP term, the equivalent of a "Route" in PipeWire). The session manager +usually disables this, as it handles this task instead. This can be +enabled in custom configurations without the session manager handling this. + +@PAR@ device-prop api.acp.probe-rate # integer +Sets the samplerate used for probing the ALSA devices and collecting the +profiles and ports. + +@PAR@ device-prop api.acp.pro-channels # integer +Sets the number of channels to use when probing the "Pro Audio" profile. +Normally, the maximum amount of channels will be used but with this setting +this can be reduced, which can make it possible to use other samplerates on +some devices. + +@PAR@ device-prop api.alsa.split-enable # boolean +\parblock +\copydoc SPA_KEY_API_ALSA_SPLIT_ENABLE +\endparblock + +## Node properties + +@PAR@ node-prop audio.channels # integer +The number of audio channels to open the device with. Defaults depends on the profile of the device. + +@PAR@ node-prop audio.rate # integer +The audio rate to open the device with. Default is 0, which means to open the device with a rate as close to the graph rate as possible. + +@PAR@ node-prop audio.format # string +The audio format to open the device in. By default this is "UNKNOWN", which will open the device in the best possible bits (32/24/16/8..). You can force a format like S16_LE or S32_LE. + +@PAR@ node-prop audio.position # JSON array of strings +The audio position of the channels in the device. This is auto detected based on the profile. You can configure an array of channel positions, like "[ FL, FR ]". + +@PAR@ node-prop audio.allowed-rates # JSON array of integers +\parblock +The allowed audio rates to open the device with. Default is "[ ]", which means the device can be opened in any supported rate. + +Only rates from the array will be used to open the device. When the graph is running with a rate not listed in the allowed-rates, the resampler will be used to resample to the nearest allowed rate. +\endparblock + +@PAR@ node-prop api.alsa.period-size # integer +The period size to open the device in. By default this is 0, which will open the device in the default period size to minimize latency. + +@PAR@ node-prop api.alsa.period-num # integer +The amount of periods to use in the device. By default this is 0, which means to use as many as possible. + +@PAR@ node-prop api.alsa.headroom # integer +The amount of extra space to keep in the ringbuffer. The default is 0. Higher values can be configured when the device read and write pointers are not accurately reported. + +@PAR@ node-prop api.alsa.start-delay = 0 # integer +Some devices need some time before they can report accurate hardware pointer +positions. In those cases, an extra start delay can be added to compensate +for this startup delay. This sets the startup delay in samples. + +@PAR@ node-prop api.alsa.disable-mmap = false # boolean +Disable mmap operation of the device and use the ALSA read/write API instead. Default is false, mmap is preferred. + +@PAR@ node-prop api.alsa.disable-batch # boolean +Ignore the ALSA batch flag. If the batch flag is set, ALSA will need an extra period to update the read/write pointers. Ignore this flag from ALSA can reduce the latency. Default is false. + +@PAR@ node-prop api.alsa.use-chmap # boolean +Use the driver provided channel map. Default is true when using UCM, false otherwise because many driver don't report this correctly. + +@PAR@ node-prop api.alsa.multi-rate # boolean +Allow devices from the same card to be opened in multiple sample rates. Default is true. Some older drivers did not properly advertise the capabilities of the device and only really supported opening the device in one rate. + +@PAR@ node-prop api.alsa.htimestamp = false # boolean +Use ALSA htimestamps in scheduling, instead of the system clock. +Some ALSA drivers produce bad timestamps, so this is not enabled by default +and will be disabled at runtime if it looks like the ALSA timestamps are bad. + +@PAR@ node-prop api.alsa.htimestamp.max-errors # integer +Specify the number of consecutive errors before htimestamp is disabled. +Setting this to 0 makes htimestamp never get disabled. + +@PAR@ node-prop api.alsa.disable-tsched = false # boolean +Disable timer-based scheduling, and use IRQ for scheduling instead. +The "Pro Audio" profile will usually enable this setting, if it is expected it works on the hardware. + +@PAR@ node-prop api.alsa.auto-link = false # boolean +Link follower PCM devices to the driver PCM device when using IRQ-based scheduling. +The "Pro Audio" profile will usually enable this setting, if it is expected it works on the hardware. + +@PAR@ node-prop latency.internal.rate # integer +Static set the device systemic latency, in samples at playback rate. + +@PAR@ node-prop latency.internal.ns # integer +Static set the device systemic latency, in nanoseconds. + +@PAR@ node-prop api.alsa.path # string +UNDOCUMENTED + +@PAR@ node-prop api.alsa.open.ucm # boolean +Open device using UCM. + +@PAR@ node-prop api.alsa.bind-ctls # boolean +UNDOCUMENTED + +@PAR@ node-prop iec958.codecs # JSON array of string +Enable only specific IEC958 codecs. This can be used to disable some codecs the hardware supports. +Available values: PCM, AC3, DTS, MPEG, MPEG2-AAC, EAC3, TRUEHD, DTSHD + +@PAR@ device-prop api.alsa.split.parent # boolean +\parblock +\copydoc SPA_KEY_API_ALSA_SPLIT_PARENT +\endparblock + +@PAR@ node-prop api.alsa.split.position # JSON +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_POSITION +Informative property. +\endparblock + +@PAR@ node-prop api.alsa.split.hw-position # JSON +\parblock +\copybrief SPA_KEY_API_ALSA_SPLIT_HW_POSITION +Informative property. +\endparblock + + +# BLUETOOTH PROPERTIES @IDX@ props + +## Monitor properties + +The following are settings for the Bluetooth device monitor, not device or +node properties: + +@PAR@ monitor-prop bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source bap_bcast_sink bap_bcast_source hfp_hf hfp_ag ] # JSON array of string +\parblock +Enabled roles. + +Currently some headsets (Sony WH-1000XM3) are not working with +both hsp_ag and hfp_ag enabled, so by default we enable only HFP. + +Supported roles: +- `hsp_hs` (HSP Headset), +- `hsp_ag` (HSP Audio Gateway), +- `hfp_hf` (HFP Hands-Free), +- `hfp_ag` (HFP Audio Gateway) +- `a2dp_sink` (A2DP Audio Sink) +- `a2dp_source` (A2DP Audio Source) +- `bap_sink` (LE Audio Basic Audio Profile Sink) +- `bap_source` (LE Audio Basic Audio Profile Source) +- `bap_bcast_sink` (LE Audio Basic Audio Profile Broadcast Sink) +- `bap_bcast_source` (LE Audio Basic Audio Profile Broadcast Source) +\endparblock + +@PAR@ monitor-prop bluez5.codecs # JSON array of string +Enabled A2DP codecs (default: all). Possible values: `sbc`, `sbc_xq`, +`aac`, `aac_eld`, `aptx`, `aptx_hd`, `aptx_ll`, `aptx_ll_duplex`, +`faststream`, `faststream_duplex`, `lc3plus_h3`, `ldac`, `opus_05`, +`opus_05_51`, `opus_05_71`, `opus_05_duplex`, `opus_05_pro`, `opus_g`, +`lc3`. + +@PAR@ monitor-prop bluez5.default.rate # integer +Default audio rate. + +@PAR@ monitor-prop bluez5.default.channels # integer +Default audio channels. + +@PAR@ monitor-prop bluez5.hfphsp-backend # integer +HFP/HSP backend (default: native). Available values: any, none, hsphfpd, ofono, native + +@PAR@ monitor-prop bluez5.hfphsp-backend-native-modem # string + +@PAR@ monitor-prop bluez5.dummy-avrcp player # boolean +Register dummy AVRCP player. Some devices have wrongly functioning +volume or playback controls if this is not enabled. Default: false + +@PAR@ monitor-prop bluez5.enable-sbc-xq # boolean +Override device quirk list and enable SBC-XQ for devices for which it is disabled. + +@PAR@ monitor-prop bluez5.enable-msbc # boolean +Override device quirk list and enable MSBC for devices for which it is disabled. + +@PAR@ monitor-prop bluez5.enable-hw-volume # boolean +Override device quirk list and enable hardware volume fo devices for which it is disabled. + +@PAR@ monitor-prop bluez5.hw-offload-sco # boolean +\parblock +HFP/HSP hardware offload SCO support (default: false). + +This feature requires a custom configuration that routes SCO audio to ALSA nodes, +in a platform-specific way. See `tests/examples/bt-pinephone.lua` in WirePlumber for an example. +Do not enable this setting if you don't know what all this means, as it won't work. +\endparblock + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.channels = 3 # integer +PipeWire Opus Pro audio profile channel count. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.coupled-streams = 1 # integer +PipeWire Opus Pro audio profile coupled stream count. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.locations = "FL,FR,LFE" # string +PipeWire Opus Pro audio profile audio channel locations. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.max-bitrate = 600000 # integer +PipeWire Opus Pro audio profile max bitrate. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.frame-dms = 50 # integer +PipeWire Opus Pro audio profile frame duration (1/10 ms). + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.bidi.channels = 1 # integer +PipeWire Opus Pro audio profile duplex channels. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.bidi.coupled-streams = 0 # integer +PipeWire Opus Pro audio profile duplex coupled stream count. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.bidi.locations = "FC" # string +PipeWire Opus Pro audio profile duplex coupled channel locations. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000 # integer +PipeWire Opus Pro audio profile duplex max bitrate. + +@PAR@ monitor-prop bluez5.a2dp.opus.pro.bidi.frame-dms = 400 # integer +PipeWire Opus Pro audio profile duplex frame duration (1/10 ms). + +@PAR@ monitor-prop bluez5.bcast_source.config = [] # JSON +\parblock +Example: +``` +bluez5.bcast_source.config = [ + { + "broadcast_code": "Børne House", + "encryption: false, + "sync_factor": 2, + "bis": [ + { # BIS configuration + "qos_preset": "16_2_1", # QOS preset name from table Table 6.4 from BAP_v1.0.1. + "audio_channel_allocation": 1, # audio channel allocation configuration for the BIS + "metadata": [ # metadata configurations for the BIS + { "type": 1, "value": [ 1, 1 ] } + ] + } + ] + } +] +``` +\endparblock + +@PAR@ monitor-prop bluez5.bap-server-capabilities.rates # Array of integers +Supported sampling frequencies for the LC3 codec (default: all). +Possible values: +`8000`, `16000`, `24000`, `32000`, `44100`, `48000` + +@PAR@ monitor-prop bluez5.bap-server-capabilities.durations # Array of doubles +Supported frame durations for the LC3 codec (default: all). +Possible values: +`7.5`, `10` + +@PAR@ monitor-prop bluez5.bap-server-capabilities.channels # Array of integers +Supported audio channel counts for the LC3 codec (default: [1, 2]). +Possible values: +`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8` + +@PAR@ monitor-prop bluez5.bap-server-capabilities.framelen_min # integer +Minimum number of octets supported per codec frame for the LC3 codec (default: 20). + +@PAR@ monitor-prop bluez5.bap-server-capabilities.framelen_max # integer +Maximum number of octets supported per codec frame for the LC3 codec (default: 400). + +@PAR@ monitor-prop bluez5.bap-server-capabilities.max_frames # integer +Maximum number of codec frames supported per SDU for the LC3 codec (default: 2). + +## Device properties + +@PAR@ device-prop bluez5.auto-connect # boolean +Auto-connect devices on start up. Disabled by default if +the property is not specified. + +@PAR@ device-prop bluez5.hw-volume = [ hfp_ag hsp_ag a2dp_source ] # JSON array of string +Profiles for which to enable hardware volume control. + +@PAR@ device-prop bluez5.profile # string +Initial device profile. This usually has no effect as the session manager +overrides it. + +@PAR@ device-prop bluez5.a2dp.ldac.quality = "auto" # string +LDAC encoding quality +Available values: +- auto (Adaptive Bitrate, default) +- hq (High Quality, 990/909kbps) +- sq (Standard Quality, 660/606kbps) +- mq (Mobile use Quality, 330/303kbps) + +@PAR@ device-prop bluez5.a2dp.aac.bitratemode = 0 # integer +AAC variable bitrate mode. +Available values: 0 (cbr, default), 1-5 (quality level) + +@PAR@ device-prop bluez5.a2dp.opus.pro.application = "audio" # string +PipeWire Opus Pro Audio encoding mode: audio, voip, lowdelay + +@PAR@ device-prop bluez5.a2dp.opus.pro.bidi.application = "audio" # string +PipeWire Opus Pro Audio duplex encoding mode: audio, voip, lowdelay + +@PAR@ device-prop bluez5.bap.cig = "auto" # integer, or 'auto' +Set CIG ID for BAP unicast streams of the device. + +## Node properties + +@PAR@ node-prop bluez5.media-source-role # string +\parblock +Media source role for Bluetooth clients connecting to +this instance. Available values: + - playback: playing stream to speakers + - input: appear as source node. +\endparblock + +# PORT PROPERTIES @IDX@ props + +Port properties are usually not directly configurable via PipeWire +configuration files, as they are determined by applications creating +them. Below are some port properties may interesting for users: + +@PAR@ port-prop port.name # string +\parblock +\copydoc PW_KEY_PORT_NAME +\endparblock + +@PAR@ port-prop port.alias # string +\parblock +\copydoc PW_KEY_PORT_ALIAS +\endparblock + +\see pw_keys in the API documentation for a full list. + +# LINK PROPERTIES @IDX@ props + +Link properties are usually not directly configurable via PipeWire +configuration files, as they are determined by applications creating +them. + +\see pw_keys in the API documentation for a full list. + +# CLIENT PROPERTIES @IDX@ props + +Client properties are usually not directly configurable via PipeWire +configuration files, as they are determined by the application +connecting to PipeWire. Clients are however affected by the settings +in \ref page_man_pipewire_conf_5 "pipewire.conf(5)" and session +manager settings. + +\note Only the properties `pipewire.*` are safe to use for security +purposes such as identifying applications and their capabilities, as +clients can set and change other properties freely. + +Below are some client properties may interesting for users. + +@PAR@ client-prop application.name # string +\parblock +\copydoc PW_KEY_APP_NAME +\endparblock + +@PAR@ client-prop application.process.id # integer +\parblock +\copydoc PW_KEY_APP_PROCESS_ID +\endparblock + +@PAR@ client-prop pipewire.sec.pid # integer +\parblock +Client pid, set by protocol. + +Note that for PulseAudio applications, this is the PID of the +`pipewire-pulse` process. +\endparblock + +\see pw_keys in the API documentation for a full list. + +# RUNTIME SETTINGS @IDX@ props + +Objects such as devices and nodes also have *parameters* that can be +modified after the object has been created. For example, the active +device profile, channel volumes, and so on. + +For some objects, the *parameters* also allow changing some of +the *properties*. The settings of most ALSA and virtual device parameters +can be configured also at runtime. + +These settings are available in device *parameter* called `Props` in its +`params` field. They can be seen e.g. using `pw-dump ` for an ALSA device: + +```json +{ +... + "Props": [ + { + ... + "params": [ + "audio.channels", + 2, + "audio.rate", + 0, + "audio.format", + "UNKNOWN", + "audio.position", + "[ FL, FR ]", + "audio.allowed-rates", + "[ ]", + "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.multi-rate", + true, + "latency.internal.rate", + 0, + "latency.internal.ns", + 0, + "clock.name", + "api.alsa.c-1" + ] + } +... +``` + +They generally have the same names and meaning as the corresponding properties. + +One or more `params` can be changed using \ref page_man_pw-cli_1 "pw-cli(1)": +``` +pw-cli s Props '{ params = [ "api.alsa.headroom" 1024 ] }' +``` +These settings are not saved and need to be reapplied for each session manager restart. + +# ALSA CARD PROFILES @IDX@ props + +The sound card profiles ("Analog stereo", "Analog stereo duplex", ...) except "Pro Audio" come from two sources: + +- UCM: ALSA Use Case Manager: the profile configuration system from ALSA. See https://github.com/alsa-project/alsa-ucm-conf/ +- ACP ("Alsa Card Profiles"): Pulseaudio's profile system ported to PipeWire. See https://www.freedesktop.org/wiki/Software/PulseAudio/Backends/ALSA/Profiles/ + +See the above links on how to configure these systems. + +For ACP, PipeWire looks for the profile configuration files under + +- ~/.config/alsa-card-profile +- /etc/alsa-card-profile +- /usr/share/alsa-card-profile/mixer`. + +The `path` and `profile-set` files are in subdirectories `paths` and `profile-sets` of these directories. +It is possible to override individual files locally by putting a modified copy into the ACP directories under `~/.config` or `/etc`. + +# OTHER OBJECT TYPES @IDX@ props + +Technically, PipeWire objects is what are manipulated by applications +using the PipeWire API. + +The list of object types that are usually "exported" (i.e. appear in +\ref page_man_pw-dump_1 "pw-dump(1)" output) is larger than considered +above: + +- Node +- Device +- Port +- Link +- Client +- Metadata +- Module +- Profiler +- SecurityContext + +Monitors do not appear in this list; they are not usually exported, +and technically also Device objects. They are considered above as a +separate object type because they have configurable properties. + +Metadata objects are what is manipulated with +\ref page_man_pw-metadata_1 "pw-metadata(1)" + +Modules can be loaded in configuration files, or by PipeWire +applications. + +The Profiler and SecurityContext objects only provide corresponding +PipeWire APIs. + +# INDEX {#pipewire-props__index} + +## Monitor properties + +@SECREF@ monitor-prop + +## Device properties + +@SECREF@ device-prop + +## Node properties + +@SECREF@ node-prop + +## Port properties + +@SECREF@ port-prop + +## Client properties + +@SECREF@ client-prop + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_conf_5 "pipewire.conf(5)" diff --git a/doc/dox/config/pipewire-pulse-modules.7.md b/doc/dox/config/pipewire-pulse-modules.7.md new file mode 100644 index 0000000..21ef3fe --- /dev/null +++ b/doc/dox/config/pipewire-pulse-modules.7.md @@ -0,0 +1,20 @@ +\page page_man_pipewire-pulse-modules_7 pipewire-pulse-modules + +PipeWire Pulseaudio modules + +# DESCRIPTION + +\include{doc} pulse-modules.inc + +# BUILT-IN MODULES + +$(PIPEWIRE_PULSE_MODULES) + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire-pulse_1 "pipewire-pulse(1)" diff --git a/doc/dox/config/pipewire-pulse.conf.5.md b/doc/dox/config/pipewire-pulse.conf.5.md new file mode 100644 index 0000000..d1a65cf --- /dev/null +++ b/doc/dox/config/pipewire-pulse.conf.5.md @@ -0,0 +1,195 @@ +\page page_man_pipewire-pulse_conf_5 pipewire-pulse.conf + +The PipeWire Pulseaudio server configuration file + +\tableofcontents + +# SYNOPSIS + +*$XDG_CONFIG_HOME/pipewire/pipewire-pulse.conf* + +*$(PIPEWIRE_CONFIG_DIR)/pipewire-pulse.conf* + +*$(PIPEWIRE_CONFDATADIR)/pipewire-pulse.conf* + +*$(PIPEWIRE_CONFDATADIR)/pipewire-pulse.conf.d/* + +*$(PIPEWIRE_CONFIG_DIR)/pipewire-pulse.conf.d/* + +*$XDG_CONFIG_HOME/pipewire/pipewire-pulse.conf.d/* + +# DESCRIPTION + +Configuration for PipeWire's PulseAudio-compatible daemon. + +The configuration file format and lookup logic is the same as for \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +Drop-in configuration files `pipewire-pulse.conf.d/*.conf` can be used, and are recommended. +See \ref pipewire_conf__drop-in_configuration_files "pipewire.conf(5)". + +# CONFIGURATION FILE SECTIONS @IDX@ pipewire-pulse.conf + +\par stream.properties +Dictionary. These properties configure the PipeWire Pulseaudio server +properties. + +\par stream.rules +Dictionary. These properties configure the PipeWire Pulseaudio server +properties. + +\par pulse.properties +Dictionary. These properties configure the PipeWire Pulseaudio server +properties. + +\par pulse.cmd +Array of dictionaries. A set of commands to be executed on startup. + +\par pulse.rules +Array of dictionaries. A set of match rules and actions to apply to +clients. + +See \ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)" +for the detailed description. + +In addition, the PipeWire context configuration sections +may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". + +# STREAM PROPERTIES @IDX@ pipewire-pulse.conf + +The `stream.properties` section contains properties for streams created +by the pipewire-pulse server. + +Available options are described in +\ref client_conf__stream_properties "pipewire-client.conf(5) stream.properties". + +Some of these properties map to the PulseAudio `/etc/pulse/default.pa` config entries as follows: + +| PulseAudio | PipeWire | Notes | +| ------------------------------ | --------------------- | -------------------- | +| remixing-use-all-sink-channels | channelmix.upmix | | +| remixing-produce-lfe | channelmix.lfe-cutoff | Set to > 0 to enable | +| remixing-consume-lfe | channelmix.mix-lfe | | +| lfe-crossover-freq | channelmix.lfe-cutoff | | + +## Example + +```css +# ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf + +stream.properties = { + #node.latency = 1024/48000 + #node.autoconnect = true + #resample.disable = false + #resample.quality = 4 + #monitor.channel-volumes = false + #channelmix.disable = false + #channelmix.min-volume = 0.0 + #channelmix.max-volume = 10.0 + #channelmix.normalize = false + #channelmix.mix-lfe = true + #channelmix.upmix = true + #channelmix.upmix-method = psd # none, simple + #channelmix.lfe-cutoff = 150.0 + #channelmix.fc-cutoff = 12000.0 + #channelmix.rear-delay = 12.0 + #channelmix.stereo-widen = 0.0 + #channelmix.hilbert-taps = 0 + #dither.noise = 0 + #dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5 + #debug.wav-path = "" +} +``` + +# STREAM RULES @IDX@ pipewire-pulse.conf + +The `stream.rules` section works the same as +\ref client_conf__stream_rules "pipewire-client.conf(5) stream.rules". + +# PULSEAUDIO PROPERTIES @IDX@ pipewire-pulse.conf + +For `pulse.properties` section, +see \ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)" +for available options. + +# PULSEAUDIO RULES @IDX@ pipewire-pulse.conf + +For each client, a set of rules can be written in `pulse.rules` +section to configure quirks of the client or to force some pulse +specific stream configuration. + +The general look of this section is as follows and follows the layout of +\ref pipewire_conf__match_rules "match rules, see pipewire(1)". + +See \ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)" +for available options. + +## Example + +```css +# ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf + +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 + } + } + } +] +``` + +# PULSEAUDIO COMMANDS @IDX@ pipewire-pulse.conf + +As part of the server startup procedure you can execute some +additional commands with the `pulse.cmd` section in +`pipewire-pulse.conf`. + +```css +# ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf + +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" ] } +] +... +``` + +Additional commands can also be run via the +\ref pipewire_conf__context_exec "context.exec section, see pipewire.conf(5)". + +Supported commands: + +@PAR@ pipewire-pulse.conf load-module +Load the specified Pulseaudio module on startup, as if using **pactl(1)** +to load the module. + +# PULSEAUDIO MODULES @IDX@ pipewire-pulse.conf + +For contents of section `pulse.modules`, +see \ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)". + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)", +\ref page_man_pipewire_conf_5 "pipewire.conf(5)", +\ref page_man_pipewire-pulse_1 "pipewire-pulse(1)", +\ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)" diff --git a/doc/dox/config/pipewire.conf.5.md b/doc/dox/config/pipewire.conf.5.md new file mode 100644 index 0000000..dcb1a0a --- /dev/null +++ b/doc/dox/config/pipewire.conf.5.md @@ -0,0 +1,696 @@ +\page page_man_pipewire_conf_5 pipewire.conf + +The PipeWire server configuration file + +\tableofcontents + +# 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 file is looked up in the order listed in the +[SYNOPSIS](#synopsis). The environment variables `PIPEWIRE_CONFIG_DIR`, +`PIPEWIRE_CONFIG_PREFIX` and `PIPEWIRE_CONFIG_NAME` can be used to +specify an alternative config directory, subdirectory and file +respectively. + +Other PipeWire configuration files generally follow the same lookup +logic, replacing `pipewire.conf` with the name of the particular +config file. + +# DROP-IN CONFIGURATION FILES @IDX@ pipewire.conf + +All `*.conf` files in the `pipewire.conf.d/` directories are loaded +and merged into the configuration. Dictionary sections are merged, +overriding properties if they already existed, and array sections are +appended to. The drop-in files have same format as the main +configuration file, but only contain the settings to be modified. + +As the `pipewire.conf` configuration file contains various parts +that must be present for correct functioning, using drop-in files +for configuration is recommended. + +## Example + +A configuration file `~/.config/pipewire/pipewire.conf.d/custom.conf` +to change the value of the `default.clock.min-quantum` setting in `pipewire.conf`: + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.properties = { + default.clock.min-quantum = 128 +} +``` + +# CONFIGURATION FILE FORMAT @IDX@ pipewire.conf + +The configuration file is in "SPA" JSON format. + +The configuration file contains top-level keys, which are the sections. +The value of a section is either a dictionary, `{ }`, or an +array, `[ ]`. Section and dictionary item declarations +have `KEY = VALUE` form, and are separated by whitespace. +For example: + +``` +context.properties = { # top-level dictionary section + + key1 = value # a simple value + + key2 = { key1 = value1 key2 = value2 } # a dictionary with two entries + + key3 = [ value1 value2 ] # an array with two entries + + key4 = [ { k = v1 } { k = v2 } ] # an array of dictionaries + +} + +context.modules = [ # top-level array section + + value1 + + value2 + +] +``` + +The configuration files can also be written in standard JSON syntax, +but for easier manual editing, the relaxed "SPA" variant is allowed. +In "SPA" JSON: + +- `:` to delimit keys and values can be substituted by `=` or a space. +- \" around keys and string can be omitted as long as no special + characters are used in the strings. +- `,` to separate objects can be replaced with a whitespace character. +- `#` can be used to start a comment until the line end + +# CONFIGURATION FILE SECTIONS @IDX@ pipewire.conf + +\par context.properties +Dictionary. These properties configure the PipeWire instance. + +\par context.spa-libs +Dictionary. Maps plugin features with globs to a spa library. + +\par context.modules +Array of dictionaries. Each entry in the array is a dictionary with the +*name* of the module to load, including optional *args* and *flags*. +Most modules support being loaded multiple times. + +\par context.objects +Array of dictionaries. Each entry in the array is a dictionary +containing the *factory* to create an object from and optional extra +arguments specific to that factory. + +\par context.exec +\parblock +Array of dictionaries. Each entry in the array is dictionary containing +the *path* of a program to execute on startup and optional *args*. + +This array used to contain an entry to start the session manager but +this mode of operation has since been demoted to development aid. Avoid +starting a session manager in this way in production environment. +\endparblock + +\par node.rules +Array of dictionaries. Match rules for modifying node properties +on the server. + +\par device.rules +Array of dictionaries. Match rules for modifying device properties +on the server. + + +# CONTEXT PROPERTIES @IDX@ pipewire.conf + +Available PipeWire properties in `context.properties` and possible +default values. + +@PAR@ pipewire.conf clock.power-of-two-quantum = true +The quantum requests from the clients and the final graph quantum are +rounded down to a power of two. A power of two quantum can be more +efficient for many processing tasks. + +@PAR@ pipewire.conf context.data-loop.library.name.system +The name of the shared library to use for the system functions for the data processing +thread. This can typically be changed if the data thread is running on a realtime +kernel such as EVL. + +@PAR@ pipewire.conf loop.rt-prio = -1 +The priority of the data loops. The data loops are used to schedule the nodes in the graph. +A value of -1 uses the default realtime priority from the module-rt. A value of 0 disables +realtime scheduling for the data loops. + +@PAR@ pipewire.conf loop.class = [ data.rt .. ] +An array of classes of the data loops. Normally nodes are assigned to a loop by name or by class. +Nodes are by default assigned to the data.rt class so it is good to have a data loop +of this class as well. + +@PAR@ pipewire.conf context.num-data-loops = 1 +The number of data loops to create. By default 1 data-loop is created and all nodes are +scheduled in this thread. A value of 0 disables the real-time data loops and schedules +all nodes in the main thread. A value of -1 spawns as many data threads as there are +cpu cores. + +@PAR@ pipewire.conf context.data-loops = [ ... ] +This controls the data loops that will be created for the context. Is is an array of +data loop specifications, one entry for each data loop to start: +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.data-loops = [ + { + #library.name.system = support/libspa-support + loop.rt-prio = -1 + loop.class = [ data.rt .. ] + thread.name = data-loop.0 + thread.affinity = [ 0 1 ] + } + ... +] +``` +A specific priority, classes and name can be given with loop.rt-prio, loop.class and +thread.name respectively. It is also possible to pin the data loop to specific CPU +cores with the thread.affinity property. + +@PAR@ pipewire.conf core.daemon = false +Makes the PipeWire process, started with this config, a daemon +process. This means that it will manage and schedule a graph for +clients. You would also want to configure a core.name to give it a +well known name. + +@PAR@ pipewire.conf core.name = pipewire-0 +The name of the PipeWire context. This will also be the name of the +PipeWire socket clients can connect to. + +@PAR@ pipewire.conf cpu.zero.denormals = false +Configures the CPU to zero denormals automatically. This will be +enabled for the data processing thread only, when enabled. + +@PAR@ pipewire.conf cpu.vm.name = null +This will be set automatically when the context is created and will +contain the name of the VM. It is typically used to write match rules +to set extra properties. + +@PAR@ pipewire.conf default.clock.rate = 48000 +The default clock rate determines the real time duration of the +min/max/default quantums. You might want to change the quantums when +you change the default clock rate to maintain the same duration for +the quantums. + +@PAR@ pipewire.conf default.clock.allowed-rates = [ ] +It is possible to specify up to 32 alternative sample rates. The graph +sample rate will be switched when devices are idle. Note that this is +not enabled by default for now because of various kernel and Bluetooth +issues. Note that the min/max/default quantum values are scaled when +the samplerate changes. + +@PAR@ pipewire.conf default.clock.min-quantum = 32 +Default minimum quantum. + +@PAR@ pipewire.conf default.clock.max-quantum = 8192 +Default maximum quantum. + +@PAR@ pipewire.conf default.clock.quantum = 1024 +Default quantum used when no client specifies one. + +@PAR@ pipewire.conf default.clock.quantum-limit = 8192 +Maximum quantum to reserve space for. This is the maximum buffer size used +in the graph, regardless of the samplerate. + +@PAR@ pipewire.conf default.clock.quantum-floor = 4 +Minimum quantum to reserve space for. This is the minimum buffer size used +in the graph, regardless of the samplerate. + +@PAR@ pipewire.conf default.video.width +Default video width + +@PAR@ pipewire.conf default.video.height +Default video height + +@PAR@ pipewire.conf default.video.rate.num +Default video rate numerator + +@PAR@ pipewire.conf default.video.rate.denom +Default video rate denominator + +@PAR@ pipewire.conf library.name.system = support/libspa-support +The name of the shared library to use for the system functions for the main thread. + +@PAR@ pipewire.conf link.max-buffers = 64 +The maximum number of buffers to negotiate between nodes. Note that version < 3 clients +can only support 16 buffers. More buffers is almost always worse than less, latency +and memory wise. + +@PAR@ pipewire.conf log.level = 2 +The default log level used by the process. + +@PAR@ pipewire.conf mem.allow-mlock = true +Try to mlock the memory for the realtime processes. Locked memory will +not be swapped out by the kernel and avoid hickups in the processing +threads. + +@PAR@ pipewire.conf mem.warn-mlock = false +Warn about failures to lock memory. + +@PAR@ pipewire.conf mem.mlock-all = false +Try to mlock all current and future memory by the process. + +@PAR@ pipewire.conf settings.check-quantum = false +Check if the quantum in the settings metadata update is compatible +with the configured limits. + +@PAR@ pipewire.conf settings.check-rate = false +Check if the rate in the settings metadata update is compatible +with the configured limits. + +@PAR@ pipewire.conf support.dbus = true +Enable DBus support. This will enable DBus support in the various modules that require +it. Disable this if you want to globally disable DBus support in the process. + +@PAR@ pipewire.conf vm.overrides = { default.clock.min-quantum = 1024 } +Any property in the vm.overrides property object will override the property +in the context.properties when PipeWire detects it is running in a VM. This +is deprecated, use the context.properties.rules instead. + +@PAR@ pipewire.conf context.modules.allow-empty = false +By default, a warning is logged when there are no context.modules loaded because this +likely indicates there is a problem. Some applications might load the modules themselves +and when they set this property to true, no warning will be logged. + +The context properties may also contain custom values. For example, +the `context.modules` and `context.objects` sections can declare +additional conditions that control whether a module or object is loaded +depending on what properties are present. + +# SPA LIBRARIES @IDX@ pipewire.conf + +SPA plugins are loaded based on their factory-name. This is a well +known name that uniquely describes the features that the plugin should +have. The `context.spa-libs` section provides a mapping between the +factory-name and the plugin where the factory can be found. + +Factory names can contain a wildcard to group several related factories into one +plugin. The plugin is loaded from the first matching factory-name. + +## Example + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.spa-libs = { + 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 + video.convert.* = videoconvert/libspa-videoconvert +} +``` + +# MODULES @IDX@ pipewire.conf + +PipeWire modules to be loaded. See +\ref page_man_libpipewire-modules_7 "libpipewire-modules(7)". + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.modules = [ + #{ name = MODULENAME + # ( args = { KEY = VALUE ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { KEY = VALUE ... } ... ] ) + #} + # +] +``` + +\par name +Name of module to be loaded + +\par args = { } +Arguments passed to the module + +\par flags = [ ] +Loading flags. `ifexists` to only load module if it exists, +and `nofail` to not fail PipeWire startup if the module fails to load. + +\par condition = [ ] +A \ref pipewire_conf__match_rules "match rule" `matches` condition. +The module is loaded only if one of the expressions in the array matches +to a context property. + +# CONTEXT OBJECTS @IDX@ pipewire.conf + +The `context.objects` section allows you to make some objects from factories (usually created +by loading modules in `context.modules`). + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.objects = [ + #{ factory = + # ( args = { = ... } ) + # ( flags = [ ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} +] +``` +This section can be used to make nodes or links between nodes. + +\par factory +Name of the factory to create the object. + +\par args = { } +Arguments passed to the factory. + +\par flags = [ ] +Flag `nofail` to not fail PipeWire startup if the object fails to load. + +\par condition = [ ] +A \ref pipewire_conf__match_rules "match rule" `matches` condition. +The object is created only if one of the expressions in the array matches +to a context property. + +## Example + +This fragment creates a new dummy driver node, but only if +`core.daemon` property is true: + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.objects = [ + { factory = spa-node-factory + args = { + factory.name = support.node.driver + node.name = Dummy-Driver + node.group = pipewire.dummy + priority.driver = 20000 + }, + condition = [ { core.daemon = true } ] + } +] +``` + +# COMMAND EXECUTION @IDX@ pipewire.conf + +The `context.exec` section can be used to start arbitrary commands as +part of the initialization of the PipeWire program. + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.exec = [ + #{ path = + # ( args = "" | [ ... ] ) + # ( condition = [ { = ... } ... ] ) + #} +] +``` + +\par path +Program to execute. + +\par args +Arguments to the program. + +\par condition +A \ref pipewire_conf__match_rules "match rule" `matches` condition. +The object is created only if one of the expressions in the array matches +to a context property. + +## Example + +The following fragment executes a pactl command with the given arguments: + +``` +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.exec = [ + { path = "pactl" args = "load-module module-always-sink" } +] +``` + +# MATCH RULES @IDX@ pipewire.conf + +Some configuration file sections contain match rules. This makes it +possible to perform some action when an object (usually a node or +stream) is created/updated that matches certain properties. + +The general rules object follows the following pattern: +```css + = [ + { + matches = [ + # any of the following sets of properties are matched, if + # any matches, the actions are executed + { + # = + # all keys must match the value. ! negates. ~ starts regex. + #application.process.binary = "teams" + #application.name = "~speech-dispatcher.*" + + # Absence of property can be tested by comparing to null + #pipewire.sec.flatpak = null + } + { + # more matches here... + } + ... + ] + actions = { + = + ... + } + } +] +``` +Match rules are an array of rules. + +A rule is always a JSON object with two keys: matches and actions. The matches key is used to +define the conditions that need to be met for the rule to be evaluated as true, and the actions +key is used to define the actions that are performed when the rule is evaluated as true. + +The matches key is always a JSON array of objects, where each object defines a condition that needs +to be met. Each condition is a list of key-value pairs, where the key is the name of the property +that is being matched, and the value is the value that the property needs to have. Within a condition, +all the key-value pairs are combined with a logical AND, and all the conditions in the matches +array are combined with a logical OR. + +The actions key is always a JSON object, where each key-value pair defines an action that is +performed when the rule is evaluated as true. The action name is specific to the rule and is +defined by the rule’s documentation, but most frequently you will see the update-props action, +which is used to update the properties of the matched object. + +In the matches array, it is also possible to use regular expressions to match property values. +For example, to match all nodes with a name that starts with my_, you can use the following condition: + +```css +matches = [ + { + node.name = "~my_.*" + } +] +``` + +The ~ character signifies that the value is a regular expression. The exact syntax of the regular +expressions is the POSIX extended regex syntax, as described in the regex (7) man page. + +In addition to regular expressions, you may also use the ! character to negate a condition. For +example, to match all nodes with a name that does not start with my_, you can use the following condition: + +```css +matches = [ + { + node.name = "!~my_.*" + } +] +``` + +The ! character can be used with or without a regular expression. For example, to match all +nodes with a name that is not equal to my_node, you can use the following condition: + +```css +matches = [ + { + node.name = "!my_node" + } +] +``` + +The null value has a special meaning; it checks if the property is not available +(or unset). To check if a property is not set: + +```css +matches = [ + { + node.name = null + } +] +``` + +To check the existence of a property, one can use the !null condition, for example: + +```css +matches = [ + { + node.name = "!null" + } + { + node.name = !null # simplified syntax + } +] +``` +To handle the "null" string, one needs to escape the string. For example, to check +if a property has the string value "null", use: + +```css +matches = [ + { + node.name = "null" + } +] +``` +To handle anything but the "null" string, use: + +```css +matches = [ + { + node.name = "!\"null\"" + } + { + node.name = !"null" # simplified syntax + } +] +``` + + +# CONTEXT PROPERTIES RULES @IDX@ pipewire.conf + +`context.properties.rules` can be used to dynamically update the properties +based on other properties. + +A typical case is to update custom settings when running inside a VM. +The `cpu.vm.name` is automatically set when running in a VM with the name of +the VM. A match rule can be written to set custom properties like this: + +```css +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +context.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + # These overrides are only applied when running in a vm. + default.clock.min-quantum = 1024 + } + } + } +} +``` + +# NODE RULES @IDX@ pipewire.conf + +The node.rules are evaluated every time the properties on a node are set +or updated. This can be used on the server side to override client set +properties on arbitrary nodes. + +`node.rules` provides an `update-props` action that takes an object with +properties that are updated on the node object. + +Add a `node.rules` section in the config file like this: + +```css +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +node.rules = [ + { + matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + client.name = "jack_simple_client" + } + ] + actions = { + update-props = { + node.force-quantum = 512 + } + } + } +] +``` + +Will set the `node.force-quantum` property of `jack_simple_client` to 512. + +# DEVICE RULES @IDX@ pipewire.conf + +The device.rules are evaluated every time the properties on a device are set +or updated. This can be used on the server side to override client set +properties on arbitrary devices. + +`device.rules` provides an `update-props` action that takes an object with +properties that are updated on the device object. + +Add a `device.rules` section in the config file like this: + +```css +# ~/.config/pipewire/pipewire.conf.d/custom.conf + +device.rules = [ + { + matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + device.name = ""v4l2_device.pci-0000_00_14.0-usb-0_1.2_1.0 + } + ] + actions = { + update-props = { + device.description = "My Webcam" + } + } + } +] +``` + +Will set the `device.description` property of the device with the given `device.name` +to "My Webcam". + + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", +\ref page_man_libpipewire-modules_7 "libpipewire-modules(7)" +\ref page_man_pipewire-pulse_conf_5 "pipewire-pulse.conf(5)" +\ref page_man_pipewire-client_conf_5 "pipewire-client.conf(5)" diff --git a/doc/dox/config/xref.md b/doc/dox/config/xref.md new file mode 100644 index 0000000..507d09b --- /dev/null +++ b/doc/dox/config/xref.md @@ -0,0 +1,51 @@ +\page page_config_xref Index + +\ref page_man_pipewire_conf_5 "pipewire.conf" + +@SECREF@ pipewire.conf + +\ref page_man_pipewire-pulse_conf_5 "pipewire-pulse.conf" + +@SECREF@ pipewire-pulse.conf + +\ref page_man_pipewire-client_conf_5 "client.conf" + +@SECREF@ client.conf + +\ref page_man_pipewire-jack_conf_5 "jack.conf" + +@SECREF@ jack.conf + +**Runtime settings** + +@SECREF@ pipewire-settings + +**Environment variables** + +@SECREF@ pipewire-env client-env jack-env pulse-env + +**Object properties** + +@SECREF@ props + +**Monitor properties** + +@SECREF@ monitor-prop + +**Device properties** + +@SECREF@ device-prop + +**Node properties** + +@SECREF@ node-prop + +**Port properties** + +@SECREF@ port-prop + +**Client properties** + +@SECREF@ client-prop + +\see pw_keys in API documentation. diff --git a/doc/dox/index.dox b/doc/dox/index.dox new file mode 100644 index 0000000..6688ca1 --- /dev/null +++ b/doc/dox/index.dox @@ -0,0 +1,52 @@ +/** \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. + +# Documentation + +- \ref page_config +- \ref page_programs +- \ref page_modules +- \ref page_pulse_modules + +See our [Wiki](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/home) for +more information on how to configure and use 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_programs to introspect and use the \ref page_daemon. +- A \ref page_library to develop PipeWire applications and plugins (\ref + page_tutorial "tutorial"). +- The \ref page_spa used by both the \ref page_daemon and in the \ref + page_library. + +# API Documentation + +See \ref page_api. + +# Resources + +- [PipeWire and AGL](https://wiki.automotivelinux.org/_media/pipewire_agl_20181206.pdf) +- [LAC 2020 Paper](https://lac2020.sciencesconf.org//data/proceedings.pdf) and + [Video](https://tube.aquilenet.fr/w/uy8PJyMnBrpBFNEZ9D48Uu) +- [PipeWire Under The Hood](https://venam.nixers.net/blog/unix/2021/06/23/pipewire-under-the-hood.html) +- [PipeWire: The Linux audio/video bus (LWN)](https://lwn.net/Articles/847412) +- [PipeWire Wikipedia](https://en.wikipedia.org/wiki/PipeWire) +- [Bluetooth, PipeWire and Whatsapp calls](https://gjhenrique.com/pipewire.html) +- [Intoduction to PipeWire](https://bootlin.com/blog/an-introduction-to-pipewire/) +- [A custom PipeWire node](https://bootlin.com/blog/a-custom-pipewire-node/) + +*/ diff --git a/doc/dox/internals/access.dox b/doc/dox/internals/access.dox new file mode 100644 index 0000000..fd45663 --- /dev/null +++ b/doc/dox/internals/access.dox @@ -0,0 +1,124 @@ +/** \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. + +Depending on the resolution, it grants permissions to the client as follows: + +- `"unrestricted"`: ALL permissions are set on the core + object and the client will be able to resume. +- any other value: No permissions are set on the core object + and the client will be 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 not unrestricted, 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/dox/internals/audio.dox b/doc/dox/internals/audio.dox new file mode 100644 index 0000000..b39e869 --- /dev/null +++ b/doc/dox/internals/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/dox/internals/daemon.dox b/doc/dox/internals/daemon.dox new file mode 100644 index 0000000..aede037 --- /dev/null +++ b/doc/dox/internals/daemon.dox @@ -0,0 +1,177 @@ +/** \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 (from files ending with a .conf extension) 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=[][,:][,:,...]` 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 performant version of + `*:`. 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. +- `` 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. + +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=`: Redirect the log to the given filename. +- `PIPEWIRE_LOG_LINE=false`: Don't log filename, function, and source code line. +- `PIPEWIRE_LOG_COLOR=true/false/force`: Enable/disable color logging, and optionally force + colors even when logging to a file. + +*/ diff --git a/doc/dox/internals/design.dox b/doc/dox/internals/design.dox new file mode 100644 index 0000000..1473360 --- /dev/null +++ b/doc/dox/internals/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_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/dox/internals/dma-buf.dox b/doc/dox/internals/dma-buf.dox new file mode 100644 index 0000000..7456976 --- /dev/null +++ b/doc/dox/internals/dma-buf.dox @@ -0,0 +1,305 @@ +/** \page page_dma_buf DMA-BUF Sharing + +PipeWire supports sharing Direct Memory Access buffers (DMA-BUFs) between +clients via the \ref 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 \ref 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 \ref 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 \ref 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 \ref SPA_POD_PROP_FLAG_DONT_FIXATE (see param_changed hook, For producers). + +The second stream parameter should not contain any \ref SPA_FORMAT_VIDEO_modifier +property. + +To prioritise DMA-BUFs place those \ref SPA_PARAM_EnumFormat containing modifiers +first, when emitting them to PipeWire. + +## param_changed Hook + +When the `param_changed` hook is called for a \ref 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 +\ref SPA_PARAM_BUFFERS_dataType property to `1 << SPA_DATA_DmaBuf`. If they were +not negotiated, fall back to shared memory by setting the +\ref SPA_PARAM_BUFFERS_dataType property to `1 << SPA_DATA_MemFd`, +`1 << SPA_DATA_MemPtr`, or both. + +While consumers only have to parse the resulting \ref 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 \ref 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 \ref 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 + \ref 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 \ref EnumFormat by announcing a \ref SPA_PARAM_EnumFormat with + only one modifier in the \ref SPA_CHOICE_Enum and without the + \ref SPA_POD_PROP_FLAG_DONT_FIXATE flag, followed by the previous announced + \ref EnumFormat. This will retrigger the `param_changed` event with an + \ref SPA_PARAM_Format as described below. + If the \ref SPA_PARAM_Format contains a modifier key, without the flag + \ref SPA_POD_PROP_FLAG_DONT_FIXATE, it should only contain one value in the + \ref SPA_CHOICE_Enum. In this case announce the \ref 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. + You might also want to add the option of adding explicit sync support to the + buffers, as explained below. + +Note: When test allocating a buffer, collect all possible modifiers, while omitting +`DRM_FORMAT_MOD_INVALID` from the \ref 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 \ref SPA_DATA_MemFd or +\ref SPA_DATA_MemPtr use the fallback SHM import mechanism. +If it's \ref 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: that the n_datas +might also contain extra fds for things like sync_timelime metadata, you need +to take this into account when persing the planes. + +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. + +# SPA param video format helpers + +SPA offers helper functions to parse and build a spa_pod object to/from the spa_video_info_* +struct. The flags \ref SPA_VIDEO_FLAG_MODIFIER and \ref SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED +are used to indicate modifier usage with the format. `SPA_VIDEO_FLAG_MODIFIER` declares the +parsed/provided spa_video_info_* struct contains valid modifier information. For legacy +reasons `spa_format_video_*_build` will announce any modifier != 0 even when this flag is +unused. `SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED` is exclusive to the parse helpers and +declares that the parsed spa_pod contains modifier information which needs to be fixated as +described above. The list of available modifiers has to be parsed manually from the spa_pod +object. + +- \ref spa_video_info_raw, \ref spa_format_video_raw_parse, \ref spa_format_video_raw_build +- \ref spa_video_info_dsp, \ref spa_format_video_dsp_parse, \ref spa_format_video_dsp_build + +# 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. + +# Explicit sync + +In addition to DMABUF, a set of synchronization primitives (a SyncObjTimeline) and +associated metadata can be negotiated on the buffers. + +The explicit sync step is performed *after* the Format has been negotiated. + +## Query support for explicit sync in the driver. + +You might first want to check that the drm render you are using is capable of explicit +sync by checking support for DRM_CAP_SYNCOBJ and DRM_CAP_SYNCOBJ_TIMELINE before +attempting to negotiate explicit sync. + +## Provide space in the buffer for explicit sync + +Explicit sync requires two extra fds in the buffers and an extra +\ref SPA_META_SyncTimeline metadata structure. + +The metadata structure will only be allocated when both sides support explicit +sync. We can use this to make a fallback \ref SPA_PARAM_Buffers so that we can +support both explicit sync and a fallback to implicit sync. + +So, first announce support for \ref SPA_META_SyncTimeline by adding the +\ref SPA_TYPE_OBJECT_ParamMeta object to the stream: + +``` + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_SyncTimeline), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_sync_timeline))); +``` + +Next make a \ref SPA_PARAM_Buffers that depends on the negotiation of the SyncTimelime metadata: + +``` + spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); + spa_pod_builder_add(&b, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(3), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<datas[buf->n_datas - 2].fd, &acquire_handle); + drmSyncobjFDToHandle(drm_fd, buf->datas[buf->n_datas - 1].fd, &release_handle); +``` + +## Use the SPA_META_SyncTimeline when processing buffers + +The \ref struct spa_meta_sync_timeline contains 2 fields: the acquire_point and +release_point. + +Producers will start a render operation on the DMABUF of the buffer and place +the acquire_point in the \ref struct spa_meta_sync_timeline. When the rendering is +complete, the producer should signal the acquire_point on the acquire SyncObjTimeline. + +Producers will also add a release_point on the release SyncObjTimeline. They are +only allowed to reuse the buffer when the release_point has been signaled. + +Consumers use the acquire_point to wait for rendering to complete before processing +the buffer. This can be offloaded to the hardware when submitting the rendering +operation or it can be done explicitly with drmSyncobjTimelineWait() on the acquire +SyncObjTimeline handle and the acquire_point of the metadata. + +Consumers should then also signal the release_point on the release SyncObjTimeline when +they complete processing the buffer. This can be done in the hardware as part of +the render pipeline or explicitly with drmSyncobjTimelineSignal() on the release +handle and the release_point of the metadata. + + +*/ diff --git a/doc/dox/internals/index.dox b/doc/dox/internals/index.dox new file mode 100644 index 0000000..f7152d6 --- /dev/null +++ b/doc/dox/internals/index.dox @@ -0,0 +1,26 @@ +/** \page page_internals Internals + +# 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 +- \subpage page_scheduling +- \subpage page_native_protocol + + +# Components + +- \subpage page_daemon +- \subpage page_session_manager + +# Backends + +- \subpage page_pulseaudio + +*/ diff --git a/doc/dox/internals/library.dox b/doc/dox/internals/library.dox new file mode 100644 index 0000000..ff18cb9 --- /dev/null +++ b/doc/dox/internals/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 advertised 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/dox/internals/midi.dox b/doc/dox/internals/midi.dox new file mode 100644 index 0000000..1228300 --- /dev/null +++ b/doc/dox/internals/midi.dox @@ -0,0 +1,115 @@ +/** \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. + +As of 1.4, SPA_CONTROL_UMP (Universal Midi Packet) is the prefered format +for the MIDI 1.0 and 2.0 messages in the \ref spa_pod_sequence. Conversion +to SPA_CONTROL_Midi is performed for legacy applications. + +## The PipeWire Daemon + +Nothing special is implemented for MIDI. Negotiation of formats +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 + +## Session manager (Wireplumber) + +The session manager uses the \ref SPA_NAME_API_ALSA_SEQ_BRIDGE plugin for +the MIDI features. This creates a single SPA Node with ports per +MIDI client/stream. + +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, \ref SPA_CONTROL_OSC and +\ref SPA_CONTROL_UMP types. On output ports, the JACK event stream is +converted to control messages in a similar way. + +Normally, all MIDI and UMP messages are converted to MIDI1 jack events unless +the JACK port was created with an explcit "32 bits raw UMP" format, in which +case the raw UMP is passed to the JACK application directly. For output ports, +the JACK events are assumed to be MIDI1 and converted to UMP unless the port +has the "32 bit raw UMP" format, in which case the UMP messages are simply +passed on. + +There is a 1 to 1 mapping between the JACK events and control +messages so there is no information loss or need for complicated +conversions. + +*/ diff --git a/doc/dox/internals/objects.dox b/doc/dox/internals/objects.dox new file mode 100644 index 0000000..f67b6b6 --- /dev/null +++ b/doc/dox/internals/objects.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/dox/internals/portal.dox b/doc/dox/internals/portal.dox new file mode 100644 index 0000000..935e261 --- /dev/null +++ b/doc/dox/internals/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/docs/doc-org.freedesktop.portal.Camera.html) +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/docs/doc-org.freedesktop.impl.portal.PermissionStore.html). + +\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/dox/internals/protocol.dox b/doc/dox/internals/protocol.dox new file mode 100644 index 0000000..dadebd7 --- /dev/null +++ b/doc/dox/internals/protocol.dox @@ -0,0 +1,1691 @@ +/** \page page_native_protocol Native Protocol + +PipeWire has a pluggable client/server IPC protocol. + +The reference implementation uses unix sockets and is implemented in +\ref page_module_protocol_native. + +We document the messages here. + +\tableofcontents + +# Message header {#native-protocol-message-header} + +Each message on the unix socket contains a 16 bytes header and a +variable length payload size: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Id | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | opcode | size | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | seq | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | n_fds | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | payload POD | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | optional footer POD | + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + + +These are four uint32 words to be read in native endianness. + +- Id: the message id this is the destination resource/proxy id +- opcode: the opcode on the resource/proxy interface +- size: the size of the payload and optional footer of the message +- seq: an increasing sequence number for each message +- n_fds: number of file descriptors in this message. + +The sender should send along with each message the fds that belong to +the message. If there are more than the maximum number of fds in the +message than can fit in one message, the message is split into multiple +parts. + +A receiver needs to first read the header to know the size of the +complete message. After collecting the complete message, it can start +processing it. + +The payload is a single POD see \ref page_spa_pod for details. + +After the payload, there is an optional footer POD object. + +# Making a connection {#native-protocol-making-connection} + +First a connection is made to a unix domain socket. By default, the socket is +named as "pipewire-0" and searched in the following directories: + + - getenv("PIPEWIRE_RUNTIME_DIR") + - getenv("XDG_RUNTIME_DIR") + - getenv("USERPROFILE") + + +The client opens the socket and allocates a Core proxy with id 0 and a Client +proxy with id 1. The proxy is nothing more than a way to identify the remote +objects with an id. Usually they also contain the type and version of the +remote object and some helpers (interfaces) to dispatch messages. + +The server will allocate a new Core resource with id 0. Resources are the +counterpart of client proxies and also typically contain extra information +and helpers (interfaces) to dispatch messages. + +The client sends the Core::Hello message on the socket to start the +communication. + +The server binds the client resource with id 1 to the client. This means +that the client object on the server will now have a resource with a +client side proxy associated with it. + +When the client sends a message for a certain proxy, the server side +resource with the same id for the client is found and the message is +dispatched to the object associated with the resource. + +Similarly when a server sends a message on a resource, the client will +find the proxy with the same id and dispatch the message to it's +associated object. + +The client then sends client properties to the server. + +``` + client server + |---------------------------------------->| + | open socket | + | | + |---------------------------------------->| + | Core::Hello(version) | + | | + |---------------------------------------->| + | Client::UpdateProperties | + . . +``` + +This completes the setup of the client. The newly connected client will +appear in the registry at this point. + +# Core proxy/resource {#native-protocol-core} + +The core is always the object with Id 0. + +## Core Methods (Id 0) + +### Core::Hello (Opcode 1) + +The first message sent by a client is the Hello message and contains the +version number of the client. + +``` + Struct( + Int: version + ) +``` + +The version is 3. + +### Core::Sync (Opcode 2) + +The Sync message will result in a Done event from the server. When the Done +event is received, the client can be sure that all operations before the Sync +method have been completed. + +``` + Struct( + Int: id + Int: seq + ) +``` + +- id: the id will be returned in the Done event. +- seq: is usually generated automatically and will be returned in the Done event. + +### Core::Pong (Opcode 3) + +Is sent from the client to the server when the server emits the Ping event. +The id and seq should be copied from the Ping event. + +``` + Struct( + Int: id + Int: seq + ) +``` + +### Core::Error (Opcode 4) + +An error occurred in an object on the client. + +``` + Struct( + Int: id + Int: seq + Int: res + String: message + ) +``` + +- id: The id of the proxy that is in error. +- seq: a seq number from the failing request (if any) +- res: a negative errno style error code +- message: an error message + +### Core::GetRegistry (Opcode 5) + +A client requests to bind to the registry object and list the available objects +on the server. + +Like with all bindings, first the client allocates a new proxy id and puts this +as the new_id field. Methods and Events can then be sent and received on the +new_id (in the message Id field). + +``` + Struct( + Int: version + Int: new_id + ) +``` +- version: the version of the registry interface used on the client +- new_id: the id of the new proxy with the registry interface + +After this method, the server will start sending Registry::Global events +to the proxy with new_id. + +``` + client server + | | + | new proxy(new_id) | + |---------------------------------------->| new resource(new_id) + | Core::GetRegistry(version) | + | | + |<----------------------------------------| new_id + | Registry::Global() | + |<----------------------------------------| + | Registry::Global() | + . . +``` + +There is no explicit last Global event to signal that the last object +has been received. The usual way of knowing this is to send a Core::Sync +method right after the Core::GetRegistry method and to wait for the +Core::Done event. + +``` + client server + | | + | new proxy(new_id) | + |---------------------------------------->| new resource(new_id) + | Core::GetRegistry(version) | + |---------------------------------------->| + | seq = Core::Sync | + | | + |<----------------------------------------| new_id + | Registry::Global() | + |<----------------------------------------| + | Registry::Global() | + . . + |<----------------------------------------| + | Core::Done(seq) | + . . +``` + +### Core::CreateObject (Opcode 6) + +Create a new object from a factory of a certain type. + +The client allocates a new_id for the proxy. The server will allocate a new +resource with the same new_id and from then on, Methods and Events will be +exchanged between the new object of the given type. + +``` + Struct( + String: factory_name + String: type + Int: version + Struct( + Int: n_items + (String: key + String: value)* + ): props + Int: new_id + ) +``` + +- factory_name: the name of a server factory object to use +- type: the type of the object to create, this is also the type of the + interface of the new_id proxy. +- props: extra properties to create the object +- new_id: the proxy id of the new object + +### Core::Destroy (Opcode 7) + +Destroy an object. + +``` + Struct( + Int: id + ) +``` + +- id: the proxy id of the object to destroy. + +## Core Events + +Core events are emitted by the server. + +### Core::Info (Opcode 0) + +Emitted by the server upon connection with the more information about the +server. + +``` + Struct( + Int: id + Int: cookie + String: user_name + String: host_name + String: version + String: name + Long: change_mask + Struct( + Int: n_items + (String: key + String: value)* + ): props + ) +``` + +- id: the id of the server (0) +- cookie: a unique cookie for this server +- user_name: the name of the user running the server +- host_name: the name of the host running the server +- version: a version string of the server +- name: the name of the server +- change_mask: a set of bits with changes to the info +- props: optional key/value properties, valid when change_mask has (1<<0) + +### Core::Done (Opcode 1) + +Emitted as a result of a client Sync method. + +``` + Struct( + Int: id + Int: seq + ) +``` + +id and seq are passed from the client Sync request. + +### Core::Ping (Opcode 2) + +Emitted by the server when it wants to check if a client is alive or ensure +that it has processed the previous events. + +``` + Struct( + Int: id + Int: seq + ) +``` + +- id: the object id to ping +- seq: usually automatically generated. The client should pass this in the Pong + method reply. + +### Core::Error (Opcode 3) + +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. + +``` + Struct( + Int: id + Int: seq + Int: res + String: message + ) +``` + +- id: The id of the resource that is in error. +- seq: a seq number from the failing request (if any) +- res: a negative errno style error code +- message: an error message + + +### Core::RemoveId (Opcode 4) + +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. + +``` + Struct( + Int: id + ) +``` + +- id: a proxy id that was removed + +### Core::BoundId (Opcode 5) + +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. This event is deprecated, the BoundProps event should +be used because it also contains extra properties. + +``` + Struct( + Int: id + Int: global_id + ) +``` + +- id: a proxy id +- global_id: the global_id as it will appear in the registry. + +### Core::AddMem (Opcode 6) + +Memory is given to a client as fd of a certain memory type. +Further references to this fd will be made with the per memory +unique identifier id. + +``` + Struct( + Int: id + Id: type + Fd: fd + Int: flags + ) +``` + +- id: a server allocated id for this memory +- type: the memory type, see enum spa_data_type +- fd: the index of the fd sent with this message +- flags: extra flags + +### Core::RemoveMem (Opcode 7) + +Remove memory for a client with the given id + +``` + Struct( + Int: id + ) +``` + +- id: the id of the memory to remove. This is the Id from AddMem. + +### Core::BoundProps (Opcode 8) + +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. + +``` + Struct( + Int: id + Int: global_id + Struct( + Int: n_items + (String: key + String: value)* + ): props + ) +``` + +- id: a proxy id +- global_id: the global_id as it will appear in the registry. +- props: the properties of the global + +# Registry proxy/resource {#native-protocol-registry} + +The registry is obtained with the GetRegistry method on the Core object. +The Id depends on the new_id that was provided. + +## Registry Methods + +### Registry::Bind (Opcode 1) + +Bind to the global object with 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. + +``` + Struct( + Int: id + String: type + Int: version + Int: new_id + ) +``` + +- id: the global_id to bind to +- type: the type of the global id +- version: the client version of the interface for type +- new_id: the client proxy id for the global object + +### Registry::Destroy (Opcode 2) + +Try to destroy the global object with id. This might fail when the +client does not have permission. + +``` + Struct( + Int: id + ) +``` + +- id: the global id to destroy. + +## Registry Events + +### Registry::Global (Opcode 0) + +Notify a client about a new global object. + +``` + Struct( + Int: id + Int: permissions + String: type + Int: version + Struct( + Int: n_items + (String: key + String: value)* + ): props + ) +``` + +- id: the global id +- permissions: permission bits +- type: the type of object +- version: the server version of the object +- props: extra global properties + +### Registry::GlobalRemove (Opcode 1) + +A global with id was removed + +``` + Struct( + Int: id + ) +``` + +- id: the global id that was removed. + +# PipeWire:Interface:Client {#native-protocol-client} + +The client object represents a client connect to the PipeWire server. +Permissions of the client can be managed. + +The currently connected client always has the Client object with +proxy id 1. + +## Client Methods + +### Client::Error (Opcode 1) + +Is used to send an error to a client. + +``` + Struct( + Int: id + Int: res + String: error + ) +``` + +- id: a client proxy id to send the error to +- res: a negative errno style error code +- error: an error message + +### Client::UpdateProperties (Opcode 2) + +Is used to update the properties of a client. + +``` + Struct( + Struct( + Int: n_items + (String: key + String: value)* + ): props + ) +``` +- props: properties to update on the client. + +### Client::GetPermissions (Opcode 3) + +Get the currently configured permissions on the client. + +``` + Struct( + Int: index + Int: num + ) +``` +- index: the start index of the permissions to get +- num: the number of permissions to get + +This method will result in at most num Permission Events. + +### Client::UpdatePermissions (Opcode 4) + +Update the permissions of the global objects using the +provided array with permissions + + +``` + Struct( + Int: n_permissions + ( Int: id + Int: permission + )* + ) +``` + +- n_permissions: number of permissions +- id: the global id +- permissions: the permissions for the global id + +## Client Events + +### Client::Info (Opcode 0) + +Get client information updates. This is emitted when binding to a client or +when the client info is updated later. + +``` + Struct( + Int: id + Long: change_mask + Struct( + Int: n_items + (String: key + String: value)* + ): props + ) +``` +- id: the global id of the client +- change_mask: the changes emitted by this event +- props: properties of this object, valid when change_mask has (1<<0) + +### Client::Permissions (Opcode 1) + +Emitted as the reply of the GetPermissions method. + +``` + Struct( + Int: index + Struct( + Int: n_permissions + (Int: id + Int: permission)* + ) + ) +``` +- index: index of the first permission +- n_permissions: the number of permission entries +- id: the global id of the object +- permissions: the permission for the given id + +# PipeWire:Interface:Device {#native-protocol-device} + +A device is an object that manages other devices or nodes. + +The usual flow is to bind to the Device object. This will result in an +Info event. From the Info event one can find the available params to +enumerate. + +## Device methods + +### Device::SubscribeParams (Opcode 1) + +Automatically emit Param events for the given ids when they are changed. + +``` + Struct( + Array[Id]: ids + ) +``` +- ids: and array of param Id to subscribe to + +### Device::EnumParams (Opcode 2) + +Enumerate the values of a param. This will result in Param events. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: num + Pod: filter + ) +``` +- seq: an automatically generated sequence number, will be copied into the reply +- id: the param id to enumerate. +- index: the first param index to retrieve +- num: the number of params to retrieve +- filter: an optional filter object for the param. + +### Device::SetParam (Opcode 3) + +Set a parameter on the Device. + +``` + Struct( + Id: id + Int: flags + Pod: param + ) +``` +- id: the param id to set. +- flags: extra flags +- param: the param object to set + +## Device events + +### Device::Info (Opcode 0) + +The info event is emitted when binding or when the device information changed. + +``` + Struct( + Int: id + Long: change_mask + Struct( + Int: n_items + ( String: key + String: value )* + ): props + Struct( + Int: n_params + ( Int: id + Int: flags )* + ): param_info + ) +``` + +- id: the id of the global +- change_mask: a bitmask of changed fields +- props: extra properties, valid when change_mask is (1<<0) +- param_info: info about the parameters, valid when change_mask is (1<<1) + For each parameter, the id and current flags are given. + - param_info.id : see enum spa_param_type + - param_info.flags: struct spa_param_info.flags + +### Device::Param (Opcode 1) + +Emitted as a result of EnumParams or SubscribeParams. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: next + Pod: param + ) +``` +- seq: the sequence number send by the client EnumParams or server generated + in the SubscribeParams case. +- id: the param id that is reported, see enum spa_param_type +- index: the index of the parameter +- next: the index of the next parameter +- param: the parameter. The object type depends on the id + + +# PipeWire:Interface:Factory {#native-protocol-factory} + +A factory is an object that allows one to create new objects. + +## Factory methods + +A factory has no methods + +## Factory events + +### Factory::Info (Opcode 0) + +Info is emitted when binding to the factory global or when the information changed. + +``` + Struct( + Int: id + String: name + String: type + Int: version + Long: change_mask + Struct( + Int: n_items + ( String: key + String: value )* + ): props + ) +``` +- id: the global id of the factory +- name: the name of the factory. This can be used as the name for Core::CreateObject +- type: the object type produced by this factory +- version: the version of the object interface +- change_mask: bitfield of changed values. +- props: optional properties of the factory, valid when change_mask is (1<<0) + + +# PipeWire:Interface:Link {#native-protocol-link} + +A link is a connection between 2 ports. + +## Link methods + +A link has no methods + +## Link events + +### Link::Info (Opcode 0) + +Info is emitted when binding to the link global or when the information changed. + +``` + Struct( + Int: id + Int: output_node_id + Int: output_port_id + Int: input_node_id + Int: input_port_id + Long: change_mask + Int: state + String: error + Pod: format + Struct( + Int: n_items + ( String: key + String: value )* + ): props + ) +``` + +- id: the global id of the link +- output_node_id: the global id of the output node +- output_port_id: the global id of the output port +- input_node_id: the global id of the input node +- input_port_id: the global id of the input port +- change_mask: bitfield of changed values. +- state: the state of the link, valid when change_mask has (1<<0) + - see enum pw_link_state for values +- error: an optional error string +- format: an optional format for the link, valid when change_mask has (1<<1) +- props: optional properties of the link, valid when change_mask is (1<<2) + + +# PipeWire:Interface:Module {#native-protocol-module} + +A Module provides dynamically loaded functionality + +## Module methods + +A module has no methods + +## Module events + +### Module::Info (Opcode 0) + +Info is emitted when binding to the module global or when the information changed. + +``` + Struct( + Int: id + String: name + String: filename + String: args + Long: change_mask + Struct( + Int: n_items + ( String: key + String: value )* + ): props + ) +``` + +- id: the global id of the module +- name: the name of the module +- filename: the filename of the module +- args: arguments passed when loading the module +- change_mask: bitfield of changed values. +- props: optional properties of the module, valid when change_mask has (1<<0) + + +# PipeWire:Interface:Node {#native-protocol-node} + +A Node is a processing element in the graph + +## Node methods + +### Node::SubscribeParams (Opcode 1) + +Automatically emit Param events for the given ids when they are changed. + +``` + Struct( + Array[Id]: ids + ) +``` +- ids: and array of param Id to subscribe to + +### Node::EnumParams (Opcode 2) + +Enumerate the values of a param. This will result in Param events. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: num + Pod: filter + ) +``` +- seq: an automatically generated sequence number, will be copied into the reply +- id: the param id to enumerate. +- index: the first param index to retrieve +- num: the number of params to retrieve +- filter: an optional filter object for the param. + +### Node::SetParam (Opcode 3) + +Set a parameter on the Node. + +``` + Struct( + Id: id + Int: flags + Pod: param + ) +``` +- id: the param id to set. +- flags: extra flags +- param: the param object to set + +### Node::SendCommand (Opcode 4) + +Send a Command to the node. + +``` + Struct( + Pod: command + ) +``` +- command: the command to send. See enum spa_node_command + +## Node events + +### Node::Info (Opcode 0) + +The info event is emitted when binding or when the node information changed. + +``` + Struct( + Int: id + Int: max_input_ports + Int: max_output_ports + Long: change_mask + Int: n_input_ports + Int: n_output_ports + Id: state + String: error + Struct( + Int: n_items + ( String: key + String: value )* + ): props + Struct( + Int: n_params + ( Int: id + Int: flags )* + ): param_info + ) +``` + +- id: the id of the node global +- max_input_port: the maximum input ports for the node +- max_output_port: the maximum output ports for the node +- change_mask: a bitmask of changed fields +- n_input_port: the number of input ports, when change_mask has (1<<0) +- n_output_port: the number of output ports, when change_mask has (1<<1) +- state: the current node state, when change_mask has (1<<2) + - See enum pw_node_state for values +- error: an error message. +- props: extra properties, valid when change_mask is (1<<3) +- param_info: info about the parameters, valid when change_mask is (1<<4) + For each parameter, the id and current flags are given. + - param_info.id : see enum spa_param_type + - param_info.flags: struct spa_param_info.flags + +### Node::Param (Opcode 1) + +Emitted as a result of EnumParams or SubscribeParams. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: next + Pod: param + ) +``` +- seq: the sequence number send by the client EnumParams or server generated + in the SubscribeParams case. +- id: the param id that is reported, see enum spa_param_type +- index: the index of the parameter +- next: the index of the next parameter +- param: the parameter. The object type depends on the id + + +# PipeWire:Interface:Port {#native-protocol-port} + +A port is part of a node and allows links with other ports. + +## Port methods + +### Port::SubscribeParams (Opcode 1) + +Automatically emit Param events for the given ids when they are changed. + +``` + Struct( + Array[Id]: ids + ) +``` +- ids: and array of param Id to subscribe to + +### Port::EnumParams (Opcode 2) + +Enumerate the values of a param. This will result in Param events. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: num + Pod: filter + ) +``` +- seq: an automatically generated sequence number, will be copied into the reply +- id: the param id to enumerate. +- index: the first param index to retrieve +- num: the number of params to retrieve +- filter: an optional filter object for the param. + +## Port events + +### Port::Info (Opcode 0) + +The info event is emitted when binding or when the port information changed. + +``` + Struct( + Int: id + Int: direction + Long: change_mask + Struct( + Int: n_items + ( String: key + String: value )* + ): props + Struct( + Int: n_params + ( Int: id + Int: flags )* + ): param_info + ) +``` + +- id: the id of the port global +- direction: the direction of the port, see enum pw_direction +- change_mask: a bitmask of changed fields +- props: extra properties, valid when change_mask is (1<<0) +- param_info: info about the parameters, valid when change_mask is (1<<1) + For each parameter, the id and current flags are given. + - param_info.id : see enum spa_param_type + - param_info.flags: struct spa_param_info.flags + +### Port::Param (Opcode 1) + +Emitted as a result of EnumParams or SubscribeParams. + +``` + Struct( + Int: seq + Id: id + Int: index + Int: next + Pod: param + ) +``` +- seq: the sequence number send by the client EnumParams or server generated + in the SubscribeParams case. +- id: the param id that is reported, see enum spa_param_type +- index: the index of the parameter +- next: the index of the next parameter + + +# PipeWire:Interface:ClientNode {#native-protocol-clientnode} + +The ClientNode object is created from the `client-node` factory that is provided +by the `libpipewire-module-client-node` module. + +It creates a server Node that can be controlled from a client. Processing will happen +in the client. It is used by pw-stream and pw-filter to implement the PipeWire media +processing nodes. + +## ClientNode methods + +### ClientNode::GetNode (Opcode 1) + +Get the node object associated with the client-node. This binds to the server side +Node object. + +``` + Struct( + Int: version + Int: new_id + ) +``` +- version: the Node version to bind as +- new_id: the proxy id + +### ClientNode::Update (Opcode 2) + +Update the params and info of the node. + +``` + Struct( + Int: change_mask + Int: n_params + ( Pod: param )* + Struct( + Int: max_input_ports + Int: max_output_ports + Long: change_mask + Long: flags + Int: n_items + ( String: key + String: value )* + Int: n_params + ( Id: id + Int: flags )* + ): info + ) +``` +- change_mask: a bitfield of changed items +- n_params: number of update params, valid when change_mask has (1<<0) +- param: an updated param +- info: an updated `struct spa_node_info`, valid when change_mask has (1<<1) + - max_input_ports: maximum input ports + - max_output_ports: maximum output ports + - change_mask: bitmask of changed items + - flags: flags, see `struct spa_node_info`, when change_mask has (1<<0) + - n_items: updated properties, valid when info.change_mask has (1<<1) + - n_params: updated `struct spa_param_info`, valid when info.change_mask has (1<<2) + + +### ClientNode::PortUpdate (Opcode 3) + +Create, Update or destroy a node port. + +When the port is not known on the server, the port is created. +When info is None, the port is destroyed. +Otherwise, the port information is updated. + +``` + Struct( + Int: direction + Int: port_id + Int: change_mask + Int: n_params + ( Pod: param )* + Struct( + Long: change_mask + Long: flags + Int: rate_num + Int: rate_denom + Int: n_items + ( String: key + String: value )* + Int: n_params + ( Id: id + Int: flags )* + ): info + ) +``` +- direction: the port direction +- port_id: the port id +- change_mask: a bitfield of changed items +- n_params: number of updated params, valid when change_mask has (1<<0) +- param: n_params updated params +- info: an updated `struct spa_port_info`, valid when change_mask has (1<<1) + - change_mask: bitmask of changed items + - flags: flags, see `struct spa_port_info`, when change_mask has (1<<0) + - rate_num: updated rate numerator + - rate_denom: updated rate denominator, when info.change_mask has (1<<1) + - n_items: updated properties, valid when info.change_mask has (1<<2) + - n_params: updated `struct spa_param_info`, valid when info.change_mask has (1<<3) + +### ClientNode::SetActive (Opcode 4) + +Set the node active or inactive. + +``` + Struct( + Bool: active + ) +``` +- active: the new state of the node + +### ClientNode::Event (Opcode 5) + +Emit an event on the node. + +``` + Struct( + Pod: event + ) +``` +- event: the event to emit. See `enum spa_node_event`. + +### ClientNode::PortBuffers (Opcode 6) + +This method is used by the client when it has allocated buffers for a port. +It is usually called right after the UseBuffers event to let the server know +about the the newly allocated buffer memory. + +``` + Struct( + Int: direction + Int: port_id + Int: mix_id + Int: n_buffers + ( Int: n_datas + ( Id: type + Fd: memfd + Int: flags + Int: mapoffset + Int: maxsize + )* + )* + ) +``` +- direction: the direction of the port +- port_id: the port id +- mix_id: the mix id of the port +- n_buffer: the number of buffers +- n_data: for each buffer the number of data planes +- type: the plane memory type +- memfd: the plane memfd +- mapoffset: the start offset of where the buffer memory starts +- maxsize: the maximum size of the memory. + +## ClientNode events + +### ClientNode::Transport (Opcode 0) + +The server will allocate the activation record and eventfd for the node and +transfer this to the client with the Transport event. + +``` + Struct( + Fd: readfd + Fd: writefd + Int: memfd + Int: offset + Int: size + ) +``` +- readfd: the eventfd to start processing +- writefd: the eventfd to signal when the driver completes and profiling is + enabled. +- memfd: the index of the memfd of the activation record +- offset: the offset in memfd of the start of the activation record +- size: the size of the activation record + +The activation record is currently an internal data structure that is +not yet ABI stable. + +The writefd is meant to wake up the server after the driver completes so +that the profiler can collect the information. The profiler is active +when the pw_node_activation::flags fields has PW_NODE_ACTIVATION_FLAG_PROFILER +set. When the profiler is disabled (or when the node is not driving), this +eventfd should not be signaled. + +### ClientNode::SetParam (Opcode 1) + +Set a parameter on the Node. + +``` + Struct( + Id: id + Int: flags + Pod: param + ) +``` +- id: the param id to set. +- flags: extra flags +- param: the param object to set + + +### ClientNode::SetIO (Opcode 2) + +Set an IO area on the node. + +``` + Struct( + Id: id + Int: memid + Int: offset + Int: size + ) +``` +- id: the io area id to set. +- the memid to use, this is signaled with Core::AddMem +- offset: the start offset in the memory area +- the size of the io area + +### ClientNode::Event (Opcode 3) + +Emit an event on the node. + +``` + Struct( + Pod: event + ) +``` +- event: the event to emit. See `enum spa_node_event`. + +### ClientNode::Command (Opcode 4) + +Send a command on the node. + +``` + Struct( + Pod: command + ) +``` +- command: the command to send. See `enum spa_node_command`. + +### ClientNode::AddPort (Opcode 5) + +Add a new port to the node + +``` + Struct( + Int: direction + Int: port_id + Struct( + Int: n_items + ( String: key + String: value )* + ): props + ) +``` +- direction: the direction of the new port +- port_id: the port id of the new port +- props: optional extra properties for the port + +### ClientNode::RemovePort (Opcode 6) + +Remove a port from the node + +``` + Struct( + Int: direction + Int: port_id + ) +``` +- direction: the direction of the port to remove +- port_id: the port id of the port to remove + + +### ClientNode::PortSetParam (Opcode 7) + +Set a parameter on the Port of the node. + +``` + Struct( + Int: direction + Int: port_id + Id: id + Int: flags + Pod: param + ) +``` +- direction: the direction of the port +- port_id: the port id of the port +- id: the param id to set. +- flags: extra flags +- param: the param object to set + +### ClientNode::UseBuffers (Opcode 8) + +Use a set of buffers on the mixer port + +``` + Struct( + Int: direction + Int: port_id + Int: mix_id + Int: flags + Int: n_buffers + ( Int: memid + Int: offset + Int: size + Int: n_metas + ( Id: type + Int: size + )*: meta + Int: n_datas + ( Id: type + Int: data + Int: flags + Int: mapoffset + Int: maxsize + )*: data + )*: buffer + ) +``` +- direction: the direction of the port +- port_id: the port id of the port +- mix_id: the mixer id of the port +- flags: extra flags +- id: the param id to set. +- flags: extra flags + - SPA_NODE_BUFFERS_FLAG_ALLOC (1<<0) to let the client allocate + buffers, in which case the PortBuffers method needs to be called + with the allocated buffer memory. +- n_buffers: the number of buffers +- memid: the memory id of the buffer metadata and or data +- offset: the offset in memid of the buffer +- size: the size of the buffer metadata or data +- n_metas: number of metadata. The buffer memory first contains this + number of metadata parts of the given type and size + - type: metadata type + - size: metadata size, round up with 8 to get to the next metadata +- n_data: the number of datablocks (planes) per buffer + - type: the data type, this can be: + - SPA_DATA_MemId to reference a memfd from Core:AddMem + - SPA_DATA_MemPtr to reference this buffer memid + - data: contains the memid or offset in the memid + - flags: extra flags for the data + - mapoffset: the offset in memfd + - maxsize: the maxsize of the memory in memfd + +### ClientNode::PortSetIO (Opcode 9) + +Set an IO area on a mixer port. + +``` + Struct( + Int: direction + Int: port_id + Int: mix_id + Id: id + Int: memid + Int: offset + Int: size + ) +``` +- direction: the direction of the port +- port_id: the port id of the port +- mix_id: the mix id of the port +- id: the IO area to set. See enum spa_io_type +- memid: the memid of the io area, added with Core::AddMem +- offset: the offset in the memid +- size: the size of the IO area + + +### ClientNode::SetActivation (Opcode 10) + +Notify the client of the activation record of a peer node. This activation record +should be triggered when this node finishes processing. + +``` + Struct( + Int: node_id + Fd: signalfd + Int: memid + Int: offset + Int: size + ) +``` +- node_id: the node_id of the peer node +- signalfd: the eventfd of the peer node +- memid: the memid of the activation record of the peer from Core:AddMem +- offset: the offset in memid +- size: the size of the activation record + +### ClientNode::PortSetMixInfo (Opcode 11) + +Notify the node of the peer of a mixer port. This can be used to track the peer +ports of a node. + +``` + Struct( + Int: direction + Int: port_id + Int: mix_id + Int: peer_id + Struct( + Int: n_items + ( String: key + String: value )* + ): props + ) +``` +- direction: the direction of the port +- port_id: the port id of the port +- mix_id: the mix id of the port +- peer_id: the id of the peer port +- props: optional properties + +# PipeWire:Interface:Metadata {#native-protocol-metadata} + +Metadata is a shared database of settings and properties. + +## Metadata methods + +### Metadata::SetProperty (Opcode 1) + +Set a property in the metadata store. + +``` + Struct( + Int: subject + String: key + String: type + String: value + ) +``` +- subject: the id of the object, this needs to be a valid global_id +- key: a key +- type: an optional type +- value: a value + + +### Metadata::Clear (Opcode 2) + +Clear the metadata store. + +``` + Struct( + None + ) +``` + +## Metadata events + +### Metadata::Property (Opcode 0) + +A metadata key changed. This is also emitted when binding to the metadata. + +``` + Struct( + Int: subject + String: key + String: type + String: value + ) +``` +- subject: the id of the object, this is a valid global_id +- key: a key +- type: an optional type +- value: a value + +# PipeWire:Interface:Profiler {#native-protocol-profiler} + +The profiler object allows one to receive profiler information of the pipewire +graph. + +## Profiler methods + +The profiler has no methods + +## Profiler events + +### Profiler::Profile (Opcode 0) + +``` + Struct( + Pod: object + ) +``` +- object: a SPA_TYPE_OBJECT_Profiler object. See enum spa_profiler + + +# Footer {#native-protocol-footer} + +The message footer contains additional messages, not directed to the +destination object defined by the `Id` field. + +The footer consists of a single POD, immediately following the payload +POD. The footer POD consists of a sequence of footer opcode Ids and +footer payload Structs containing their arguments: + +``` + Struct( + Id: opcode1, + Struct { ... }, + Id: opcode2, + Struct { ... }, + ... + ) +``` + +The footer opcodes are separate for server-to-client (`core`) and +client-to-server (`client`) directions. + +The message footer is processed before other parts of the message, +including the object Id lookup. + +## Core Generation (Footer Opcode 0) + +``` + Struct( + Long: registry_generation, + ) +``` + +Indicates to the client what is the current registry generation +number of the \ref pw_context on the server side. + +The server shall include this footer in the next message it sends that +follows the increment of the registry generation number. + +\see \ref native-protocol-registry-generation + +## Client Generation (Footer Opcode 0) + +``` + Struct( + Long: client_generation, + ) +``` + +Indicates to the server what is the last registry generation number +the client has processed. + +The client shall include this footer in the next message it sends, +after it has processed an incoming message whose footer includes a +registry generation update. + +\see \ref native-protocol-registry-generation + +# Registry generation {#native-protocol-registry-generation} + +The registry generation is a 64-bit integer in the PipeWire server +\ref pw_context that increments by one when the server allocates a new +global \ref PW_KEY_OBJECT_ID for an object. + +The server keeps track of each global *id* as a tuple ( *id*, *object +generation* ) where *object generation* is the registry generation +value when the *id* was allocated. + +When an object is destroyed, its *id* value may be reallocated to a +different object. Because the protocol is asynchronous, the +object *id* alone is not sufficient to uniquely identify objects. + +The server looks up objects based on a tuple ( *id*, *generation* ) as +follows: + +1. Look up the \ref pw_global based on the *id*. + +2. The lookup fails if there is no global for the *id*, or + if *generation* < *object generation*. + +The protocol message generally contains only the object *id*. The +registry generation part is passed around as follows: + +1. The server sends the current *registry generation* to clients in the + protocol footer, if it has changed. + +2. The clients keep track of the latest registry generation of the + messages they have processed. This is the *client generation*. + +3. Each client sends their *client generation* in the protocol footer + of the next message to the server, if its value has changed. + +4. The server keeps track for each client the *client generation* they + have sent back. + +5. In each protocol message received from client, the server considers + each object *id* as tuple ( *id*, *client generation* ). + +This allows the server to know if the object *id* the client refers to was +already destroyed, but the client has not yet processed the message +indicating that the *id* is gone. The server indicates failed lookups +of this type with error code ESTALE to the client. + +If a client has not sent any *client generation* updates to the +server, then the server will not do any registry generation checks in +object lookups. This is for backward compatibility only. + +The registry generation is an internal detail of the server +implementation and the native protocol, and is not visible to clients +in the PipeWire API. To identify objects uniquely, clients can use +\ref PW_KEY_OBJECT_SERIAL, which are unique for objects and not +reused, unlike \ref PW_KEY_OBJECT_ID + +\see \ref PW_KEY_OBJECT_SERIAL + +*/ diff --git a/doc/dox/internals/pulseaudio.dox b/doc/dox/internals/pulseaudio.dox new file mode 100644 index 0000000..2ac19a9 --- /dev/null +++ b/doc/dox/internals/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/dox/internals/scheduling.dox b/doc/dox/internals/scheduling.dox new file mode 100644 index 0000000..989aa03 --- /dev/null +++ b/doc/dox/internals/scheduling.dox @@ -0,0 +1,348 @@ +/** \page page_scheduling Graph Scheduling + +This document tries to explain how the PipeWire graph is scheduled. + +Graph are constructed from linked nodes together with their ports. This +results in a dependency graph between nodes. Special care is taken for +loopback links so that the graph remains a directed graph. + +# Processing threads + +The server (and clients) have two processing threads: + +- A main thread that will do all IPC with clients and server and configures the + nodes in the graph for processing. +- A (or more) data processing thread that only does the data processing. + + +The data processing threads are given realtime priority and are designed to +run with as little overhead as possible. All of the node resources such as +buffers, io areas and metadata will be set up in shared memory before the +node is scheduled to run. + +This document describes the processing that happens in the data processing +thread after the main-thread has configured it. + +# Nodes + +Nodes are objects with 0 or more input and output ports. + +Each node also has: + +- an eventfd to signal the node that it can start processing +- an activation record that lives in shared memory with memfd. + +``` + eventfd + +-^---------+ + | | + in out + | | + +-v---------+ + activation { + status:OK, // bitmask of NEED_DATA, HAVE_DATA or OK + pending:0, // number of unsatisfied dependencies to be able to run + required:0 // number of dependencies with other nodes + } +``` + +The activation record has the following information: + + - processing state and pending dependencies. As long as there are pending dependencies + the node can not be processed. This is the only relevant information for actually + scheduling the graph and is shown in the above illustration. + - Current status of the node and profiling info (TRIGGERED, AWAKE, FINISHED, timestamps + when the node changed state). + - Timing information, mostly for drivers when the processing started, the time, duration + and rate (quantum) etc.. + - Information about repositions (seek) and timebase owners. + + +# Links + +When two nodes are linked together, the output node becomes a dependency for the input +node. This means the input node can only start processing when the output node is finished. + +This dependency is reflected in the required counter in the activation record. In below +illustration, B's required field is incremented with 1. The pending field is set to the +required field when the graph is started. Node A will keep a list of all targets (B) that it +is a dependency of. + +This dependency update is only performed when the link is ready (negotiated) and the nodes +are ready to schedule (runnable). + + +``` + eventfd eventfd + +-^---------+ +-^---------+ + | | link | | + in A out ---------------------> in B out + | | | | + +-v---------+ +-v---------+ + activation { target activation { + status:OK, --------------------> status:OK, + pending:0, pending:1, + required:0 required:1 + } } +``` + +Multiple links between A and B will only result in 1 target link between A and B. + + +# Drivers + +The graph can only run if there is a driver node that is in some way linked to an +active node. + +The driver is special because it will have to initiate the processing in the graph. It +will use a timer or some sort of interrupt from hardware to start the cycle. + +Any node can also be a candidate for a driver (when the node.driver property is true). +PipeWire will select the node with the highest priority.driver property as the driver. + +Nodes will be assigned to the driver node they will be scheduled with. Each node holds +a reference to the driver and increments the required field of the driver. + +When a node is ready to be scheduled, the driver adds the node to its list of targets +and increments the required field. + + +``` + eventfd eventfd + +-^---------+ +-^---------+ + | | link | | + in A out ---------------------> in B out + | | | | + +-v---------+ +-v---------+ + activation { target activation { + status:OK, --------------------> status:OK, + pending:0, pending:0, + required:1 required:2 + } } + | ^ ^ + | | / / + | | / / + | | / / + | | / / + | | / / + v | /-------------/ / + activation { / + status:OK, V---------------/ + pending:0, + required:2 + } + +-^---------+ + | | + | driver | + | | + +-v---------+ + eventfd +``` + +As seen in the illustration above, the driver holds a link to each node it needs to +schedule and each node holds a link to the driver. Some nodes hold a link to other +nodes. + +It is possible that the driver is the same as a node in the graph (for example node A) +but conceptually, the links above are still valid. + +The driver will then start processing the graph by emitting the ready signal. PipeWire +will then: + + - Check the previous cycle. Did it complete? Mark xrun on unfinished nodes. + - Perform reposition requests if any, timebase changes, etc.. + - The pending counter of each follower node is set to the required field. + - It then loops over all targets of the driver and atomically decrements the required + field of the activation record. When the required field is 0, the eventfd is signaled + and the node can be scheduled. + +In our example above, Node A and B will have their pending state decremented. Node A +will be 0 and will be triggered first (node B has 2 pending dependencies to start with and +will not be triggered yet). The driver itself also has 2 dependencies left and will not +be triggered (complete) yet. + +## Scheduling node A + +When the eventfd is signaled on a node, we say the node is triggered and it will be able +to process data. It consumes the input on the input ports and produces more data on the +output ports. + +After processing, node A goes through the list of targets and decrements each pending +field (node A has a reference to B and the driver). + +In our above example, the driver is decremented (from 2 to 1) but is not yet triggered. +node B is decremented (from 1 to 0) and is triggered by writing to the eventfd. + +## Scheduling node B + +Node B is scheduled and processes the input from node A. It then goes through the list of +targets and decrements the pending fields. It decrements the pending field of the +driver (from 1 to 0) and triggers the driver. + +## Scheduling the driver + +The graph always completes after the driver is triggered and scheduled. All required +fields from all the nodes in the target list of the driver are now 0. + +The driver calculates some stats about cpu time etc. + +# Remote nodes. + +For remote nodes, the eventfd and the activation is transferred from the server +to the client. + +This means that writing to the remote client eventfd will wake the client directly +without going to the server first. + +All remote clients also get the activation and eventfd of the peer and driver they +are linked to and can directly trigger peers and drivers without going to the +server first. + +## Remote driver nodes. + +Remote drivers start the graph cycle directly without going to the server first. + +After they complete (and only when the profiler is active), they will trigger an +extra eventfd to signal the server that the graph completed. This is used by the +server to generate the profiler info. + +## Lazy scheduling + +Normally, a driver will wake up the graph and all the followers need to process +the data in sync. There are cases where: + + 1. the follower might not be ready to process the data + 2. the driver rate is not ideal, the follower rate is better + 3. the driver might not know when new data is available in the follower and + might wake up the graph too often. + +In these cases, the driver and follower roles need to be reversed and a mechanism +needs to be provided so that the follower can know when it is worth processing the +graph. + +For notifying when the graph is ready to be processed, (non driver) nodes can send +a RequestProcess event which will arrive as a RequestProcess command in the driver. +The driver can then decide to run the graph or not. + +When the graph is started or partially controlled by RequestProcess events and +commands we say we have lazy scheduling. The driver is not always scheduling according +to its own rhythm but also depending on the follower. + +We can't just enable lazy scheduling when no follower will emit RequestProcess events +or when no driver will listen for RequestProcess commands. Two new node properties are +defined: + + - node.supports-lazy = 0 | 1 | ... + + 0 means lazy scheduling as a driver is not supported + >1 means lazy scheduling as a driver is supported with increasing preference + + - node.supports-request + + 0 means request events as a follower are not supported + >1 means request events as a follower are supported with increasing preference + + We can only enable lazy scheduling when both the driver and (at least one) follower + has the node.supports-lazy and node.supports-request property respectively. + + Node can end up as a driver (is_driver()) and lazy scheduling can be enabled (is_lazy()), + which results in the following cases: + + driver producer + -> node.driver = true + -> is_driving() && !is_lazy() + -> calls trigger_process() to start the graph + + lazy producer + -> node.driver = true + -> node.supports-lazy = 1 + -> is_driving() && is_lazy() + -> listens for RequestProcess and calls trigger_process() to start the graph + + requesting producer + -> node.supports-request = 1 + -> !is_driving() && is_lazy() + -> emits RequestProcess to suggest starting the graph + + follower producer + -> !is_driving() && !is_lazy() + + + driver consumer + -> node.driver = true + -> is_driving() && !is_lazy() + -> calls trigger_process() to start the graph + + lazy consumer + -> node.driver = true + -> node.supports-lazy = 1 + -> is_driving() && is_lazy() + -> listens for RequestProcess and calls trigger_process() to start the graph + + requesting consumer + -> node.supports-request = 1 + -> !is_driving() && is_lazy() + -> emits RequestProcess to suggest starting the graph + + follower consumer + -> !is_driving() && !is_lazy() + + +Some use cases: + + 1. Screensharing - driver producer, follower consumer + - The producer starts the graph when a new frame is available. + - The consumer consumes the new frames. + -> throttles to the rate of the producer and idles when no frames + are available. + + producer + - node.driver = true + + consumer + - node.driver = false + + -> producer selected as driver, consumer is simple follower. + lazy scheduling inactive (no lazy driver or no request follower) + + + 2. headless server - requesting producer, (semi) lazy driver consumer + + - The producer emits RequestProcess when new frames are available. + - The consumer requests new frames from the producer according to its + refresh rate when there are RequestProcess commands. + -> this throttles the framerate to the consumer but idles when there is + no activity on the producer. + + producer + - node.driver = true + - node.supports-request = 1 + + consumer + - node.driver = true + - node.supports-lazy = 2 + + -> consumer is selected as driver (lazy > request) + lazy scheduling active (1 lazy driver and at least 1 request follower) + + + 3. frame encoder - lazy driver producer, requesting follower consumer + + - The consumer pulls a frame when it is ready to encode the next one. + - The producer produces the next frame on demand. + -> throttles the speed to the consumer without idle. + + producer + - node.driver = true + - node.supports-lazy = 1 + + consumer + - node.driver = true + - node.supports-request = 1 + + -> producer is selected as driver (lazy <= request) + lazy scheduling active (1 lazy driver and at least 1 request follower) + + + +*/ diff --git a/doc/dox/internals/session-manager.dox b/doc/dox/internals/session-manager.dox new file mode 100644 index 0000000..d21ba9a --- /dev/null +++ b/doc/dox/internals/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/dox/modules.dox b/doc/dox/modules.dox new file mode 100644 index 0000000..4e93581 --- /dev/null +++ b/doc/dox/modules.dox @@ -0,0 +1,100 @@ +/** \page page_modules Modules + +A PipeWire module is effectively a PipeWire client in an `.so` file that +shares the \ref pw_context with the loading entity. Usually modules are +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_filter +- \subpage page_module_example_sink +- \subpage page_module_example_source +- \subpage page_module_fallback_sink +- \subpage page_module_ffado_driver +- \subpage page_module_filter_chain +- \subpage page_module_jackdbus_detect +- \subpage page_module_jack_tunnel +- \subpage page_module_link_factory +- \subpage page_module_loopback +- \subpage page_module_metadata +- \subpage page_module_netjack2_driver +- \subpage page_module_netjack2_manager +- \subpage page_module_parametric_equalizer +- \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_sap +- \subpage page_module_rtp_sink +- \subpage page_module_rtp_source +- \subpage page_module_rtp_session +- \subpage page_module_rt +- \subpage page_module_spa_node +- \subpage page_module_spa_node_factory +- \subpage page_module_spa_device +- \subpage page_module_spa_device_factory +- \subpage page_module_session_manager +- \subpage page_module_snapcast_discover +- \subpage page_module_vban_recv +- \subpage page_module_vban_send +- \subpage page_module_x11_bell +- \subpage page_module_zeroconf_discover + +*/ diff --git a/doc/dox/overview.dox b/doc/dox/overview.dox new file mode 100644 index 0000000..1490cd4 --- /dev/null +++ b/doc/dox/overview.dox @@ -0,0 +1,149 @@ +/** \page page_overview Overview + +# Concepts + +## The PipeWire Server + +PipeWire is a graph-based processing framework, that focuses on handling multimedia data (audio, video and MIDI mainly). + +A PipeWire graph is composed of nodes. +Each node takes an arbitrary number of inputs called ports, does some processing over this multimedia data, and sends data out of its output ports. +The edges in the graph are here called links. +They are capable of connecting an output port to an input port. + +Nodes can have an arbitrary number of ports. +A node with only output ports is often called a source, and a sink is a node that only possesses input ports. + +The PipeWire server provides the implementation of some of these nodes itself. +Most importantly, it uses alsa-lib like any other ALSA client to expose statically configured ALSA devices as nodes. +For example + +- a stereo ALSA PCM playback device can appear as a sink with two input ports: front-left and front-right or +- a virtual ALSA device, to which clients which attempt to use ALSA directly connect, can appear as a source with two output ports: front-left and front right. + +Similar mechanisms exist to interface with and accommodate applications which use JACK or Pulseaudio. + +NOTE: `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. + +Other nodes are implemented by PipeWire clients. + +## The PipeWire clients + +PipeWire clients can be any process. +They can speak to the PipeWire server through a UNIX domain socket using the PipeWire native protocol. +Besides implementing nodes, they may control the graph. + +### Graph control + +The PipeWire server itself does not perform any management of the graph; +context-dependent behaviour such as monitoring for new ALSA devices, and configuring them so that they appear as nodes, or linking nodes is not done automatically. +It rather provides an API that allows spawning, linking and controlling these nodes. +This API is then relied upon by clients to control the graph structure, without having to worry about the graph execution process. + +A recommended pattern that is often used is a single client be a daemon that deals with the session and policy management. Two implementations are known as of today: + +- pipewire-media-session, which was the first implementation of a session manager.c + Today, it is used mainly in debugging scenarios. +- WirePlumber, which takes a modular approach: + It provides another, higher-level API compared to the PipeWire one, and runs Lua scripts that implement the management logic using the said API. + It ships with default scripts and configuration that handle linking policies as well as monitoring and automatic spawning of ALSA, bluez, libcamera and v4l2 devices. + The API is available for any process, not only from WirePlumber’s Lua scripts. + +### Node implementation + +With the nodes which they implement, clients can send multimedia data into the graph or obtain multimedia data from the graph. +A client can create multiple PipeWire nodes. +That allows one to create more complex applications; +a browser would for example be able to create a node per tab that requests the ability to play audio, letting the session manager handle the routing: +This allows the user to route different tab sources to different sinks. +Another example would be an application that requires many inputs. + +## API Semantics + +The current state of the PipeWire server and its capabilities, and the PipeWire graph are exposed towards clients -- including introspection tools like `pw-dump` -- as a collection of objects, each of which has a specific type. +These objects have associated parameters, and properties, methods, events, and permissions. + +Parameters of an object are data with a specific, well defined meaning, which can be modified and read-out in a controlled fashion through the PipeWire API. +They are used to configure the object at run-time. +Parameters are the key that allow WirePlumber to negotiate data formats and port configuration with nodes by providing information such as: + +- Multiple, supported sample rates +- Channel count +- Positions sample format +- Available monitor ports + +Properties of an object are additional data which have been attached on the behalf of modules and of which the PipeWire server has no native understanding. +Certain properties are, by convention, expected for specific object types. + +Each object type has a list of methods that it needs to implement. + +The session manager is responsible for defining the list of permissions each client has. Each permission entry is an object ID and four flags. The four flags are: + +- Read: the object can be seen and events can be received; +- Write: the object can be modified, usually through methods (which requires the execute flag) +- eXecute: methods can be called; +- Metadata: metadata can be set on the object. +- Link: any link can be made even to a port that is not visible by the owner of the port. + +### Object types + +The following are the known types and their most important, specialized parameters and methods: + +#### Core + +The core is the heart of the PipeWire server. +There can only be one core per server and it has the identifier zero. +It represents global properties of the server. + +#### Clients + +A client object is the representation of an open connection with a client process with the server. + +#### Modules + +Modules are dynamic libraries that are loaded at run time in the clients and in the server and do arbitrary things, such as creating devices or provide methods to create links, nodes, etc. + +Modules in PipeWire can only be loaded in their own process. A client, for example, can not load a module in the server. + +#### Nodes + +Nodes are the core data processing entities in PipeWire. +They may produce data (capture devices, signal generators, ...), consume data (playback devices, network endpoints, ...) or both (filters). +Notes have a method `process`, which eats up data from input ports and provides data for each output port. + +#### Ports + +Ports are the entry and exit point of data for a Node. +A port can either be used for input or output (but not both). +For nodes that work with audio, one type of configuration is whether they have `dsp` ports or a `passthrough` port. +In `dsp` mode, there is one port for channel of multichannel audio (so two ports for stereo audio, for example), and data is always in 32-bit floating point format. +In `passthrough` mode, there is one port for multichannel data in a format that is negotiated between ports. + +#### Links + +Data flows between nodes when there is a Link between their ports. +Links may be `"passive"` in which case the existence of the link does not automatically cause data to flow between those nodes (some link in the graph must be `"active"` for the graph to have data flow). + +#### Devices + +A device is a handle representing an underlying API, which is then used to create nodes or other devices. +Examples of devices are ALSA PCM cards or V4L2 devices. +A device has a profile, which allows one to configure them. + +#### Factories + +A factory is an object whose sole capability is to create other objects. +Once a factory is created, it can only emit the type of object it declared. +Those are most often delivered as a module: the module creates the factory and stays alive to keep it accessible for clients. + +### Common parameters and methods + +Every object implement at least the add_listener method, that allows any client to register event listeners. +Events are used through the PipeWire API to expose information about an object that might change over time (the state of a node for example). + +## Context + +The PipeWire server and PipeWire clients use the PipeWire API through their respective `pw_context`, the so called PipeWire context. +When a PipeWire context is created, it finds and parses a configuration file from the filesystem according to the rules of loading configuration files. + +*/ diff --git a/doc/dox/programs/index.md b/doc/dox/programs/index.md new file mode 100644 index 0000000..991ec72 --- /dev/null +++ b/doc/dox/programs/index.md @@ -0,0 +1,27 @@ +\page page_programs Programs + +Manual pages: + +- \subpage page_man_pipewire_1 +- \subpage page_man_pipewire-pulse_1 +- \subpage page_man_pw-cat_1 +- \subpage page_man_pw-cli_1 +- \subpage page_man_pw-config_1 +- \subpage page_man_pw-container_1 +- \subpage page_man_pw-dot_1 +- \subpage page_man_pw-dump_1 +- \subpage page_man_pw-jack_1 +- \subpage page_man_pw-link_1 +- \subpage page_man_pw-loopback_1 +- \subpage page_man_pw-metadata_1 +- \subpage page_man_pw-mididump_1 +- \subpage page_man_pw-mon_1 +- \subpage page_man_pw-profiler_1 +- \subpage page_man_pw-reserve_1 +- \subpage page_man_pw-top_1 +- \subpage page_man_pw-v4l2_1 +- \subpage page_man_spa-acp-tool_1 +- \subpage page_man_spa-inspect_1 +- \subpage page_man_spa-json-dump_1 +- \subpage page_man_spa-monitor_1 +- \subpage page_man_spa-resample_1 diff --git a/doc/dox/programs/pipewire-pulse.1.md b/doc/dox/programs/pipewire-pulse.1.md new file mode 100644 index 0000000..66d2ec8 --- /dev/null +++ b/doc/dox/programs/pipewire-pulse.1.md @@ -0,0 +1,58 @@ +\page page_man_pipewire-pulse_1 pipewire-pulse + +The PipeWire PulseAudio replacement + +# SYNOPSIS + +**pipewire-pulse** \[*options*\] + +# DESCRIPTION + +**pipewire-pulse** starts a PulseAudio-compatible daemon that integrates +with the PipeWire media server, by running a pipewire process through a +systemd service. This daemon is a drop-in replacement for the PulseAudio +daemon. + +# OPTIONS + +\par -h | \--help +Show help. + +\par -v | \--verbose +Increase the verbosity by one level. This option may be specified +multiple times. + +\par \--version +Show version information. + +\par -c | \--config=FILE +Load the given config file (Default: pipewire-pulse.conf). + +# ENVIRONMENT VARIABLES + +The generic \ref pipewire-env "pipewire(1) environment variables" +are supported. + +In addition: + +@PAR@ pulse-env PULSE_RUNTIME_PATH + +@PAR@ pulse-env XDG_RUNTIME_DIR +Directory where to create the native protocol pulseaudio socket. + +@PAR@ pulse-env PULSE_LATENCY_MSEC +Extra buffering latency in milliseconds. This controls buffering +logic in `libpulse` and may be set for PulseAudio client applications +to adjust their buffering. (Setting it on the `pipewire-pulse` server +has no effect.) + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire-pulse_conf_5 "pipewire-pulse.conf(5)", +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pipewire-pulse-modules_7 "pipewire-pulse-modules(7)" diff --git a/doc/dox/programs/pipewire.1.md b/doc/dox/programs/pipewire.1.md new file mode 100644 index 0000000..43f03e3 --- /dev/null +++ b/doc/dox/programs/pipewire.1.md @@ -0,0 +1,251 @@ +\page page_man_pipewire_1 pipewire + +The PipeWire media server + +\tableofcontents + +# SYNOPSIS + +**pipewire** \[*options*\] + +# DESCRIPTION + +PipeWire is a service that facilitates sharing of multimedia content +between devices and applications. + +The **pipewire** daemon reads a config file that is further documented +in \ref page_man_pipewire_conf_5 "pipewire.conf(5)" manual page. + +# OPTIONS + +\par -h | \--help +Show help. + +\par -v | \--verbose +Increase the verbosity by one level. This option may be specified +multiple times. + +\par \--version +Show version information. + +\par -c | \--config=FILE +Load the given config file (Default: pipewire.conf). + +\par -P | \--properties=PROPS +Add the given properties as a SPA JSON object to the context. + +# RUNTIME SETTINGS @IDX@ pipewire + +A PipeWire daemon will also expose a settings metadata object that can +be used to change some settings at runtime. + +Normally these settings can bypass any of the restrictions listed in +the config options above, such as quantum and samplerate values. + +The settings can be modified using \ref page_man_pw-metadata_1 "pw-metadata(1)": +``` +pw-metadata -n settings # list settings +pw-metadata -n settings 0 # list server settings +pw-metadata -n settings 0 log.level 2 # modify a server setting +``` + +@PAR@ pipewire-settings log.level = INTEGER +Change the log level of the PipeWire daemon. + +@PAR@ pipewire-settings clock.rate = INTEGER +The default samplerate. + +@PAR@ pipewire-settings clock.allowed-rates = [ RATE1 RATE2... ] +The allowed samplerates. + +@PAR@ pipewire-settings clock.force-rate = INTEGER +\parblock +Temporarily forces the graph to operate in a fixed sample rate. +Both DSP processing and devices will switch to the new rate immediately. +Running streams (PulseAudio, native and ALSA applications) will automatically +resample to match the new rate. + +Set the value to 0 to allow the sample rate to vary again. +\endparblock + +@PAR@ pipewire-settings clock.quantum = INTEGER +The default quantum (buffer size). + +@PAR@ pipewire-settings clock.min-quantum = INTEGER +Smallest quantum to be used. + +@PAR@ pipewire-settings clock.max-quantum = INTEGER +Largest quantum to be used. + +@PAR@ pipewire-settings clock.force-quantum = INTEGER +\parblock +Temporarily force the graph to operate in a fixed quantum. + +Set the value to 0 to allow the quantum to vary again. +\endparblock + +# ENVIRONMENT VARIABLES @IDX@ pipewire-env + +## Socket directories + +@PAR@ pipewire-env PIPEWIRE_RUNTIME_DIR + +@PAR@ pipewire-env XDG_RUNTIME_DIR + +@PAR@ pipewire-env USERPROFILE +Used to find the PipeWire socket on the server (and native clients). + +@PAR@ pipewire-env PIPEWIRE_CORE +Name of the socket to make. + +@PAR@ pipewire-env PIPEWIRE_REMOTE +Name of the socket to connect to. + +@PAR@ pipewire-env PIPEWIRE_DAEMON +If set to true then the process becomes a new PipeWire server. + +## Config directories, config file name and prefix + +@PAR@ pipewire-env PIPEWIRE_CONFIG_DIR + +@PAR@ pipewire-env XDG_CONFIG_HOME + +@PAR@ pipewire-env HOME +Used to find the config file directories. + +@PAR@ pipewire-env PIPEWIRE_CONFIG_PREFIX + +@PAR@ pipewire-env PIPEWIRE_CONFIG_NAME +Used to override the application provided +config prefix and config name. + +@PAR@ pipewire-env PIPEWIRE_NO_CONFIG +Enables (false) or disables (true) overriding on the default configuration. + +## Context information + +As part of a client context, the following information is collected +from environment variables and placed in the context properties: + +@PAR@ pipewire-env LANG +The current language in `application.language`. + +@PAR@ pipewire-env XDG_SESSION_ID +Set as the `application.process.session-id` property. + +@PAR@ pipewire-env DISPLAY +Is set as the `window.x11.display` property. + +## Modules + +@PAR@ pipewire-env PIPEWIRE_MODULE_DIR +Sets the directory where to find PipeWire modules. + +@PAR@ pipewire-env SPA_SUPPORT_LIB +The name of the SPA support lib to load. This can be used to switch to +an alternative support library, for example, to run on the EVL realtime kernel. + +## Logging options + +@PAR@ pipewire-env JOURNAL_STREAM +Is used to parse the stream used for the journal. This is usually configured by +systemd. + +@PAR@ pipewire-env PIPEWIRE_LOG_LINE +Enables the logging of line numbers. Default true. + +@PAR@ pipewire-env PIPEWIRE_LOG_TIMESTAMP +Logging timestamp type: "local", "monotonic", "realtime", "none". +Default "local". + +@PAR@ pipewire-env PIPEWIRE_LOG +Specifies a log file to use instead of the default logger. + +@PAR@ pipewire-env PIPEWIRE_LOG_SYSTEMD +Enables the use of systemd for the logger, default true. + +## Other settings + +@PAR@ pipewire-env PIPEWIRE_CPU +Selects the CPU and flags. This is a bitmask of any of the \ref CPU flags + +@PAR@ pipewire-env PIPEWIRE_VM +Selects the Virtual Machine PipeWire is running on. This can be any of the \ref CPU "VM" +types. + +@PAR@ pipewire-env DISABLE_RTKIT +Disables the use of RTKit or the Realtime Portal for realtime scheduling. + +@PAR@ pipewire-env NO_COLOR +Disables the use of colors in the console output. + +## Debugging options + +@PAR@ pipewire-env PIPEWIRE_DLCLOSE +Enables (true) or disables (false) the use of dlclose when a shared library +is no longer in use. When debugging, it might make sense to disable dlclose to be able to get +debugging symbols from the object. + +## Stream options + +@PAR@ pipewire-env PIPEWIRE_NODE +Makes a stream connect to a specific `object.serial` or `node.name`. + +@PAR@ pipewire-env PIPEWIRE_PROPS +Adds extra properties to a stream or filter. + +@PAR@ pipewire-env PIPEWIRE_QUANTUM +Forces a specific rate and buffer-size for the stream or filter. + +@PAR@ pipewire-env PIPEWIRE_LATENCY +Sets a specific latency for a stream or filter. This is only a suggestion but +the configured latency will not be larger. + +@PAR@ pipewire-env PIPEWIRE_RATE +Sets a rate for a stream or filter. This is only a suggestion. The rate will be +switched when the graph is idle. + +@PAR@ pipewire-env PIPEWIRE_AUTOCONNECT +Overrides the default stream autoconnect settings. + +## Plugin options + +@PAR@ pipewire-env SPA_PLUGIN_DIR +Is used to locate SPA plugins. + +@PAR@ pipewire-env SPA_DATA_DIR +Is used to locate plugin specific config files. This is used by the +bluetooth plugin currently to locate the quirks database. + +@PAR@ pipewire-env SPA_DEBUG +Set the log level for SPA plugins. This is usually controlled by the `PIPEWIRE_DEBUG` variable +when the plugins are managed by PipeWire but some standalone tools (like spa-inspect) uses this +variable. + +@PAR@ pipewire-env ACP_BUILDDIR +If set, the ACP profiles are loaded from the builddir. + +@PAR@ pipewire-env ACP_PATHS_DIR + +@PAR@ pipewire-env ACP_PROFILES_DIR +Used to locate the ACP paths and profile directories respectively. + +@PAR@ pipewire-env LADSPA_PATH +Comma separated list of directories where the ladspa plugins can be found. + +@PAR@ pipewire-env LIBJACK_PATH +Directory where the jack1 or jack2 libjack.so can be found. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pw-top_1 "pw-top(1)", +\ref page_man_pw-dump_1 "pw-dump(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", +\ref page_man_pw-cat_1 "pw-cat(1)", +\ref page_man_pw-cli_1 "pw-cli(1)", +\ref page_man_libpipewire-modules_7 "libpipewire-modules(7)" diff --git a/doc/dox/programs/pw-cat.1.md b/doc/dox/programs/pw-cat.1.md new file mode 100644 index 0000000..4860c42 --- /dev/null +++ b/doc/dox/programs/pw-cat.1.md @@ -0,0 +1,163 @@ +\page page_man_pw-cat_1 pw-cat + +Play and record media with PipeWire + +# SYNOPSIS + +**pw-cat** \[*options*\] \[*FILE* \| -\] + +**pw-play** \[*options*\] \[*FILE* \| -\] + +**pw-record** \[*options*\] \[*FILE* \| -\] + +**pw-midiplay** \[*options*\] \[*FILE* \| -\] + +**pw-midirecord** \[*options*\] \[*FILE* \| -\] + +**pw-dsdplay** \[*options*\] \[*FILE* \| -\] + +# DESCRIPTION + +**pw-cat** is a simple tool for playing back or capturing raw or encoded +media files on a PipeWire server. It understands all audio file formats +supported by `libsndfile` for PCM capture and playback. When capturing +PCM, the filename extension is used to guess the file format with the +WAV file format as the default. + +It understands standard MIDI files for playback and recording. This tool +will not render MIDI files, it will simply make the MIDI events +available to the graph. You need a MIDI renderer such as qsynth, +timidity or a hardware MIDI rendered to hear the MIDI. + +DSD playback is supported with the DSF file format. This tool will only +work with native DSD capable hardware and will produce an error when no +such hardware was found. + +When the *FILE* is - input and output will be raw data from STDIN and +STDOUT respectively. + +# OPTIONS + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -v | \--verbose +Verbose operation. + +\par -R | \--remote=NAME +The name the *remote* instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -p | \--playback +Playback mode. Read data from the specified file, and play it back. If +the tool is called under the name **pw-play** or **pw-midiplay** this is +the default. + +\par -r | \--record +Recording mode. Capture data and write it to the specified file. If the +tool is called under the name **pw-record** or **pw-midirecord** this is +the default. + +\par -m | \--midi +MIDI mode. *FILE* is a MIDI file. If the tool is called under the name +**pw-midiplay** or **pw-midirecord** this is the default. Note that this +program will *not* render the MIDI events into audible samples, it will +simply provide the MIDI events in the graph. You need a separate MIDI +renderer such as qsynth, timidity or a hardware renderer to hear the +MIDI. + +\par -d | \--dsd +DSD mode. *FILE* is a DSF file. If the tool is called under the name +**pw-dsdplay** this is the default. Note that this program will *not* +render the DSD audio. You need a DSD capable device to play DSD content +or this program will exit with an error. + +\par \--media-type=VALUE +Set the media type property (default Audio/Midi depending on mode). The +media type is used by the session manager to select a suitable target to +link to. + +\par \--media-category=VALUE +Set the media category property (default Playback/Capture depending on +mode). The media type is used by the session manager to select a +suitable target to link to. + +\par \--media-role=VALUE +Set the media role property (default Music). The media type is used by +the session manager to select a suitable target to link to. + +\par \--target=VALUE +\parblock +Set a node target (default auto). The value can be: + +- **auto**: Automatically select (Default) + +- **0**: Don't try to link this node + +- \: The object.serial or the node.name of a target node +\endparblock + +\par \--latency=VALUE\[*units*\] +\parblock +Set the node latency (default 100ms) + +The latency determines the minimum amount of time it takes for a sample +to travel from application to device (playback) and from device to +application (capture). + +The latency determines the size of the buffers that the application will +be able to fill. Lower latency means smaller buffers but higher +overhead. Higher latency means larger buffers and lower overhead. + +Units can be **s** for seconds, **ms** for milliseconds, **us** for +microseconds, **ns** for nanoseconds. If no units are given, the latency +value is samples with the samplerate of the file. +\endparblock + +\par -P | \--properties=VALUE +Set extra stream properties as a JSON object. + +\par -q | \--quality=VALUE +Resampler quality. When the samplerate of the source or destination file +does not match the samplerate of the server, the data will be resampled. +Higher quality uses more CPU. Values between 0 and 15 are allowed, the +default quality is 4. + +\par \--rate=VALUE +The sample rate, default 48000. + +\par \--channels=VALUE +The number of channels, default 2. + +\par \--channel-map=VALUE +The channelmap. Possible values include: **mono**, **stereo**, +**surround-21**, **quad**, **surround-22**, **surround-40**, +**surround-31**, **surround-41**, **surround-50**, **surround-51**, +**surround-51r**, **surround-70**, **surround-71** or a comma separated +list of channel names: **FL**, **FR**, **FC**, **LFE**, **SL**, **SR**, +**FLC**, **FRC**, **RC**, **RL**, **RR**, **TC**, **TFL**, **TFC**, +**TFR**, **TRL**, **TRC**, **TRR**, **RLC**, **RRC**, **FLW**, **FRW**, +**LFE2**, **FLH**, **FCH**, **FRH**, **TFLC**, **TFRC**, **TSL**, +**TSR**, **LLFR**, **RLFE**, **BC**, **BLC**, **BRC** + +\par \--format=VALUE +The sample format to use. One of: **u8**, **s8**, **s16** (default), +**s24**, **s32**, **f32**, **f64**. + +\par \--volume=VALUE +The stream volume, default 1.000. Depending on the locale you have +configured, "," or "." may be used as a decimal separator. Check with +**locale** command. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", diff --git a/doc/dox/programs/pw-cli.1.md b/doc/dox/programs/pw-cli.1.md new file mode 100644 index 0000000..12ee0fd --- /dev/null +++ b/doc/dox/programs/pw-cli.1.md @@ -0,0 +1,192 @@ +\page page_man_pw-cli_1 pw-cli + +The PipeWire Command Line Interface + +# SYNOPSIS + +**pw-cli** \[*command*\] + +# DESCRIPTION + +Interact with a PipeWire instance. + +When a command is given, **pw-cli** will execute the command and exit + +When no command is given, **pw-cli** starts an interactive session with +the default PipeWire instance *pipewire-0*. + +Connections to other, remote instances can be made. The current instance +name is displayed at the prompt. + +Note that **pw-cli** also creates a local PipeWire instance. Some +commands operate on the current (remote) instance and some on the local +instance, such as module loading. + +Use the 'help' command to list the available commands. + +# GENERAL COMMANDS + +\par help | h +Show a quick help on the commands available. It also lists the aliases +for many commands. + +\par quit | q +Exit from **pw-cli** + +# MODULE MANAGEMENT + +Modules are loaded and unloaded in the local instance, thus the pw-cli +binary itself and can add functionality or objects to the local +instance. It is not possible in PipeWire to load modules in another +instance. + +\par load-module *name* \[*arguments...*\] +\parblock +Load a module specified by its name and arguments in the local instance. +For most modules it is OK to be loaded more than once. + +This command returns a module variable that can be used to unload the +module. + +The locally module is *not* visible in the remote instance. It is not +possible in PipeWire to load modules in a remote instance. +\endparblock + +\par unload-module *module-var* +Unload a module, specified either by its variable. + +# OBJECT INTROSPECTION + +\par list-objects +List the objects of the current instance. + +Objects are listed with their *id*, *type* and *version*. + +\par info *id* | *all* +Get information about a specific object or *all* objects. + +Requesting info about an object will also notify you of changes. + +# WORKING WITH REMOTES + +\par connect \[*remote-name*\] +\parblock +Connect to a remote instance and make this the new current instance. + +If no remote name is specified, a connection is made to the default +remote instance, usually *pipewire-0*. + +The special remote name called *internal* can be used to connect to the +local **pw-cli** PipeWire instance. + +This command returns a remote var that can be used to disconnect or +switch remotes. +\endparblock + +\par disconnect \[*remote-var*\] +\parblock +Disconnect from a *remote instance*. + +If no remote name is specified, the current instance is disconnected. +\endparblock + +\par list-remotes +List all *remote instances*. + +\par switch-remote \[*remote-var*\] +\parblock +Make the specified *remote* the current instance. + +If no remote name is specified, the first instance is made current. +\endparblock + +# NODE MANAGEMENT + +\par create-node *factory-name* \[*properties...*\] +\parblock +Create a node from a factory in the current instance. + +Properties are key=value pairs separated by whitespace. + +This command returns a *node variable*. +\endparblock + +\par export-node *node-id* \[*remote-var*\] +Export a node from the local instance to the specified instance. When no +instance is specified, the node will be exported to the current +instance. + +# DEVICE MANAGEMENT + +\par create-device *factory-name* \[*properties...*\] +\parblock +Create a device from a factory in the current instance. + +Properties are key=value pairs separated by whitespace. + +This command returns a *device variable*. +\endparblock + +# LINK MANAGEMENT + +\par create-link *node-id* *port-id* *node-id* *port-id* \[*properties...*\] +\parblock +Create a link between 2 nodes and ports. + +Port *ids* and Node *ids* can be set to `-` to automatically select a node or +a port. + +Port *ids* can be `*` to automatically link matching ports ids in the nodes. + +Properties are key=value pairs separated by whitespace. + +This command returns one or more *link variables*. +\endparblock + +# GLOBALS MANAGEMENT + +\par destroy *object-id* +Destroy a global object. + +# PARAMETER MANAGEMENT + +\par enum-params *object-id* *param-id* +\parblock +Enumerate params of an object. + +*param-id* can also be given as the param short name. +\endparblock + +\par set-param *object-id* *param-id* *param-json* +\parblock +Set param of an object. + +*param-id* can also be given as the param short name. +\endparblock + +# PERMISSION MANAGEMENT + +\par permissions *client-id* *object-id* *permission* +\parblock +Set permissions for a client. + +*object-id* can be *-1* to set the default permissions. +\endparblock + +\par get-permissions *client-id* +Get permissions of a client. + +# COMMAND MANAGEMENT + +\par send-command *object-id* +Send a command to an object. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", diff --git a/doc/dox/programs/pw-config.1.md b/doc/dox/programs/pw-config.1.md new file mode 100644 index 0000000..898f12e --- /dev/null +++ b/doc/dox/programs/pw-config.1.md @@ -0,0 +1,97 @@ +\page page_man_pw-config_1 pw-config + +Debug PipeWire Config parsing + +# SYNOPSIS + +**pw-config** \[*options*\] paths + +**pw-config** \[*options*\] list \[*SECTION*\] + +**pw-config** \[*options*\] merge *SECTION* + +# DESCRIPTION + +List config paths and config sections and display the parsed output. + +This tool can be used to get an overview of the config file that will be +parsed by the PipeWire server and clients. + +# COMMON OPTIONS + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -n | \--name=NAME +Config Name (default 'pipewire.conf') + +\par -p | \--prefix=PREFIX +Config Prefix (default '') + +\par -L | \--no-newline +Omit newlines after values + +\par -r | \--recurse +Reformat config sections recursively + +\par -N | \--no-colors +Disable color output + +\par -C | \-color\[=WHEN\] +whether to enable color support. WHEN is +*never*, *always*, or *auto* + +# LISTING PATHS + +Specify the paths command. It will display all the config files that +will be parsed and in what order. + +# LISTING CONFIG SECTIONS + +Specify the list command with an optional *SECTION* to list the +configuration fragments used for *SECTION*. Without a *SECTION*, all +sections will be listed. + +Use the -r options to reformat the sections. + +# MERGING A CONFIG SECTION + +With the merge option and a *SECTION*, pw-config will merge all config +files into a merged config section and dump the results. This will be +the section used by the client or server. + +Use the -r options to reformat the sections. + +# EXAMPLES + +\par pw-config +List all config files that will be used + +\par pw-config -n pipewire-pulse.conf +List all config files that will be used by the PipeWire pulseaudio +server. + +\par pw-config -n pipewire-pulse.conf list +List all config sections used by the PipeWire pulseaudio server + +\par pw-config -n jack.conf list context.properties +List the context.properties fragments used by the JACK clients + +\par pw-config -n jack.conf merge context.properties +List the merged context.properties used by the JACK clients + +\par pw-config -n pipewire.conf -r merge context.modules +List the merged context.modules used by the PipeWire server and reformat + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-dump_1 "pw-dump(1)", diff --git a/doc/dox/programs/pw-container.1.md b/doc/dox/programs/pw-container.1.md new file mode 100644 index 0000000..6401729 --- /dev/null +++ b/doc/dox/programs/pw-container.1.md @@ -0,0 +1,71 @@ +\page page_man_pw-container_1 pw-container + +The PipeWire container utility + +# SYNOPSIS + +**pw-container** \[*options*\] \[*PROGRAM*\] + +# DESCRIPTION + +Run a program in a new security context [1]. + +**pw-container** will create a new temporary unix socket and uses the +SecurityContext extension API to create a server on this socket with +the given properties. Clients created from this server socket will have +the security properties attached to them. + +This can be used to simulate the behaviour of Flatpak or other containers. + +Without any arguments, **pw-container** simply creates the new socket +and prints the address on stdout. Other PipeWire programs can then be run +with `PIPEWIRE_REMOTE=` to connect through this security +context. + +When *PROGRAM* is given, the `PIPEWIRE_REMOTE` env variable will be set +and *PROGRAM* will be passed to system(). Argument to *PROGRAM* need to be +properly quoted. + +# OPTIONS + +\par -P | \--properties=VALUE +Set extra context properties as a JSON object. + +\par -r | \--remote=NAME +The name the *remote* instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +# EXIT STATUS + +If the security context was successfully created, **pw-container** does +not exit until terminated with a signal. It exits with status 0 if terminated by +SIGINT or SIGTERM in this case. + +Otherwise, it exits with nonzero exit status. + +# EXAMPLES + +**pw-container** 'pw-dump i 0' + +Run pw-dump of the Core object. Note the difference in the object permissions +when running pw-dump with and without **pw-container**. + +**pw-container** 'pw-dump pw-dump' + +Run pw-dump of itself. Note the difference in the Client security tokens when +running pw-dump with and without **pw-container**. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +[1] https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/security-context-v1.xml - Creating a security context diff --git a/doc/dox/programs/pw-dot.1.md b/doc/dox/programs/pw-dot.1.md new file mode 100644 index 0000000..ab30c0d --- /dev/null +++ b/doc/dox/programs/pw-dot.1.md @@ -0,0 +1,56 @@ +\page page_man_pw-dot_1 pw-dot + +The PipeWire dot graph dump + +# SYNOPSIS + +**pw-dot** \[*options*\] + +# DESCRIPTION + +Create a .dot file of the PipeWire graph. + +The .dot file can then be visualized with a tool like **dotty** or +rendered to a PNG file with `dot -Tpng pw.dot -o pw.png`. + +# OPTIONS + +\par -r | \--remote=NAME +The name the remote instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -a | \--all +Show all object types. + +\par -s | \--smart +Show linked objects only. + +\par -d | \--detail +Show all object properties. + +\par -o FILE | \--output=FILE +Output file name (Default pw.dot). Use - for stdout. + +\par -L | \--lr +Lay the graph from left to right, instead of dot's default top to +bottom. + +\par -9 | \--90 +Lay the graph using 90-degree angles in edges. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-cli_1 "pw-cli(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", diff --git a/doc/dox/programs/pw-dump.1.md b/doc/dox/programs/pw-dump.1.md new file mode 100644 index 0000000..cf507e5 --- /dev/null +++ b/doc/dox/programs/pw-dump.1.md @@ -0,0 +1,43 @@ +\page page_man_pw-dump_1 pw-dump + +The PipeWire state dumper + +# SYNOPSIS + +**pw-dump** \[*options*\] + +# DESCRIPTION + +The *pw-dump* program produces a representation of the current PipeWire +state as JSON, including the information on nodes, devices, modules, +ports, and other objects. + +# OPTIONS + +\par -h | \--help +Show help. + +\par -r | \--remote=NAME +The name of the *remote* instance to dump. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -m | \--monitor +Monitor PipeWire state changes, and output JSON arrays describing +changes. + +\par -N | \--no-colors +Disable color output. + +\par -C | \--color=WHEN +Whether to enable color support. WHEN is `never`, `always`, or `auto`. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-cli_1 "pw-cli(1)", +\ref page_man_pw-top_1 "pw-top(1)", diff --git a/doc/dox/programs/pw-jack.1.md b/doc/dox/programs/pw-jack.1.md new file mode 100644 index 0000000..7724792 --- /dev/null +++ b/doc/dox/programs/pw-jack.1.md @@ -0,0 +1,45 @@ +\page page_man_pw-jack_1 pw-jack + +Use PipeWire instead of JACK + +# SYNOPSIS + +**pw-jack** \[*options*\] *COMMAND* \[*ARGUMENTS...*\] + +# DESCRIPTION + +**pw-jack** modifies the `LD_LIBRARY_PATH` environment variable so that +applications will load PipeWire's reimplementation of the JACK client +libraries instead of JACK's own libraries. This results in JACK clients +being redirected to PipeWire. + +If PipeWire's reimplementation of the JACK client libraries has been +installed as a system-wide replacement for JACK's own libraries, then +the whole system already behaves in that way, in which case **pw-jack** +has no practical effect. + +# OPTIONS + +\par -h +Show help. + +\par -r NAME +The name of the remote instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -v +Verbose operation. + +# EXAMPLES + +\par pw-jack sndfile-jackplay /usr/share/sounds/freedesktop/stereo/bell.oga + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +**jackd(1)**, diff --git a/doc/dox/programs/pw-link.1.md b/doc/dox/programs/pw-link.1.md new file mode 100644 index 0000000..83a1d83 --- /dev/null +++ b/doc/dox/programs/pw-link.1.md @@ -0,0 +1,135 @@ +\page page_man_pw-link_1 pw-link + +The PipeWire Link Command + +# SYNOPSIS + +**pw-link** \[*options*\] -o-l \[*out-pattern*\] \[*in-pattern*\] + +**pw-link** \[*options*\] *output* *input* + +**pw-link** \[*options*\] -d *output* *input* + +**pw-link** \[*options*\] -d *link-id* + +# DESCRIPTION + +List, create and destroy links between PipeWire ports. + +# COMMON OPTIONS + +\par -r | \--remote=NAME +The name the *remote* instance to monitor. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +# LISTING PORTS AND LINKS + +Specify one of -o, -i or -l to list the matching optional input and +output ports and their links. + +\par -o | \--output +List output ports + +\par -i | \--input +List input ports + +\par -l | \--links +List links + +\par -m | \--monitor +Monitor links and ports. **pw-link** will not exit but monitor and print +new and destroyed ports or links. + +\par -I | \--id +List IDs. Also list the unique link and port ids. + +\par -v | \--verbose +Verbose port properties. Also list the port-object-path and the +port-alias. + +# CONNECTING PORTS + +Without any list option (-i, -o or -l), the given ports will be linked. +Valid port specifications are: + +*port-id* + +As obtained with the -I option when listing ports. + +*node-name:port-name* + +As obtained when listing ports. + +*port-object-path* + +As obtained from the first alternative name for the port when listing +them with the -v option. + +*port-alias* + +As obtained from the second alternative name for the ports when listing +them with the -v option. + +Extra options when linking can be given: + +\par -L | \--linger +Linger. Will create a link that exists after **pw-link** is destroyed. +This is the default behaviour, unless the -m option is given. + +\par -P | \--passive +Passive link. A passive link will keep both nodes it links inactive +unless another non-passive link is activating the nodes. You can use +this to link a sink to a filter and have them both suspended when +nothing else is linked to either of them. + +\par -p | \--props=PROPS +Properties as JSON object. Give extra properties when creaing the link. + +# DISCONNECTING PORTS + +When the -d option is given, an existing link between port is destroyed. + +To disconnect port, a single *link-id*, as obtained when listing links +with the -I option, or two port specifications can be given. See the +connecting ports section for valid port specifications. + +\par -d | \--disconnect +Disconnect ports + +# EXAMPLES + +**pw-link** -iol + +List all port and their links. + +**pw-link** -lm + +List all links and monitor changes until **pw-link** is stopped. + +**pw-link** paplay:output_FL alsa_output.pci-0000_00_1b.0.analog-stereo:playback_FL + +Link the given output port to the input port. + +**pw-link** -lI + +List links and their Id. + +**pw-link** -d 89 + +Destroy the link with id 89. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-cli_1 "pw-cli(1)" diff --git a/doc/dox/programs/pw-loopback.1.md b/doc/dox/programs/pw-loopback.1.md new file mode 100644 index 0000000..dea3433 --- /dev/null +++ b/doc/dox/programs/pw-loopback.1.md @@ -0,0 +1,67 @@ +\page page_man_pw-loopback_1 pw-loopback + +PipeWire loopback client + +# SYNOPSIS + +**pw-loopback** \[*options*\] + +# DESCRIPTION + +The *pw-loopback* program is a PipeWire client that uses the PipeWire +loopback module to create loopback nodes, with configuration given via +the command-line options. + +# OPTIONS + +\par -h | \--help +Show help. + +\par -r | \--remote=NAME +The name of the *remote* instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -n | \--name=NAME +Name of the loopback node + +\par -g | \--group=NAME +Name of the loopback node group + +\par -c | \--channels=NUMBER +Number of channels to provide + +\par -m | \--channel-map=MAP +Channel map (default `[ FL, FR ]`) + +\par -l | \--latency=LATENCY +Desired latency in ms + +\par -d | \--delay=DELAY +Added delay in seconds (floating point allowed) + +\par -C | \--capture=TARGET +Target device to capture from + +\par -P | \--playback=TARGET +Target device to play to + +\par -i | \--capture-props=PROPS +Wanted properties of capture node (in JSON) + +\par -o | \--playback-props=PROPS +Wanted properties of capture node (in JSON) + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-cat_1 "pw-cat(1)", +**pactl(1)** + +Other ways to create loopback nodes are adding the loopback module in +the configuration of a PipeWire daemon, or loading the loopback module +using Pulseaudio commands (`pactl load-module module-loopback ...`). diff --git a/doc/dox/programs/pw-metadata.1.md b/doc/dox/programs/pw-metadata.1.md new file mode 100644 index 0000000..77078de --- /dev/null +++ b/doc/dox/programs/pw-metadata.1.md @@ -0,0 +1,73 @@ +\page page_man_pw-metadata_1 pw-metadata + +The PipeWire metadata + +# SYNOPSIS + +**pw-metadata** \[*options*\] \[*id* \[*key* \[*value* \[*type* \] \] \] \] + +# DESCRIPTION + +Monitor, set and delete metadata on PipeWire objects. + +Metadata are key/type/value triplets attached to objects identified by +*id*. The metadata is shared between all applications binding to the +same metadata object. When an object is destroyed, all its metadata is +automatically removed. + +When no *value* is given, **pw-metadata** will query and log the +metadata matching the optional arguments *id* and *key*. Without any +arguments, all metadata is displayed. + +When *value* is given, **pw-metadata** will set the metadata for *id* +and *key* to *value* and an optional *type*. + +# OPTIONS + +\par -r | \--remote=NAME +The name the remote instance to use. If left unspecified, a connection +is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -l | \--list +List available metadata objects + +\par -m | \--monitor +Keeps running and log the changes to the metadata. + +\par -d | \--delete +Delete all metadata for *id* or for the specified *key* of object *id*. +Without any option, all metadata is removed. + +\par -n | \--name +Metadata name (Default: "default"). + +# EXAMPLES + +**pw-metadata** + +Show metadata in default name. + +**pw-metadata** -n settings 0 + +Display settings. + +**pw-metadata** -n settings 0 clock.quantum 1024 + +Change clock.quantum to 1024. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-mon_1 "pw-mon(1)", +\ref page_man_pw-cli_1 "pw-cli(1)", diff --git a/doc/dox/programs/pw-mididump.1.md b/doc/dox/programs/pw-mididump.1.md new file mode 100644 index 0000000..83b229f --- /dev/null +++ b/doc/dox/programs/pw-mididump.1.md @@ -0,0 +1,38 @@ +\page page_man_pw-mididump_1 pw-mididump + +The PipeWire MIDI dump + +# SYNOPSIS + +**pw-mididump** \[*options*\] \[*FILE*\] + +# DESCRIPTION + +Dump MIDI messages to stdout. + +When a MIDI file is given, the events inside the file are printed. + +When no file is given, **pw-mididump** creates a PipeWire MIDI input +stream and will print all MIDI events received on the port to stdout. + +# OPTIONS + +\par -r | \--remote=NAME +The name the remote instance to monitor. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-cat_1 "pw-cat(1)" diff --git a/doc/dox/programs/pw-mon.1.md b/doc/dox/programs/pw-mon.1.md new file mode 100644 index 0000000..6d9b639 --- /dev/null +++ b/doc/dox/programs/pw-mon.1.md @@ -0,0 +1,36 @@ +\page page_man_pw-mon_1 pw-mon + +The PipeWire monitor + +# SYNOPSIS + +**pw-mon** \[*options*\] + +# DESCRIPTION + +Monitor objects on the PipeWire instance. + +# OPTIONS + +\par -r | \--remote=NAME +The name the *remote* instance to monitor. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -N | \--color=WHEN +Whether to use color, one of 'never', 'always', or 'auto'. The default +is 'auto'. **-N** is equivalent to **--color=never**. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/programs/pw-profiler.1.md b/doc/dox/programs/pw-profiler.1.md new file mode 100644 index 0000000..a02e5a2 --- /dev/null +++ b/doc/dox/programs/pw-profiler.1.md @@ -0,0 +1,46 @@ +\page page_man_pw-profiler_1 pw-profiler + +The PipeWire profiler + +# SYNOPSIS + +**pw-profiler** \[*options*\] + +# DESCRIPTION + +Start profiling a PipeWire instance. + +If the server has the profiler module loaded, this program will connect +to it and log the profiler data. Profiler data contains times and +durations when processing nodes and devices started and completed. + +When this program is stopped, a set of **gnuplot** files and a script to +generate SVG files from the .plot files is generated, along with a .html +file to visualize the profiling results in a browser. + +This function uses the same data used by *pw-top*. + +# OPTIONS + +\par -r | \--remote=NAME +The name the remote instance to monitor. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +\par -o | \--output=FILE +Profiler output name (default "profiler.log"). + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-top_1 "pw-top(1)" diff --git a/doc/dox/programs/pw-reserve.1.md b/doc/dox/programs/pw-reserve.1.md new file mode 100644 index 0000000..a03151c --- /dev/null +++ b/doc/dox/programs/pw-reserve.1.md @@ -0,0 +1,79 @@ +\page page_man_pw-reserve_1 pw-reserve + +The PipeWire device reservation utility + +# SYNOPSIS + +**pw-reserve** \[*options*\] + +# DESCRIPTION + +Reserves a device using the DBus `org.freedesktop.ReserveDevice1` +device reservation scheme [1], waiting until terminated by `SIGINT` or +another signal. + +It can also request other applications to release a device. This can +be used to make audio servers such as PipeWire, Pulseaudio, JACK, or +other applications that respect the device reservation protocol, to +ignore a device, or to release a sound device they are already using +so that it can be used by other applications. + +# OPTIONS + +\par -r | \--release +Request any client currently holding the device to release it, and try +to reserve it after that. If this option is not given and the device +is already in use, **pw-reserve** will exit with error status. + +\par -n NAME | \--name=NAME +\parblock +Name of the device to reserve. By convention, this is + +- AudioN: for ALSA card number N + +**pw-reserve** can reserve any device name, however PipeWire does +not currently support other values than listed above. +\endparblock + +\par -a NAME | \--appname=NAME +Application name to use when reserving the device. + +\par -p PRIO | \--priority=PRIO +Priority to use when reserving the device. + +\par -m | \--monitor +Monitor reservations of a given device, instead of reserving it. + +\par -h | \--help +Show help. + +\par \--version +Show version information. + +# EXIT STATUS + +If the device reservation succeeds, **pw-reserve** does not exit until +terminated with a signal. It exits with status 0 if terminated by +SIGINT or SIGTERM in this case. + +Otherwise, it exits with nonzero exit status. + +# EXAMPLES + +**pw-reserve** -n Audio0 + +Reserve ALSA card 0, and exit with error if it is already reserved. + +**pw-reserve** -n Audio0 -r + +Reserve ALSA card 0, requesting any applications that have reserved +the device to release it for us. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +[1] https://git.0pointer.net/reserve.git/tree/reserve.txt - A simple device reservation scheme with DBus diff --git a/doc/dox/programs/pw-top.1.md b/doc/dox/programs/pw-top.1.md new file mode 100644 index 0000000..c57c33b --- /dev/null +++ b/doc/dox/programs/pw-top.1.md @@ -0,0 +1,209 @@ +\page page_man_pw-top_1 pw-top + +The PipeWire process viewer + +# SYNOPSIS + +**pw-top** \[*options*\] + +# DESCRIPTION + +The *pw-top* program provides a dynamic real-time view of the pipewire +node and device statistics. + +A hierarchical view is shown of Driver nodes and follower nodes. The +Driver nodes are actively using a timer to schedule dataflow in the +followers. The followers of a driver node as shown below their driver +with a + sign in a tree-like representation. + +The columns presented are as follows: + +\par S +\parblock +Node status. + +- E = ERROR +- C = CREATING +- S = SUSPENDED +- I = IDLE +- R = RUNNING +- t = RUNNING + transport starting +- T = RUNNING + transport running +\endparblock + +\par ID +The ID of the pipewire node/device, as found in *pw-dump* and +*pw-cli* + +\par QUANT +\parblock +The current quantum (for drivers) and the suggested quantum for +follower nodes. + +The quantum by itself needs to be divided by the RATE column to +calculate the duration of a scheduling period in fractions of a +second. + +For a QUANT of 1024 and a RATE of 48000, the duration of one period +in the graph is 1024/48000 or 21.3 milliseconds. + +Follower nodes can have a 0 QUANT field, which means that the node +does not have a suggestion for the quantum and thus uses what the +driver selected. + +The driver will use the lowest quantum of any of the followers. If +none of the followers select a quantum, the default quantum in the +pipewire configuration file will be used. + +The QUANT on the drivers usually translates directly into the number +of audio samples processed per processing cycle of the graph. + +See also + +\endparblock + +\par RATE +\parblock +The current rate (for drivers) and the suggested rate for follower +nodes. + +This is the rate at which the *graph* processes data and needs to be +combined with the QUANT value to derive the duration of a processing +cycle in the graph. + +Some nodes can have a 0 RATE, which means that they don\'t have any +rate suggestion for the graph. Nodes that suggest a rate can make +the graph switch rates if the graph is otherwise idle and the new +rate is allowed as a possible graph rate (see the pipewire +configuration file). + +The RATE on (audio) driver nodes usually also translates directly to +the samplerate used by the device. Although some devices might not +be able to operate at the given samplerate, in which case resampling +will need to be done. The negotiated samplerate with the device and +stream can be found in the FORMAT column. + +\endparblock + +\par WAIT +\parblock +The waiting time of a node is the elapsed time between when the node +is ready to start processing and when it actually started +processing. + +For Driver nodes, this is the time between when the node wakes up to +start processing the graph and when the driver (and thus also the +graph) completes a cycle. The WAIT time for driver is thus the +elapsed time processing the graph. + +For follower nodes, it is the time spent between being woken up +(when all dependencies of the node are satisfied) and when +processing starts. The WAIT time for follower nodes is thus mostly +caused by context switching. + +A value of \-\-- means that the node was not signaled. A value of ++++ means that the node was signaled but not awake. +\endparblock + +\par BUSY +\parblock +The processing time is started when the node starts processing until +it completes and wakes up the next nodes in the graph. + +A value of \-\-- means that the node was not started. A value of +++ +means that the node was started but did not complete. +\endparblock + +\par W/Q +\parblock +Ratio of WAIT / QUANT. + +The W/Q time of the driver node is a good measure of the graph load. +The running averages of the driver W/Q ratios are used as the DSP +load in other (JACK) tools. + +Values of \-\-- and +++ are copied from the WAIT column. + +\endparblock + +\par B/Q +\parblock +Ratio of BUSY / QUANT + +This is a good measure of the load of a particular driver or +follower node. + +Values of \-\-- and +++ are copied from the BUSY column. +\endparblock + +\par ERR +\parblock +Total of Xruns and Errors + +Xruns for drivers are when the graph did not complete a cycle. This +can be because a node in the graph also has an Xrun. It can also be +caused when scheduling delays cause a deadline to be missed, causing +a hardware Xrun. + +Xruns for followers are incremented when the node started processing +but did not complete before the end of the graph cycle deadline. +\endparblock + +\par FORMAT +\parblock +The format used by the driver node or the stream. This is the +hardware format negotiated with the device or stream. + +If the stream of driver has a different rate than the graph, +resampling will be done. + +For raw audio formats, the layout is \ \ +\. + +For IEC958 passthrough audio formats, the layout is IEC958 \ +\. + +For DSD formats, the layout is \ \. + +For Video formats, the layout is \ +\x\. +\endparblock + +\par NAME +\parblock +Name assigned to the device/node, as found in *pw-dump* node.name + +Names are prefixed by *+* when they are linked to a driver (entry +above with no +) +\endparblock + + +# OPTIONS + +\par -h | \--help +Show help. + +\par -b | \--batch-mode +Run in non-interactive batch mode, similar to top\'s batch mode. + +\par -n | \--iterations=NUMBER +Exit after NUMBER of batch iterations. Only used in batch mode. + +\par -r | \--remote=NAME +The name the *remote* instance to monitor. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -V | \--version +Show version information. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", +\ref page_man_pw-dump_1 "pw-dump(1)", +\ref page_man_pw-cli_1 "pw-cli(1)", +\ref page_man_pw-profiler_1 "pw-profiler(1)" diff --git a/doc/dox/programs/pw-v4l2.1.md b/doc/dox/programs/pw-v4l2.1.md new file mode 100644 index 0000000..c3ceaa2 --- /dev/null +++ b/doc/dox/programs/pw-v4l2.1.md @@ -0,0 +1,40 @@ +\page page_man_pw-v4l2_1 pw-v4l2 + +Use PipeWire instead of V4L2 + +# SYNOPSIS + +**pw-v4l2** \[*options*\] *COMMAND* \[*ARGUMENTS...*\] + +# DESCRIPTION + +**pw-v4l2** runs a command using a compatibility layer that maps PipeWire +video devices to be visible to applications using V4L2. + +This is implemented by preloading a shared library via LD_PRELOAD, +which translates library calls that try to access V4L2 devices. + +# OPTIONS + +\par -h +Show help. + +\par -r NAME +The name of the remote instance to connect to. If left unspecified, a +connection is made to the default PipeWire instance. + +\par -v +Verbose operation. + +# EXAMPLES + +**pw-v4l2** v4l2-ctl --list-devices + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)", diff --git a/doc/dox/programs/spa-acp-tool.1.md b/doc/dox/programs/spa-acp-tool.1.md new file mode 100644 index 0000000..402a606 --- /dev/null +++ b/doc/dox/programs/spa-acp-tool.1.md @@ -0,0 +1,94 @@ +\page page_man_spa-acp-tool_1 spa-acp-tool + +The PipeWire ALSA profile debugging utility + +# SYNOPSIS + +**spa-acp-tool** \[*OPTIONS*\] \[*COMMAND*\] + +# DESCRIPTION + +Debug tool for exercising the ALSA card profile probing code, without +running PipeWire. + +May be used to debug problems where PipeWire has incorrectly +functioning ALSA card profiles. + +# OPTIONS + +\par -h | \--help +Show help + +\par -v | \--verbose +Increase verbosity by one level + +\par -c NUMBER | \--card NUMBER +Select which card to probe + +\par -p | \--properties +Additional properties to pass to ACP, e.g. `key=value ...`. + +# COMMANDS + +\par help | h +Show available commands + +\par quit | q +Quit + +\par card ID | c ID +Probe card + +\par info | i +List card info + +\par list | l +List all objects + +\par list-verbose | lv +List all data + +\par list-profiles [ID] | lpr [ID] +List profiles + +\par set-profile ID | spr ID +Activate a profile + +\par list-ports [ID] | lp [ID] +List ports + +\par set-port ID | sp ID +Activate a port + +\par list-devices [ID] | ld [ID] +List available devices + +\par get-volume ID | gv ID +Get volume from device + +\par set-volume ID VOL | v ID VOL +Set volume on device + +\par inc-volume ID | v+ ID +Increase volume on device + +\par dec-volume ID | v- ID +Decrease volume on device + +\par get-mute ID | gm ID +Get mute state from device + +\par set-mute ID VAL | sm ID VAL +Set mute on device + +\par toggle-mute ID | m ID +Toggle mute on device + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/programs/spa-inspect.1.md b/doc/dox/programs/spa-inspect.1.md new file mode 100644 index 0000000..c64830e --- /dev/null +++ b/doc/dox/programs/spa-inspect.1.md @@ -0,0 +1,28 @@ +\page page_man_spa-inspect_1 spa-inspect + +The PipeWire SPA plugin information utility + +# SYNOPSIS + +**spa-inspect** *FILE* + +# DESCRIPTION + +Displays information about a SPA plugin. + +Lists the SPA factories contained, and tries to instantiate them. + +# EXAMPLES + +**spa-inspect** $(SPA_PLUGINDIR)/bluez5/libspa-codec-bluez5-sbc.so + +Display information about a plugin. + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/programs/spa-json-dump.1.md b/doc/dox/programs/spa-json-dump.1.md new file mode 100644 index 0000000..aa744da --- /dev/null +++ b/doc/dox/programs/spa-json-dump.1.md @@ -0,0 +1,24 @@ +\page page_man_spa-json-dump_1 spa-json-dump + +SPA JSON to JSON converter + +# SYNOPSIS + +**spa-json** *[FILE]* + +# DESCRIPTION + +Reads a SPA JSON file or stdin, and outputs it as standard JSON. + +# EXAMPLES + +**spa-json-dump** $(PIPEWIRE_CONFDATADIR)/pipewire.conf + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/programs/spa-monitor.1.md b/doc/dox/programs/spa-monitor.1.md new file mode 100644 index 0000000..5fccec1 --- /dev/null +++ b/doc/dox/programs/spa-monitor.1.md @@ -0,0 +1,26 @@ +\page page_man_spa-monitor_1 spa-monitor + +The PipeWire SPA device debugging utility + +# SYNOPSIS + +**spa-monitor** *FILE* + +# DESCRIPTION + +Load a SPA plugin and instantiate a device from it. + +This is only useful for debugging device plugins. + +# EXAMPLES + +**spa-monitor** $(SPA_PLUGINDIR)/jack/libspa-jack.so + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/programs/spa-resample.1.md b/doc/dox/programs/spa-resample.1.md new file mode 100644 index 0000000..a961c2f --- /dev/null +++ b/doc/dox/programs/spa-resample.1.md @@ -0,0 +1,47 @@ +\page page_man_spa-resample_1 spa-resample + +The PipeWire resampler debugging utility + +# SYNOPSIS + +**spa-resample** \[*OPTIONS*\] *INFILE* *OUTFILE* + +# DESCRIPTION + +Use the PipeWire resampler to resample input file to output file, +following the given options. + +This is useful only for testing the resampler. + +# OPTIONS + +\par -r RATE | \--rate=RATE +Output sample rate. + +\par -f FORMAT | \--format=FORMAT +Output sample format (s8 | s16 | s32 | f32 | f64). + +\par -q QUALITY | \--quality=QUALITY +Resampler output quality (0-14). + +\par -c FLAGS | \--cpuflags=FLAGS +See \ref spa_cpu "spa/support/cpu.h". + +\par -h +Show help. + +\par -v +Verbose operation. + +# EXAMPLES + +**spa-resample** -r 48000 -f s32 in.wav out.wav + +# AUTHORS + +The PipeWire Developers <$(PACKAGE_BUGREPORT)>; +PipeWire is available from <$(PACKAGE_URL)> + +# SEE ALSO + +\ref page_man_pipewire_1 "pipewire(1)" diff --git a/doc/dox/pulse-modules.dox b/doc/dox/pulse-modules.dox new file mode 100644 index 0000000..ba5ce0a --- /dev/null +++ b/doc/dox/pulse-modules.dox @@ -0,0 +1,42 @@ +/** \page page_pulse_modules Pulseaudio Modules + +\include{doc} pulse-modules.inc + +# List of built-in modules: + +- \subpage page_pulse_module_alsa_sink +- \subpage page_pulse_module_alsa_source +- \subpage page_pulse_module_always_sink +- \subpage page_pulse_module_combine_sink +- \subpage page_pulse_module_device_manager +- \subpage page_pulse_module_device_restore +- \subpage page_pulse_module_echo_cancel +- \subpage page_pulse_module_gsettings +- \subpage page_pulse_module_jackdbus_detect +- \subpage page_pulse_module_ladspa_sink +- \subpage page_pulse_module_ladspa_source +- \subpage page_pulse_module_loopback +- \subpage page_pulse_module_native_protocol_tcp +- \subpage page_pulse_module_null_sink +- \subpage page_pulse_module_pipe_sink +- \subpage page_pulse_module_pipe_source +- \subpage page_pulse_module_raop_discover +- \subpage page_pulse_module_remap_sink +- \subpage page_pulse_module_remap_source +- \subpage page_pulse_module_roc_sink +- \subpage page_pulse_module_roc_sink_input +- \subpage page_pulse_module_roc_source +- \subpage page_pulse_module_rtp_recv +- \subpage page_pulse_module_rtp_send +- \subpage page_pulse_module_simple_protocol_tcp +- \subpage page_pulse_module_stream_restore +- \subpage page_pulse_module_switch_on_connect +- \subpage page_pulse_module_tunnel_sink +- \subpage page_pulse_module_tunnel_source +- \subpage page_pulse_module_virtual_sink +- \subpage page_pulse_module_virtual_source +- \subpage page_pulse_module_x11_bell +- \subpage page_pulse_module_zeroconf_discover +- \subpage page_pulse_module_zeroconf_publish + +*/ diff --git a/doc/dox/pulse-modules.inc b/doc/dox/pulse-modules.inc new file mode 100644 index 0000000..7a1c605 --- /dev/null +++ b/doc/dox/pulse-modules.inc @@ -0,0 +1,113 @@ +PipeWire's Pulseaudio emulation implements several Pulseaudio modules. +It only supports its own built-in modules, and cannot load external +modules written for Pulseaudio. + +# Loading modules + +The built-in modules can be loaded using Pulseaudio client programs, +for example `pactl load-module `. They +can also added to `pipewire-pulse.conf`, typically by a drop-in file +in `~/.config/pipewire/pipewire-pulse.conf.d/` containing the module +name and its arguments +``` +# ~/.config/pipewire/pipewire-pulse.conf.d/custom.conf + +pulse.cmd = [ + { cmd = "load-module" args = "module-null-sink sink_name=foo" flags = [ ] } +] +``` + +To list all modules currently loaded, with their arguments: +``` +pactl list modules +``` + +For a short list of loaded modules: +``` +pactl list modules short +``` + +Modules may be unloaded using either the module-name or index number: + +``` +pactl load-module +pactl unload-module +``` + +# Common module options + +Most modules that create streams/devices support the following properties: + +## sink_name, source_name + +Name for the sink (resp. source). Allowed characters in the name are a-z, A-Z, numbers, period (.) and underscore (_). The length must be 1-128 characters. + +## format + +The sample format. The supported audio formats are: + +### PCM + - u8: unsigned 8-bit integer + - aLaw: A-law encoded 8-bit integer + - uLaw: μ-law encoded 8-bit integer + - s16le: signed 16-bit little-endian integer + - s16be: signed 16-bit big-endian integer + - s16, s16ne: native-endian aliases for s16le or s16be + - s16re: reverse-endian alias for s16le or s16be + - float32le: 32-bit little-endian float + - float32be: 32-bit big-endian float + - float32, float32ne: native-endian aliases for float32le or float32be + - float32re: reverse-endian alias for float32le or float32be + - s32le: signed 32-bit little-endian integer + - s32be: signed 32-bit big-endian integer + - s32, s32ne: native-endian aliases for s32le or s32be + - s32re: reverse-endian alias for s32le or s32be + - s24le: signed 24-bit little-endian integer (note: ALSA calls this "S24_3LE") + - s24be: signed 24-bit big-endian integer (note: ALSA calls this "S24_3BE") + - s24, s24ne: native-endian aliases for s24le or s24be + - s24re: reverse-endian alias for s24le or s24be + - s24-32le: signed 24-bit little-endian integer, packed into a 32-bit integer so that the 8 most significant bits are ignored (note: ALSA calls this "S24_LE") + - s24-32be: signed 24-bit big-endian integer, packed into a 32-bit integer so that the 8 most significant bits are ignored (note: ALSA calls this "S24_BE") + - s24-32, s24-32ne: native-endian aliases for s24-32le or s24-32be + - s24-32re: reverse-endian alias for s24-32le or s24-32be + +### Compressed audio formats + +Below is a list of all supported compressed formats. The code at the beginning of each line is used whenever a textual identifier for a format is needed (for example in configuration files or on the command line). The formats whose identifier ends with -iec61937 have to be wrapped in IEC 61937 frames, which makes the compressed audio behave more like normal PCM audio. + + - ac3-iec61937: Dolby Digital (DD / AC-3 / A/52) + - eac3-iec61937: Dolby Digital Plus (DD+ / E-AC-3) + - mpeg-iec61937: MPEG-1 or MPEG-2 Part 3 (not MPEG-2 AAC) + - dts-iec61937: DTS + - mpeg2-aac-iec61937: MPEG-2 AAC (supported since PulseAudio 4.0) + - truehd-iec61937: Dolby TrueHD (added in PulseAudio 13.0, but doesn't work yet in practice) + - dtshd-iec61937: DTS-HD Master Audio (added in PulseAudio 13.0, but doesn't work yet in practice) + - pcm: PCM (not a compressed format, but listed here, because pcm is one of the recognized encoding identifiers) + - any: (special identifier for indicating that any encoding can be used) + + +## rate + +The sample rate. + +##channels + +Number of audio channels. + +## channel_map + +A channel map. A list of comma-separated channel names. The currently defined channel names are: +`left`, `right`, `mono`, `center`, `front-left`, `front-right`, `front-center`, +`rear-center`, `rear-left`, `rear-right`, `lfe`, `subwoofer`, `front-left-of-center`, +`front-right-of-center`, `side-left`, `side-right`, `aux0`, `aux1` to `aux15`, `top-center`, +`top-front-left`, `top-front-right`, `top-front-center`, `top-rear-left`, `top-rear-right`, +`top-rear-center` + +## sink_properties, source_properties + +Set additional properties of the sink/source. For example, you can set the description directly +when the module is loaded by setting this parameter. + +``` +load-module module-alsa-sink sink_name=headphones sink_properties=device.description=Headphones +``` diff --git a/doc/dox/tutorial/index.dox b/doc/dox/tutorial/index.dox new file mode 100644 index 0000000..9f178e3 --- /dev/null +++ b/doc/dox/tutorial/index.dox @@ -0,0 +1,21 @@ +/** \page page_tutorial API Tutorial + +Welcome to the PipeWire API 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/dox/tutorial/tutorial1.dox b/doc/dox/tutorial/tutorial1.dox new file mode 100644 index 0000000..6e88cce --- /dev/null +++ b/doc/dox/tutorial/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/dox/tutorial/tutorial2.dox b/doc/dox/tutorial/tutorial2.dox new file mode 100644 index 0000000..1688aed --- /dev/null +++ b/doc/dox/tutorial/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, ®istry_listener, + ®istry_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/dox/tutorial/tutorial3.dox b/doc/dox/tutorial/tutorial3.dox new file mode 100644 index 0000000..776ea14 --- /dev/null +++ b/doc/dox/tutorial/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/dox/tutorial/tutorial4.dox b/doc/dox/tutorial/tutorial4.dox new file mode 100644 index 0000000..b8d1707 --- /dev/null +++ b/doc/dox/tutorial/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/dox/tutorial/tutorial5.dox b/doc/dox/tutorial/tutorial5.dox new file mode 100644 index 0000000..e73c1cf --- /dev/null +++ b/doc/dox/tutorial/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/dox/tutorial/tutorial6.dox b/doc/dox/tutorial/tutorial6.dox new file mode 100644 index 0000000..0cee850 --- /dev/null +++ b/doc/dox/tutorial/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/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/examples/tutorial1.c b/doc/examples/tutorial1.c new file mode 100644 index 0000000..2fc2ca4 --- /dev/null +++ b/doc/examples/tutorial1.c @@ -0,0 +1,19 @@ +/* + [title] + \ref page_tutorial1 + [title] + */ +/* [code] */ +#include + +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/examples/tutorial2.c b/doc/examples/tutorial2.c new file mode 100644 index 0000000..e6ead68 --- /dev/null +++ b/doc/examples/tutorial2.c @@ -0,0 +1,56 @@ +/* + [title] + \ref page_tutorial2 + [title] + */ +/* [code] */ +#include + +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, ®istry_listener, + ®istry_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/examples/tutorial3.c b/doc/examples/tutorial3.c new file mode 100644 index 0000000..6470740 --- /dev/null +++ b/doc/examples/tutorial3.c @@ -0,0 +1,91 @@ +/* + [title] + \ref page_tutorial3 + [title] + */ +/* [code] */ +#include + +/* [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; + int err; + + pw_core_add_listener(core, &core_listener, &core_events, &d); + + d.pending = pw_core_sync(core, PW_ID_CORE, 0); + + if ((err = pw_main_loop_run(loop)) < 0) + printf("main_loop_run error:%d!\n", err); + + 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, ®istry_listener, + ®istry_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/examples/tutorial4.c b/doc/examples/tutorial4.c new file mode 100644 index 0000000..0b691fe --- /dev/null +++ b/doc/examples/tutorial4.c @@ -0,0 +1,120 @@ +/* + [title] + \ref page_tutorial4 + [title] + */ +/* [code] */ +#include + +#include + +#include + +#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; + if (b->requested) + n_frames = SPA_MIN(b->requested, n_frames); + + for (i = 0; i < n_frames; i++) { + data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE; + if (data->accumulator >= M_PI_M2) + data->accumulator -= M_PI_M2; + + /* sin() gives a value between -1.0 and 1.0, we first apply + * the volume and then scale with 32767.0 to get a 16 bits value + * between [-32767 32767]. + * Another common method to convert a double to + * 16 bits is to multiple by 32768.0 and then clamp to + * [-32768 32767] to get the full 16 bits range. */ + val = sin(data->accumulator) * DEFAULT_VOLUME * 32767.0; + 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/examples/tutorial5.c b/doc/examples/tutorial5.c new file mode 100644 index 0000000..e49da91 --- /dev/null +++ b/doc/examples/tutorial5.c @@ -0,0 +1,141 @@ +/* + [title] + \ref page_tutorial5 + [title] + */ +/* [code] */ +#include +#include +#include + +#include + +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/examples/tutorial6.c b/doc/examples/tutorial6.c new file mode 100644 index 0000000..45d4485 --- /dev/null +++ b/doc/examples/tutorial6.c @@ -0,0 +1,97 @@ +/* + [title] + \ref page_tutorial6 + [title] + */ +/* [code] */ +#include + +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, + ®istry_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/input-filter-h.sh b/doc/input-filter-h.sh new file mode 100755 index 0000000..9e5bff0 --- /dev/null +++ b/doc/input-filter-h.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env 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/@;') + +printf "/** \\\\file\n\`%s\`\n" "$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. +# Strip SPA_FORMAT_ARG_FUNC(1) etc. things that confuse doxygen +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@;' \ + -e 's@^[ ]*SPA_FORMAT_ARG_FUNC([0-9, ]*)@@;' \ + -e 's@[ ]*SPA_PRINTF_FUNC([0-9, ]*);@;@;' \ + -e 's@^[ ]*SPA_WARN_UNUSED_RESULT@ @;' \ + -e 's@ SPA_SENTINEL;@;@;' \ + -e 's@ SPA_UNUSED,@,@;' \ +< "$FILENAME" diff --git a/doc/input-filter-md.py b/doc/input-filter-md.py new file mode 100755 index 0000000..60aa462 --- /dev/null +++ b/doc/input-filter-md.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; eval: (blacken-mode); -*- +r"""input-filter-md.py FILENAME +input-filter-md.py --index FILENAMES... + +Doxygen .md input filter that adds extended syntax. +With --index, generates an index file. +Assumes BUILD_DIR environment variable is set. + +@PAR@
(...) + + Adds an index item and expands to + + \anchor + \par (...) + +@SECREF@
+ + Expands to + + \secreflist + \refitem ... + ... + \endsecreflist + + containing all index items from the specified section. + +# Section title @IDX@
+ + Adds the section title to the index, and expands to an anchor + + # Section title {#key} + +The index keys can be used in \ref and have format + + {section}__{name} + +where the parts are converted to lowercase and _ replaces +non-alphanumerics. + +""" +import sys +import re +import os + + +def index_key(section, name): + key = f"{section}__{name}".lower() + return re.sub(r"[^A-Za-z0-9_-]", "_", key) + + +BUILD_DIR = os.environ["BUILD_DIR"] +PAR_RE = r"^@PAR@\s+([^\s]*)[ \t]+(\S+)(.*)$" +IDX_RE = r"^(#+)(.*)@IDX@[ \t]+(\S+)[ \t]*$" +SECREF_RE = r"^@SECREF@[ \t]+([^\n]*)[ \t]*$" + + +def main(args): + fn = args[0] + with open(fn, "r") as f: + text = f.read() + + def par(m): + section = m.group(1) + name = m.group(2) + rest = m.group(3).strip() + key = index_key(section, name) + return f"\\anchor {key}\n\\par {name} {rest}" + + def idx(m): + level = m.group(1) + title = name = m.group(2).strip() + section = m.group(3) + if title == title.upper(): + name = name.capitalize() + key = index_key(section, name) + return f"{level} {title} {{#{key}}}" + + def secref(m): + import os + import json + + secs = m.group(1).split() + + with open(os.path.join(BUILD_DIR, "index.json"), "r") as f: + index = json.load(f) + + items = {} + + for sec in secs: + if sec not in index: + print(f"{fn}: no index '{sec}'", file=sys.stderr) + else: + for name, key in index[sec].items(): + if name in items: + pkey, psec = items.pop(name) + nname = f"{name} ({sec})" + items[nname] = (key, sec) + if pkey is not None: + pname = f"{name} ({psec})" + items[pname] = (pkey, psec) + items[name] = (None, None) + else: + items[name] = (key, sec) + + text = [r"\secreflist"] + for name, (key, sec) in sorted(items.items()): + if key is not None: + text.append(rf'\refitem {key} "{name}"') + text.append(r"\endsecreflist") + text = "\n".join(text) + return f"{text}\n" + + text = re.sub(PAR_RE, par, text, flags=re.M) + text = re.sub(IDX_RE, idx, text, flags=re.M) + text = re.sub(SECREF_RE, secref, text, flags=re.M) + + print(text) + + +def main_index(args): + import json + + sections = {} + + for fn in set(args): + with open(fn, "r") as f: + load_index(sections, f.read()) + + result = {} + + for section, items in sections.items(): + for name in items: + key = index_key(section, name) + result.setdefault(section, {})[name] = key + + with open(os.path.join(BUILD_DIR, "index.json"), "w") as f: + json.dump(result, f) + + +def load_index(sections, text): + def par(m): + section = m.group(1) + name = m.group(2) + sections.setdefault(section, []).append(name) + return "" + + def idx(m): + name = m.group(2).strip() + section = m.group(3) + if name == name.upper(): + name = name.capitalize() + sections.setdefault(section, []).append(name) + return "" + + text = re.sub(PAR_RE, par, text, flags=re.M) + text = re.sub(IDX_RE, idx, text, flags=re.M) + + +if __name__ == "__main__": + if len(sys.argv) >= 2 and sys.argv[1] == "--index": + main_index(sys.argv[2:]) + elif len(sys.argv) == 2: + main(sys.argv[1:]) + else: + print(__doc__.strip()) + sys.exit(1) diff --git a/doc/input-filter.py b/doc/input-filter.py new file mode 100755 index 0000000..2b64a2d --- /dev/null +++ b/doc/input-filter.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; eval: (blacken-mode); -*- +r""" +Doxygen input filter that: + +- adds \privatesection to all files +- removes macros +- parses pulse_module_options and substitutes it into @pulse_module_options@ + +This is used for .c files, and causes Doxygen to not include +any symbols from them, unless they also appeared in a header file. + +The Pulse module option parsing is used in documentation of Pulseaudio modules. +""" +import sys +import re +import os + + +def main(): + fn = sys.argv[1] + with open(fn, "r") as f: + text = f.read() + + text = re.sub("#define.*", "", text) + + if "@pulse_module_options@" in text: + m = re.search( + r"static const char[* ]*const pulse_module_options\s+=\s+(.*?\")\s*;\s*$", + text, + re.M | re.S, + ) + if m: + res = [] + for line in m.group(1).splitlines(): + m = re.match(r"\s*\"\s*([a-z0-9_]+)\s*=\s*(.*)\"\s*$", line) + if m: + name = m.group(1) + value = m.group(2).strip().strip("<>") + res.append(f"- `{name}`: {value}") + + res = "\n * ".join(res) + text = text.replace("@pulse_module_options@", res) + + if os.path.basename(fn).startswith("module-") and fn.endswith(".c"): + text = re.sub(r"^ \* ##", r" * #", text, flags=re.M) + + print("/** \\privatesection */") + print(text) + + +if __name__ == "__main__": + main() diff --git a/doc/man-fixup.py b/doc/man-fixup.py new file mode 100755 index 0000000..829aec3 --- /dev/null +++ b/doc/man-fixup.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +# -*- mode: python; coding: utf-8; eval: (blacken-mode); -*- +r""" +Fetch right Doxygen man file, replace dummy parts, and fixup nroff +""" +import argparse +import re +import sys +from subprocess import call +from pathlib import Path + + +def main(): + p = argparse.ArgumentParser(description=__doc__.strip()) + p.add_argument("htmldir", type=Path) + p.add_argument("page") + p.add_argument("name") + p.add_argument("section") + p.add_argument("version") + args = p.parse_args() + + page, name, section, version = args.page, args.name, args.section, args.version + + mandir = args.htmldir / ".." / "man" / "man3" + fn = mandir / f"{page}.3" + + # Doxygen < 1.9.7 names .md file output differently... + if not fn.exists(): + page2 = page.replace("page_man_", "md_doc_dox_programs_").replace("-", "_") + fn = mandir / f"{page2}.3" + if not fn.exists(): + page2 = page.replace("page_man_", "md_doc_dox_config_").replace("-", "_") + fn = mandir / f"{page2}.3" + else: + page2 = None + + try: + with open(fn, "r") as f: + text = f.read() + except: + print(f"ERROR: man file {fn} missing!", file=sys.stderr) + call(["ls", "-R", str(args.htmldir / ".." / "man")], stdout=sys.stderr) + raise + + text = text.replace(page, name) + if page2 is not None: + text = text.replace(page2, name) + + # Replace bad nroff header + text = re.sub( + r"^(\.TH[^\n]*)\n", + rf'.TH "{name}" {section} "{version}" "PipeWire" \\" -*- nroff -*-\n', + text, + ) + + # Fixup name field (can't be done in Doxygen, otherwise HTML looks bac) + text = re.sub( + rf"^\.SH NAME\s*\n{name} \\- {name}\s*\n\.PP\n *", + rf".SH NAME\n{name} \\- ", + text, + count=1, + flags=re.M, + ) + + # Add DESCRIPTION section if missing and NAME field has extra stuff + if not re.search(r"^\.SH DESCRIPTION\s*\n", text): + text = re.sub( + r"^(.SH NAME\s*\n[^\.].*\n)\.PP\s*\n([^\.\n ]+)", + r"\1.SH DESCRIPTION\n.PP\n\2", + text, + count=1, + flags=re.M, + ) + + # Upcase titles + def upcase(m): + return m.group(0).upper() + + text = re.sub(r"^\.SH .*?$", upcase, text, flags=re.M) + + # Replace PW_KEY_*, SPA_KEY_* by their values + def pw_key(m): + key = m.group(0) + key = key.replace("PW_KEY_", "").lower().replace("_", ".") + if key in ("protocol", "access", "client.access") or key.startswith("sec."): + return f"pipewire.{key}" + return key + + def spa_key(m): + key = m.group(0) + return key.replace("SPA_KEY_", "").lower().replace("_", ".") + + text = re.sub(r"PW_KEY_[A-Z_]+", pw_key, text, flags=re.S) + text = re.sub(r"SPA_KEY_[A-Z_]+", spa_key, text, flags=re.S) + + print(text) + + +if __name__ == "__main__": + main() diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..5f2e28f --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,288 @@ +fs = import('fs') + +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()) + +doc_prefix_value = get_option('doc-prefix-value') +doc_sysconfdir_value = get_option('doc-sysconfdir-value') + +if doc_prefix_value == '' and doc_sysconfdir_value == '' + doc_spa_plugindir = spa_plugindir + doc_pipewire_configdir = pipewire_configdir + doc_pipewire_confdatadir = pipewire_confdatadir +else + if doc_prefix_value == '' + doc_prefix_value = get_option('prefix') + endif + if doc_sysconfdir_value == '' + doc_sysconfdir_value = get_option('sysconfdir') + endif + doc_spa_plugindir = doc_prefix_value / get_option('libdir') / spa_name + doc_pipewire_configdir = doc_prefix_value / doc_sysconfdir_value / 'pipewire' + doc_pipewire_confdatadir = doc_prefix_value / get_option('datadir') / 'pipewire' +endif + +doxygen_env = environment() +doxygen_env.set('PACKAGE_NAME', meson.project_name()) +doxygen_env.set('PACKAGE_VERSION', meson.project_version()) +doxygen_env.set('PACKAGE_URL', 'https://pipewire.org') +doxygen_env.set('PACKAGE_BUGREPORT', 'https://gitlab.freedesktop.org/pipewire/pipewire/issues') +doxygen_env.set('PIPEWIRE_CONFIG_DIR', doc_pipewire_configdir) +doxygen_env.set('PIPEWIRE_CONFDATADIR', doc_pipewire_confdatadir) +doxygen_env.set('SPA_PLUGINDIR', doc_spa_plugindir) +doxygen_env.set('BUILD_DIR', 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 +# tree.dox should be first to determine the ordering. +extra_docs = [ + 'tree.dox', + 'dox/index.dox', + 'dox/overview.dox', + 'dox/modules.dox', + 'dox/pulse-modules.dox', + 'dox/programs/index.md', + 'dox/config/index.md', + 'dox/config/xref.md', + 'dox/internals/index.dox', + 'dox/internals/design.dox', + 'dox/internals/access.dox', + 'dox/internals/midi.dox', + 'dox/internals/portal.dox', + 'dox/internals/daemon.dox', + 'dox/internals/library.dox', + 'dox/internals/session-manager.dox', + 'dox/internals/objects.dox', + 'dox/internals/audio.dox', + 'dox/internals/scheduling.dox', + 'dox/internals/protocol.dox', + 'dox/internals/pulseaudio.dox', + 'dox/internals/dma-buf.dox', + 'dox/tutorial/index.dox', + 'dox/tutorial/tutorial1.dox', + 'dox/tutorial/tutorial2.dox', + 'dox/tutorial/tutorial3.dox', + 'dox/tutorial/tutorial4.dox', + 'dox/tutorial/tutorial5.dox', + 'dox/tutorial/tutorial6.dox', + 'dox/api/index.dox', + 'dox/api/spa-index.dox', + 'dox/api/spa-plugins.dox', + 'dox/api/spa-design.dox', + 'dox/api/spa-pod.dox', + 'dox/api/spa-buffer.dox', +] + +manpage_docs = [ + 'dox/config/pipewire-pulse.conf.5.md', + 'dox/config/pipewire.conf.5.md', + 'dox/config/pipewire-client.conf.5.md', + 'dox/config/pipewire-jack.conf.5.md', + 'dox/config/pipewire-props.7.md', + 'dox/config/pipewire-filter-chain.conf.5.md', + 'dox/config/pipewire-pulse-modules.7.md', + 'dox/config/libpipewire-modules.7.md', + 'dox/programs/pipewire-pulse.1.md', + 'dox/programs/pipewire.1.md', + 'dox/programs/pw-cat.1.md', + 'dox/programs/pw-cli.1.md', + 'dox/programs/pw-config.1.md', + 'dox/programs/pw-container.1.md', + 'dox/programs/pw-dot.1.md', + 'dox/programs/pw-dump.1.md', + 'dox/programs/pw-jack.1.md', + 'dox/programs/pw-link.1.md', + 'dox/programs/pw-loopback.1.md', + 'dox/programs/pw-metadata.1.md', + 'dox/programs/pw-mididump.1.md', + 'dox/programs/pw-mon.1.md', + 'dox/programs/pw-profiler.1.md', + 'dox/programs/pw-reserve.1.md', + 'dox/programs/pw-top.1.md', + 'dox/programs/pw-v4l2.1.md', + 'dox/programs/spa-acp-tool.1.md', + 'dox/programs/spa-inspect.1.md', + 'dox/programs/spa-json-dump.1.md', + 'dox/programs/spa-monitor.1.md', + 'dox/programs/spa-resample.1.md', +] + +manpages = [] + +foreach m : manpage_docs + name = fs.stem(fs.name(m)) + pagepart = name.replace('.', '_') + manpages += [[name, f'page_man_@pagepart@']] + extra_docs += m +endforeach + +inputs = [] +foreach extra : extra_docs + inputs += meson.project_source_root() / 'doc' / extra +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 +foreach h : pipewire_module_protocol_pulse_sources + inputs += meson.project_source_root() / 'src' / 'modules' / h +endforeach +input_dirs = [ meson.project_source_root() / 'spa' / 'include' / 'spa' ] + +path_prefixes = [ + 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', +] +example_dep_files = [] +foreach h : example_files + example_dep_files += ['examples/' + h] +endforeach +foreach h : examples + example_files += [h + '.c'] + example_dep_files += ['../src/examples/' + h + '.c'] +endforeach +foreach h : spa_examples + example_files += ['spa/examples/' + h + '.c'] + example_dep_files += ['../spa/examples/' + h + '.c'] +endforeach + +example_doxygen = [] +example_ref = [] +foreach h : example_files + example_doxygen += ['\\example ' + h, + '\\snippet{doc} ' + h + ' title', + '
', + '\\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' ] + +module_manpage_list = [] +foreach m : module_sources + name = fs.stem(m) + pagepart = name.replace('-', '_') + module_manpage_list += f'\\ref page_@pagepart@ "libpipewire-@name@(7)"' + manpages += [[f'libpipewire-@name@.7', f'page_@pagepart@']] +endforeach + +doxygen_env.set('LIBPIPEWIRE_MODULES', '
  • ' + '
  • '.join(module_manpage_list) + '
') + +pulse_module_manpage_list = [] +foreach m : pipewire_module_protocol_pulse_sources + name = fs.stem(fs.name(m)) + if m.contains('/modules/') and name.startswith('module-') + pagepart = name.replace('-', '_') + pulse_module_manpage_list += f'\\ref page_pulse_@pagepart@ "pipewire-pulse-@name@(7)"' + manpages += [[f'pipewire-pulse-@name@.7', f'page_pulse_@pagepart@']] + endif +endforeach + +doxygen_env.set('PIPEWIRE_PULSE_MODULES', '
  • ' + '
  • '.join(pulse_module_manpage_list) + '
') + +doxygen_layout = meson.project_source_root() / 'doc' / 'DoxygenLayout.xml' +doxygen_filter_c = meson.project_source_root() / 'doc' / 'input-filter.py' +doxygen_filter_h = meson.project_source_root() / 'doc' / 'input-filter-h.sh' +doxygen_filter_md = meson.project_source_root() / 'doc' / 'input-filter-md.py' + +doxyfile_conf.set('inputs', ' '.join(inputs + input_dirs)) +doxyfile_conf.set('cssfiles', ' '.join(cssfiles)) +doxyfile_conf.set('layout', doxygen_layout) +doxyfile_conf.set('path_prefixes', ' '.join(path_prefixes)) +doxyfile_conf.set('c_input_filter', doxygen_filter_c) +doxyfile_conf.set('h_input_filter', doxygen_filter_h) +doxyfile_conf.set('md_input_filter', doxygen_filter_md) + +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 + +index_json = custom_target('index.json', + command: [ doxygen_filter_md, '--index', '@INPUT@' ], + input: extra_docs + manpage_docs, + output: 'index.json', + env: doxygen_env +) + +html_target = custom_target('pipewire-docs', + input: [ doxyfile, doxygen_layout, example_dep_files, examples_dox, + doxygen_filter_c, doxygen_filter_h, index_json ] + inputs + cssfiles, + output: [ 'html' ], + command: [ doxygen, doxyfile ], + env: doxygen_env, + install: install_docs, + install_tag: 'doc', + install_dir: docdir) + + +man_fixup = files('man-fixup.py') + +manfiles = [] + +foreach m : manpages + file = m.get(0) + page = m.get(1) + name = fs.stem(file) + section = file.split('.').get(-1) + + manfiles += custom_target(file, + command : [ python, man_fixup, '@INPUT@', page, name, section, meson.project_version() ], + output : file, + input : html_target, + depend_files : [ man_fixup ], + capture : true, + install : install_man, + install_tag: 'man', + install_dir : get_option('mandir') / 'man' + section + ) +endforeach diff --git a/doc/tree.dox b/doc/tree.dox new file mode 100644 index 0000000..ecc43d6 --- /dev/null +++ b/doc/tree.dox @@ -0,0 +1,134 @@ +/** + +This determines the ordering of items in Doxygen sidebar. + +\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_json_pod +\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 +\{ +\} + +\page page_overview +\page page_config +\page page_programs +\page page_modules +\page page_pulse_modules +\page page_internals +\page page_api +\page page_tutorial + +*/ diff --git a/include/valgrind/memcheck.h b/include/valgrind/memcheck.h new file mode 100644 index 0000000..3509b58 --- /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 addressability 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 addressability 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 addressability of an + lvalue to be checked. If suitable addressability 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 + +/* 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 "**** " 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/meson.build b/meson.build new file mode 100644 index 0000000..8b5728e --- /dev/null +++ b/meson.build @@ -0,0 +1,626 @@ +project('pipewire', ['c' ], + version : '1.4.2', + license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], + meson_version : '>= 0.61.1', + 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_minor = pipewire_version_major.to_int() * 1000 + pipewire_version_minor.to_int() * 100 + pipewire_version_micro.to_int() +libversion = '@0@.@1@.0'.format(soversion, libversion_minor) + +# LADI/jack +# 3, for PipeWire being the third JACK implementation, after JACK1 and jackdmp/JACK2) +jack_version_major = 3 +jack_version_minor = libversion_minor +# libjack[server] version has 0 for major (for compatibility with other implementations), +# 3 for minor, and "1000*major + 100*minor + micro" as micro version (the minor libpipewire soversion number) +libjackversion = '@0@.@1@.@2@'.format(soversion, jack_version_major, jack_version_minor) +# jack[server] version has 3 for major +# and pipewire's "1000*major + 100*minor + micro" as minor version +jackversion = '@0@.@1@.@2@'.format(jack_version_major, jack_version_minor, 0) + +pipewire_name = 'pipewire-@0@'.format(apiversion) +spa_name = 'spa-@0@'.format(spaversion) + +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 + +cc = meson.get_compiler('c') +cc_native = meson.get_compiler('c', native: true) + +if cc.has_header('features.h') and cc.get_define('__GLIBC__', prefix: '#include ') != '' + # 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') + +common_flags = [ + '-fvisibility=hidden', + '-fno-strict-aliasing', + '-Werror=suggest-attribute=format', + '-Wsign-compare', + '-Wpointer-arith', + '-Wpointer-sign', + '-Werror=format', + '-Wno-error=format-overflow', # avoid some "‘%s’ directive argument is null" + '-Wformat-security', + '-Wimplicit-fallthrough', + '-Wmissing-braces', + '-Wtype-limits', + '-Wvariadic-macros', + '-Wmaybe-uninitialized', + '-Wno-missing-field-initializers', + '-Wno-unused-parameter', + '-Wno-pedantic', + '-Wdeprecated-declarations', + '-Wunused-result', + '-Werror=return-type', + '-Werror=float-conversion', + '-Werror=constant-conversion', +] + +cc_flags = common_flags + [ + '-D_GNU_SOURCE', + '-DFASTPATH', +# '-DSPA_DEBUG_MEMCPY', + '-Werror=implicit-function-declaration', + '-Werror=incompatible-pointer-types', + '-Werror=int-conversion', + '-Werror=old-style-declaration', + '-Werror=old-style-definition', + '-Werror=missing-parameter-type', + '-Werror=strict-prototypes', +] +add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') + +cc_flags_native = cc_native.get_supported_arguments(cc_flags) + +have_cpp = add_languages('cpp', native: false, required : false) + +if have_cpp + cxx = meson.get_compiler('cpp') + cxx_flags = common_flags + [ '-Wno-c99-designator' ] + add_project_arguments(cxx.get_supported_arguments(cxx_flags), language: 'cpp') +endif + +have_sse = false +have_sse2 = false +have_ssse3 = false +have_sse41 = false +have_fma = false +have_avx = false +have_avx2 = false +if host_machine.cpu_family() in ['x86', 'x86_64'] + 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) +endif + +have_neon = false +if host_machine.cpu_family() == 'aarch64' + if cc.compiles(''' + #include + 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 + 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 + +have_rvv = false +if host_machine.cpu_family() == 'riscv64' + if cc.compiles(''' + int main() { + __asm__ __volatile__ ( + ".option arch, +v\nvsetivli zero, 0, e8, m1, ta, ma" + ); + } + ''', + name : 'riscv64 V Support') + have_rvv = true + endif +endif + +libatomic = cc.find_library('atomic', required : false) + +test_8_byte_atomic = ''' +#include + +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('PREFIX', prefix) +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_DATA_DIR', alsadatadir) +cdata.set('RTPRIO_SERVER', get_option('rtprio-server')) +cdata.set('RTPRIO_CLIENT', get_option('rtprio-client')) + +if host_machine.endian() == 'big' + cdata.set('WORDS_BIGENDIAN', 1) +endif + +check_headers = [ + ['sys/auxv.h', 'HAVE_SYS_AUXV_H'], + ['sys/mount.h', 'HAVE_SYS_MOUNT_H'], + ['sys/param.h', 'HAVE_SYS_PARAM_H'], + ['sys/random.h', 'HAVE_SYS_RANDOM_H'], + ['sys/vfs.h', 'HAVE_SYS_VFS_H'], + ['pwd.h', 'HAVE_PWD_H'], + ['grp.h', 'HAVE_GRP_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 ') != '') + +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()) + +logind_dep = dependency(get_option('logind-provider'), required: get_option('logind')) +summary({'logind': logind_dep.found()}, bool_yn: true) +cdata.set('HAVE_LOGIND', logind_dep.found()) + +selinux_dep = dependency('libselinux', required: get_option('selinux')) +summary({'libselinux': selinux_dep.found()}, bool_yn: true) +cdata.set('HAVE_SELINUX', selinux_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) +mathlib_native = cc_native.find_library('m', required : false) +rt_lib = cc.find_library('rt', required : false) # clock_gettime +dl_lib = cc.find_library('dl', required : false) +pthread_lib = dependency('threads') +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) +fftw_dep = dependency('fftw3f', required : false) +summary({'fftw3f (filter-chain convolver)': fftw_dep.found()}, bool_yn: true, section: 'Misc dependencies') +cdata.set('HAVE_FFTW', fftw_dep.found()) + +if get_option('readline').disabled() + readline_dep = dependency('', required: false) +else + readline_dep = dependency('readline', required : false) + if not readline_dep.found() + readline_dep = cc.find_library('readline', required : get_option('readline')) + endif +endif + +# Both the FFmpeg SPA plugin and the pw-cat FFmpeg integration use libavcodec. +# But only the latter also needs libavformat and libavutil. +# 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()) + avutil_dep = dependency('libavutil', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) + swscale_dep = dependency('libswscale', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) +else + avcodec_dep = dependency('', required: false) +endif +cdata.set('HAVE_PW_CAT_FFMPEG_INTEGRATION', pw_cat_ffmpeg.allowed()) + +opus_dep = dependency('opus', required : get_option('opus')) +summary({'opus (Bluetooth, RTP)': opus_dep.found()}, bool_yn: true, section: 'Misc dependencies') +cdata.set('HAVE_OPUS', opus_dep.found()) + +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) + +gsettings_gio_dep = dependency('gio-2.0', version : '>= 2.26.0', required : get_option('gsettings')) +summary({'GIO (GSettings)': gsettings_gio_dep.found()}, bool_yn: true, section: 'Misc dependencies') +if not gsettings_gio_dep.found() and get_option('gsettings-pulse-schema').enabled() + error('`gsettings-pulse-schema` is enabled but `gio` was not found.') +endif + +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-base-1.0': {}, + 'gstreamer-video-1.0': {}, + 'gstreamer-audio-1.0': {}, + 'gstreamer-allocators-1.0': {}, +} + +gst_dep = [] +gst_dma_drm_found = false +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] + + if depname == 'gstreamer-allocators-1.0' and dep.version().version_compare('>= 1.23.1') + gst_dma_drm_found = true + endif +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()) + +summary({'gstreamer DMA_DRM support': gst_dma_drm_found}, bool_yn: true, section: 'Backend') +cdata.set('HAVE_GSTREAMER_DMA_DRM', gst_dma_drm_found) + +if get_option('echo-cancel-webrtc').disabled() + webrtc_dep = dependency('', required: false) + summary({'WebRTC Echo Canceling': webrtc_dep.found()}, bool_yn: false, section: 'Misc dependencies') +else + webrtc_dep = dependency('webrtc-audio-processing-2', + version : ['>= 2.0' ], + required : false) + cdata.set('HAVE_WEBRTC2', webrtc_dep.found()) + if webrtc_dep.found() + summary({'WebRTC Echo Canceling >= 2.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies') + else + webrtc_dep = dependency('webrtc-audio-processing-1', + version : ['>= 1.2' ], + required : false) + cdata.set('HAVE_WEBRTC1', webrtc_dep.found()) + if webrtc_dep.found() + summary({'WebRTC Echo Canceling >= 1.2': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies') + else + webrtc_dep = dependency('webrtc-audio-processing', + version : ['>= 0.2', '< 1.0'], + required : false) + cdata.set('HAVE_WEBRTC', webrtc_dep.found()) + if webrtc_dep.found() + summary({'WebRTC Echo Canceling < 1.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies') + else + # If deps are not found on the system but it's enabled, try to fallback to the subproject + webrtc_dep = dependency('webrtc-audio-processing-2', + version : ['>= 2.0' ], + required : get_option('echo-cancel-webrtc')) + cdata.set('HAVE_WEBRTC2', webrtc_dep.found()) + summary({'WebRTC Echo Canceling > 2.0': webrtc_dep.found()}, bool_yn: true, section: 'Misc dependencies') + endif + endif + endif +endif + +# 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.2.6', 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) + +libffado_dep = dependency('libffado', required: get_option('libffado')) +summary({'ffado': libffado_dep.found()}, bool_yn: true) +glib2_snap_dep = dependency('glib-2.0', required : get_option('snap')) +gio2_snap_dep = dependency('gio-2.0', required : get_option('snap')) +apparmor_snap_dep = dependency('libapparmor', required : get_option('snap')) +if dependency('snapd-glib-2', required: false).found() + snap_dep = dependency('snapd-glib-2', required : get_option('snap')) +else + snap_dep = dependency('snapd-glib', required : get_option('snap')) +endif +if snap_dep.found() and glib2_snap_dep.found() and gio2_snap_dep.found() and apparmor_snap_dep.found() + cdata.set('HAVE_SNAP', true) + snap_deps = [glib2_snap_dep, gio2_snap_dep, snap_dep, apparmor_snap_dep] +endif +summary({'GLib-2.0 (Snap support)': glib2_snap_dep.found()}, bool_yn: true, section: 'Misc dependencies') +summary({'Gio-2.0 (Snap support)': gio2_snap_dep.found()}, bool_yn: true, section: 'Misc dependencies') +summary({'Apparmor (Snap support)': apparmor_snap_dep.found()}, bool_yn: true, section: 'Misc dependencies') +summary({'Snapd-glib (Snap support)': snap_dep.found()}, bool_yn: true, section: 'Misc dependencies') + +check_functions = [ + ['gettid', '#include ', ['-D_GNU_SOURCE'], []], + ['memfd_create', '#include ', ['-D_GNU_SOURCE'], []], + ['getrandom', '#include \n#include ', ['-D_GNU_SOURCE'], []], + ['random_r', '#include ', ['-D_GNU_SOURCE'], []], + ['reallocarray', '#include ', ['-D_GNU_SOURCE'], []], + ['sigabbrev_np', '#include ', ['-D_GNU_SOURCE'], []], + ['XSetIOErrorExitHandler', '#include ', [], [x11_dep]], + ['malloc_trim', '#include ', [], []], + ['malloc_info', '#include ', [], []], +] + +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_docs = get_option('man').enabled() or get_option('docs').enabled() +if get_option('man').allowed() or get_option('docs').allowed() + doxygen = find_program('doxygen', required : generate_docs, version : '>=1.9') + pymod = import('python') + python = pymod.find_installation('python3', required: generate_docs) + generate_docs = doxygen.found() and python.found() +endif + +install_docs = get_option('docs').require(generate_docs).allowed() +install_man = get_option('man').require(generate_docs).allowed() + +summary({'Documentation ': install_docs}, bool_yn: true) +summary({'Man pages ': install_man}, bool_yn: true) + +if generate_docs + 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('ACP_PATHS_DIR', srcdir / 'spa' / 'plugins' / 'alsa' / 'mixer' / 'paths') +devenv.set('ACP_PROFILES_DIR', srcdir / 'spa' / 'plugins' / 'alsa' / 'mixer' / 'profile-sets') + +devenv.prepend('GST_PLUGIN_PATH', builddir / 'src'/ 'gst') +devenv.prepend('ALSA_PLUGIN_DIR', builddir / 'pipewire-alsa' / 'alsa-plugins') +devenv.prepend('LD_LIBRARY_PATH', builddir / 'pipewire-jack' / 'src') + +devenv.set('PW_UNINSTALLED', '1') + +meson.add_devenv(devenv) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..dc1b339 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,381 @@ +option('docdir', + type : 'string', + description : 'Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() )') +option('docs', + description: 'Documentation', + type: 'feature', + value: 'disabled') +option('man', + description: 'Manual pages', + type: 'feature', + value: 'disabled') +option('examples', + description: 'Build examples', + type: 'feature', + value: 'enabled') +option('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('logind', + description: 'Enable logind integration', + type: 'feature', + value: 'auto') +option('logind-provider', + description: 'Provider for logind integration', + type: 'combo', + choices: ['libelogind', 'libsystemd'], + value: 'libsystemd') +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('selinux', + description: 'Enable SELinux integration', + type: 'feature', + value: 'auto') +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('resampler-precomp-tuples', + description: 'Array of "inrate,outrate[,quality]" tuples to precompute resampler coefficients for', + type: 'array', + value: [ '32000,44100', '32000,48000', '48000,44100', '44100,48000' ]) +option('bluez5', + description: 'Enable bluez5 spa plugin integration', + type: 'feature', + 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: 'auto') +option('bluez5-codec-g722', + description: 'Enable G722 open source codec implementation', + type: 'feature', + value: 'auto') +option('control', + description: 'Enable control spa plugin integration', + type: 'feature', + 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('libmysofa', + description: 'Enable code that depends on libmysofa', + 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: 'auto') +option('pam-defaults-install', + description: 'Install limits.d file modifying defaults for all PAM users. Only for old kernels/systemd!', + type: 'boolean', + value: false) +option('pam-memlock-default', + description : 'The default memlock value for any PAM user in kilobytes. Multiples of 64 recommended.', + type : 'integer', + min: 640, + value: 8192) +option('rlimits-install', + description: 'Install PAM limits.d file. Voids all following rlimits-* options, if false', + type: 'boolean', + value: true) +option('rlimits-match', + description : 'PAM match rule for the generated limits.d file. @ denotes matching a group.', + type : 'string', + value: '@pipewire') +option('rtprio-server', + description : 'PipeWire server realtime priority', + type : 'integer', + min: 11, + max: 99, + value: 88) +option('rtprio-client', + description : 'PipeWire clients realtime priority', + type : 'integer', + min: 11, + max: 99, + value: 83) +option('rlimits-rtprio', + description : 'RR and FIFO scheduler priority permitted for realtime threads of the matching user(s)', + type : 'integer', + min: 11, + max: 99, + value: 95) +option('rlimits-memlock', + description : 'kB of memory each process of the user matched by the rule can lock. Can be unlimited .', + type : 'string', + value: '4194304') +option('rlimits-nice', + description : 'Not niceness permitted for non-realtime threads of the matching user(s)', + type : 'integer', + min: -20, + max: -1, + value: -19) +option('opus', + description: 'Enable code that depends on opus', + type: 'feature', + value: 'auto') +option('libffado', + description: 'Enable code that depends on libffado', + type: 'feature', + value: 'auto') +option('gsettings-pulse-schema', + description: 'Install gsettings schema for pulseaudio', + type: 'feature', + value: 'auto') +option('snap', + description : 'Enable Snap permissions support.', + type : 'feature', + value : 'auto') +option('doc-prefix-value', + description : 'Installation prefix to show in documentation instead of the actual value.', + type : 'string', + value : '') +option('doc-sysconfdir-value', + description : 'Sysconf data directory to show in documentation instead of the actual value.', + type : 'string', + value : '') +option('ebur128', + description: 'Enable code that depends on ebur128', + type: 'feature', + value: 'auto') diff --git a/pipewire-alsa/alsa-plugins/ctl_pipewire.c b/pipewire-alsa/alsa-plugins/ctl_pipewire.c new file mode 100644 index 0000000..7fcfd57 --- /dev/null +++ b/pipewire-alsa/alsa-plugins/ctl_pipewire.c @@ -0,0 +1,1452 @@ +/* CTL - PipeWire plugin */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +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_device*)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 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 || + spa_json_str_object_find(value, strlen(value), "name", + ctl->default_sink, sizeof(ctl->default_sink)) < 0) + ctl->default_sink[0] = '\0'; + pw_log_debug("found default sink: %s", ctl->default_sink); + } + if (key == NULL || spa_streq(key, "default.audio.source")) { + if (value == NULL || + spa_json_str_object_find(value, strlen(value), "name", + ctl->default_source, sizeof(ctl->default_source)) < 0) + ctl->default_source[0] = '\0'; + pw_log_debug("found default source: %s", ctl->default_source); + } + } + 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 < 0) { + err = ctl->fd; + goto error; + } + + ctl->context = pw_context_new(loop, + pw_properties_new( + PW_KEY_CLIENT_API, "alsa", + 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, + ®istry_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..bd6836e --- /dev/null +++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c @@ -0,0 +1,1515 @@ +/* PCM - PipeWire plugin */ +/* SPDX-FileCopyrightText: Copyright © 2017 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#define __USE_GNU + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 negotiated: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 transferred; + uint64_t buffered; + int64_t now; + uintptr_t seq; + + struct spa_audio_info requested; + struct spa_audio_info format; +} snd_pcm_pipewire_t; + +static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io); + +static int update_active(snd_pcm_ioplug_t *io) +{ + snd_pcm_pipewire_t *pw = io->private_data; + snd_pcm_sframes_t avail; + bool active; + uint64_t val; + + avail = snd_pcm_ioplug_avail(io, pw->hw_ptr, io->appl_ptr); + + if (pw->error > 0) { + active = true; + } + else if (io->state == SND_PCM_STATE_DRAINING) { + active = pw->drained; + } + else if (avail >= 0 && avail < (snd_pcm_sframes_t)pw->min_avail) { + active = false; + } + else if (avail >= (snd_pcm_sframes_t)pw->min_avail) { + active = true; + } + else { + active = false; + } + + pw_log_trace("%p: avail:%lu min-avail:%lu state:%s hw:%lu appl:%lu active:%d state:%s", + pw, avail, pw->min_avail, snd_pcm_state_name(io->state), + pw->hw_ptr, io->appl_ptr, active, + snd_pcm_state_name(io->state)); + + if (active) + spa_system_eventfd_write(pw->system, io->poll_fd, 1); + else + spa_system_eventfd_read(pw->system, io->poll_fd, &val); + + return active; +} + +static 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_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 && update_active(io)) + *revents |= (io->stream == SND_PCM_STREAM_PLAYBACK) ? POLLOUT : POLLIN; + + pw_log_trace_fp("poll %d", *revents); + + 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; + int64_t diff; + + do { + seq1 = SPA_SEQ_READ(pw->seq); + + delay = pw->delay + pw->transferred; + 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 = SPA_SEQ_READ(pw->seq); + } while (!SPA_SEQ_READ_SUCCESS(seq1, seq2)); + + if (now != 0 && (io->state == SND_PCM_STATE_RUNNING || + io->state == SND_PCM_STATE_DRAINING)) { + diff = pw_stream_get_nsec(pw->stream) - 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); + if (areas != NULL) { + 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; + + if (spa_format_audio_parse(param, &pw->format) < 0) { + pw->error = -EINVAL; + } else { + switch (pw->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + break; + case SPA_MEDIA_SUBTYPE_dsd: + if (pw->format.info.dsd.interleave != pw->requested.info.dsd.interleave || + pw->format.info.dsd.bitorder != pw->requested.info.dsd.bitorder) { + pw->error = -EINVAL; + } + break; + } + } + if (pw->error < 0) { + pw_thread_loop_signal(pw->main_loop, false); + return; + } + + io->period_size = pw->min_avail; + + buffers = SPA_CLAMP(io->buffer_size / io->period_size, MIN_BUFFERS, MAX_BUFFERS); + 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); + + pw->negotiated = true; + pw_thread_loop_signal(pw->main_loop, false); +} + +static void on_stream_state_changed(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) +{ + snd_pcm_pipewire_t *pw = data; + + if (state == PW_STREAM_STATE_ERROR) { + pw_log_warn("%s", error); + pw->error = -errno; + update_active(&pw->io); + } +} + +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; + + SPA_SEQ_WRITE(pw->seq); + + if (pw->now != pwt.now) { + pw->transferred = 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->transferred += xfer; + + /* more then requested data transferred, use them in next iteration */ + pw->buffered = (want == 0 || pw->transferred < want) ? 0 : (pw->transferred % want); + + pw->now = pwt.now; + SPA_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, + .state_changed = on_stream_state_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) { + int event; + snd_pcm_sw_params_get_period_event(swparams, &event); + if (event) + pw->min_avail = io->period_size; + else + 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_build(&b, SPA_PARAM_EnumFormat, &pw->format); + + if (pw->stream != NULL) { + pw_stream_set_active(pw->stream, false); + pw_stream_update_properties(pw->stream, &pw->props->dict); + pw_stream_update_params(pw->stream, params, 1); + pw_stream_set_active(pw->stream, true); + 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->negotiated = false; + 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; + + while (!pw->negotiated && pw->error >= 0) + pw_thread_loop_wait(pw->main_loop); + if (pw->error < 0) + goto error; + + pw_thread_loop_unlock(pw->main_loop); + + return 0; + +error: + pw_thread_loop_unlock(pw->main_loop); + return pw->error < 0 ? pw->error : -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(uint32_t channels, uint32_t position[SPA_AUDIO_MAX_CHANNELS]) +{ + 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; + return 1; + case 1: + 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; + const char *fmt_str = NULL; + + 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; + } + + pw->requested.media_type = SPA_MEDIA_TYPE_audio; + switch(io->format) { + case SND_PCM_FORMAT_U8: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = planar ? SPA_AUDIO_FORMAT_U8P : SPA_AUDIO_FORMAT_U8; + break; + case SND_PCM_FORMAT_S16_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_LE(planar, S16); + break; + case SND_PCM_FORMAT_S16_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_BE(planar, S16); + break; + case SND_PCM_FORMAT_S24_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_LE(planar, S24_32); + break; + case SND_PCM_FORMAT_S24_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_BE(planar, S24_32); + break; + case SND_PCM_FORMAT_S32_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_LE(planar, S32); + break; + case SND_PCM_FORMAT_S32_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_BE(planar, S32); + break; + case SND_PCM_FORMAT_S24_3LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_LE(planar, S24); + break; + case SND_PCM_FORMAT_S24_3BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_BE(planar, S24); + break; + case SND_PCM_FORMAT_FLOAT_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_LE(planar, F32); + break; + case SND_PCM_FORMAT_FLOAT_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_raw; + pw->requested.info.raw.format = _FORMAT_BE(planar, F32); + break; + case SND_PCM_FORMAT_DSD_U32_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd; + pw->requested.info.dsd.interleave = 4; + break; + case SND_PCM_FORMAT_DSD_U32_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd; + pw->requested.info.dsd.interleave = -4; + break; + case SND_PCM_FORMAT_DSD_U16_BE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd; + pw->requested.info.dsd.interleave = 2; + break; + case SND_PCM_FORMAT_DSD_U16_LE: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd; + pw->requested.info.dsd.interleave = -2; + break; + case SND_PCM_FORMAT_DSD_U8: + pw->requested.media_subtype = SPA_MEDIA_SUBTYPE_dsd; + pw->requested.info.dsd.interleave = 1; + break; + default: + SNDERR("PipeWire: invalid format: %d\n", io->format); + return -EINVAL; + } + switch (pw->requested.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + pw->requested.info.raw.channels = io->channels; + pw->requested.info.raw.rate = io->rate; + set_default_channels(io->channels, pw->requested.info.raw.position); + fmt_str = spa_type_audio_format_to_short_name(pw->requested.info.raw.format); + pw->format = pw->requested; + break; + case SPA_MEDIA_SUBTYPE_dsd: + pw->requested.info.dsd.bitorder = SPA_PARAM_BITORDER_msb; + pw->requested.info.dsd.channels = io->channels; + pw->requested.info.dsd.rate = io->rate * SPA_ABS(pw->requested.info.dsd.interleave); + set_default_channels(io->channels, pw->requested.info.dsd.position); + pw->format = pw->requested; + /* we need to let the server decide these values */ + pw->format.info.dsd.bitorder = 0; + pw->format.info.dsd.interleave = 0; + fmt_str = "DSD"; + break; + default: + return -EIO; + } + + pw->sample_bits = snd_pcm_format_physical_width(io->format); + if (planar) { + 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, fmt_str, + 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; + uint32_t *position; + + switch (pw->requested.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + pw->requested.info.raw.channels = map->channels; + position = pw->requested.info.raw.position; + break; + case SPA_MEDIA_SUBTYPE_dsd: + pw->requested.info.dsd.channels = map->channels; + position = pw->requested.info.dsd.position; + break; + default: + return -EINVAL; + } + for (i = 0; i < map->channels; i++) { + 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, + 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, channels, *position; + + switch (pw->requested.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + channels = pw->requested.info.raw.channels; + position = pw->requested.info.raw.position; + break; + case SPA_MEDIA_SUBTYPE_dsd: + channels = pw->requested.info.dsd.channels; + position = pw->requested.info.dsd.position; + break; + default: + return NULL; + } + + map = calloc(1, sizeof(snd_pcm_chmap_t) + + channels * sizeof(unsigned int)); + map->channels = channels; + for (i = 0; i < channels; i++) + map->pos[i] = channel_to_chmap(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_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, +}; + +#define MAX_VALS 64 +struct param_info { + const char *prop; + int key; +#define TYPE_LIST 0 +#define TYPE_MIN_MAX 1 + int type; + unsigned int vals[MAX_VALS]; + unsigned int n_vals; + int (*collect) (const char *str, int len, unsigned int *val); + +}; + +static int collect_access(const char *str, int len, unsigned int *val) +{ + char key[64]; + + if (spa_json_parse_stringn(str, len, key, sizeof(key)) <= 0) + return -EINVAL; + + if (strcasecmp(key, "MMAP_INTERLEAVED") == 0) + *val = SND_PCM_ACCESS_MMAP_INTERLEAVED; + else if (strcasecmp(key, "MMAP_NONINTERLEAVED") == 0) + *val = SND_PCM_ACCESS_MMAP_NONINTERLEAVED; + else if (strcasecmp(key, "RW_INTERLEAVED") == 0) + *val = SND_PCM_ACCESS_RW_INTERLEAVED; + else if (strcasecmp(key, "RW_NONINTERLEAVED") == 0) + *val = SND_PCM_ACCESS_RW_NONINTERLEAVED; + else + return -EINVAL; + return 0; +} + +static int collect_format(const char *str, int len, unsigned int *val) +{ + char key[64]; + snd_pcm_format_t fmt; + + if (spa_json_parse_stringn(str, len, key, sizeof(key)) < 0) + return -EINVAL; + + fmt = snd_pcm_format_value(key); + if (fmt != SND_PCM_FORMAT_UNKNOWN) + *val = fmt; + else + return -EINVAL; + return 0; +} + +static int collect_int(const char *str, int len, unsigned int *val) +{ + int v; + if (spa_json_parse_int(str, len, &v) > 0) + *val = v; + else + return -EINVAL; + return 0; +} + +struct param_info infos[] = { + { "alsa.access", SND_PCM_IOPLUG_HW_ACCESS, TYPE_LIST, + { SND_PCM_ACCESS_MMAP_INTERLEAVED, + SND_PCM_ACCESS_MMAP_NONINTERLEAVED, + SND_PCM_ACCESS_RW_INTERLEAVED, + SND_PCM_ACCESS_RW_NONINTERLEAVED }, 4, collect_access }, + { "alsa.format", SND_PCM_IOPLUG_HW_FORMAT, TYPE_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, + /* we don't add DSD formats here, use alsa.formats to + * force this. Because we can't convert to/from DSD, enabling this + * might fail when the system has no native DSD + * SND_PCM_FORMAT_DSD_U32_BE, + * SND_PCM_FORMAT_DSD_U32_LE, + * SND_PCM_FORMAT_DSD_U16_BE, + * SND_PCM_FORMAT_DSD_U16_LE, + * SND_PCM_FORMAT_DSD_U8 */ + }, 7, collect_format }, + { "alsa.rate", SND_PCM_IOPLUG_HW_RATE, TYPE_MIN_MAX, + { 1, MAX_RATE }, 2, collect_int }, + { "alsa.channels", SND_PCM_IOPLUG_HW_CHANNELS, TYPE_MIN_MAX, + { 1, MAX_CHANNELS }, 2, collect_int }, + { "alsa.buffer-bytes", SND_PCM_IOPLUG_HW_BUFFER_BYTES, TYPE_MIN_MAX, + { MIN_BUFFER_BYTES, MAX_BUFFER_BYTES }, 2, collect_int }, + { "alsa.period-bytes", SND_PCM_IOPLUG_HW_PERIOD_BYTES, TYPE_MIN_MAX, + { MIN_PERIOD_BYTES, MAX_PERIOD_BYTES }, 2, collect_int }, + { "alsa.periods", SND_PCM_IOPLUG_HW_PERIODS, TYPE_MIN_MAX, + { MIN_BUFFERS, 1024 }, 2, collect_int }, +}; + +static struct param_info *param_info_by_key(int key) +{ + SPA_FOR_EACH_ELEMENT_VAR(infos, p) { + if (p->key == key) + return p; + } + return NULL; +} + +static int parse_value(const char *str, struct param_info *info) +{ + struct spa_json it[2]; + unsigned int v; + const char *val; + int len; + + if ((len = spa_json_begin(&it[0], str, strlen(str), &val)) <= 0) + return -EINVAL; + + if (spa_json_is_array(val, len)) { + info->type = TYPE_LIST; + info->n_vals = 0; + spa_json_enter(&it[0], &it[1]); + while ((len = spa_json_next(&it[1], &val)) > 0 && info->n_vals < MAX_VALS) { + if (info->collect(val, len, &v) < 0) + continue; + info->vals[info->n_vals++] = v; + } + } + else if (spa_json_is_object(val, len)) { + char key[64]; + info->type = TYPE_MIN_MAX; + info->n_vals = 2; + spa_json_enter(&it[0], &it[1]); + while ((len = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) { + if (info->collect(val, len, &v) < 0) + continue; + if (spa_streq(key, "min")) + info->vals[0] = v; + else if (spa_streq(key, "max")) + info->vals[1] = v; + } + } + else if (info->collect(val, len, &v) >= 0) { + info->type = TYPE_LIST; + info->vals[0] = v; + info->n_vals = 1; + } + return 0; +} + +static int set_constraint(snd_pcm_pipewire_t *pw, int key) +{ + struct param_info *p = param_info_by_key(key), info; + const char *str; + int err; + + if (p == NULL) + return -EINVAL; + + info = *p; + + str = pw_properties_get(pw->props, p->prop); + if (str != NULL) + parse_value(str, &info); + + switch (info.type) { + case TYPE_LIST: + pw_log_info("%s: list %d", p->prop, info.n_vals); + err = snd_pcm_ioplug_set_param_list(&pw->io, key, info.n_vals, info.vals); + break; + case TYPE_MIN_MAX: + pw_log_info("%s: min:%u max:%u", p->prop, info.vals[0], info.vals[1]); + err = snd_pcm_ioplug_set_param_minmax(&pw->io, key, info.vals[0], info.vals[1]); + break; + default: + return -EIO; + } + if (err < 0) + pw_log_warn("Can't set param %s: %s", info.prop, snd_strerror(err)); + + return err; + +} +static int pipewire_set_hw_constraint(snd_pcm_pipewire_t *pw) +{ + int err; + if ((err = set_constraint(pw, SND_PCM_IOPLUG_HW_ACCESS)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_FORMAT)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_RATE)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_CHANNELS)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_PERIOD_BYTES)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_BUFFER_BYTES)) < 0 || + (err = set_constraint(pw, SND_PCM_IOPLUG_HW_PERIODS)) < 0) + 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_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.%s", + stream == SND_PCM_STREAM_PLAYBACK ? "playback" : "capture", + pw_get_prgname()); + if (pw_properties_get(pw->props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_setf(pw->props, PW_KEY_NODE_DESCRIPTION, "ALSA %s [%s]", + stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture", + pw_get_prgname()); + if (pw_properties_get(pw->props, PW_KEY_MEDIA_NAME) == NULL) + pw_properties_setf(pw->props, PW_KEY_MEDIA_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)); + + if ((str = pw_properties_get(pw->props, "alsa.deny")) != NULL && + spa_atob(str)) { + err = -EACCES; + goto error; + } + + 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); + if (pw->fd < 0) { + err = pw->fd; + goto error; + } + + 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 (spa_strstartswith(pw_get_library_version(), "0.2")) + 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..347ed8d --- /dev/null +++ b/pipewire-alsa/tests/test-pipewire-alsa-stress.c @@ -0,0 +1,131 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Axis Communications AB */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Stress test using pipewire-alsa. + [title] + */ + +#include +#include +#include +#include + +#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(¶ms); + 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/ump-source.c b/pipewire-jack/examples/ump-source.c new file mode 100644 index 0000000..274d724 --- /dev/null +++ b/pipewire-jack/examples/ump-source.c @@ -0,0 +1,114 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include + +#define MAX_BUFFERS 64 + +struct data { + const char *path; + + jack_client_t *client; + const char *client_name; + jack_port_t *out_port; + + int cycle; + uint64_t position; + uint64_t next_sample; + uint64_t period; +}; + +static int +process (jack_nframes_t nframes, void *arg) +{ + struct data *d = (struct data*)arg; + void *buf; + uint32_t event[2]; + + buf = jack_port_get_buffer (d->out_port, nframes); + jack_midi_clear_buffer(buf); + + while (d->position >= d->next_sample && d->position + nframes > d->next_sample) { + uint64_t pos = d->position - d->next_sample; + + if (d->cycle == 0) { + /* MIDI 2.0 note on, channel 0, middle C, max velocity, no attribute */ + event[0] = 0x40903c00; + event[1] = 0xffff0000; + } else { + /* MIDI 2.0 note off, channel 0, middle C, max velocity, no attribute */ + event[0] = 0x40803c00; + event[1] = 0xffff0000; + } + + d->cycle ^= 1; + + jack_midi_event_write(buf, pos, (const jack_midi_data_t *) event, sizeof(event)); + + d->next_sample += d->period; + } + d->position += nframes; + return 0; +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + jack_options_t options = JackNullOption; + jack_status_t status; + + data.client = jack_client_open ("ump-source", options, &status); + if (data.client == NULL) { + fprintf (stderr, "jack_client_open() failed, " + "status = 0x%2.0x\n", status); + if (status & JackServerFailed) { + fprintf (stderr, "Unable to connect to JACK server\n"); + } + exit (1); + } + if (status & JackServerStarted) { + fprintf (stderr, "JACK server started\n"); + } + if (status & JackNameNotUnique) { + data.client_name = jack_get_client_name(data.client); + fprintf (stderr, "unique name `%s' assigned\n", data.client_name); + } + + /* send 2 events per second */ + data.period = jack_get_sample_rate(data.client) / 2; + + jack_set_process_callback (data.client, process, &data); + + /* the UMP port type allows both sending and receiving of UMP + * messages, which can contain MIDI 1.0 and MIDI 2.0 messages. */ + data.out_port = jack_port_register (data.client, "output", + JACK_DEFAULT_MIDI_TYPE, + JackPortIsOutput | JackPortIsMIDI2, 0); + + if (data.out_port == NULL) { + fprintf(stderr, "no more JACK ports available\n"); + exit (1); + } + + if (jack_activate (data.client)) { + fprintf (stderr, "cannot activate client"); + exit (1); + } + + while (1) { + sleep (1); + } + + jack_client_close (data.client); + + return 0; +} diff --git a/pipewire-jack/examples/video-dsp-play.c b/pipewire-jack/examples/video-dsp-play.c new file mode 100644 index 0000000..5bca72a --- /dev/null +++ b/pipewire-jack/examples/video-dsp-play.c @@ -0,0 +1,185 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#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..d7a0728 --- /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 +#include +#include +#if !defined(sun) && !defined(__sun__) +#include +#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..cf870d1 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +* +*/ + +#ifndef __jack_intclient_h__ +#define __jack_intclient_h__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +/** + * 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. + * + * Optional parameters: depending on corresponding [@a options + * bits] additional parameters may follow @a status (in this order). + * + * @arg [@ref JackLoadName] (char *) load_name is the shared + * object file from which to load the new internal client (otherwise + * use the @a client_name). + * + * @arg [@ref JackLoadInit] (char *) load_init 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..525f4d7 --- /dev/null +++ b/pipewire-jack/jack/jack.h @@ -0,0 +1,1475 @@ +/* + 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_h__ +#define __jack_h__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include +#include + +/** + * 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 + * before jack.h. + *************************************************************/ + +#include + +/** + * 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. + * + * + * Optional parameters: depending on corresponding [@a options + * bits] additional parameters may follow @a status (in this order). + * + * @arg [@ref JackServerName] (char *) server_name 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. + * + * IMPORTANT: Most JACK clients do NOT need to register a latency + * callback. + * + * 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: + * + * capture latency: 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. + * + * playback latency: 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..45c74a5 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_jslist_h__ +#define __jack_jslist_h__ + +#include +#include + +#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..39ecb29 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +/** + * @file jack/metadata.h + * @ingroup publicheader + * @brief JACK Metadata API + * + */ + +#ifndef __jack_metadata_h__ +#define __jack_metadata_h__ + +#include + +#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..c0fd4e3 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + + +#ifndef __JACK_MIDIPORT_H +#define __JACK_MIDIPORT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + + +/** 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..ee14e52 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __net_h__ +#define __net_h__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include +#include + +#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..59d4b25 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef _RINGBUFFER_H +#define _RINGBUFFER_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +/** @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; + size_t write_ptr; + 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..8bdd180 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef __jack_session_h__ +#define __jack_session_h__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @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://new-session-manager.jackaudio.org/ + * @{ + */ + + +/** + * 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 + +/** + * @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 // mingw gives warning if we include windows.h before winsock2.h + #endif + + #include + + #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 + #include + #else /* other compilers ...*/ + #include + #include + #include + #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 // 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 + #endif + #include + #include + #include + + /** + * 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..97be006 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_thread_h__ +#define __jack_thread_h__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + +/* 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..d3254cb --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_transport_h__ +#define __jack_transport_h__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @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..206d727 --- /dev/null +++ b/pipewire-jack/jack/types.h @@ -0,0 +1,775 @@ +/* + 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_types_h__ +#define __jack_types_h__ + +#include + +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 "". + */ +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 (char *) server_name parameter. + */ + JackServerName = 0x04, + + /** + * Load internal client from optional (char *) + * load_name. Otherwise use the @a client_name. + */ + JackLoadName = 0x08, + + /** + * Pass optional (char *) load_init 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, + + /** + * if JackPortIsCV is set, then the port buffer represents audio-rate + * control data rather than audio. + * + * Clients SHOULD prevent connections between Audio and CV ports. + * + * To make the ports more meaningful, clients can add meta-data to them. + * It is recommended to set these 2 in particular: + * - http://lv2plug.in/ns/lv2core#minimum + * - http://lv2plug.in/ns/lv2core#maximum + */ + JackPortIsCV = 0x20, + + /** + * if JackPortIsMIDI2 is set, then the port expects to receive MIDI2 data. + * + * JACK will automatically convert MIDI1 data into MIDI2 for this port. + * + * for ports without this flag JACK will convert MIDI2 into MIDI1 + * as much possible, some events might be skipped. + */ + JackPortIsMIDI2 = 0x20, + +}; + +/** + * 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 */ + JackTickDouble = 0x200, /**< double-resolution tick */ + +} jack_position_bits_t; + +/** all valid position bits */ +#define JACK_POSITION_MASK (JackPositionBBT|JackPositionTimecode) +#define EXTENDED_TIME_INFO + +/** transport tick_double member is available for use */ +#define JACK_TICK_DOUBLE + +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. */ + + /* JACK extra transport fields */ + + double tick_double; /**< current tick-within-beat in double resolution. + Should be assumed zero if JackTickDouble is not set. + Since older versions of JACK do not expose this variable, + the macro JACK_TICK_DOUBLE is provided, + which can be used as build-time detection. */ + + /* 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[5]; + + /* 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..108bf14 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +*/ + +#ifndef __jack_uuid_h__ +#define __jack_uuid_h__ + +#include + +#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..13842a1 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 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..494cd57 --- /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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 + * 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..8299e8a --- /dev/null +++ b/pipewire-jack/src/control.c @@ -0,0 +1,452 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Florian Hülsmann */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include + +#include +#include + +#include + +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 true; +} + +SPA_EXPORT +bool jackctl_server_close(jackctl_server_t * server) +{ + // stub + pw_log_warn("%p: not implemented", server); + return true; +} + +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..7db25ca --- /dev/null +++ b/pipewire-jack/src/dummy.c @@ -0,0 +1,19 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include + +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..54435f0 --- /dev/null +++ b/pipewire-jack/src/export.c @@ -0,0 +1,18 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#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..0630d96 --- /dev/null +++ b/pipewire-jack/src/meson.build @@ -0,0 +1,120 @@ +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' + libjack_path_enable = '' +elif libjack_path == get_option('libdir') or libjack_path == pipewire_libdir + libjack_path = pipewire_libdir + libjack_path_dlopen = libjack_path + libjack_path_enable = '#' +else + libjack_path_dlopen = libjack_path + libjack_path_enable = '' +endif + +tools_config = configuration_data() +tools_config.set('LIBJACK_PATH', libjack_path_dlopen) +tools_config.set('LIBJACK_PATH_ENABLE', libjack_path_enable) + +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 : libjackversion, + 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 : libjackversion, + 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 : libjackversion, + 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 : jackversion, + extra_cflags : '-D_REENTRANT', + unescaped_variables: ['server_libs=-L${libdir} -ljackserver', 'jack_implementation=pipewire']) + + pkgconfig.generate(filebase : 'jackserver', + libraries : [pipewire_jackserver], + name : 'jackserver', + description : 'PipeWire JACK Control API', + version : jackversion, + unescaped_variables: ['jack_implementation=pipewire']) +endif + +if sdl_dep.found() + 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 +executable('ump-source', + '../examples/ump-source.c', + include_directories : [jack_inc], + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'examples' / 'jack', + dependencies : [mathlib], + link_with: pipewire_jack, +) diff --git a/pipewire-jack/src/metadata.c b/pipewire-jack/src/metadata.c new file mode 100644 index 0000000..b38fad4 --- /dev/null +++ b/pipewire-jack/src/metadata.c @@ -0,0 +1,416 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +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 = do_sync(c); +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; + 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); + + 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; + + pw_log_info("remove id:%u (%"PRIu64") '%s'", o->id, subject, key); + pw_metadata_set_property(c->metadata->proxy, + o->id, key, NULL, NULL); + res = do_sync(c); +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; + struct object *o; + uint32_t serial; + int res = -1; + + spa_return_val_if_fail(c != 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; + + pw_log_info("remove id:%u (%"PRIu64")", o->id, subject); + pw_metadata_set_property(c->metadata->proxy, + o->id, NULL, NULL, NULL); + res = do_sync(c); +done: + pw_thread_loop_unlock(c->context.loop); + + return res; +} + +SPA_EXPORT +int jack_remove_all_properties (jack_client_t* client) +{ + int res; + 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); + res = do_sync(c); + pw_thread_loop_unlock(c->context.loop); + + return res; +} + +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); + pw_thread_loop_lock(c->context.loop); + c->property_callback = callback; + c->property_arg = arg; + pw_thread_loop_unlock(c->context.loop); + return 0; +} diff --git a/pipewire-jack/src/net.c b/pipewire-jack/src/net.c new file mode 100644 index 0000000..d6a651c --- /dev/null +++ b/pipewire-jack/src/net.c @@ -0,0 +1,149 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include + +#include + +#include + +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..dc3b9f9 --- /dev/null +++ b/pipewire-jack/src/pipewire-jack-extensions.h @@ -0,0 +1,39 @@ +/* PipeWire JACK extensions */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_JACK_EXTENSIONS_H +#define PIPEWIRE_JACK_EXTENSIONS_H +#include + +#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); + +int jack_set_sample_rate (jack_client_t *client, jack_nframes_t nframes); + +/* raw OSC message */ +#define JACK_DEFAULT_OSC_TYPE "8 bit raw OSC" + +/* MIDI 2.0 UMP type. This contains raw UMP data, which can have MIDI 1.0 or + * MIDI 2.0 packets. The data is an array of 32 bit ints. */ +#define JACK_DEFAULT_UMP_TYPE "32 bit raw UMP" + +#ifdef __cplusplus +} +#endif + +#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..839a9fc --- /dev/null +++ b/pipewire-jack/src/pipewire-jack.c @@ -0,0 +1,7779 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2024 Nedko Arnaudov */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "pipewire/extensions/client-node.h" +#include "pipewire/extensions/metadata.h" +#include "pipewire-jack-extensions.h" + +/* 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 RTPRIO_CLIENT + +#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_CLIENT_PORTS 768 + +#define MAX_ALIGN 32 +#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_VIDEO 1 +#define TYPE_ID_MIDI 2 +#define TYPE_ID_OSC 3 +#define TYPE_ID_UMP 4 +#define TYPE_ID_OTHER 5 + +#define TYPE_ID_IS_EVENT(t) ((t) >= TYPE_ID_MIDI && (t) <= TYPE_ID_UMP) +#define TYPE_ID_CAN_OSC(t) ((t) == TYPE_ID_MIDI || (t) == TYPE_ID_OSC) +#define TYPE_ID_IS_HIDDEN(t) ((t) >= TYPE_ID_OTHER) +#define TYPE_ID_IS_COMPATIBLE(a,b)(((a) == (b)) || (TYPE_ID_IS_EVENT(a) && TYPE_ID_IS_EVENT(b))) + +#define SELF_CONNECT_ALLOW 0 +#define SELF_CONNECT_FAIL_EXT -1 +#define SELF_CONNECT_IGNORE_EXT 1 +#define SELF_CONNECT_FAIL_ALL -2 +#define SELF_CONNECT_IGNORE_ALL 2 + +#define OTHER_CONNECT_ALLOW 1 +#define OTHER_CONNECT_FAIL -1 +#define OTHER_CONNECT_IGNORE 0 + +#define NOTIFY_BUFFER_SIZE (1u<<13) +#define NOTIFY_BUFFER_MASK (NOTIFY_BUFFER_SIZE-1) + +struct notify { +#define NOTIFY_ACTIVE_FLAG (1<<0) + +#define NOTIFY_TYPE_NONE ((0<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_REGISTRATION ((1<<4)) +#define NOTIFY_TYPE_PORTREGISTRATION ((2<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_CONNECT ((3<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_BUFFER_FRAMES ((4<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_SAMPLE_RATE ((5<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_FREEWHEEL ((6<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_SHUTDOWN ((7<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_LATENCY ((8<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_TOTAL_LATENCY ((9<<4)|NOTIFY_ACTIVE_FLAG) + int type; + struct object *object; + int arg1; + const char *msg; +}; + +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; + uint32_t max_frames; +}; + +static struct globals globals; +static bool mlock_warned = false; + +#define MIDI_SCRATCH_FRAMES 8192 +static thread_local float midi_scratch[MIDI_SCRATCH_FRAMES]; + + +#define OBJECT_CHUNK 8 +#define RECYCLE_THRESHOLD 128 + +typedef void (*mix_func) (float *dst, float *src[], uint32_t n_src, bool aligned, uint32_t n_samples); + +struct object { + struct spa_list link; + + struct client *client; + +#define INTERFACE_Invalid 0 +#define INTERFACE_Port 1 +#define INTERFACE_Node 2 +#define INTERFACE_Link 3 +#define INTERFACE_Client 4 + uint32_t type; + uint32_t id; + uint32_t serial; + + union { + struct { + char name[1024]; + int32_t pid; + } pwclient; + struct { + char name[JACK_CLIENT_NAME_SIZE+1]; + char node_name[512]; + int32_t priority; + uint32_t client_id; + unsigned is_jack:1; + unsigned is_running:1; + } node; + struct { + uint32_t src; + uint32_t dst; + uint32_t src_serial; + uint32_t dst_serial; + bool src_ours; + bool dst_ours; + 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; + int registered; + unsigned int visible; + unsigned int removing:1; + unsigned int removed:1; + unsigned int to_free: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[2]; + + struct spa_list queue; + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + unsigned int to_free:1; +}; + +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[2]; + struct spa_list mix; + uint32_t n_mix; + struct mix *global_mix; + + struct port *tied; + + unsigned int empty_out:1; + unsigned int zeroed:1; + unsigned int to_free:1; + + void *(*get_buffer) (struct port *p, jack_nframes_t frames); + + float *emptyptr; + float empty[]; +}; + +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; + void (*trigger) (struct link *l, uint64_t nsec); +}; + +struct context { + struct pw_loop *l; + struct pw_thread_loop *loop; /* thread_lock protects all below */ + struct pw_context *context; + struct pw_loop *nl; + struct pw_thread_loop *notify; + + 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 frame_times { + uint64_t frames; + uint64_t nsec; + uint64_t next_nsec; + uint32_t buffer_frames; + uint32_t sample_rate; + double rate_diff; +}; + +struct client { + char name[JACK_CLIENT_NAME_SIZE+1]; + + 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_loop *l; + 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 object *object; + + struct spa_source *socket_source; + struct spa_source *notify_source; + void *notify_buffer; + struct spa_ringbuffer notify_ring; + + 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]; + uint32_t n_ports; + + 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; + unsigned int prepared:1; + unsigned int first:1; + unsigned int thread_entered:1; + } 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 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 show_midi: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 other_connect_mode; + int rt_max; + unsigned int fix_midi_events:1; + unsigned int global_buffer_size:1; + unsigned int global_sample_rate:1; + unsigned int passive_links:1; + unsigned int pending_callbacks:1; + int frozen_callbacks; + char filter_char; + uint32_t max_ports; + unsigned int fill_aliases:1; + unsigned int writable_input:1; + unsigned int async:1; + unsigned int flag_midi2:1; + + uint32_t max_frames; + uint32_t max_align; + mix_func mix_function; + + jack_position_t jack_position; + jack_transport_state_t jack_state; + struct frame_times jack_times; +}; + +#define return_val_if_fail(expr, val) \ +({ \ + if (SPA_UNLIKELY(!(expr))) { \ + pw_log_warn("'%s' failed at %s:%u %s()", \ + #expr , __FILE__, __LINE__, __func__); \ + return (val); \ + } \ +}) + +#define return_if_fail(expr) \ +({ \ + if (SPA_UNLIKELY(!(expr))) { \ + pw_log_warn("'%s' failed at %s:%u %s()", \ + #expr , __FILE__, __LINE__, __func__); \ + return; \ + } \ +}) + +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; + } + o[0].to_free = true; + 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) { + pw_log_debug("%p: recycle object:%p remived:%d type:%d id:%u/%u %u/%u", + c, o, o->removed, o->type, o->id, o->serial, + c->context.free_count, remain); + if (o->removed) { + 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 %u/%u", c, o, o->type, + c->context.free_count, RECYCLE_THRESHOLD); + 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 inline struct object *port_to_object(const jack_port_t *port) +{ + return (struct object*)port; +} +static inline jack_port_t *object_to_port(struct object *o) +{ + return (jack_port_t*)o; +} + +struct io_info { + struct mix *mix; + void *data; + size_t size; +}; + +static int +do_mix_set_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + const struct io_info *info = data; + struct port *port = info->mix->port; + if (info->data) { + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + info->mix->io[0] = &ab->buffers[port->direction]; + info->mix->io[1] = &ab->buffers[port->direction^1]; + } else if (info->size >= sizeof(struct spa_io_buffers)) { + info->mix->io[0] = info->data; + info->mix->io[1] = info->data; + } else { + info->mix->io[0] = NULL; + info->mix->io[1] = NULL; + } + if (port->n_mix++ == 0 && port->global_mix != NULL) { + port->global_mix->io[0] = &port->io[0]; + port->global_mix->io[1] = &port->io[1]; + } + } else { + info->mix->io[0] = NULL; + info->mix->io[1] = NULL; + if (port->n_mix > 0 && --port->n_mix == 0 && port->global_mix != NULL) { + port->global_mix->io[0] = NULL; + port->global_mix->io[1] = NULL; + } + } + return 0; +} + +static inline void mix_set_io(struct mix *mix, void *data, size_t size) +{ + struct io_info info = { .mix = mix, .data = data, .size = size }; + pw_data_loop_invoke(mix->port->client->loop, + do_mix_set_io, SPA_ID_INVALID, &info, sizeof(info), false, NULL); +} + +static void init_mix(struct mix *mix, uint32_t mix_id, struct port *port, uint32_t peer_id) +{ + pw_log_debug("create %p mix:%d peer:%d", port, mix_id, peer_id); + mix->id = mix_id; + mix->peer_id = peer_id; + mix->port = port; + mix->peer_port = NULL; + mix->io[0] = mix->io[1] = NULL; + mix->n_buffers = 0; + spa_list_init(&mix->queue); + if (mix_id == SPA_ID_INVALID) { + port->global_mix = mix; + if (port->n_mix > 0) + mix_set_io(port->global_mix, &port->io, sizeof(port->io)); + } +} +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_port_peer(struct port *port, uint32_t peer_id) +{ + struct mix *mix; + spa_list_for_each(mix, &port->mix, port_link) { + pw_log_trace("%p %d %d", port, mix->peer_id, peer_id); + 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 *create_mix(struct client *c, struct port *port, + uint32_t mix_id, uint32_t peer_id) +{ + struct mix *mix; + uint32_t i; + + if (spa_list_is_empty(&c->free_mix)) { + mix = calloc(OBJECT_CHUNK, sizeof(struct mix)); + if (mix == NULL) + return NULL; + mix[0].to_free = true; + 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, peer_id); + + 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) +{ + struct port *port = mix->port; + + clear_buffers(c, mix); + spa_list_remove(&mix->port_link); + if (mix->id == SPA_ID_INVALID) + 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, port_size; + + if (c->n_ports >= c->max_ports) { + errno = ENOSPC; + return NULL; + } + + if (spa_list_is_empty(&c->free_ports)) { + port_size = sizeof(struct port) + (c->max_frames * sizeof(float)) + c->max_align; + + p = calloc(OBJECT_CHUNK, port_size); + if (p == NULL) + return NULL; + p[0].to_free = true; + for (i = 0; i < OBJECT_CHUNK; i++) { + struct port *t = SPA_PTROFF(p, port_size * i, struct port); + spa_list_append(&c->free_ports, &t->link); + } + } + p = spa_list_first(&c->free_ports, struct port, link); + spa_list_remove(&p->link); + + o = alloc_object(c, INTERFACE_Port); + if (o == NULL) + return NULL; + + 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, c->max_align, float); + p->port_id = pw_map_insert_new(&c->ports[direction], p); + c->n_ports++; + + 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, bool free) +{ + struct mix *m; + + spa_list_consume(m, &p->mix, port_link) + free_mix(c, m); + + c->n_ports--; + pw_map_remove(&c->ports[p->direction], p->port_id); + pw_properties_free(p->props); + spa_list_append(&c->free_ports, &p->link); + if (free) + free_object(c, p->object); + else + p->object->removing = true; +} + +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 inline bool client_port_visible(struct client *c, struct object *o) +{ + if (o->port.port != NULL && o->port.port->client == c) + return true; + return o->visible; +} + +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 || + (!client_port_visible(c, o))) + 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_client(struct client *c, uint32_t client_id) +{ + return find_type(c, client_id, INTERFACE_Client, false); +} + +static struct object *find_link(struct client *c, uint32_t src, uint32_t dst) +{ + struct object *l; + + 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; +} + +#if defined (__SSE__) +#include +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 = PW_MAJOR; + if (micro_ptr) + *micro_ptr = PW_MINOR; + if (proto_ptr) + *proto_ptr = PW_MICRO; +} + +#define do_callback_expr(c,expr,callback,do_emit,...) \ +({ \ + if (c->callback && do_emit) { \ + 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 { \ + (expr); \ + pw_log_debug("skip " #callback \ + " cb:%p do_emit:%d", c->callback, \ + do_emit); \ + } \ +}) + +#define do_callback(c,callback,do_emit,...) do_callback_expr(c,(void)0,callback,do_emit,__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]; + int major, minor, micro, proto; + jack_get_version(&major, &minor, µ, &proto); + snprintf(name, sizeof(name), "%d.%d.%d.%d (using PipeWire %s)", + major, minor, micro, proto, pw_get_library_version()); + return name; +} + +#define freeze_callbacks(c) \ +({ \ + (c)->frozen_callbacks++; \ + }) + +#define check_callbacks(c) \ +({ \ + if ((c)->frozen_callbacks == 0 && (c)->pending_callbacks) \ + pw_loop_signal_event((c)->context.nl, (c)->notify_source); \ + }) +#define thaw_callbacks(c) \ +({ \ + (c)->frozen_callbacks--; \ + check_callbacks(c); \ + }) + +static void on_notify_event(void *data, uint64_t count) +{ + struct client *c = data; + struct object *o; + int32_t avail; + uint32_t index; + struct notify *notify; + bool do_graph = false, do_recompute_capture = false, do_recompute_playback = false; + + pw_thread_loop_lock(c->context.loop); + if (c->frozen_callbacks != 0 || !c->pending_callbacks) + goto done; + + pw_log_debug("%p: enter active:%u", c, c->active); + + c->pending_callbacks = false; + + freeze_callbacks(c); + + avail = spa_ringbuffer_get_read_index(&c->notify_ring, &index); + while (avail > 0) { + notify = SPA_PTROFF(c->notify_buffer, index & NOTIFY_BUFFER_MASK, struct notify); + + o = notify->object; + pw_log_debug("%p: dequeue notify index:%08x %p type:%d %p arg1:%d", c, + index, notify, notify->type, o, notify->arg1); + + switch (notify->type) { + case NOTIFY_TYPE_REGISTRATION: + if (o->registered == notify->arg1) + break; + pw_log_debug("%p: node %u %s %u", c, o->serial, + o->node.name, notify->arg1); + do_callback(c, registration_callback, true, + o->node.name, + notify->arg1, + c->registration_arg); + break; + case NOTIFY_TYPE_PORTREGISTRATION: + if (o->registered == notify->arg1) + break; + pw_log_debug("%p: port %u %s %u", c, o->serial, + o->port.name, notify->arg1); + do_callback(c, portregistration_callback, c->active, + o->serial, + notify->arg1, + c->portregistration_arg); + break; + case NOTIFY_TYPE_CONNECT: + if (o->registered == notify->arg1) + break; + pw_log_debug("%p: link %u %u -> %u %u", c, o->serial, + o->port_link.src_serial, + o->port_link.dst, notify->arg1); + do_callback(c, connect_callback, c->active, + o->port_link.src_serial, + o->port_link.dst_serial, + notify->arg1, + c->connect_arg); + + do_graph = true; + do_recompute_capture = do_recompute_playback = true; + break; + case NOTIFY_TYPE_BUFFER_FRAMES: + pw_log_debug("%p: buffer frames %d -> %d", c, c->buffer_frames, notify->arg1); + if (c->buffer_frames != (uint32_t)notify->arg1) { + do_callback_expr(c, c->buffer_frames = notify->arg1, + bufsize_callback, c->active, + notify->arg1, c->bufsize_arg); + do_recompute_capture = do_recompute_playback = true; + } + break; + case NOTIFY_TYPE_SAMPLE_RATE: + pw_log_debug("%p: sample rate %d -> %d", c, c->sample_rate, notify->arg1); + if (c->sample_rate != (uint32_t)notify->arg1) { + do_callback_expr(c, c->sample_rate = notify->arg1, + srate_callback, c->active, + notify->arg1, c->srate_arg); + } + break; + case NOTIFY_TYPE_FREEWHEEL: + pw_log_debug("%p: freewheel %d", c, notify->arg1); + do_callback(c, freewheel_callback, c->active, + notify->arg1, c->freewheel_arg); + break; + case NOTIFY_TYPE_SHUTDOWN: + pw_log_debug("%p: shutdown %d %s", c, notify->arg1, notify->msg); + if (c->info_shutdown_callback) + do_callback(c, info_shutdown_callback, c->active, + notify->arg1, notify->msg, + c->info_shutdown_arg); + else + do_callback(c, shutdown_callback, c->active, c->shutdown_arg); + break; + case NOTIFY_TYPE_LATENCY: + pw_log_debug("%p: latency %d", c, notify->arg1); + if (notify->arg1 == JackCaptureLatency) + do_recompute_capture = true; + else if (notify->arg1 == JackPlaybackLatency) + do_recompute_playback = true; + break; + case NOTIFY_TYPE_TOTAL_LATENCY: + pw_log_debug("%p: total latency", c); + do_recompute_capture = do_recompute_playback = true; + break; + default: + break; + } + if (o != NULL) { + o->registered = notify->arg1; + if (notify->arg1 == 0 && o->removing) { + o->removing = false; + free_object(c, o); + } + } + avail -= sizeof(struct notify); + index += sizeof(struct notify); + spa_ringbuffer_read_update(&c->notify_ring, index); + } + if (do_recompute_capture) + do_callback(c, latency_callback, c->active, JackCaptureLatency, c->latency_arg); + if (do_recompute_playback) + do_callback(c, latency_callback, c->active, JackPlaybackLatency, c->latency_arg); + if (do_graph) + do_callback(c, graph_callback, c->active, c->graph_arg); + + thaw_callbacks(c); +done: + pw_log_debug("%p: leave", c); + pw_thread_loop_unlock(c->context.loop); +} + +static int queue_notify(struct client *c, int type, struct object *o, int arg1, const char *msg) +{ + int32_t filled; + uint32_t index; + struct notify *notify; + bool emit = false; + int res = 0; + + switch (type) { + case NOTIFY_TYPE_REGISTRATION: + emit = c->registration_callback != NULL && o != NULL; + break; + case NOTIFY_TYPE_PORTREGISTRATION: + emit = c->portregistration_callback != NULL && o != NULL; + o->visible = arg1; + break; + case NOTIFY_TYPE_CONNECT: + emit = c->connect_callback != NULL && o != NULL; + break; + case NOTIFY_TYPE_BUFFER_FRAMES: + emit = c->bufsize_callback != NULL; + break; + case NOTIFY_TYPE_SAMPLE_RATE: + emit = c->srate_callback != NULL; + break; + case NOTIFY_TYPE_FREEWHEEL: + emit = c->freewheel_callback != NULL; + break; + case NOTIFY_TYPE_SHUTDOWN: + emit = c->info_shutdown_callback != NULL || c->shutdown_callback != NULL; + break; + case NOTIFY_TYPE_LATENCY: + case NOTIFY_TYPE_TOTAL_LATENCY: + emit = c->latency_callback != NULL; + break; + default: + break; + } + if (!emit || ((type & NOTIFY_ACTIVE_FLAG) && !c->active)) { + switch (type) { + case NOTIFY_TYPE_BUFFER_FRAMES: + if (!emit) { + c->buffer_frames = arg1; + queue_notify(c, NOTIFY_TYPE_TOTAL_LATENCY, NULL, 0, NULL); + } + break; + case NOTIFY_TYPE_SAMPLE_RATE: + if (!emit) + c->sample_rate = arg1; + break; + } + pw_log_debug("%p: skip notify %08x active:%d emit:%d", c, type, + c->active, emit); + if (o != NULL) { + o->registered = arg1; + if (arg1 == 0 && o->removing) { + o->removing = false; + free_object(c, o); + } + } + return res; + } + + pthread_mutex_lock(&c->context.lock); + filled = spa_ringbuffer_get_write_index(&c->notify_ring, &index); + if (filled < 0 || filled + sizeof(struct notify) > NOTIFY_BUFFER_SIZE) { + pw_log_warn("%p: notify queue full %d", c, type); + res = -ENOSPC; + goto done; + } + + notify = SPA_PTROFF(c->notify_buffer, index & NOTIFY_BUFFER_MASK, struct notify); + notify->type = type; + notify->object = o; + notify->arg1 = arg1; + notify->msg = msg; + pw_log_debug("%p: queue notify index:%08x %p type:%d %p arg1:%d msg:%s", c, + index, notify, notify->type, o, notify->arg1, notify->msg); + index += sizeof(struct notify); + spa_ringbuffer_write_update(&c->notify_ring, index); + c->pending_callbacks = true; + check_callbacks(c); +done: + pthread_mutex_unlock(&c->context.lock); + return res; +} + +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) { + /* This happens when we did something on a proxy that + * was destroyed on the server already */ + if (res == -ENOENT) + return; + + client->last_res = res; + if (res == -EPIPE && !client->destroyed) { + queue_notify(client, NOTIFY_TYPE_SHUTDOWN, + NULL, JackFailure | JackServerError, + "JACK server has been closed"); + } + } + 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); + if (client->pending_sync < 0) + return 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_props(void *data, uint32_t global_id, const struct spa_dict *props) +{ + struct client *client = data; + client->node_id = global_id; + if (props) + pw_properties_update(client->props, props); +} + +static const struct pw_proxy_events node_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_node_removed, + .destroy = on_node_destroy, + .bound_props = on_node_bound_props, +}; + +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->l, c->socket_source); + c->socket_source = NULL; + } +} + +static inline void queue_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 inline struct buffer *dequeue_buffer(struct client *c, struct mix *mix) +{ + struct buffer *b; + + if (SPA_UNLIKELY(spa_list_is_empty(&mix->queue))) + return NULL; + + b = spa_list_first(&mix->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + pw_log_trace_fp("%p: port %p: dequeue buffer %d", c, mix->port, b->id); + + return b; +} + +static inline bool is_osc(jack_midi_event_t *ev) +{ + return ev->size >= 1 && (ev->buffer[0] == '#' || ev->buffer[0] == '/'); +} + +static size_t convert_from_event(void *midi, void *buffer, size_t size, uint32_t type) +{ + struct spa_pod_builder b = { 0, }; + uint32_t i, count; + struct spa_pod_frame f; + uint32_t event_type; + + switch (type) { + case TYPE_ID_MIDI: + case TYPE_ID_OSC: + /* we handle MIDI as OSC, check below */ + event_type = SPA_CONTROL_OSC; + break; + case TYPE_ID_UMP: + event_type = SPA_CONTROL_UMP; + break; + default: + return 0; + } + count = jack_midi_get_event_count(midi); + + spa_pod_builder_init(&b, buffer, size); + 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); + + if (type != TYPE_ID_MIDI || is_osc(&ev)) { + /* no midi port or it's OSC */ + spa_pod_builder_control(&b, ev.time, event_type); + spa_pod_builder_bytes(&b, ev.buffer, ev.size); + } else { + /* midi port and it's not OSC, convert to UMP */ + uint8_t *data = ev.buffer; + size_t size = ev.size; + uint64_t state = 0; + + while (size > 0) { + uint32_t ump[4]; + int ump_size = spa_ump_from_midi(&data, &size, + ump, sizeof(ump), 0, &state); + if (ump_size <= 0) + break; + spa_pod_builder_control(&b, ev.time, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ump, ump_size); + } + } + } + spa_pod_builder_pop(&b, &f); + return b.state.offset; +} + +static inline int event_compare(uint8_t s1, uint8_t s2) +{ + /* 11 (controller) > 12 (program change) > + * 8 (note off) > 9 (note on) > 10 (aftertouch) > + * 13 (channel pressure) > 14 (pitch bend) */ + static int priotab[] = { 5,4,3,7,6,2,1,0 }; + if ((s1 & 0xf) != (s2 & 0xf)) + return 0; + return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7]; +} + +static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b) +{ + if (a->offset < b->offset) + return -1; + if (a->offset > b->offset) + return 1; + if (a->type != b->type) + return 0; + switch(a->type) { + case SPA_CONTROL_Midi: + { + uint8_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value); + if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1) + return 0; + return event_compare(sa[0], sb[0]); + } + case SPA_CONTROL_UMP: + { + uint32_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value); + if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4) + return 0; + if ((sa[0] >> 28) != 2 || (sa[0] >> 28) != 4 || + (sb[0] >> 28) != 2 || (sb[0] >> 28) != 4) + return 0; + return event_compare(sa[0] >> 16, sb[0] >> 16); + } + default: + return 0; + } +} + +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 jack_midi_data_t* midi_event_reserve(void *port_buffer, + jack_nframes_t time, size_t data_size) +{ + struct midi_buffer *mb = port_buffer; + uint8_t *res = NULL; + + /* 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); + } 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); + } else { + struct midi_event *events = SPA_PTROFF(mb, sizeof(*mb), struct midi_event); + struct midi_event *ev = &events[mb->event_count]; + + 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 = mb->buffer_size - 1 - mb->write_pos; + res = SPA_PTROFF(mb, ev->byte_offset, uint8_t); + } + mb->event_count += 1; + } + return res; +} + +static inline int midi_event_write(void *port_buffer, + jack_nframes_t time, + const jack_midi_data_t *data, + size_t data_size, bool fix) +{ + jack_midi_data_t *retbuf = midi_event_reserve (port_buffer, time, data_size); + if (SPA_UNLIKELY(retbuf == NULL)) + return -ENOBUFS; + memcpy (retbuf, data, data_size); + if (fix) + fix_midi_event(retbuf, data_size); + return 0; +} + +static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix, uint32_t type) +{ + struct spa_pod_control *c[n_seq]; + uint64_t state = 0; + uint32_t i; + int res = 0; + + 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_OSC: + if (!TYPE_ID_CAN_OSC(type)) + break; + SPA_FALLTHROUGH; + case SPA_CONTROL_Midi: + { + uint8_t *data = SPA_POD_BODY(&next->value); + size_t size = SPA_POD_BODY_SIZE(&next->value); + + if (type == TYPE_ID_UMP) { + while (size > 0) { + uint32_t ump[4]; + int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state); + if (ump_size <= 0) + break; + if ((res = midi_event_write(midi, next->offset, + (uint8_t*)ump, ump_size, false)) < 0) + break; + } + } else { + res = midi_event_write(midi, next->offset, data, size, fix); + } + if (res < 0) + pw_log_warn("midi %p: can't write event: %s", midi, + spa_strerror(res)); + break; + } + case SPA_CONTROL_UMP: + { + void *data = SPA_POD_BODY(&next->value); + size_t size = SPA_POD_BODY_SIZE(&next->value); + uint8_t ev[32]; + + if (type == TYPE_ID_MIDI) { + int ev_size = spa_ump_to_midi(data, size, ev, sizeof(ev)); + if (ev_size <= 0) + break; + size = ev_size; + data = ev; + } else if (type != TYPE_ID_UMP) + break; + + if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0) + pw_log_warn("midi %p: can't write event: %s", midi, + spa_strerror(res)); + } + } + c[next_index] = spa_pod_control_next(c[next_index]); + } +} + + +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; + struct spa_io_buffers *io; + uint32_t cycle = p->client->rt.position->clock.cycle & 1; + + 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 io:%p", + c, p->object->port.name, p->port_id, frames, + mix->n_buffers, mix->io); + + if (SPA_UNLIKELY((io = mix->io[cycle]) == NULL || mix->n_buffers == 0)) + return NULL; + + if (io->status == SPA_STATUS_HAVE_DATA && + io->buffer_id < mix->n_buffers) { + b = &mix->buffers[io->buffer_id]; + d = &b->datas[0]; + } else { + if (mix->n_buffers == 1) { + b = &mix->buffers[0]; + } else { + if (io->buffer_id < mix->n_buffers) + queue_buffer(c, mix, io->buffer_id); + b = dequeue_buffer(c, mix); + + if (SPA_UNLIKELY(b == NULL)) { + pw_log_warn("port %p: out of buffers %d", p, mix->n_buffers); + io->buffer_id = SPA_ID_INVALID; + return NULL; + } + } + d = &b->datas[0]; + d->chunk->offset = 0; + d->chunk->size = c->buffer_frames * sizeof(float); + d->chunk->stride = stride; + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + } + ptr = d->data; + if (buf) + *buf = b; + return ptr; +} + +static inline void process_empty(struct port *p, uint32_t frames) +{ + struct client *c = p->client; + void *ptr, *src = p->emptyptr; + struct port *tied = p->tied; + uint32_t type = p->object->port.type_id; + + if (SPA_UNLIKELY(tied != NULL)) { + if ((src = tied->get_buffer(tied, frames)) == NULL) + src = p->emptyptr; + } + + switch (type) { + case TYPE_ID_AUDIO: + ptr = get_buffer_output(p, frames, sizeof(float), NULL); + if (SPA_LIKELY(ptr != NULL)) + memcpy(ptr, src, frames * sizeof(float)); + break; + case TYPE_ID_MIDI: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + { + struct buffer *b; + ptr = get_buffer_output(p, c->max_frames, 1, &b); + if (SPA_LIKELY(ptr != NULL)) { + /* first build the complete pod in scratch memory, then copy it + * to the target buffer. This makes it possible for multiple threads + * to do this concurrently */ + b->datas[0].chunk->size = convert_from_event(src, midi_scratch, + MIDI_SCRATCH_FRAMES * sizeof(float), type); + memcpy(ptr, midi_scratch, b->datas[0].chunk->size); + } + 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, uint32_t cycle) +{ + struct mix *mix; + struct spa_io_buffers *io; + + if (SPA_UNLIKELY(p->empty_out || p->tied)) + process_empty(p, frames); + + if (p->global_mix == NULL || (io = p->global_mix->io[cycle]) == NULL) + return; + + spa_list_for_each(mix, &p->mix, port_link) { + if (SPA_LIKELY(mix->io[cycle] != NULL)) + *mix->io[cycle] = *io; + } +} + +static void complete_process(struct client *c, uint32_t frames) +{ + struct port *p; + struct mix *mix; + union pw_map_item *item; + uint32_t cycle = c->rt.position->clock.cycle & 1; + + 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, cycle); + p->io[cycle].status = SPA_STATUS_NEED_DATA; + } + 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[cycle] != NULL)) + mix->io[cycle]->status = SPA_STATUS_NEED_DATA; + } + } +} + +static inline void debug_position(struct client *c, jack_position_t *p) +{ +#define pw_log_custom pw_log_trace_fp + pw_log_custom("usecs: %"PRIu64, p->usecs); + pw_log_custom("frame_rate: %u", p->frame_rate); + pw_log_custom("frame: %u", p->frame); + pw_log_custom("valid: %08x", p->valid); + + if (p->valid & JackPositionBBT) { + pw_log_custom("BBT"); + pw_log_custom(" bar: %u", p->bar); + pw_log_custom(" beat: %u", p->beat); + pw_log_custom(" tick: %u", p->tick); + pw_log_custom(" bar_start_tick: %f", p->bar_start_tick); + pw_log_custom(" beats_per_bar: %f", p->beats_per_bar); + pw_log_custom(" beat_type: %f", p->beat_type); + pw_log_custom(" ticks_per_beat: %f", p->ticks_per_beat); + pw_log_custom(" beats_per_minute: %f", p->beats_per_minute); + } + if (p->valid & JackPositionTimecode) { + pw_log_custom("Timecode:"); + pw_log_custom(" frame_time: %f", p->frame_time); + pw_log_custom(" next_time: %f", p->next_time); + } + if (p->valid & JackBBTFrameOffset) { + pw_log_custom("BBTFrameOffset:"); + pw_log_custom(" bbt_offset: %u", p->bbt_offset); + } + if (p->valid & JackAudioVideoRatio) { + pw_log_custom("AudioVideoRatio:"); + pw_log_custom(" audio_frames_per_video_frame: %f", p->audio_frames_per_video_frame); + } + if (p->valid & JackVideoFrameOffset) { + pw_log_custom("JackVideoFrameOffset:"); + pw_log_custom(" video_offset: %u", p->video_offset); + } +#undef pw_log_custom +} + +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.ticks_per_beat = s->ticks_per_beat; + d->bar.bar_start_tick = s->bar_start_tick; + d->bar.bpm = s->beats_per_minute; + d->bar.beat = s->bar * 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 frame_times *t) +{ + 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++; + t->frames = s->clock.position; + t->nsec = s->clock.nsec; + d->usecs = t->nsec / SPA_NSEC_PER_USEC; + t->next_nsec = s->clock.next_nsec; + t->rate_diff = s->clock.rate_diff; + t->buffer_frames = s->clock.duration; + d->frame_rate = t->sample_rate = s->clock.rate.denom; + + if ((int64_t)s->clock.position < s->offset) { + d->frame = seg->position; + } else { + running = s->clock.position - s->offset; + if (running >= seg->start && + (seg->duration == 0 || running < seg->start + seg->duration)) + d->frame = (unsigned int)((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 = seg->bar.ticks_per_beat; + d->bar_start_tick = seg->bar.bar_start_tick; + d->beats_per_minute = seg->bar.bpm; + + abs_beat = seg->bar.beat; + + d->bar = (int32_t) (abs_beat / d->beats_per_bar); + beats = (long int) (d->bar * d->beats_per_bar); + d->beat = (int32_t) (abs_beat - beats); + beats += d->beat; + d->tick = (int32_t) ((abs_beat - beats) * d->ticks_per_beat); + d->beat++; + } + d->unique_2 = d->unique_1; + return state; +} + +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) + queue_notify(c, NOTIFY_TYPE_BUFFER_FRAMES, NULL, buffer_frames, NULL); + else + c->buffer_frames = buffer_frames; + } + return c->buffer_frames == buffer_frames; +} + +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->sample_rate != (uint32_t)-1) + queue_notify(c, NOTIFY_TYPE_SAMPLE_RATE, NULL, sample_rate, NULL); + else + c->sample_rate = sample_rate; + } + return c->sample_rate == sample_rate; +} + +static inline uint32_t cycle_run(struct client *c) +{ + uint64_t cmd; + 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); + activation->xrun_count += cmd - 1; + activation->xrun_time = activation->awake_time; + activation->xrun_delay = 0; + activation->max_delay = SPA_MAX(activation->max_delay, 0u); + } + + if (!SPA_ATOMIC_CAS(activation->status, + PW_NODE_ACTIVATION_TRIGGERED, + PW_NODE_ACTIVATION_AWAKE)) + return 0; + + activation->awake_time = get_time_ns(c->l->system); + + if (SPA_UNLIKELY(c->rt.first)) { + if (c->thread_init_callback) + c->thread_init_callback(c->thread_init_arg); + c->rt.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, &c->jack_times); + + 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 void trigger_link_v1(struct link *l, uint64_t nsec) +{ + struct client *c = l->client; + struct pw_node_activation *a = l->activation; + struct pw_node_activation_state *state = &a->state[0]; + uint64_t cmd = 1; + + pw_log_trace_fp("%p: link %p-%d %p %d/%d", c, l, l->node_id, state, + state->pending, state->required); + + if (pw_node_activation_state_dec(state)) { + if (SPA_ATOMIC_CAS(a->status, + PW_NODE_ACTIVATION_NOT_TRIGGERED, + PW_NODE_ACTIVATION_TRIGGERED)) { + a->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 void trigger_link_v0(struct link *l, uint64_t nsec) +{ + struct client *c = l->client; + struct pw_node_activation *a = l->activation; + struct pw_node_activation_state *state = &a->state[0]; + uint64_t cmd = 1; + + pw_log_trace_fp("%p: link %p-%d %p %d/%d", c, l, l->node_id, state, + state->pending, state->required); + + if (pw_node_activation_state_dec(state)) { + SPA_ATOMIC_STORE(a->status, PW_NODE_ACTIVATION_TRIGGERED); + a->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 deactivate_link(struct client *c, struct link *l, uint64_t trigger) +{ + if (!c->async && trigger != 0) + l->trigger(l, trigger); +} + +static inline void signal_sync(struct client *c) +{ + uint64_t nsec; + struct link *l; + struct pw_node_activation *activation = c->activation; + int old_status; + + complete_process(c, c->buffer_frames); + + nsec = get_time_ns(c->l->system); + old_status = SPA_ATOMIC_XCHG(activation->status, PW_NODE_ACTIVATION_FINISHED); + activation->finish_time = nsec; + + if (c->async || old_status != PW_NODE_ACTIVATION_AWAKE) + return; + + spa_list_for_each(l, &c->rt.target_links, target_link) + l->trigger(l, nsec); +} + +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->rt.thread_entered) { + c->rt.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 void free_link(struct link *link) +{ + pw_log_debug("free link %p", link); + pw_memmap_free(link->mem); + close(link->signalfd); + free(link); +} + +static int +do_clean_transport(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + struct link *l; + pw_log_debug("%p: clean transport", c); + client_remove_source(c); + spa_list_consume(l, &c->rt.target_links, target_link) + spa_list_remove(&l->target_link); + return 0; +} + +static void clean_transport(struct client *c) +{ + struct link *l; + + if (!c->has_transport) + return; + + /* We assume the data-loop is unlocked now and can process our + * clean function. This is reasonable, the cleanup function is run when + * closing the client, which should join the data-thread. */ + pw_data_loop_invoke(c->loop, do_clean_transport, 1, NULL, 0, true, c); + + spa_list_consume(l, &c->links, link) { + spa_list_remove(&l->link); + free_link(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); + + c->activation->client_version = PW_VERSION_NODE_ACTIVATION; + + close(writefd); + c->socket_source = pw_loop_add_io(c->l, + 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 = SPA_ATOMIC_LOAD(a->segment_owner[0]); + if (owner == c->node_id) + return 0; + + /* try to become owner */ + if (c->timeowner_conditional) { + if (!SPA_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 { + SPA_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_debug("%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); + } + + queue_notify(c, NOTIFY_TYPE_FREEWHEEL, NULL, freewheeling, NULL); + + 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 +do_memmap_free(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + struct pw_memmap *mm = *((struct pw_memmap **)data); + pw_log_trace("memmap %p free", mm); + pw_memmap_free(mm); + pw_core_set_paused(c->core, false); + return 0; +} + +static int +do_queue_memmap_free(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + pw_loop_invoke(c->context.l, do_memmap_free, 0, data, size, false, c); + return 0; +} + +static void queue_memmap_free(struct client *c, struct pw_memmap *mem) +{ + if (mem != NULL) { + mem->tag[0] = SPA_ID_INVALID; + pw_core_set_paused(c->core, true); + pw_data_loop_invoke(c->loop, + do_queue_memmap_free, SPA_ID_INVALID, &mem, sizeof(&mem), false, c); + } +} + +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: %m", 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); + c->activation->active_driver_id = c->driver_id; + queue_memmap_free(c, old); + old = NULL; + 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 do_prepare_client(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("%p prepared:%d ", c, c->rt.prepared); + if (c->rt.prepared) + return 0; + + SPA_ATOMIC_STORE(c->activation->status, PW_NODE_ACTIVATION_FINISHED); + pw_loop_update_io(c->l, + c->socket_source, + SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP); + + c->rt.first = true; + c->rt.thread_entered = false; + c->rt.prepared = true; + return 0; +} + +static int do_unprepare_client(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + int old_state; + uint64_t trigger = 0; + struct link *l; + + pw_log_debug("%p prepared:%d ", c, c->rt.prepared); + + old_state = SPA_ATOMIC_XCHG(c->activation->status, PW_NODE_ACTIVATION_INACTIVE); + if (old_state != PW_NODE_ACTIVATION_FINISHED) + trigger = get_time_ns(c->l->system); + + if (!c->rt.prepared) + return 0; + + spa_list_for_each(l, &c->rt.target_links, target_link) { + if (!c->async && trigger != 0) + l->trigger(l, trigger); + } + + pw_loop_update_io(c->l, + c->socket_source, SPA_IO_ERR | SPA_IO_HUP); + + c->rt.prepared = false; + return 0; +} + +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_data_loop_invoke(c->loop, + do_unprepare_client, SPA_ID_INVALID, NULL, 0, false, c); + c->started = false; + } + break; + + case SPA_NODE_COMMAND_Start: + if (!c->started) { + pw_data_loop_invoke(c->loop, + do_prepare_client, SPA_ID_INVALID, NULL, 0, false, c); + c->started = true; + } + 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_UMP: + case TYPE_ID_OSC: + 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: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_Format, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + 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: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_STEP_Int( + c->max_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(1, 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_io_async(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_AsyncBuffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_async_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[7]; + 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_debug("port %s: update", p->object->port.name); + + p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + + param_enum_format(c, p, ¶ms[0], &b); + param_format(c, p, ¶ms[1], &b); + param_buffers(c, p, ¶ms[2], &b); + param_io(c, p, ¶ms[3], &b); + param_io_async(c, p, ¶ms[4], &b); + param_latency(c, p, ¶ms[5], &b); + param_latency_other(c, p, ¶ms[6], &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[7]; + uint8_t buffer[4096]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + param_enum_format(c, p, ¶ms[0], &b); + param_format(c, p, ¶ms[1], &b); + param_buffers(c, p, ¶ms[2], &b); + param_io(c, p, ¶ms[3], &b); + param_io_async(c, p, ¶ms[4], &b); + param_latency(c, p, ¶ms[5], &b); + param_latency_other(c, p, ¶ms[6], &b); + + pw_log_debug("port %s: update", p->object->port.name); + + p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + p->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + + 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; +} + +static void port_check_latency(struct port *p, const struct spa_latency_info *latency) +{ + struct spa_latency_info *current; + struct client *c = p->client; + struct object *o = p->object; + + current = &o->port.latency[latency->direction]; + if (spa_latency_info_compare(current, latency) == 0) + return; + *current = *latency; + + 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); + port_update_latency(p); +} + +/* 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; + union pw_map_item *item; + enum spa_direction direction; + struct port *p; + + if (mode == JackPlaybackLatency) + direction = SPA_DIRECTION_INPUT; + else + direction = SPA_DIRECTION_OUTPUT; + + default_latency(c, direction, &latency); + + pw_array_for_each(item, &c->ports[direction].items) { + if (pw_map_item_is_free(item)) + continue; + p = item->data; + port_check_latency(p, &latency); + } +} + +/* 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) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(p->direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (info.direction == p->direction) + return 0; + + 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 == SPA_DIRECTION_INPUT) + mode = JackPlaybackLatency; + else + mode = JackCaptureLatency; + + if (c->latency_callback) + queue_notify(c, NOTIFY_TYPE_LATENCY, NULL, mode, NULL); + 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 void midi_init_buffer(void *data, uint32_t max_frames, uint32_t nframes) +{ + struct midi_buffer *mb = data; + mb->magic = MIDI_BUFFER_MAGIC; + mb->buffer_size = max_frames * sizeof(float); + mb->nframes = nframes; + mb->write_pos = 0; + mb->event_count = 0; + mb->lost_events = 0; +} + +static inline void *init_buffer(struct port *p, uint32_t nframes) +{ + struct client *c = p->client; + void *data = p->emptyptr; + if (p->zeroed) + return data; + + if (TYPE_ID_IS_EVENT(p->object->port.type_id)) { + struct midi_buffer *mb = data; + midi_init_buffer(data, c->max_frames, nframes); + pw_log_debug("port %p: init midi buffer size:%d frames:%d", p, + mb->buffer_size, nframes); + } else + memset(data, 0, c->max_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 = find_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); + res = -ENOSPC; + goto done; + } + + fl = PW_MEMMAP_FLAG_READ; + /* Make the buffer writable when output. Some apps write to the input buffer + * so we want to make them writable as well if the option is selected. + * We can't use a PRIVATE mapping here because then we might not see changes + * in the buffer by other apps (see mmap man page). */ + if (direction == SPA_DIRECTION_OUTPUT || + (p->object->port.type_id != TYPE_ID_VIDEO && c->writable_input)) + fl |= PW_MEMMAP_FLAG_WRITE; + + /* 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) + queue_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_errorf((struct pw_proxy*)c->node, res, + "port_use_buffers(%u:%u:%u): %s", direction, port_id, + mix_id, 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 = find_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; + size = 0; + } 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: %m", 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: + case SPA_IO_AsyncBuffers: + mix_set_io(mix, ptr, size); + queue_memmap_free(c, old); + old = NULL; + break; + default: + break; + } + +exit_free: + pw_memmap_free(old); +exit: + if (res < 0) + pw_proxy_errorf((struct pw_proxy*)c->node, res, + "port_set_io(%u:%u:%u %u): %s", direction, port_id, + mix_id, id, spa_strerror(res)); + return res; +} + +static int +do_add_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", link); + spa_list_append(&c->rt.target_links, &link->target_link); + return 0; +} + +static int +do_remove_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", link); + spa_list_remove(&link->target_link); + + if (c->rt.prepared) { + int old_state = SPA_ATOMIC_LOAD(c->activation->status); + uint64_t trigger = 0; + if (old_state != PW_NODE_ACTIVATION_FINISHED) + trigger = get_time_ns(c->l->system); + deactivate_link(c, link, trigger); + } + return 0; +} + +static int +do_free_link(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + struct link *l = *((struct link **)data); + free_link(l); + pw_core_set_paused(c->core, false); + return 0; +} + +static int +do_queue_free_link(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + pw_loop_invoke(c->context.l, do_free_link, 0, data, size, false, c); + return 0; +} + +static void queue_free_link(struct client *c, struct link *l) +{ + pw_core_set_paused(c->core, true); + pw_data_loop_invoke(c->loop, + do_queue_free_link, SPA_ID_INVALID, &l, sizeof(&l), false, c); +} + +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 (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: %m", c, mem_id); + res = -EINVAL; + goto exit; + } + ptr = mm->ptr; + } + + if (c->node_id == node_id) { + pw_log_debug("%p: our activation %u: %u %u %u %p", c, node_id, + mem_id, offset, size, ptr); + } else { + 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; + link->trigger = link->activation->server_version < 1 ? trigger_link_v0 : trigger_link_v1; + spa_list_append(&c->links, &link->link); + + pw_data_loop_invoke(c->loop, + do_add_link, SPA_ID_INVALID, NULL, 0, false, link); + } + else { + link = find_activation(&c->links, node_id); + if (link == NULL) { + res = -EINVAL; + goto exit; + } + spa_list_remove(&link->link); + + pw_data_loop_invoke(c->loop, + do_remove_link, SPA_ID_INVALID, NULL, 0, false, link); + queue_free_link(c, link); + } + + if (c->driver_id == node_id) + update_driver_activation(c); + + exit: + if (res < 0) + pw_proxy_errorf((struct pw_proxy*)c->node, res, + "set_activation(%u): %s", node_id, 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; + int res = 0; + + if (p == NULL || !p->valid) { + res = peer_id == SPA_ID_INVALID ? 0 : -EINVAL; + goto exit; + } + + mix = find_mix(c, p, mix_id); + + pw_log_debug("%p: port %p mix:%d peer_id:%u info:%p", c, p, mix_id, + peer_id, props); + + if (peer_id == SPA_ID_INVALID) { + if (mix == NULL) { + res = -ENOENT; + goto exit; + } + free_mix(c, mix); + } else { + if (mix != NULL) { + res = -EEXIST; + goto exit; + } + mix = create_mix(c, p, mix_id, peer_id); + } +exit: + if (res < 0) + pw_proxy_errorf((struct pw_proxy*)c->node, res, + "set_mix_info(%u:%u:%u %u): %s", direction, port_id, + mix_id, peer_id, spa_strerror(res)); + return res; +} + +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_dict_item *items; + struct spa_dict copy; + char creator_ptr[64]; + + pw_log_info("create thread"); + if (globals.creator != NULL) { + uint32_t i, n_items = props ? props->n_items : 0; + + items = alloca((n_items + 1) * sizeof(*items)); + + for (i = 0; i < n_items; i++) + items[i] = props->items[i]; + + snprintf(creator_ptr, sizeof(creator_ptr), "pointer:%p", globals.creator); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_THREAD_CREATOR, + creator_ptr); + + copy = SPA_DICT_INIT(items, n_items); + props = © + } + return spa_thread_utils_create(c->context.old_thread_utils, props, start, arg); +} + +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_VIDEO_TYPE, port_type)) + return TYPE_ID_VIDEO; + else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type)) + return TYPE_ID_MIDI; + else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type)) + return TYPE_ID_OSC; + else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type)) + return TYPE_ID_UMP; + else if (spa_streq("other", port_type)) + return TYPE_ID_OTHER; + else + 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_VIDEO: + return JACK_DEFAULT_VIDEO_TYPE; + case TYPE_ID_MIDI: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + /* all returned as MIDI */ + return JACK_DEFAULT_MIDI_TYPE; + case TYPE_ID_OTHER: + return "other"; + default: + return NULL; + } +} + +static const char* type_to_format_dsp(jack_port_type_id_t type_id) +{ + switch(type_id) { + case TYPE_ID_AUDIO: + return JACK_DEFAULT_AUDIO_TYPE; + case TYPE_ID_VIDEO: + return JACK_DEFAULT_VIDEO_TYPE; + case TYPE_ID_OSC: + return JACK_DEFAULT_OSC_TYPE; + case TYPE_ID_MIDI: + return JACK_DEFAULT_MIDI_TYPE; + case TYPE_ID_UMP: + return JACK_DEFAULT_UMP_TYPE; + default: + return NULL; + } +} + +static bool type_is_dsp(jack_port_type_id_t type_id) +{ + switch(type_id) { + case TYPE_ID_AUDIO: + case TYPE_ID_MIDI: + case TYPE_ID_VIDEO: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + return true; + default: + return false; + } +} + +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 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 (spa_json_str_object_find(value, strlen(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 (spa_json_str_object_find(value, strlen(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 bool node_is_active(struct client *c, struct object *n) +{ + return !n->node.is_jack || + (c->node_id == n->id ? c->active : n->node.is_running); +} + +static void node_info(void *data, const struct pw_node_info *info) +{ + struct object *n = data; + struct client *c = n->client; + bool active; + + if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { + /* JACK clients always need ALWAYS_PROCESS=true or else they don't + * conform to the JACK API. We would try to hide the ports of + * PAUSED JACK clients, for example, even if they are active. */ + const char *str = spa_dict_lookup(info->props, PW_KEY_NODE_ALWAYS_PROCESS); + n->node.is_jack = str ? spa_atob(str) : false; + } + + n->node.is_running = info->state == PW_NODE_STATE_RUNNING; + active = node_is_active(c, n); + + pw_log_debug("DSP node %d %08"PRIx64" jack:%u state change %s running:%d", info->id, + info->change_mask, n->node.is_jack, + pw_node_state_as_string(info->state), n->node.is_running); + + if (info->change_mask & PW_NODE_CHANGE_MASK_STATE) { + struct object *p, *l; + spa_list_for_each(p, &c->context.objects, link) { + if (p->type != INTERFACE_Port || p->removed || + p->port.node_id != info->id) + continue; + if (active) + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, p, 1, NULL); + else { + spa_list_for_each(l, &c->context.objects, link) { + if (l->type != INTERFACE_Link || l->removed || + (l->port_link.src_serial != p->serial && + l->port_link.dst_serial != p->serial)) + continue; + queue_notify(c, NOTIFY_TYPE_CONNECT, l, 0, NULL); + } + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, p, 0, NULL); + } + } + } +} + +static const struct pw_node_events node_events = { + PW_VERSION_NODE_EVENTS, + .info = node_info, +}; + +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_EVENTS, + .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 do_emit = true, do_sync = false; + uint32_t serial; + const char *app; + + 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_Client)) { + app = spa_dict_lookup(props, PW_KEY_APP_NAME); + + if ((str = spa_dict_lookup(props, PW_KEY_SEC_PID)) != NULL) { + pw_log_debug("%p: pid of \"%s\" is \"%s\"", c, app, str); + } else { + pw_log_debug("%p: pid of \"%s\" is unknown", c, app); + } + + o = alloc_object(c, INTERFACE_Client); + if (o == NULL) + goto exit; + + o->pwclient.pid = (int32_t)atoi(str); + snprintf(o->pwclient.name, sizeof(o->pwclient.name), "%s", app); + + pw_log_debug("%p: add pw client %d (%s) pid %llu", c, id, app, (unsigned long long)o->pwclient.pid); + + pthread_mutex_lock(&c->context.lock); + spa_list_append(&c->context.objects, &o->link); + pthread_mutex_unlock(&c->context.lock); + } + else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { + const char *node_name; + char tmp[JACK_CLIENT_NAME_SIZE+1]; + + o = alloc_object(c, INTERFACE_Node); + if (o == NULL) + goto exit; + + 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); + + 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 { + do_emit = ot == NULL; + snprintf(o->node.name, sizeof(o->node.name), "%s", tmp); + } + if (id == c->node_id) { + pw_log_debug("%p: add our node %d", c, id); + snprintf(c->name, sizeof(c->name), "%s", o->node.name); + c->object = o; + c->serial = serial; + } + + if ((str = spa_dict_lookup(props, PW_KEY_PRIORITY_SESSION)) != NULL) + o->node.priority = pw_properties_parse_int(str); + if ((str = spa_dict_lookup(props, PW_KEY_CLIENT_API)) != NULL) + o->node.is_jack = spa_streq(str, "jack"); + + pw_log_debug("%p: add node %d", c, id); + + if (o->node.is_jack) { + o->proxy = pw_registry_bind(c->registry, + id, type, PW_VERSION_NODE, 0); + if (o->proxy) { + pw_proxy_add_listener(o->proxy, + &o->proxy_listener, &proxy_events, o); + pw_proxy_add_object_listener(o->proxy, + &o->object_listener, &node_events, o); + do_sync = true; + } + } + 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; + + if (type_id == TYPE_ID_UMP && c->flag_midi2) + flags |= JackPortIsMIDI2; + + spa_dict_for_each(item, props) { + if (spa_streq(item->key, PW_KEY_PORT_DIRECTION)) { + if (spa_streq(item->value, "in")) + 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; + if (TYPE_ID_IS_EVENT(type_id) && !c->show_midi) + 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); + + do_emit = node_is_active(c, ot); + + 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); + + if (type_is_dsp(type_id)) + pw_port_subscribe_params((struct pw_port*)o->proxy, + ids, 1); + do_sync = true; + } + pthread_mutex_lock(&c->context.lock); + 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); + + o->port.type_id = type_id; + } + + if (c->fill_aliases) { + 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.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); + if (o == NULL) + goto exit; + + 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; + + if (o->port_link.our_input != NULL && + o->port_link.our_output != NULL) { + struct mix *mix; + mix = find_port_peer(o->port_link.our_output, o->port_link.dst); + if (mix != NULL) + mix->peer_port = o->port_link.our_input; + mix = find_port_peer(o->port_link.our_input, o->port_link.src); + if (mix != NULL) + mix->peer_port = o->port_link.our_output; + } + 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(c->metadata->proxy, + &c->metadata->listener, + &metadata_events, c); + do_sync = true; + } 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); + do_sync = true; + } + goto exit; + } + else { + goto exit; + } + + o->id = id; + o->serial = serial; + + switch (o->type) { + case INTERFACE_Node: + pw_log_info("%p: client added \"%s\" emit:%d", c, o->node.name, do_emit); + if (do_emit) + queue_notify(c, NOTIFY_TYPE_REGISTRATION, o, 1, NULL); + break; + + case INTERFACE_Port: + pw_log_info("%p: port added %u/%u \"%s\" emit:%d", c, o->id, + o->serial, o->port.name, do_emit); + if (do_emit) + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, o, 1, NULL); + break; + + case INTERFACE_Link: + pw_log_info("%p: link %u %u/%u -> %u/%u added", c, + o->id, o->port_link.src, o->port_link.src_serial, + o->port_link.dst, o->port_link.dst_serial); + if (do_emit) + queue_notify(c, NOTIFY_TYPE_CONNECT, o, 1, NULL); + break; + } + + exit: + if (do_sync) + c->pending_sync = pw_proxy_sync((struct pw_proxy*)c->core, + c->pending_sync); + 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; + + 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_Client: + free_object(c, o); + break; + 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); + queue_notify(c, NOTIFY_TYPE_REGISTRATION, o, 0, NULL); + } else { + free_object(c, o); + } + break; + case INTERFACE_Port: + pw_log_info("%p: port %u/%u removed \"%s\"", c, o->id, o->serial, o->port.name); + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, o, 0, NULL); + break; + case INTERFACE_Link: + if (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); + queue_notify(c, NOTIFY_TYPE_CONNECT, o, 0, NULL); + } else { + pw_log_warn("unlink between unknown ports %d and %d", + o->port_link.src, o->port_link.dst); + free_object(c, o); + } + break; + } + + 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; +} + +static struct client * g_first_client; + +SPA_EXPORT +jack_client_t * jack_client_open (const char *client_name, + jack_options_t options, + jack_status_t *status_ptr, ...) +{ + struct client *client; + const struct spa_support *support; + uint32_t n_support; + const char *str; + struct spa_cpu *cpu_iface; + const struct pw_properties *props; + va_list ap; + jack_status_t status; + if (getenv("PIPEWIRE_NOJACK") != NULL || + getenv("PIPEWIRE_INTERNAL") != NULL || + spa_strstartswith(pw_get_library_version(), "0.2")) + goto disabled; + + 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_ptr); + 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( + PW_KEY_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); + if (client->context.loop == NULL) + goto no_props; + 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->context.notify = pw_thread_loop_new(client->name, NULL); + if (client->context.notify == NULL) + goto no_props; + client->context.nl = pw_thread_loop_get_loop(client->context.notify); + + globals.max_frames = client->max_frames = client->context.context->settings.clock_quantum_limit; + + client->notify_source = pw_loop_add_event(client->context.nl, + on_notify_event, client); + client->notify_buffer = calloc(1, NOTIFY_BUFFER_SIZE + sizeof(struct notify)); + spa_ringbuffer_init(&client->notify_ring); + + pw_context_conf_update_props(client->context.context, + "jack.properties", client->props); + + props = pw_context_get_properties(client->context.context); + + client->allow_mlock = pw_properties_get_bool(props, "mem.allow-mlock", true); + client->warn_mlock = pw_properties_get_bool(props, "mem.warn-mlock", false); + + pw_context_conf_section_match_rules(client->context.context, "jack.rules", + &props->dict, execute_match, client); + + support = pw_context_get_support(client->context.context, &n_support); + + client->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) + client->mix_function = mix_sse; +#endif + client->max_align = spa_cpu_get_max_align(cpu_iface); + } else { + client->max_align = MAX_ALIGN; + } + 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 = pw_context_get_data_loop(client->context.context); + client->l = pw_data_loop_get_loop(client->loop); + 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, + ®istry_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_FORCE_RATE, + "%u", q.denom); + pw_properties_setf(client->props, PW_KEY_NODE_FORCE_QUANTUM, + "%u", q.num); + } 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, "jack.passive-links", 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, "group.dsp.0"); + 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->show_midi = pw_properties_get_bool(client->props, "jack.show-midi", 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->passive_links = pw_properties_get_bool(client->props, "jack.passive-links", 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->global_sample_rate = pw_properties_get_bool(client->props, "jack.global-sample-rate", false); + client->max_ports = pw_properties_get_uint32(client->props, "jack.max-client-ports", MAX_CLIENT_PORTS); + client->fill_aliases = pw_properties_get_bool(client->props, "jack.fill-aliases", false); + client->writable_input = pw_properties_get_bool(client->props, "jack.writable-input", true); + client->async = pw_properties_get_bool(client->props, PW_KEY_NODE_ASYNC, false); + client->flag_midi2 = pw_properties_get_bool(client->props, "jack.flag-midi2", false); + + client->self_connect_mode = SELF_CONNECT_ALLOW; + if ((str = pw_properties_get(client->props, "jack.self-connect-mode")) != NULL) { + 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->other_connect_mode = OTHER_CONNECT_ALLOW; + if ((str = pw_properties_get(client->props, "jack.other-connect-mode")) != NULL) { + if (spa_streq(str, "fail")) + client->other_connect_mode = OTHER_CONNECT_FAIL; + else if (spa_streq(str, "ignore")) + client->other_connect_mode = OTHER_CONNECT_IGNORE; + } + + client->rt_max = pw_properties_get_int32(client->props, "rt.prio", DEFAULT_RT_MAX); + + status = 0; + if (status_ptr) + *status_ptr = status; + + client->pending_sync = pw_proxy_sync((struct pw_proxy*)client->core, client->pending_sync); + + while (true) { + pw_thread_loop_wait(client->context.loop); + + if (client->last_res < 0) + goto init_failed; + + if (client->pending_sync == client->last_sync) + break; + } + + if (!spa_streq(client->name, client_name)) { + status |= JackNameNotUnique; + if (status_ptr) + *status_ptr = status; + if (options & JackUseExactName) + goto exit_unlock; + } + pw_thread_loop_unlock(client->context.loop); + + if (g_first_client == NULL) + g_first_client = client; + + pw_thread_loop_start(client->context.notify); + + pw_log_info("%p: opened", client); + return (jack_client_t *)client; + +no_props: + status = JackFailure | JackInitFailure; + if (status_ptr) + *status_ptr = status; + goto exit; +init_failed: + status = JackFailure | JackInitFailure; + if (status_ptr) + *status_ptr = status; + goto exit_unlock; +server_failed: + status = JackFailure | JackServerFailed; + if (status_ptr) + *status_ptr = status; + goto exit_unlock; +exit_unlock: + pw_thread_loop_unlock(client->context.loop); +exit: + pw_log_info("%p: error %d", client, status); + jack_client_close((jack_client_t *) client); + return NULL; +disabled: + pw_log_warn("JACK is disabled"); + status = JackFailure | JackInitFailure; + if (status_ptr) + *status_ptr = status; + 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; + union pw_map_item *item; + struct mix *m, *tm; + struct port *p, *tp; + int res; + + return_val_if_fail(c != NULL, -EINVAL); + + pw_log_info("%p: close", client); + + if (g_first_client == c) + g_first_client = NULL; + + c->destroyed = true; + + res = jack_deactivate(client); + + clean_transport(c); + + if (c->context.loop) { + pw_loop_invoke(c->context.l, NULL, 0, NULL, 0, false, c); + pw_thread_loop_stop(c->context.loop); + } + if (c->context.notify) { + queue_notify(c, NOTIFY_TYPE_REGISTRATION, c->object, 0, NULL); + pw_loop_invoke(c->context.nl, NULL, 0, NULL, 0, false, c); + pw_thread_loop_stop(c->context.notify); + } + + 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); + } + + globals.thread_utils = pw_thread_utils_get(); + + if (c->context.context) + pw_context_destroy(c->context.context); + + if (c->notify_source) + pw_loop_destroy_source(c->context.nl, c->notify_source); + free(c->notify_buffer); + + if (c->context.loop) + pw_thread_loop_destroy(c->context.loop); + if (c->context.notify) + pw_thread_loop_destroy(c->context.notify); + + pw_log_debug("%p: free", client); + + pw_array_for_each(item, &c->ports[SPA_DIRECTION_OUTPUT].items) { + if (pw_map_item_is_free(item)) + continue; + free_port(c, item->data, false); + } + pw_array_for_each(item, &c->ports[SPA_DIRECTION_INPUT].items) { + if (pw_map_item_is_free(item)) + continue; + free_port(c, item->data, false); + } + pthread_mutex_lock(&globals.lock); + spa_list_consume(o, &c->context.objects, link) { + bool to_free = o->to_free; + spa_list_remove(&o->link); + memset(o, 0, sizeof(struct object)); + o->to_free = to_free; + spa_list_append(&globals.free_objects, &o->link); + } + pthread_mutex_unlock(&globals.lock); + + spa_list_for_each_safe(m, tm, &c->free_mix, link) { + if (!m->to_free) + spa_list_remove(&m->link); + } + spa_list_consume(m, &c->free_mix, link) { + spa_list_remove(&m->link); + free(m); + } + spa_list_for_each_safe(p, tp, &c->free_ports, link) { + if (!p->to_free) + spa_list_remove(&p->link); + } + spa_list_consume(p, &c->free_ports, link) { + spa_list_remove(&p->link); + free(p); + } + 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; + 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; + 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; + 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; + 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; + 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; + + return_val_if_fail(c != NULL, NULL); + 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; + + return_val_if_fail(c != NULL, NULL); + 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; +} + +static int +do_emit_buffer_size(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct client *c = user_data; + c->buffer_frames = c->rt.position->clock.duration; + pw_log_debug("%p: emit buffersize %d", c, c->buffer_frames); + c->bufsize_callback(c->buffer_frames, c->bufsize_arg); + return 0; +} + +SPA_EXPORT +int jack_activate (jack_client_t *client) +{ + struct client *c = (struct client *) client; + struct object *o; + int res = 0; + + 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); + freeze_callbacks(c); + + /* reemit buffer_frames */ + c->buffer_frames = 0; + + pw_data_loop_start(c->loop); + c->active = true; + + if ((res = do_activate(c)) < 0) + goto done; + + c->activation->pending_new_pos = true; + c->activation->pending_sync = true; + + spa_list_for_each(o, &c->context.objects, link) { + if (o->type != INTERFACE_Port || o->port.port == NULL || + o->port.port->client != c || !o->port.port->valid) + continue; + o->registered = 0; + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, o, 1, NULL); + } +done: + if (res < 0) { + c->active = false; + pw_data_loop_stop(c->loop); + } else if (SPA_LIKELY(c->bufsize_callback != NULL)) { + pw_thread_loop_unlock(c->context.loop); + pw_data_loop_invoke(c->loop, + do_emit_buffer_size, SPA_ID_INVALID, NULL, 0, true, c); + pw_thread_loop_lock(c->context.loop); + } + pw_log_debug("%p: activate result:%d", c, res); + thaw_callbacks(c); + pw_thread_loop_unlock(c->context.loop); + + return res; +} + +SPA_EXPORT +int jack_deactivate (jack_client_t *client) +{ + struct object *o; + struct client *c = (struct client *) client; + int res; + + 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); + freeze_callbacks(c); + + pw_data_loop_stop(c->loop); + + pw_client_node_set_active(c->node, false); + + spa_list_for_each(o, &c->context.objects, link) { + if (o->type != INTERFACE_Link || o->removed) + continue; + if (o->port_link.src_ours || o->port_link.dst_ours) + pw_registry_destroy(c->registry, o->id); + } + + spa_list_for_each(o, &c->context.objects, link) { + if (o->type != INTERFACE_Port || o->port.port == NULL || + o->port.port->client != c || !o->port.port->valid) + continue; + queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, o, 0, NULL); + } + c->activation->pending_new_pos = false; + c->activation->pending_sync = false; + + c->active = false; + + res = do_sync(c); + + thaw_callbacks(c); + pw_thread_loop_unlock(c->context.loop); + + return res; +} + +SPA_EXPORT +int jack_get_client_pid (const char *name) +{ + struct object *on, *oc; + + if (g_first_client == NULL) return 0; + + on = find_node(g_first_client, name); + if (on == NULL) { + pw_log_warn("unknown (jack-client) node \"%s\"", name); + return 0; + } + + oc = find_client(g_first_client, on->node.client_id); + if (oc == NULL) { + pw_log_warn("unknown (pw) client %d", (int)on->node.client_id); + return 0; + } + + pw_log_info("pid %d (%s)", (int)oc->pwclient.pid, oc->pwclient.name); + + return (int)oc->pwclient.pid; +} + +SPA_EXPORT +jack_native_thread_t jack_client_thread_id (jack_client_t *client) +{ + struct client *c = (struct client *) client; + + 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) +{ + struct client *c = (struct client *) client; + return_val_if_fail(c != NULL, 0); + return !c->freewheeling; +} + +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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + const char *str; + + pw_log_info("%p: freewheel %d", client, onoff); + + pw_thread_loop_lock(c->context.loop); + str = pw_properties_get(c->props, PW_KEY_NODE_GROUP); + if (str != NULL) { + char *p = strstr(str, ",pipewire.freewheel"); + if (p == NULL) + p = strstr(str, "pipewire.freewheel"); + if (p == NULL && onoff) + pw_properties_setf(c->props, PW_KEY_NODE_GROUP, + "%s,pipewire.freewheel", str); + else if (p != NULL && !onoff) { + pw_log_info("%s %d %s %.*s", p, (int)(p - str), + str, (int)(p - str), str); + pw_properties_setf(c->props, PW_KEY_NODE_GROUP, + "%.*s", (int)(p - str), str); + } + } else { + pw_properties_set(c->props, PW_KEY_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; + + 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 +int jack_set_sample_rate (jack_client_t *client, jack_nframes_t nframes) +{ + struct client *c = (struct client *) client; + + return_val_if_fail(c != NULL, -EINVAL); + + pw_log_info("%p: sample-size %u", client, nframes); + + pw_thread_loop_lock(c->context.loop); + if (c->global_sample_rate && c->settings && c->settings->proxy) { + char val[256]; + snprintf(val, sizeof(val), "%u", nframes); + pw_metadata_set_property(c->settings->proxy, 0, + "clock.force-rate", "", val); + } else { + pw_properties_setf(c->props, PW_KEY_NODE_FORCE_RATE, "%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; + + 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; + } + } + c->sample_rate = res; + pw_log_trace_fp("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; + + 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; + + 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[7]; + uint32_t n_params = 0; + struct port *p; + int res, len; + char name[REAL_JACK_PORT_NAME_SIZE+1]; + + return_val_if_fail(c != NULL, NULL); + return_val_if_fail(port_name != NULL && strlen(port_name) != 0, NULL); + 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 (type_id == TYPE_ID_MIDI && (flags & JackPortIsMIDI2)) + type_id = TYPE_ID_UMP; + + len = snprintf(name, sizeof(name), "%s:%s", c->name, port_name); + if (len < 0 || (size_t)len >= sizeof(name)) { + pw_log_warn("%p: name \"%s:%s\" too long", c, + c->name, port_name); + return NULL; + } + pthread_mutex_lock(&c->context.lock); + o = find_port_by_name(c, name); + pthread_mutex_unlock(&c->context.lock); + if (o != NULL) { + pw_log_warn("%p: name \"%s\" already exists", c, name); + 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; + strcpy(o->port.name, name); + o->port.type_id = type_id; + + init_buffer(p, c->max_frames); + + 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: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + 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: + case TYPE_ID_OSC: + case TYPE_ID_UMP: + 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, type_to_format_dsp(type_id)); + pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name); + if (flags > 0x1f) { + pw_properties_setf(p->props, PW_KEY_PORT_EXTRA, + "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, ¶ms[n_params++], &b); + param_buffers(c, p, ¶ms[n_params++], &b); + param_io(c, p, ¶ms[n_params++], &b); + param_io_async(c, p, ¶ms[n_params++], &b); + param_latency(c, p, ¶ms[n_params++], &b); + param_latency_other(c, p, ¶ms[n_params++], &b); + + pw_thread_loop_lock(c->context.loop); + if (create_mix(c, p, SPA_ID_INVALID, SPA_ID_INVALID) == NULL) { + res = -errno; + pw_log_warn("can't create mix for port %s: %m", port_name); + pw_thread_loop_unlock(c->context.loop); + goto error_free; + } + + freeze_callbacks(c); + + 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); + + thaw_callbacks(c); + pw_log_debug("%p: port %p done", c, p); + pw_thread_loop_unlock(c->context.loop); + + if (res < 0) { + pw_log_warn("can't create port %s: %s", port_name, + spa_strerror(res)); + goto error_free; + } + + return object_to_port(o); + +error_free: + free_port(c, p, true); + return NULL; +} + +static int +do_free_port(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct port *p = user_data; + struct client *c = p->client; + free_port(c, p, !c->active); + return 0; +} + +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; + struct client *c = p->client; + p->valid = false; + pw_loop_invoke(c->context.l, do_free_port, 0, NULL, 0, false, p); + 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 = port_to_object(port); + struct port *p; + int res; + + return_val_if_fail(c != NULL, -EINVAL); + return_val_if_fail(o != NULL, -EINVAL); + + pw_thread_loop_lock(c->context.loop); + freeze_callbacks(c); + + 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, false, 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)); + } +done: + thaw_callbacks(c); + pw_thread_loop_unlock(c->context.loop); + + return res; +} + +static struct buffer *get_mix_buffer(struct client *c, struct mix *mix, jack_nframes_t frames) +{ + struct spa_io_buffers *io; + uint32_t cycle = c->rt.position->clock.cycle & 1; + + if (mix->peer_port != NULL) + prepare_output(mix->peer_port, frames, cycle); + + io = mix->io[cycle]; + if (io == NULL || + io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= mix->n_buffers) + return NULL; + + return &mix->buffers[io->buffer_id]; +} + +static inline void *get_buffer_data(struct buffer *b, jack_nframes_t frames) +{ + struct spa_data *d; + uint32_t offset, size; + + 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); +} + +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; + struct client *c = p->client; + + spa_list_for_each(mix, &p->mix, port_link) { + if (mix->id == SPA_ID_INVALID) + continue; + + pw_log_trace_fp("%p: port %s mix %d.%d get buffer %d", + c, p->object->port.name, p->port_id, mix->id, frames); + + if ((b = get_mix_buffer(c, mix, frames)) == NULL) + continue; + + if ((np = get_buffer_data(b, frames)) == NULL) + continue; + + 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; + c->mix_function(ptr, mix_ptr, n_ptr, ptr_aligned, frames); + p->zeroed = false; + } + if (ptr == NULL) + ptr = init_buffer(p, frames); + return ptr; +} + +static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames) +{ + struct mix *mix; + void *ptr = p->emptyptr; + struct midi_buffer *mb = (struct midi_buffer*)midi_scratch; + struct spa_pod_sequence *seq[MAX_MIX]; + uint32_t n_seq = 0; + + spa_list_for_each(mix, &p->mix, port_link) { + struct spa_data *d; + struct buffer *b; + void *pod; + + if (mix->id == SPA_ID_INVALID) + continue; + + 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(p->client, 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; + } + midi_init_buffer(mb, MIDI_SCRATCH_FRAMES, frames); + /* first convert to a thread local scratch buffer, then memcpy into + * the per port buffer. This makes it possible to call this function concurrently + * but also have different pointers per port */ + convert_to_event(seq, n_seq, mb, p->client->fix_midi_events, p->object->port.type_id); + memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count + * sizeof(struct midi_event))); + if (mb->write_pos) { + size_t offs = mb->buffer_size - 1 - mb->write_pos; + memcpy(SPA_PTROFF(ptr, offs, void), SPA_PTROFF(mb, offs, void), mb->write_pos); + } + 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, frames); +} + +SPA_EXPORT +void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames) +{ + struct object *o = port_to_object(port); + struct port *p = NULL; + void *ptr = NULL; + struct client *c; + + return_val_if_fail(o != NULL, NULL); + + c = o->client; + if (o->type != INTERFACE_Port || c == NULL) + goto done; + + if (frames > c->max_frames) + goto done; + + if ((p = o->port.port) == NULL) { + struct mix *mix; + struct buffer *b; + + if ((mix = find_mix_peer(c, o->id)) == NULL) + goto done; + + pw_log_trace("peer mix: %p %d", mix, mix->peer_id); + + if ((b = get_mix_buffer(c, mix, frames)) == NULL) + goto done; + + if (TYPE_ID_IS_EVENT(o->port.type_id)) { + struct spa_pod_sequence *seq[1]; + struct spa_data *d; + void *pod; + + ptr = midi_scratch; + midi_init_buffer(ptr, MIDI_SCRATCH_FRAMES, frames); + + d = &b->datas[0]; + if ((pod = spa_pod_from_data(d->data, d->maxsize, + d->chunk->offset, d->chunk->size)) == NULL) + goto done; + if (!spa_pod_is_sequence(pod)) + goto done; + seq[0] = pod; + convert_to_event(seq, 1, ptr, c->fix_midi_events, o->port.type_id); + } else { + ptr = get_buffer_data(b, frames); + } + } else if (p->valid) { + ptr = p->get_buffer(p, frames); + } +done: + pw_log_trace_fp("%p: port:%p buffer:%p frames:%d", c, p, ptr, frames); + return ptr; +} + +SPA_EXPORT +jack_uuid_t jack_port_uuid (const jack_port_t *port) +{ + struct object *o = port_to_object(port); + 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 == NULL) + return NULL; + 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 = port_to_object(port); + return_val_if_fail(o != NULL, NULL); + if (o->type != INTERFACE_Port) + return NULL; + return port_name(o); +} + +SPA_EXPORT +const char * jack_port_short_name (const jack_port_t *port) +{ + struct object *o = port_to_object(port); + return_val_if_fail(o != NULL, NULL); + if (o->type != INTERFACE_Port) + return NULL; + return strchr(port_name(o), ':') + 1; +} + +SPA_EXPORT +int jack_port_flags (const jack_port_t *port) +{ + struct object *o = port_to_object(port); + return_val_if_fail(o != NULL, 0); + if (o->type != INTERFACE_Port) + return 0; + return o->port.flags; +} + +SPA_EXPORT +const char * jack_port_type (const jack_port_t *port) +{ + struct object *o = port_to_object(port); + return_val_if_fail(o != NULL, NULL); + if (o->type != INTERFACE_Port) + return NULL; + return type_to_string(o->port.type_id); +} + +SPA_EXPORT +jack_port_type_id_t jack_port_type_id (const jack_port_t *port) +{ + struct object *o = port_to_object(port); + return_val_if_fail(o != NULL, 0); + if (o->type != INTERFACE_Port) + return TYPE_ID_OTHER; + return o->port.type_id; +} + +SPA_EXPORT +int jack_port_is_mine (const jack_client_t *client, const jack_port_t *port) +{ + struct object *o = port_to_object(port); + 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 = port_to_object(port); + struct client *c; + struct object *l; + int res = 0; + + 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.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 = port_to_object(port); + struct client *c; + struct object *p, *l; + int res = 0; + + return_val_if_fail(o != NULL, 0); + 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) + 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 = port_to_object(port); + + 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 = port_to_object(port); + struct object *p, *l; + const char **res; + int count = 0; + struct pw_array tmp; + + return_val_if_fail(c != NULL, NULL); + 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) +{ + struct object *s = port_to_object(src); + struct object *d = port_to_object(dst); + struct port *sp, *dp; + + sp = s->port.port; + dp = d->port.port; + if (sp == NULL || !sp->valid || + dp == NULL || !dp->valid || + sp->client != dp->client) + return -EINVAL; + + dp->tied = sp; + return 0; +} + +SPA_EXPORT +int jack_port_untie (jack_port_t *port) +{ + struct object *o = port_to_object(port); + struct port *p; + + p = o->port.port; + if (p == NULL || !p->valid) + return -EINVAL; + p->tied = NULL; + return 0; +} + +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 = port_to_object(port); + struct port *p; + int res = 0; + + return_val_if_fail(c != NULL, -EINVAL); + return_val_if_fail(o != NULL, -EINVAL); + 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 = port_to_object(port); + struct client *c; + struct port *p; + const char *key; + int res = 0; + + return_val_if_fail(o != NULL, -EINVAL); + 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 = port_to_object(port); + struct client *c; + struct port *p; + const char *key; + int res = 0; + + return_val_if_fail(o != NULL, -EINVAL); + 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 = port_to_object(port); + int res = 0; + + return_val_if_fail(o != NULL, -EINVAL); + return_val_if_fail(aliases != NULL, -EINVAL); + return_val_if_fail(aliases[0] != NULL, -EINVAL); + 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 = port_to_object(port); + + 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; + + return_val_if_fail(c != NULL, -EINVAL); + 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(object_to_port(p), onoff); +} + +SPA_EXPORT +int jack_port_ensure_monitor (jack_port_t *port, int onoff) +{ + struct object *o = port_to_object(port); + + 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 = port_to_object(port); + 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; + + 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; + + pw_log_debug("sum %d %d", sum, c->self_connect_mode); + + /* check for other connection first */ + if (sum == 0) + return c->other_connect_mode; + + if (c->self_connect_mode == SELF_CONNECT_ALLOW) + 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]; + int res, link_res = 0; + + return_val_if_fail(c != NULL, EINVAL); + return_val_if_fail(source_port != NULL, EINVAL); + 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); + freeze_callbacks(c); + + 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) || + !TYPE_ID_IS_COMPATIBLE(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 (c->passive_links) + 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_log_debug("%p: connect %s %s done %d", client, source_port, destination_port, res); + thaw_callbacks(c); + 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; + + return_val_if_fail(c != NULL, -EINVAL); + return_val_if_fail(source_port != NULL, -EINVAL); + 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); + freeze_callbacks(c); + + 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: + thaw_callbacks(c); + 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 = port_to_object(port); + struct object *l; + int res; + + return_val_if_fail(c != NULL, -EINVAL); + return_val_if_fail(o != NULL, -EINVAL); + + pw_log_debug("%p: disconnect %p", client, port); + + pw_thread_loop_lock(c->context.loop); + freeze_callbacks(c); + + 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); + + thaw_callbacks(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) +{ + struct client *c = (struct client *) client; + + return_val_if_fail(client != NULL, 0); + 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 c->max_frames * sizeof(float); + else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type)) + return c->max_frames * sizeof(float); + else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type)) + return c->max_frames * sizeof(float); + else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type)) + return 320 * 240 * 4 * sizeof(float); + else + return 0; +} + +SPA_EXPORT +void jack_port_set_latency (jack_port_t *port, jack_nframes_t frames) +{ + struct object *o = port_to_object(port); + struct client *c; + jack_latency_range_t range = { frames, frames }; + + 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 = port_to_object(port); + struct client *c; + jack_nframes_t nframes, rate; + int direction; + struct spa_latency_info *info; + int64_t min, max; + + return_if_fail(o != NULL); + c = o->client; + + if (o->type != INTERFACE_Port || c == NULL) { + range->min = range->max = 0; + return; + } + + 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]; + + min = (int64_t)(info->min_quantum * nframes) + + info->min_rate + + (info->min_ns * (int64_t)rate) / (int64_t)SPA_NSEC_PER_SEC; + max = (int64_t)(info->max_quantum * nframes) + + info->max_rate + + (info->max_ns * (int64_t)rate) / (int64_t)SPA_NSEC_PER_SEC; + + range->min = SPA_MAX(min, 0); + range->max = SPA_MAX(max, 0); + + pw_log_debug("%p: %s get %d latency range %d %d", c, o->port.name, + mode, range->min, range->max); +} + +static int +do_port_check_latency(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct port *p = user_data; + const struct spa_latency_info *latency = data; + port_check_latency(p, latency); + 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 = port_to_object(port); + struct client *c; + enum spa_direction direction; + struct spa_latency_info latency; + jack_nframes_t nframes; + struct port *p; + + 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); + if (nframes == 0) + nframes = 1; + + latency.min_rate = range->min; + if (latency.min_rate >= (int32_t)nframes) { + latency.min_quantum = latency.min_rate / nframes; + latency.min_rate %= nframes; + } + + latency.max_rate = range->max; + if (latency.max_rate >= (int32_t)nframes) { + latency.max_quantum = latency.max_rate / nframes; + latency.max_rate %= nframes; + } + + if ((p = o->port.port) == NULL) + return; + + pw_loop_invoke(c->context.l, do_port_check_latency, 0, + &latency, sizeof(latency), false, p); +} + +SPA_EXPORT +int jack_recompute_total_latencies (jack_client_t *client) +{ + struct client *c = (struct client *) client; + return queue_notify(c, NOTIFY_TYPE_TOTAL_LATENCY, NULL, 0, NULL); +} + +static jack_nframes_t port_get_latency (jack_port_t *port) +{ + struct object *o = port_to_object(port); + jack_latency_range_t range = { 0, 0 }; + + 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; + + 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("can't 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("can't 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 || !o->visible) + 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 (TYPE_ID_IS_HIDDEN(o->port.type_id)) + 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; + + 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 object_to_port(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; + + 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 object_to_port(res); +} + +static inline void get_frame_times(struct client *c, struct frame_times *times) +{ + jack_unique_t u1; + uint32_t count = 0; + do { + u1 = c->jack_position.unique_1; + *times = c->jack_times; + if (++count == 10) { + pw_log_warn("could not get snapshot %" PRIu64 " %" PRIu64, u1, c->jack_position.unique_2); + break; + } + } while (u1 != c->jack_position.unique_2); +} + +SPA_EXPORT +jack_nframes_t jack_frames_since_cycle_start (const jack_client_t *client) +{ + struct client *c = (struct client *) client; + struct frame_times times; + int64_t diff; + + return_val_if_fail(c != NULL, 0); + + get_frame_times(c, ×); + diff = get_time_ns(c->l->system) - times.nsec; + return (jack_nframes_t) floor(((double)times.sample_rate * diff) / SPA_NSEC_PER_SEC); +} + +SPA_EXPORT +jack_nframes_t jack_frame_time (const jack_client_t *client) +{ + return jack_time_to_frames(client, jack_get_time()); +} + +SPA_EXPORT +jack_nframes_t jack_last_frame_time (const jack_client_t *client) +{ + struct client *c = (struct client *) client; + struct frame_times times; + + return_val_if_fail(c != NULL, 0); + + get_frame_times(c, ×); + + return times.frames; +} + +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 frame_times times; + + return_val_if_fail(c != NULL, -EINVAL); + + get_frame_times(c, ×); + + if (times.sample_rate == 0 || times.rate_diff == 0.0) + return -1; + + *current_frames = times.frames; + *next_usecs = times.next_nsec / SPA_NSEC_PER_USEC; + *period_usecs = (float)(times.buffer_frames * + SPA_USEC_PER_SEC / (times.sample_rate * times.rate_diff)); + *current_usecs = *next_usecs - (jack_time_t)*period_usecs; + + 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 frame_times times; + + return_val_if_fail(c != NULL, -EINVAL); + + get_frame_times(c, ×); + + if (times.buffer_frames == 0 || times.sample_rate == 0 || times.rate_diff == 0.0) + return 0; + + uint32_t nf = (uint32_t)times.frames; + uint64_t nw = times.next_nsec/SPA_NSEC_PER_USEC; + uint64_t dp = (uint64_t)(times.buffer_frames * + (float)SPA_USEC_PER_SEC / (times.sample_rate * times.rate_diff)); + uint64_t w = nw - dp; + int32_t df = frames - nf; + return w + (int64_t)rint((double) df * (double) dp / times.buffer_frames); +} + +SPA_EXPORT +jack_nframes_t jack_time_to_frames(const jack_client_t *client, jack_time_t usecs) +{ + struct client *c = (struct client *) client; + struct frame_times times; + + return_val_if_fail(c != NULL, -EINVAL); + + get_frame_times(c, ×); + + if (times.sample_rate == 0 || times.rate_diff == 0.0) + return 0; + + uint32_t nf = (uint32_t)times.frames; + uint64_t nw = times.next_nsec/SPA_NSEC_PER_USEC; + uint64_t dp = (uint64_t)(times.buffer_frames * + (float)SPA_USEC_PER_SEC / (times.sample_rate * times.rate_diff)); + uint64_t w = nw - dp; + int64_t du = usecs - w; + return nf + (int32_t)rint((double)du / (double)dp * times.buffer_frames); +} + +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; + + return_val_if_fail(c != NULL, -EINVAL); + + if ((a = c->driver_activation) == NULL) + return -EIO; + + if (!SPA_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; + + return_val_if_fail(c != NULL, -EINVAL); + + pw_thread_loop_lock(c->context.loop); + freeze_callbacks(c); + + c->sync_callback = sync_callback; + c->sync_arg = arg; + + if ((res = do_activate(c)) < 0) + goto done; + + c->activation->pending_sync = true; +done: + thaw_callbacks(c); + 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; + + 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; + + return_val_if_fail(c != NULL, -EINVAL); + return_val_if_fail(timebase_callback != NULL, -EINVAL); + + pw_thread_loop_lock(c->context.loop); + freeze_callbacks(c); + + 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: + thaw_callbacks(c); + 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; + jack_transport_state_t state; + jack_unique_t u1; + uint32_t count = 0; + + return_val_if_fail(c != NULL, JackTransportStopped); + + do { + u1 = c->jack_position.unique_1; + state = c->jack_state; + if (pos != NULL) + *pos = c->jack_position; + if (++count == 10) { + pw_log_warn("could not get snapshot %" PRIu64 " %" PRIu64, u1, c->jack_position.unique_2); + break; + } + } while (u1 != c->jack_position.unique_2); + + return state; +} + +SPA_EXPORT +jack_nframes_t jack_get_current_transport_frame (const jack_client_t *client) +{ + struct client *c = (struct client *) client; + jack_transport_state_t state; + jack_nframes_t res; + jack_position_t pos; + + return_val_if_fail(c != NULL, -EINVAL); + + state = jack_transport_query(client, &pos); + res = pos.frame; + + if (state == JackTransportRolling) { + float usecs = get_time_ns(c->l->system)/1000 - pos.usecs; + res += (jack_nframes_t)floor((((float) pos.frame_rate) / 1000000.0f) * usecs); + } + return res; +} + +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; + + 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; + SPA_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; + SPA_ATOMIC_STORE(a->command, command); +} + +static int transport_update(struct client* c, int active) +{ + pw_log_info("%p: transport %d", c, active); + + pw_thread_loop_lock(c->context.loop); + pw_properties_set(c->props, PW_KEY_NODE_SYNC, "true"); + pw_properties_set(c->props, PW_KEY_NODE_TRANSPORT, + active ? "true" : "false"); + + 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_properties_set(c->props, PW_KEY_NODE_TRANSPORT, NULL); + pw_thread_loop_unlock(c->context.loop); + + return 0; +} + +SPA_EXPORT +void jack_transport_start (jack_client_t *client) +{ + struct client *c = (struct client *) client; + return_if_fail(c != NULL); + if (c->activation->server_version < 1) + update_command(c, PW_NODE_ACTIVATION_COMMAND_START); + else + transport_update(c, true); +} + +SPA_EXPORT +void jack_transport_stop (jack_client_t *client) +{ + struct client *c = (struct client *) client; + return_if_fail(c != NULL); + if (c->activation->server_version < 1) + update_command(c, PW_NODE_ACTIVATION_COMMAND_STOP); + else + transport_update(c, false); +} + +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; + + 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; + + 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; + 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; + 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; + 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; + + 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); + return_val_if_fail(globals.thread_utils != NULL, -1); + 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); + return_val_if_fail(globals.thread_utils != NULL, -1); + 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; + + return_val_if_fail(client != NULL, -EINVAL); + return_val_if_fail(thread != NULL, -EINVAL); + 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; + + return_val_if_fail(client != NULL, -EINVAL); + + pw_log_debug("join thread %p", (void *) thread); + spa_thread_utils_join(&c->context.thread_utils, (struct spa_thread*)thread, &status); + pw_log_debug("stopped thread %p", (void *) 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; + + return_val_if_fail(client != NULL, -EINVAL); + + pw_log_debug("cancel thread %p", (void *) thread); + pthread_cancel(thread); + pw_log_debug("join thread %p", (void *) thread); + spa_thread_utils_join(&c->context.thread_utils, (struct spa_thread*)thread, &status); + pw_log_debug("stopped thread %p", (void *) 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); + if (mb == NULL || mb->magic != MIDI_BUFFER_MAGIC) + return -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; + if (mb == NULL || mb->magic != MIDI_BUFFER_MAGIC) + return; + mb->event_count = 0; + mb->write_pos = 0; + mb->lost_events = 0; +} + +SPA_EXPORT +void jack_midi_reset_buffer(void *port_buffer) +{ + midi_init_buffer(port_buffer, globals.max_frames, globals.max_frames); +} + +SPA_EXPORT +size_t jack_midi_max_event_size(void* port_buffer) +{ + struct midi_buffer *mb = port_buffer; + size_t buffer_size; + + if (mb == NULL || mb->magic != MIDI_BUFFER_MAGIC) + return 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; + } +} + +static inline int midi_buffer_check(void *port_buffer, jack_nframes_t time) +{ + struct midi_buffer *mb = port_buffer; + struct midi_event *events; + + if (SPA_UNLIKELY(mb == NULL)) { + pw_log_warn("port buffer is NULL"); + return -EINVAL; + } + if (SPA_UNLIKELY(mb->magic != MIDI_BUFFER_MAGIC)) { + pw_log_warn("port buffer is invalid"); + return -EINVAL; + } + if (SPA_UNLIKELY(time >= mb->nframes)) { + pw_log_warn("midi %p: time:%d frames:%d", port_buffer, time, mb->nframes); + return -EINVAL; + } + events = SPA_PTROFF(mb, sizeof(*mb), struct midi_event); + 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); + return -EINVAL; + } + return 0; +} + +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; + jack_midi_data_t *res; + + if (midi_buffer_check(port_buffer, time) < 0) + goto failed; + + res = midi_event_reserve(port_buffer, time, data_size); + if (res != NULL) + 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 *ptr; + int res; + + if ((res = midi_buffer_check(port_buffer, time)) < 0) + return res; + + if ((ptr = midi_event_reserve(port_buffer, time, data_size)) == NULL) + return -ENOBUFS; + + memcpy (ptr, data, data_size); + return 0; +} + +SPA_EXPORT +uint32_t jack_midi_get_lost_event_count(void *port_buffer) +{ + struct midi_buffer *mb = port_buffer; + if (mb == NULL || mb->magic != MIDI_BUFFER_MAGIC) + return 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; + + 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); +} +static void unreg(void) __attribute__ ((destructor)); +static void unreg(void) +{ + struct object *o, *to; + pthread_mutex_lock(&globals.lock); + spa_list_for_each_safe(o, to, &globals.free_objects, link) { + if (!o->to_free) + spa_list_remove(&o->link); + } + spa_list_consume(o, &globals.free_objects, link) { + spa_list_remove(&o->link); + free(o); + } + pthread_mutex_unlock(&globals.lock); + pw_deinit(); +} diff --git a/pipewire-jack/src/pw-jack.in b/pipewire-jack/src/pw-jack.in new file mode 100755 index 0000000..6c7e35a --- /dev/null +++ b/pipewire-jack/src/pw-jack.in @@ -0,0 +1,60 @@ +#!/bin/sh + +# This file is part of PipeWire. +# SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans +# SPDX-License-Identifier: MIT + +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 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 + +# shellcheck disable=SC2016 # ${LIB} is interpreted by ld.so, not the shell +@LIBJACK_PATH_ENABLE@LD_LIBRARY_PATH='@LIBJACK_PATH@'"${LD_LIBRARY_PATH+":$LD_LIBRARY_PATH"}" +@LIBJACK_PATH_ENABLE@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..ba1cc23 --- /dev/null +++ b/pipewire-jack/src/ringbuffer.c @@ -0,0 +1,282 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include + +#include + +#include + +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..33302bb --- /dev/null +++ b/pipewire-jack/src/statistics.c @@ -0,0 +1,46 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +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..2da11a9 --- /dev/null +++ b/pipewire-jack/src/uuid.c @@ -0,0 +1,91 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include + +#include + +#include + +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..d59dccf --- /dev/null +++ b/pipewire-v4l2/src/meson.build @@ -0,0 +1,42 @@ +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', + '-U_TIME_BITS', + '-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..691c8ec --- /dev/null +++ b/pipewire-v4l2/src/pipewire-v4l2.c @@ -0,0 +1,2580 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pipewire-v4l2.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +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); + } + } +} + +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 (SPA_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; + SPA_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) { + SPA_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; + SPA_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) + return res; + if (fcntl(res, F_SETFL, oflag) < 0) + pw_log_warn("fd:%d failed to set flags: %m", res); + 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, + ®istry_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 *card = NULL, *bus_info = NULL; + struct pw_node_info *info; + + if (file->node == NULL) + return -EIO; + + info = file->node->info; + + if (info != NULL && info->props != NULL) { + card = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION); + bus_info = spa_dict_lookup(info->props, SPA_KEY_API_V4L2_CAP_BUS_INFO); + } + if (card == NULL) + card = DEFAULT_CARD; + + spa_scnprintf((char*)arg->driver, sizeof(arg->driver), "%s", DEFAULT_DRIVER); + spa_scnprintf((char*)arg->card, sizeof(arg->card), "%s", card); + if (bus_info == NULL) + spa_scnprintf((char*)arg->bus_info, sizeof(arg->bus_info), "platform:%s-%d", + DEFAULT_BUS_INFO, file->node->id); + else + spa_scnprintf((char*)arg->bus_info, sizeof(arg->bus_info), "%s", bus_info); + + 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), +#ifdef V4L2_PIX_FMT_Y16_BE + MAKE_FORMAT(Y16_BE, video, raw, 2, GRAY16_BE), +#endif + 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<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 = -errno; + 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((struct pw_node*)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..66e3fbc --- /dev/null +++ b/pipewire-v4l2/src/pipewire-v4l2.h @@ -0,0 +1,18 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +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..c02f17d --- /dev/null +++ b/pipewire-v4l2/src/pw-v4l2.in @@ -0,0 +1,52 @@ +#!/bin/sh + +# This file is part of PipeWire. +# SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans +# SPDX-License-Identifier: MIT + +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 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 + # shellcheck disable=SC2016 # ${LIB} is interpreted by ld.so, not the shell + 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..4bbaa96 --- /dev/null +++ b/pipewire-v4l2/src/v4l2-func.c @@ -0,0 +1,139 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +#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); \ +} + +static inline bool needs_mode(int flags) +{ + return (flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE); +} + +SPA_EXPORT int open(const char *path, int oflag, ...) +{ + mode_t mode = 0; + if (needs_mode(oflag)) + 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) +{ + assert(!needs_mode(oflag)); + return open(path, oflag); +} + +#ifndef open64 +SPA_EXPORT int open64(const char *path, int oflag, ...) +{ + mode_t mode = 0; + if (needs_mode(oflag)) + 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) +{ + assert(!needs_mode(oflag)); + return open64(path, oflag); +} +#endif + +SPA_EXPORT int openat(int dirfd, const char *path, int oflag, ...) +{ + mode_t mode = 0; + if (needs_mode(oflag)) + 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) +{ + assert(!needs_mode(oflag)); + return openat(dirfd, path, oflag); +} + +#ifndef openat64 +SPA_EXPORT int openat64(int dirfd, const char *path, int oflag, ...) +{ + mode_t mode = 0; + if (needs_mode(oflag)) + 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) +{ + assert(!needs_mode(oflag)); + return openat64(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..a6b999f --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1,54 @@ +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 +sl +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..686d91a --- /dev/null +++ b/po/POTFILES.skip @@ -0,0 +1,2 @@ +src/daemon/systemd/system/pipewire-pulse.service.in +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 , 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 \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] \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 , 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 \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] \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..9369ed9 --- /dev/null +++ b/po/be.po @@ -0,0 +1,669 @@ +# 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č , 2016, 2023. +# +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: 2023-03-11 01:14+0300\n" +"Last-Translator: Viktar Vaŭčkievič \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: Gtranslator 42.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] \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" +" ці наўпрост сэмплы (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 (req. for rec) " +"(агаданы %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 Запусціць як фонавы сэрвіс (Default " +"false)\n" +" -r, --remote Назва адаленага сэрвіса\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 "Адключаны" + +#: 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." +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/bluez5/bluez5-device.c:1010 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Аўдыяшлюз (A2DP-крыніца і HSP/HFP AG)" + +#: 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 "Hi-Fi" + +#: 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/bg.po b/po/bg.po new file mode 100644 index 0000000..6aa5b0c --- /dev/null +++ b/po/bg.po @@ -0,0 +1,686 @@ +# Bulgarian translation of pipewire po-file. +# Copyright (C) 2023 Alexander Shopov +# Alexander Shopov , 2023. +# This file is licensed under the same terms as pipewire. +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire 1.0.0.\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: 2023-12-09 15:17+0100\n" +"Last-Translator: Alexander Shopov \n" +"Language-Team: Bulgarian \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" + +#: 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" +"\n" +" -h --help Извеждане на тази помощ\n" +" -V --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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [ОПЦИЯ…] [ФАЙЛ|-]\n" +"\n" +" -h, --help Извеждане на тази помощ\n" +" --version Извеждане на версията\n" +" -v, --verbose Включване на подробен режим\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" +" Xединица (единица е една от „s“/„ms“/„us“/„ns“)\n" +" или пряко отчитане (256)\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 Честота на отчитане (задължително при запис)\n" +" (стандартно: „%u“)\n" +" --channels Брой канали (задължително при запис)\n" +" (стандартно: „%u“)\n" +" --channel-map Карта на каналите:\n" +" едно от: „stereo“, „surround-51“, … или\n" +" списък с разделител „,“, напр. „FL,FR“\n" +" --format Формат на отчѐта %s (задължително при запис)\n" +" (стандартно: „%s“)\n" +" --volume Сила на звука на потока 0-1.0 (стандартно: %.3f)\n" +" -q --quality Качество при ново отчитане (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 Стартиране като демон (стандартно: не)\n" +" -r, --remote Име на отдалечения демон\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: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 "Слушалки 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 ms).\n" +"Най-вероятно се дължи на грешка в драйвера на ALSA „%s“. Молим да докладвате " +"това на разработчиците на ALSA." +msgstr[1] "" +"„snd_pcm_avail()“ върна неочаквано голяма стойност: %lu байта (%lu ms).\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 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: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 ms).\n" +"Най-вероятно се дължи на грешка в драйвера на ALSA „%s“. Молим да докладвате " +"това на разработчиците на ALSA." +msgstr[1] "" +"„snd_pcm_mmap_begin()“ върна неочаквано голяма стойност: %lu байта (%lu " +"ms).\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 "Аудио шлюз (източник A2DP и HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1272 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Изпълнение с висока точност (елемент-приемник A2DP, кодер „%s“)" + +#: spa/plugins/bluez5/bluez5-device.c:1275 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "" +"Двупосочна връзка с висока точност (елемент-източник/приемник A2DP, кодер " +"„%s“)" + +#: spa/plugins/bluez5/bluez5-device.c:1283 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Изпълнение с висока точност (елемент-приемник A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1285 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Двупосочна връзка с висока точност (елемент-източник/приемник A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1322 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Изпълнение с висока точност (елемент-приемник BAP, кодер „%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, кодер " +"„%s“)" + +#: spa/plugins/bluez5/bluez5-device.c:1359 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Слушалки с микрофон (HSP/HFP, кодер „%s“)<" + +#: spa/plugins/bluez5/bluez5-device.c:1364 +msgid "Headset Head Unit (HSP/HFP)" +msgstr "Слушалки с микрофон (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/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 , 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 \n" +"Language-Team: Bengali INDIA \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] \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..1b24230 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,748 @@ +# 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 , 2008. +# Agustí Grau , 2009. +# Judith Pintó Subirada +# Jordi Mas i Herǹandez, , 2022-2023 +# +# 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 , 2009, 2012. +# Robert Antoni Buj Gelonch , 2016. #zanata +# Wim Taymans , 2016. #zanata +# Robert Antoni Buj Gelonch , 2017. #zanata +# Robert Antoni Buj Gelonch , 2019. #zanata +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2023-06-06 15:28+0000\n" +"PO-Revision-Date: 2023-06-06 22:39+0200\n" +"Last-Translator: Jordi Mas i Herǹandez, ,\n" +"Language-Team: Catalan \n" +"Language: ca\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.4.2\n" + +#: src/daemon/pipewire.c:26 +#, 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/modules/module-protocol-pulse/modules/module-tunnel-sink.c:141 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Túnel cap a %s%s%s" + +#: src/modules/module-fallback-sink.c:31 +msgid "Dummy Output" +msgstr "Sortida fictícia" + +#: src/modules/module-pulse-tunnel.c:844 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Túnel per a %s@%s" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "Dispositiu desconegut" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s en %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s en %s" + +#: src/tools/pw-cat.c:974 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opcions] [|-]\n" +" -h, --help Mostra aquesta ajuda\n" +" --version Mostra la versió\n" +" -v, --verbose Habilita les operacions detallades\n" + +#: src/tools/pw-cat.c:981 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Nom del dimoni remot\n" +" --media-type Estableix el tipus de mitjà (per " +"defecte %s)\n" +" --media-category Estableix la categoria del " +"mitjà (per defecte %s)\n" +" --media-role Estableix el rol del mitjà (per " +"defecte %s)\n" +" --target Estableix l'objectiu sèrie o el nom " +"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" +" -P --properties Estableix les propietats del " +"node\n" +"\n" + +#: src/tools/pw-cat.c:999 +#, 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 Freqüència de mostreig (req. per a " +"rec) (predeterminat %u)\n" +" --channels Nombre de canals (req. per a rec) " +"(predeterminat %u)\n" +" --channel-map Mapa de canals\n" +" un dels següents: \"stereo\", " +"\"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 del flux 0-1.0 (predeterminat " +"%.3f)\n" +" -q --quality Qualitat de remostrador (0 - 15) " +"(predeterminal %d)\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Mode de reproducció\n" +" -r, --record Mode d'enregistrament\n" +" -m, --midi Mode MIDI\n" +" -d, --dsd Mode DSD\n" +" -o, --encoded Mode codificat\n" +"\n" + +#: src/tools/pw-cli.c:2220 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [opcions] [ordre]\n" +" -h, --help Mostra aquesta ajuda\n" +" --version Mostra la versió\n" +" -d, --daemon Inicia com a dimoni (fals per " +"defecte)\n" +" -r, --remote Nom del dimoni remot\n" +" -m, --monitor Monitor d'activitat\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:303 +msgid "Pro Audio" +msgstr "Pro Audio" + +#: spa/plugins/alsa/acp/acp.c:427 spa/plugins/alsa/acp/alsa-mixer.c:4648 +#: spa/plugins/bluez5/bluez5-device.c:1586 +msgid "Off" +msgstr "Inactiu" + +#: 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 l'estació d'acoblament" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Micròfon de l'estació d'acoblament" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "Entrada de línia de l'estació d'acoblament" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "Entrada de línia" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:1831 +msgid "Microphone" +msgstr "Micròfon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Micròfon frontal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Micròfon posterior" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Micròfon extern" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Micròfon intern" + +#: 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 "Control de guany automàtic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Sense control de guany automàtic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Accentuació" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Sense accentuació" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Amplificador" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Sense amplificador" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Sense accentuació dels baixos" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Sense accentuació dels baixos" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1837 +msgid "Speaker" +msgstr "Altaveu" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Auriculars" + +#: 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 "Micròfon de l'acoblador" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Micròfon d'auriculars" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Sortida analògica" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Auriculars 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Sortida mono dels auriculars" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Sortida de línia" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Sortida mono analògica" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Altaveus" + +#: 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 "Sortida 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 "Sortida multicanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Sortida del joc" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Sortida del xat" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Entrada del xat" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "Envoltant virtual 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +msgid "Analog Mono" +msgstr "Mono analògic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Analog Mono (Left)" +msgstr "Mono analògic (esquerra)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +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:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Stereo" +msgstr "Estèreo analògic" + +#: 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:1819 +msgid "Headset" +msgstr "Auriculars" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4643 +msgid "Speakerphone" +msgstr "Altaveu del telèfon" + +#: 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 "Envoltant analògic 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Analog Surround 3.0" +msgstr "Envoltant analògic 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Analog Surround 3.1" +msgstr "Envoltant analògic 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Analog Surround 4.0" +msgstr "Envoltant analògic 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 +msgid "Analog Surround 4.1" +msgstr "Envoltant analògic 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 +msgid "Analog Surround 5.0" +msgstr "Envoltant analògic 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4494 +msgid "Analog Surround 5.1" +msgstr "Envoltant analògic 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4495 +msgid "Analog Surround 6.0" +msgstr "Envoltant analògic 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4496 +msgid "Analog Surround 6.1" +msgstr "Envoltant analògic 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4497 +msgid "Analog Surround 7.0" +msgstr "Envoltant analògic 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4498 +msgid "Analog Surround 7.1" +msgstr "Envoltant analògic 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 "Envoltant digital 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4501 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Envoltant digital 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4502 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Envoltant 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 "Envoltant digital 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4505 +msgid "Chat" +msgstr "Xat" + +#: 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 "Dúplex mono analògic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4641 +msgid "Analog Stereo Duplex" +msgstr "Dúplex estèreo analògic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4644 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Dúplex estèreo digital (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4645 +msgid "Multichannel Duplex" +msgstr "Dúplex Multicanal" + +#: 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 "Xat mono + 7.1 envoltant" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4748 +#, c-format +msgid "%s Output" +msgstr "Sortida %s" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4756 +#, 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() ha retornat un valor excepcionalment gran: %lu byte (%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: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() ha retornat un valor excepcionalment gran: %li byte (%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: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() 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: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() ha retornat un valor excepcionalment gran: %lu byte " +"(%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/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(incorrecte)" + +#: spa/plugins/alsa/acp/compat.c:189 +msgid "Built-in Audio" +msgstr "Àudio intern" + +#: spa/plugins/alsa/acp/compat.c:194 +msgid "Modem" +msgstr "Mòdem" + +#: spa/plugins/bluez5/bluez5-device.c:1597 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Passarel·la d'àudio (A2DP Source & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1622 +#, 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:1625 +#, 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:1633 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Reproducció d'alta fidelitat (A2DP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1635 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1677 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Reproducció d'alta fidelitat (sortida BAP, còdec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1681 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Entrada d'alta fidelitat (font A2DP, còdec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1685 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Dúplex d'alta fidelitat (BAP Source/Sink, còdec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1693 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Reproducció d'alta fidelitat (Sortida BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1696 +msgid "High Fidelity Input (BAP Source)" +msgstr "Entrada d'alta fidelitat (Font BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1699 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Dúplex d'alta fidelitat (BAP Source/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1735 +#, 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:1740 +msgid "Headset Head Unit (HSP/HFP)" +msgstr "Unitat d'ariculars pel cap (HSP/HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1820 +#: spa/plugins/bluez5/bluez5-device.c:1825 +#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:1838 +#: spa/plugins/bluez5/bluez5-device.c:1844 +#: spa/plugins/bluez5/bluez5-device.c:1850 +#: spa/plugins/bluez5/bluez5-device.c:1856 +#: spa/plugins/bluez5/bluez5-device.c:1862 +#: spa/plugins/bluez5/bluez5-device.c:1868 +msgid "Handsfree" +msgstr "Mans lliures" + +#: spa/plugins/bluez5/bluez5-device.c:1826 +msgid "Handsfree (HFP)" +msgstr "Mans lliures (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1843 +msgid "Headphone" +msgstr "Auricular" + +#: spa/plugins/bluez5/bluez5-device.c:1849 +msgid "Portable" +msgstr "Portable" + +#: spa/plugins/bluez5/bluez5-device.c:1855 +msgid "Car" +msgstr "Cotxe" + +#: spa/plugins/bluez5/bluez5-device.c:1861 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:1867 +msgid "Phone" +msgstr "Telèfon" + +#: spa/plugins/bluez5/bluez5-device.c:1874 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:1875 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" 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 , 2008, 2009, 2012. +# Daniel Rusek , 2018. +# Marek Černocký , 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 \n" +"Language-Team: čeština \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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [volby] [|-]\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] \n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [tilvalg] \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..5ac68fe --- /dev/null +++ b/po/de.po @@ -0,0 +1,739 @@ +# 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 , 2008-2009. +# Micha Pietsch , 2008, 2009. +# Hedda Peters , 2009, 2012. +# Mario Blättermann , 2016. +# Jürgen Benvenuti , 2024. +# Christian Kirbach , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire.master-tx.de\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2024-09-27 03:27+0000\n" +"PO-Revision-Date: 2024-09-27 11:25+0200\n" +"Last-Translator: Christian Kirbach \n" +"Language-Team: German \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: Poedit 3.4.4\n" + +#: src/daemon/pipewire.c:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [Optionen]\n" +" -h, --help Diese Hilfe ausgeben\n" +" -v, --verbose Ausführlichere Ausgaben\n" +" --version Version anzeigen\n" +" -c, --config Konfiguration laden (Voreinstellung " +"%s)\n" +" -P --properties Kontext-Eigenschaften festlegen\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "PipeWire-Mediensystem" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "Das PipeWire-Mediensystem starten" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunnel zu %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Schein-Ausgabe" + +#: src/modules/module-pulse-tunnel.c:777 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunnel für %s@%s" + +#: src/modules/module-zeroconf-discover.c:320 +msgid "Unknown device" +msgstr "Unbekanntes Gerät" + +#: src/modules/module-zeroconf-discover.c:332 +#, c-format +msgid "%s on %s@%s" +msgstr "%s auf %s@%s" + +#: src/modules/module-zeroconf-discover.c:336 +#, c-format +msgid "%s on %s" +msgstr "%s auf %s" + +#: src/tools/pw-cat.c:973 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [Optionen] [|-]\n" +" -h, --help Diese Hilfe ausgeben\n" +" --version Version anzeigen\n" +" -v, --verbose Ausführliche Vorgänge einschalten\n" +"\n" +"\n" + +#: src/tools/pw-cat.c:980 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Name des entfernten Daemon\n" +" --media-type Medientyp festlegen (Vorgabe %s)\n" +" --media-category Medienkategorie festlegen (Vorgabe " +"%s)\n" +" --media-role Medienrolle festlegen (Vorgabe %s)\n" +" --target Seriennummer oder Name des " +"Knotenziels festlegen (Vorgabe %s)\n" +" 0 bedeutet keine Verbindung\n" +" --latency Knotenlatenz festlegen (Vorgabe %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" oder direkte Abtastung (256)\n" +" die Rate entspricht der " +"Quelldatei\n" +" -P --properties Knoteneigenschaften festlegen\n" +"\n" + +#: src/tools/pw-cat.c:998 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate Abtastrate (notw. für Aufzeichn.) " +"(Vorgabe %u)\n" +" --channels Anzahl der Kanäle (notw. für " +"Aufzeichn.) (Vorgabe %u)\n" +" --channel-map Kanalabbildung\n" +" eines von: »stereo«, " +"»surround-51«, … oder\n" +" eine mit Kommata getrennte Liste " +"mit Kanalnamen: z.B. »FL,FR«\n" +" --format Abtastformat %s (notw. für " +"Aufzeichn.) (Vorgabe %s)\n" +" --volume Strom-Lautstärke 0-1.0 (Vorgabe " +"%.3f)\n" +" -q --quality Qualität der Neu-Abtastung (0 - 15) " +"(Vorgabe %d)\n" +" -a, --raw RAW-Modus\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Wiedergabe-Modus\n" +" -r, --record Aufnahme-Modus\n" +" -m, --midi Midi-Modus\n" +" -d, --dsd DSD-Modus\n" +" -o, --encoded Codieren-Modus\n" +"\n" + +#: src/tools/pw-cli.c:2318 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [Optionen] [Befehl]\n" +" -h, --help Diese Hilfe ausgeben\n" +" --version Version anzeigen\n" +" -d, --daemon Als Daemon starten (Vorgabe: nein)\n" +" -r, --remote Name des entfernten Daemon\n" +" -m, --monitor Aktivitäten überwachen\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Pro Audio" + +#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1696 +msgid "Off" +msgstr "Aus" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +msgid "Input" +msgstr "Eingabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +msgid "Docking Station Input" +msgstr "Eingabe über Docking-Station" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Mikrofon der Docking-Station" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "Line-Eingang der Docking-Station" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "Line-Eingang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Microphone" +msgstr "Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Vorderes Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Rückwärtiges Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Externes Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Internes 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 "Automatische Verstärkungsregelung" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Keine automatische Verstärkungsregelung" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Kein Boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Verstärker" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Kein Verstärker" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Bassverstärkung" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Keine Bassverstärkung" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1990 +msgid "Speaker" +msgstr "Lautsprecher" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Kopfhörer" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "Analoger Eingang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "Mikrofon der Docking-Station" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Mikrofon am Sprechkopfhörer" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Analoge Ausgabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Kopfhörer 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Kopfhörer Mono-Ausgabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Line-Ausgang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Analoge Mono-Ausgabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Lautsprecher" + +#: 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 "Digitalausgang (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "Digitaleingang (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +msgid "Multichannel Input" +msgstr "Mehrkanal-Eingang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +msgid "Multichannel Output" +msgstr "Mehrkanal-Wiedergabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Spiel-Ausgang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Unterhaltungs-Ausgabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Unterhaltungs-Eingang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "Virtuelles 7.1 Surround" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +msgid "Analog Mono" +msgstr "Analoges Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Analoges Mono (links)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +msgid "Analog Mono (Right)" +msgstr "Analoges 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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Analoges Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1972 +msgid "Headset" +msgstr "Sprechkopfhörer" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Lautsprechertelefon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Mehrkanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Analog Surround 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Analog Surround 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Analog Surround 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Analog Surround 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Analog Surround 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Analog Surround 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Analog Surround 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Analog Surround 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Analog Surround 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Analog Surround 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Analog Surround 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Digitales Stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Digital Surround 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Digital Surround 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Digital Surround 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Digitales Stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Digital Surround 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Unterhaltung" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Spiel" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Analoges Mono Duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Analoges Stereo Duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Digitales Stereo Duplex (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Mehrkanal-Duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "Stereo Duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Mono-Unterhaltung + 7.1 Surround" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s-Ausgabe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s-Eingang" + +#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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() gab einen Wert zurück, der außerordentlich groß ist: %lu " +"Byte (%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() gab 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:1297 +#, 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() gab einen Wert zurück, der außerordentlich groß ist: %li " +"Byte (%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() gab 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:1344 +#, 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:1387 +#, 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() gab einen Wert zurück, der außerordentlich groß ist: " +"%lu Byte (%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() gab einen Wert zurück, der außerordentlich groß ist: " +"%lu Byte (%lu ms).\n" +"Dies ist wahrscheinlich ein Fehler im ALSA-Treiber »%s«. Bitte melden Sie " +"dieses Problem den ALSA-Entwicklern." + +#: spa/plugins/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(ungültig)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "Internes Audio" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1707 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Audio-Gateway (A2DP-Quelle und HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1755 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "High Fidelity-Wiedergabe (A2DP-Senke, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1758 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "High Fidelity Duplex (A2DP-Quelle/-Senke, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1766 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "High Fidelity-Wiedergabe (A2DP-Senke)" + +#: spa/plugins/bluez5/bluez5-device.c:1768 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "High Fidelity Duplex (A2DP-Quelle/-Senke)" + +#: spa/plugins/bluez5/bluez5-device.c:1818 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "High Fidelity-Wiedergabe (BAP-Senke, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "High Fidelity-Eingang (BAP-Quelle, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1827 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "High Fidelity Duplex (BAP-Quelle/-Senke, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1836 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "High Fidelity-Wiedergabe (BAP-Senke)" + +#: spa/plugins/bluez5/bluez5-device.c:1840 +msgid "High Fidelity Input (BAP Source)" +msgstr "High Fidelity-Eingang (BAP-Quelle)" + +#: spa/plugins/bluez5/bluez5-device.c:1843 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "High Fidelity Duplex (BAP-Quelle/-Senke)" + +#: spa/plugins/bluez5/bluez5-device.c:1892 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Sprechkopfhörer-Einheit (HSP/HFP, Codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1973 +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1985 +#: spa/plugins/bluez5/bluez5-device.c:1991 +#: spa/plugins/bluez5/bluez5-device.c:1997 +#: spa/plugins/bluez5/bluez5-device.c:2003 +#: spa/plugins/bluez5/bluez5-device.c:2009 +#: spa/plugins/bluez5/bluez5-device.c:2015 +#: spa/plugins/bluez5/bluez5-device.c:2021 +msgid "Handsfree" +msgstr "Freisprech" + +#: spa/plugins/bluez5/bluez5-device.c:1979 +msgid "Handsfree (HFP)" +msgstr "Freisprech (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1996 +msgid "Headphone" +msgstr "Kopfhörer" + +#: spa/plugins/bluez5/bluez5-device.c:2002 +msgid "Portable" +msgstr "Tragbar" + +#: spa/plugins/bluez5/bluez5-device.c:2008 +msgid "Car" +msgstr "Auto" + +#: spa/plugins/bluez5/bluez5-device.c:2014 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2020 +msgid "Phone" +msgstr "Telefon" + +#: spa/plugins/bluez5/bluez5-device.c:2027 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2028 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" + +#~ msgid "Headset Head Unit (HSP/HFP)" +#~ msgstr "Kopfhörer-Garnitur (HSP/HFP)" \ No newline at end of file diff --git a/po/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 , 2008, 2009. +# Fabian Affolter , 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 \n" +"Language-Team: German \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] \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 , 2008. +# Thalia Papoutsaki , 2009, 2012. +# Dimitris Spingos (Δημήτρης Σπίγγος) , 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 (Δημήτρης Σπίγγος) \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] \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 , 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 \n" +"Language-Team: Esperanto \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] \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 , 2009. +# Héctor Daniel Cabrera , 2009. +# Fernando Gonzalez Blanco , 2009, 2012. +# Alberto Castillo , 2016. #zanata +# Gladys Guerrero Lozano , 2016. #zanata +# Máximo Castañeda Riloba , 2016. #zanata +# Wim Taymans , 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 \n" +"Language-Team: Spanish \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] \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..a9cba41 --- /dev/null +++ b/po/fi.po @@ -0,0 +1,713 @@ +# pipewire translation to Finnish (fi). +# Copyright (C) 2008 Timo Jyrinki +# This file is distributed under the same license as the pipewire package. +# Timo Jyrinki , 2008. +# Ville-Pekka Vainio , 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: 2024-10-12 11:50+0300\n" +"PO-Revision-Date: 2024-10-12 12:04+0300\n" +"Last-Translator: Pauli Virtanen \n" +"Language-Team: Finnish \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:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [valinnat]\n" +" -h, --help Näytä tämä ohje\n" +" -v, --verbose Lisää viestien yksityiskohtaisuutta\n" +" --version Näytä versio\n" +" -c, --config Lataa asetukset (oletus %s)\n" +" -P, --properties Aseta kontekstin ominaisuudet\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "PipeWire-mediajärjestelmä" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "Käynnistä PipeWire-mediajärjestelmä" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunneli: %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Valeulostulo" + +#: src/modules/module-pulse-tunnel.c:777 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunneli: %s@%s" + +#: src/modules/module-zeroconf-discover.c:320 +msgid "Unknown device" +msgstr "Tuntematon laite" + +#: src/modules/module-zeroconf-discover.c:332 +#, c-format +msgid "%s on %s@%s" +msgstr "%s koneella %s@%s" + +#: src/modules/module-zeroconf-discover.c:336 +#, c-format +msgid "%s on %s" +msgstr "%s koneella %s" + +#: src/tools/pw-cat.c:973 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [valinnat] [|-]\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:980 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote 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 kohteen numero/nimi (oletus %s)\n" +" 0 tarkoittaa: ei linkkiä\n" +" --latency Aseta solmun viive (oletus %s)\n" +" Xyksikkö (yksikkö = s, ms, us, ns)\n" +" tai näytteiden lukumäärä (256)\n" +" näytetaajuus on tiedoston mukainen\n" +" -P --properties Aseta solmun ominaisuudet\n" +"\n" + +#: src/tools/pw-cat.c:998 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate 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" +" -a --raw Muotoilemattoman äänidatan tila\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Toisto\n" +" -r, --record Nauhoitus\n" +" -m, --midi MIDI-tila\n" +" -d, --dsd DSD-tila\n" +" -o, --encoded Koodatun audion tila\n" +"\n" + +#: src/tools/pw-cli.c:2318 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [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" +" -m, --monitor Seuraa tapahtumia\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Pro-audio" + +#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1696 +msgid "Off" +msgstr "Poissa" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +msgid "Input" +msgstr "Sisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +msgid "Docking Station Input" +msgstr "Telakan sisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Telakan mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "Telakan linjasisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "Linjasisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Microphone" +msgstr "Mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Etumikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Takamikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Ulkoinen mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Sisäinen mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c: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 "Automaattinen äänenvoimakkuuden säätö" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Ei automaattista äänenvoimakkuuden säätöä" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Vahvistus" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Ei vahvistusta" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Vahvistin" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Ei vahvistinta" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Bassonvahvistus" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Ei basson vahvistusta" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1990 +msgid "Speaker" +msgstr "Kaiutin" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Kuulokkeet" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "Analoginen sisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "Telakan mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Kuulokkeiden mikrofoni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Analoginen ulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Kuulokkeet 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Kuulokkeiden monoulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Linjaulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Analoginen monoulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Kaiuttimet" + +#: 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 "Digitaalinen ulostulo (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "Digitaalinen sisääntulo (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +msgid "Multichannel Input" +msgstr "Monikanavainen sisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +msgid "Multichannel Output" +msgstr "Monikanavainen ulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Peli-ulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Puhe-ulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Puhe-sisääntulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "Virtuaalinen tilaääni 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +msgid "Analog Mono" +msgstr "Analoginen mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Analoginen mono (vasen)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Analoginen stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1972 +msgid "Headset" +msgstr "Kuulokkeet" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Kaiutinpuhelin" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Monikanavainen" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Analoginen tilaääni 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Analoginen tilaääni 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Analoginen tilaääni 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Analoginen tilaääni 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Analoginen tilaääni 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Analoginen tilaääni 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Analoginen tilaääni 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Analoginen tilaääni 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Analoginen tilaääni 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Analoginen tilaääni 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Analoginen tilaääni 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Digitaalinen stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Digitaalinen tilaääni 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Digitaalinen tilaääni 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Digitaalinen tilaääni 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Digitaalinen stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Digitaalinen tilaääni 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Puhe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Peli" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Analoginen mono, molempisuuntainen" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Analoginen stereo, molempisuuntainen" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Digitaalinen stereo, molempisuuntainen (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Monikanavainen, molempisuuntainen" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "Stereo, molempisuuntainen" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Mono-puhe + 7.1 tilaääni" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s, ulostulo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s, sisääntulo" + +#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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:1297 +#, 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:1344 +#, 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:1387 +#, 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/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(virheellinen)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "Sisäinen äänentoisto" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modeemi" + +#: spa/plugins/bluez5/bluez5-device.c:1707 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Ääniyhdyskäytävä (A2DP-lähde & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1755 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Korkealaatuinen toisto (A2DP-kohde, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1758 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Korkealaatuinen molempisuuntainen (A2DP-lähde/kohde, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1766 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Korkealaatuinen toisto (A2DP-kohde)" + +#: spa/plugins/bluez5/bluez5-device.c:1768 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Korkealaatuinen molempisuuntainen (A2DP-lähde/kohde)" + +#: spa/plugins/bluez5/bluez5-device.c:1818 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Korkealaatuinen toisto (BAP-kohde, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Korkealaatuinen sisääntulo (BAP-lähde, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1827 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Korkealaatuinen molempisuuntainen (BAP-lähde/kohde, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1836 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Korkealaatuinen toisto (BAP-kohde)" + +#: spa/plugins/bluez5/bluez5-device.c:1840 +msgid "High Fidelity Input (BAP Source)" +msgstr "Korkealaatuinen sisääntulo (BAP-lähde)" + +#: spa/plugins/bluez5/bluez5-device.c:1843 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Korkealaatuinen molempisuuntainen (BAP-lähde/kohde)" + +#: spa/plugins/bluez5/bluez5-device.c:1892 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Kuulokemikrofoni (HSP/HFP, %s-koodekki)" + +#: spa/plugins/bluez5/bluez5-device.c:1973 +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1985 +#: spa/plugins/bluez5/bluez5-device.c:1991 +#: spa/plugins/bluez5/bluez5-device.c:1997 +#: spa/plugins/bluez5/bluez5-device.c:2003 +#: spa/plugins/bluez5/bluez5-device.c:2009 +#: spa/plugins/bluez5/bluez5-device.c:2015 +#: spa/plugins/bluez5/bluez5-device.c:2021 +msgid "Handsfree" +msgstr "Kuulokemikrofoni" + +#: spa/plugins/bluez5/bluez5-device.c:1979 +msgid "Handsfree (HFP)" +msgstr "Kuulokemikrofoni (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1996 +msgid "Headphone" +msgstr "Kuulokkeet" + +#: spa/plugins/bluez5/bluez5-device.c:2002 +msgid "Portable" +msgstr "Kannettava" + +#: spa/plugins/bluez5/bluez5-device.c:2008 +msgid "Car" +msgstr "Auto" + +#: spa/plugins/bluez5/bluez5-device.c:2014 +msgid "HiFi" +msgstr "Hi-Fi" + +#: spa/plugins/bluez5/bluez5-device.c:2020 +msgid "Phone" +msgstr "Puhelin" + +#: spa/plugins/bluez5/bluez5-device.c:2027 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2028 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" diff --git a/po/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 , 2008. +# Michaël Ughetto , 2008. +# Pablo Martin-Gomez , 2008. +# Corentin Perard , 2009. +# Thomas Canniot , 2009, 2012. +# Sam Friedmann , 2016. #zanata +# Wim Taymans , 2016. #zanata +# Edouard Duliege , 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 \n" +"Language-Team: French \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] \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 , 2011. +# mbouzada , 2011. +# Marcos Lans , 2018. +# Fran Dieguez , 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 \n" +"Language-Team: Galician >\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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opcións] [|-]\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 , 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 \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] \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 , 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 \n" +"Language-Team: Hebrew \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] \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 , 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 \n" +"Language-Team: Hindi \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] \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 , 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 \n" +"Language-Team: Croatian \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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [mogućnosti] [|-]\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 , 2012. +# Gabor Kelemen , 2016. +# Balázs Úr , 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 \n" +"Language-Team: Hungarian \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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [kapcsolók] [|-]\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..f4767be --- /dev/null +++ b/po/id.po @@ -0,0 +1,718 @@ +# 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 , 2011, 2012, 2018, 2021, 2024. +msgid "" +msgstr "" +"Project-Id-Version: PipeWire master\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/" +"issues/new\n" +"POT-Creation-Date: 2024-02-25 03:43+0300\n" +"PO-Revision-Date: 2024-11-03 14:23+0700\n" +"Last-Translator: Andika Triwidada \n" +"Language-Team: Indonesia \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=1; plural=0;\n" +"X-Generator: Poedit 3.5\n" + +#: src/daemon/pipewire.c:26 +#, 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/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunnel ke %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Keluaran Dummy" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunnel untuk %s@%s" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "Perangkat tak dikenal" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s pada %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s pada %s" + +#: src/tools/pw-cat.c:991 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opsi] [|-]\n" +" -h, --help Tampilkan bantuan ini\n" +" --version Tampilkan versi\n" +" -v, --verbose Fungsikan 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 serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote 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" +" -P --properties Atur properti simpul\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\",... atau\n" +" daftar dipisah koma dari nama " +"kanal: mis. \"FL,FR\"\n" +" --format Format cuplikan %s (perlu untuk " +"rekam) (baku %s)\n" +" --volume Volume stream 0-1.0 (baku %.3f)\n" +" -q --quality Kualitas resampler (0 - 15) (baku " +"%d)\n" +"\n" + +#: src/tools/pw-cat.c:1033 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Mode main ulang\n" +" -r, --record Mode perekaman\n" +" -m, --midi Mode midi\n" +" -d, --dsd Mode DSD\n" +" -o, --encoded Mode di-enkode\n" +"\n" + +#: src/tools/pw-cli.c:2252 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [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" +" -m, --monitor Monitor activitas\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Pro Audio" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "Mati" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +msgid "Input" +msgstr "Masukan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +msgid "Docking Station Input" +msgstr "Masukan Docking Station" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Mikrofon 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:1989 +msgid "Microphone" +msgstr "Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Mikrofon Depan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Mikrofon Belakang" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Mikrofon Eksternal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Mikrofon Internal" + +#: spa/plugins/alsa/acp/alsa-mixer.c: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 "Kendali Penguatan Otomatis (AGC)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Tanpa Kendali Penguatan Otomatis (AGC)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Tanpa Boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Penguat" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Tanpa Penguat" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Boost Bass" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Tanpa Boost Bass" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1995 +msgid "Speaker" +msgstr "Speaker" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Headphone" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "Masukan Analog" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "Mikrofon Dok" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Mikrofon Headset" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Keluaran Analog" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Headphone 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Keluaran Mono Headphone" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Line Out" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Keluaran Mono Analog" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Speaker" + +#: 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 "Keluaran Digital (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "Masukan Digital (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +msgid "Multichannel Input" +msgstr "Masukan Multikanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +msgid "Multichannel Output" +msgstr "Keluaran Multikanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Keluaran Permainan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Keluaran Obrolan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Masukan Obrolan" + +#: 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:4456 +msgid "Analog Mono" +msgstr "Analog Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Analog Mono (Kiri)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Analog Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "Headset" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Speakerphone" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Multikanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Analog Surround 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Analog Surround 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Analog Surround 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Analog Surround 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Analog Surround 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Analog Surround 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Analog Surround 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Analog Surround 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Analog Surround 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Analog Surround 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Analog Surround 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Digital Stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Digital Surround 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Digital Surround 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Surround 5.1 Digital (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Digital Stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Surround 5.1 Digital (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Obrolan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Permainan" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Dupleks Mono Analog" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Dupleks Stereo Analog" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Dupleks Stereo Digital (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Dupleks Multikanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "Dupleks Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Mono Chat + 7.1 Surround" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "Keluaran %s" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "Masukan %s" + +#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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." + +#: spa/plugins/alsa/acp/alsa-util.c:1286 +#, c-format +msgid "" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" +"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." + +#: spa/plugins/alsa/acp/alsa-util.c:1333 +#, 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:1376 +#, 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." + +#: spa/plugins/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(tak valid)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "Audio Bawaan" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Audio Gateway (Sumber A2DP & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Putar High Fidelity (Muara A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1763 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Dupleks High Fidelity (Sumber/Muara A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Putar High Fidelity (Muara A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Dupleks High Fidelity (Sumber/Muara A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Putar High Fidelity (Muara BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1828 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Masukan High Fidelity (Sumber BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Dupleks High Fidelity (Sumber/Muara BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Putar High Fidelity (Muara BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "Masukan High Fidelity (Sumber BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Dupleks High Fidelity (Sumber/Muara BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Headset Head Unit (HSP/HFP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "Handsfree" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "Handsfree (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "Headphone" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "Portabel" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "Mobil" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "Telepon" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" + +#, c-format +#~ msgid "" +#~ "%s [options]\n" +#~ " -h, --help Show this help\n" +#~ " -v, --verbose Increase verbosity by one level\n" +#~ " --version Show version\n" +#~ " -c, --config Load config (Default %s)\n" +#~ " -P --properties Set context properties\n" +#~ msgstr "" +#~ "%s [opsi]\n" +#~ " -h, --help Tampilkan bantuan ini\n" +#~ " -v, --verbose Tingkatkan kerincian satu aras\n" +#~ " --version Tampilkan versi\n" +#~ " -c, --config Muat konfig (Baku %s)\n" +#~ " -P --properties Atur properti konteks\n" diff --git a/po/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 , 2008, 2009. +# mario_santagiuliana , 2009. +# Milo Casagrande , 2009, 2012, 2015, 2019. +# Albano Battistella ,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 \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] \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 , 2009. +# Kiyoto Hashida , 2009, 2012. +# Kenzo Moriguchi , 2016. #zanata +# Ooyama Yosiyuki , 2016. #zanata +# Wim Taymans , 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 \n" +"Language-Team: Japanese \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] \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..882d852 --- /dev/null +++ b/po/ka.po @@ -0,0 +1,718 @@ +# Georgian translation of pipewire +# Copyright (C) 2023 pipewire's authors +# This file is distributed under the same license as the pipewire package. +# Temuri Doghonadze , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/" +"issues/new\n" +"POT-Creation-Date: 2023-08-07 22:01+0200\n" +"PO-Revision-Date: 2023-08-07 22:06+0200\n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: Georgian <(nothing)>\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.3.2\n" + +#: src/daemon/pipewire.c:26 +#, 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 Media System" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "PipeWire Media System-ის გაშვება" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:141 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "გვირაბი %s%s%s-მდე" + +#: src/modules/module-fallback-sink.c:31 +msgid "Dummy Output" +msgstr "ნულოვანი გამოყვანა" + +#: src/modules/module-pulse-tunnel.c:847 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "გვირაბი %s@%s-სთვის" + +#: src/modules/module-zeroconf-discover.c:311 +msgid "Unknown device" +msgstr "უცნობი მოწყობილობა" + +#: src/modules/module-zeroconf-discover.c:323 +#, c-format +msgid "%s on %s@%s" +msgstr "%s %s@%s -ზე" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s" +msgstr "%s %s-ზე" + +#: src/tools/pw-cat.c:979 +#, c-format +msgid "" +"%s [options] [|-]\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:986 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote დაშორებული დემონის სახელი\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:1004 +#, 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:1021 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback დაკვრის რეჟიმი\n" +" -r, --record ჩაწერის რეჟიმი\n" +" -m, --midi Midi რეჟიმი\n" +" -d, --dsd DSD რეჟიმი\n" +" -o, --encoded დაშიფრული რეჟიმი\n" +"\n" + +#: src/tools/pw-cli.c:2220 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [პარამეტრები] [ბრძანება]\n" +" -h, --help ამ დახმარების ჩვენება\n" +" --version ვერსიის ჩვენება\n" +" -d, --daemon დემონის სახით გაშვება (ნაგულისხმევად " +"გამორთულია)\n" +" -r, --remote დაშორებული დემონის სახელი\n" +" -m, --monitor აქტივობის მონიტორინგი\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:325 +msgid "Pro Audio" +msgstr "Pro Audio" + +#: spa/plugins/alsa/acp/acp.c:449 spa/plugins/alsa/acp/alsa-mixer.c:4648 +#: spa/plugins/bluez5/bluez5-device.c:1586 +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:1831 +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:1837 +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:1819 +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:4748 +#, c-format +msgid "%s Output" +msgstr "%s გამოყვანა" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4756 +#, 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:1597 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Audio Gateway (A2DP წყარო & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1622 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "მაღალი ხარისხის ხმა (A2DP Sink, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1625 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "მაღალი ხარისხის დუპლექსი (A2DP წყარო/Sink, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1633 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "მაღალი ხარისხის ხმა (A2DP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1635 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "მაღალი ხარისხის დუპლექსი(A2DP წყარო/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1677 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "მაღალი ხარისხის დაკვრა (BAP Sink, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1681 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "მაღალი ხარისხის შეყვანა (BAP წყარო, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1685 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "მაღალი ხარისხის დუპლექსი (BAP წყარო/Sink, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1693 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "მაღალი ხარისხის დაკვრა (BAP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1696 +msgid "High Fidelity Input (BAP Source)" +msgstr "მაღალი ხარისხის შეყვანა (BAP წყარო)" + +#: spa/plugins/bluez5/bluez5-device.c:1699 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "მაღალი ხარისხის დუპლექსი (BAP წყარო/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1735 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Headset Head Unit (HSP/HFP, კოდეკი %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1740 +msgid "Headset Head Unit (HSP/HFP)" +msgstr "Headset Head Unit (HSP/HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1820 +#: spa/plugins/bluez5/bluez5-device.c:1825 +#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:1838 +#: spa/plugins/bluez5/bluez5-device.c:1844 +#: spa/plugins/bluez5/bluez5-device.c:1850 +#: spa/plugins/bluez5/bluez5-device.c:1856 +#: spa/plugins/bluez5/bluez5-device.c:1862 +#: spa/plugins/bluez5/bluez5-device.c:1868 +msgid "Handsfree" +msgstr "ხელის გარეშე სამართავი" + +#: spa/plugins/bluez5/bluez5-device.c:1826 +msgid "Handsfree (HFP)" +msgstr "ხელის გარეშე სამართავი (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1843 +msgid "Headphone" +msgstr "ყურსაცვამი" + +#: spa/plugins/bluez5/bluez5-device.c:1849 +msgid "Portable" +msgstr "გადატანადი" + +#: spa/plugins/bluez5/bluez5-device.c:1855 +msgid "Car" +msgstr "მანქანა" + +#: spa/plugins/bluez5/bluez5-device.c:1861 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:1867 +msgid "Phone" +msgstr "ტელეფონი" + +#: spa/plugins/bluez5/bluez5-device.c:1874 +msgid "Bluetooth" +msgstr "ლურჯკბილა" + +#: spa/plugins/bluez5/bluez5-device.c:1875 +msgid "Bluetooth (HFP)" +msgstr "ბლუთუზი (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 , 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 \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] \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 , 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 \n" +"Language-Team: Kannada \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] \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 , 2013. #zanata +# KimJeongYeon , 2017. +# Sangchul Lee , 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 \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] \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] \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: \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] \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 , 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 \n" +"Language-Team: Marathi \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] \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 , 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 \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] \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..b1e5695 --- /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 , 2009. +# Reinout van Schouwen , 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 \n" +"Language-Team: Dutch \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] \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-uit" + +#: 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 , 2017. +# Nicolai Syvertsen 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 \n" +"Language-Team: Norwegian Nynorsk \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] \n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [val] \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..f2cfad8 --- /dev/null +++ b/po/oc.po @@ -0,0 +1,731 @@ +# Occitan translation of pipewire. +# Copyright (C) 2006-2008 Lennart Poettering +# This file is distributed under the same license as the pipewire package. +# Robert-André Mauchin , 2008. +# Michaël Ughetto , 2008. +# Pablo Martin-Gomez , 2008. +# Corentin Perard , 2009. +# Thomas Canniot , 2009, 2012. +# Cédric Valmary (Tot en Òc) , 2015. +# Cédric Valmary (totenoc.eu) , 2016. +# Quentin PAGÈS, 2023.-2024 +msgid "" +msgstr "" +"Project-Id-Version: pipewire trunk\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/" +"issues/new\n" +"POT-Creation-Date: 2024-02-25 03:43+0300\n" +"PO-Revision-Date: 2024-06-24 11:53+0200\n" +"Last-Translator: Quentin PAGÈS\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: Poedit 3.4.3\n" +"X-Launchpad-Export-Date: 2016-10-12 20:12+0000\n" + +#: src/daemon/pipewire.c:26 +#, 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 Afichar aquesta ajuda\n" +" --version Afichar la version\n" +" -c, --config Cargar la conf. (Defaut %s)\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "Sistèma mèdia PipeWire" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "Aviar lo sistèma mèdia PipeWire" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunèl cap a %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Sortida factícia" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunèl per %s@%s" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "Periferic desconegut" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s sus %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s sus %s" + +#: src/tools/pw-cat.c:991 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opcions] [|-]\n" +" -h, --help Afichar aquesta ajuda\n" +" --version Afichar la version\n" +" -v, --verbose Activar las operacions verbosas\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 serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Nom del demòni distant\n" +" --media-type Definir lo tipe de mèdia (per defaut " +"%s)\n" +" --media-category Definir la categoria de mèdia (per " +"defaut %s)\n" +" --media-role Definir lo ròtle del mèdia (per " +"defaut %s)\n" +" --target Definir lo numèro de seria o lo nom " +"de la cibla del nos (per defaut %s)\n" +" 0 significa ligar pas\n" +" --latency Definir la laténcia del nos (per " +"defaut %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" o escandalhatge dirècte (256)\n" +" lo taus es çò del fichièr font\n" +" -P --properties Definir las proprietats del nos\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 Taus d'escandalhatge (req. per rec) " +"(per defaut %u)\n" +" --channels Nombre de canals (req. per rec) (per " +"defaut %u)\n" +" --channel-map Mapa de canal\n" +" un de : \"stereo\", " +"\"surround-51\",... o\n" +" lista de nom de canal separats " +"per de virgula : ex. \"FL,FR\"\n" +" --format Format d'escandalhatge %s (req. per " +"rec) (per defaut %s)\n" +" --volume Volum del flux 0-1.0 (per defaut " +"%.3f)\n" +" -q --quality Qualitat del aus reescandalhatge (0 " +"- 15) (per defaut %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" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Mòde lectura\n" +" -r, --record Mòde enregistrament\n" +" -m, --midi Mòde Midi\n" +" -d, --dsd Mòde DSD\n" +" -o, --encoded %òde encodat\n" +"\n" +"\n" + +#: src/tools/pw-cli.c:2252 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [opcions] [comanda]\n" +" -h, --help Afichar aquesta ajuda\n" +" --version Afichar la version\n" +" -d, --daemon Aviar coma demòni (Per defaut " +"false)\n" +" -r, --remote Nom del demòni distant\n" +" -m, --monitor Susvelhar l’activitat\n" +"\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Àudio pro" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "Atudat" + +#: 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 l'estacion d'acuèlh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Microfòn de l'estacion d'acuèlh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "Entrada linha de l'estacion d'acuèlh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "Entrada linha" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:1989 +msgid "Microphone" +msgstr "Microfòn" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Microfòn avant" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Microfòn arrièr" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Microfòn extèrne" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Microfòn intèrne" + +#: 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 "Vidèo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2664 +msgid "Automatic Gain Control" +msgstr "Contraròtle automatic del ganh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Pas de contraròtle automatic del ganh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Sens boost" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Amplificador" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Pas d'amplificador" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Amplificacion bassas" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Pas d'amplificacion de las bassas" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1995 +msgid "Speaker" +msgstr "Nautparlaire" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Escotadors" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "Entrada analogica" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "Microfòn de l'estacion d'acuèlh" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Micro-casc" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Sortida analogica" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Casc àudio 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Sortida casc àudio analogica mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Sortida linha" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Sortida analogica mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Nauts parlaires" + +#: 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 "Sortida numerica (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "Entrada numerica (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 "Sortida multicanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Sortida jòc" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Sortida messatjariá" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Entrada messatjariá" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "Surround 7.1 virtual" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +msgid "Analog Mono" +msgstr "Mono analogic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Mono analogic (esquèrra)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +msgid "Analog Mono (Right)" +msgstr "Mono analogic (drecha)" + +#. 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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Estereo analogic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Estereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "Casc àudio" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Nautparlaire" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Multicanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Surround analogic 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Surround analogic 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Surround analogic 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Surround analogic 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Surround analogic 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Surround analogic 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Surround analogic 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Surround analogic 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Surround analogic 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Surround analogic 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Surround analogic 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Estereo numeric (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Surround numeric 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Surround numeric 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Digital Surround 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Estereo numeric (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Digital Surround 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Messatjariá instantanèa" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Jòc" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Duplèx Mono analogic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Duplèx esterèo analogic" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Duplèx estèreo numeric (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Duplèx multicanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "Duplèx estereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Messatjariá mono + Surround 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s Sortida" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s Entrada" + +#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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 octet " +"(%lu ms).\n" +"Es fòrt probablament un bug dins lo pilòt ALSA « %s ». Senhalatz-lo als " +"desvolopaires d’ALSA." +msgstr[1] "" +"snd_pcm_avail() a tornat una valor qu'es excepcionalament larga : %lu octets " +"(%lu ms).\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:1286 +#, 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 octet " +"%s%lu ms).\n" +"Es fòrt probablament un bug dins lo pilòt ALSA « %s ». Senhalatz-lo als " +"desvolopaires d’ALSA." +msgstr[1] "" +"snd_pcm_delay() a tornat una valor qu'es excepcionalament larga : %li octets " +"%s%lu ms).\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:1333 +#, 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:1376 +#, 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 " +"octet (%lu ms).\n" +"Es fòrt probablament un bug dins lo pilòt ALSA « %s ». Senhalatz-lo als " +"desvolopaires d’ALSA." +msgstr[1] "" +"snd_pcm_mmap_begin() a tornat una valor qu'es excepcionalament larga : %lu " +"octet (%lu ms).\n" +"Es fòrt probablament un bug dins lo pilòt ALSA « %s ». Senhalatz-lo als " +"desvolopaires d’ALSA." + +#: spa/plugins/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(invalid)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "Àudio integrat" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modèm" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Palanca àudio (Font A2DP & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Lectura nauta fidelitat (A2DP Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1763 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Duplèx nauta fidelitat (A2DP Source/Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Lectura nauta fidelitat (A2DP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Duplèx nauta fidelitat (A2DP Source/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Lectura nauta fidelitat (A2DP Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1828 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Duplèx nauta fidelitat (Font BAP, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Duplèx nauta fidelitat (Font BAP/Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Lectura nauta fidelitat (receptor BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "Duplèx nauta fidelitat (Font BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Duplèx nauta fidelitat (Font/Receptor BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Controlador de casc (HSP/HFP, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "Mans liuras" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "Mans liuras (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "Escotador" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "Portable" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "Telefòn de veitura" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "Telefòn" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" \ No newline at end of file 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 , 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 \n" +"Language-Team: Oriya \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] \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 , 2008. +# A S Alam , 2009. +# Jaswinder Singh , 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 \n" +"Language-Team: Punjabi/Panjabi \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] \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..144c593 --- /dev/null +++ b/po/pipewire.pot @@ -0,0 +1,637 @@ +# 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 , 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: 2024-02-25 03:43+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \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:26 +#, 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/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "" + +#: src/tools/pw-cat.c:991 +#, c-format +msgid "" +"%s [options] [|-]\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 serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" + +#: 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" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" + +#: src/tools/pw-cli.c:2252 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "" + +#: spa/plugins/alsa/acp/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:1989 +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:1995 +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:4456 +msgid "Analog Mono" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "" + +#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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:1286 +#, 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:1333 +#, 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:1376 +#, 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:193 +msgid "Built-in Audio" +msgstr "" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1763 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1828 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "" diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 0000000..6d6572a --- /dev/null +++ b/po/pl.po @@ -0,0 +1,742 @@ +# Polish translation for pipewire. +# Copyright © 2008-2025 the pipewire authors. +# This file is distributed under the same license as the pipewire package. +# Piotr Drąg , 2008, 2012-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2025-01-09 15:25+0000\n" +"PO-Revision-Date: 2025-02-09 14:55+0100\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \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:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [opcje]\n" +" -h, --help Wyświetla tę pomoc\n" +" -v, --verbose Zwiększa liczbę wyświetlanych\n" +" komunikatów o jeden poziom\n" +" --version Wyświetla wersję\n" +" -c, --config Wczytuje konfigurację (domyślnie " +"%s)\n" +" -P --properties Ustawia właściwości kontekstu\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +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:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunel do %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Głuche wyjście" + +#: src/modules/module-pulse-tunnel.c:760 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunel dla %s@%s" + +#: src/modules/module-zeroconf-discover.c:320 +msgid "Unknown device" +msgstr "Nieznane urządzenie" + +#: src/modules/module-zeroconf-discover.c:332 +#, c-format +msgid "%s on %s@%s" +msgstr "%s na %s@%s" + +#: src/modules/module-zeroconf-discover.c:336 +#, c-format +msgid "%s on %s" +msgstr "%s na %s" + +#: src/tools/pw-cat.c:973 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opcje] [|-]\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:980 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote 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 docelowy numer seryjny\n" +" lub nazwę węzła (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:998 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate 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" +" -a, --raw Tryb RAW\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Tryb odtwarzania\n" +" -r, --record Tryb nagrywania\n" +" -m, --midi Tryb MIDI\n" +" -d, --dsd Tryb DSD\n" +" -o, --encoded Tryb zakodowany\n" +"\n" + +#: src/tools/pw-cli.c:2306 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [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" +" -m, --monitor Monitoruje aktywność\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:347 +msgid "Pro Audio" +msgstr "Dźwięk w zastosowaniach profesjonalnych" + +#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1795 +msgid "Off" +msgstr "Wyłączone" + +#: 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:2139 +msgid "Microphone" +msgstr "Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "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:2145 +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:4458 +msgid "Analog Mono" +msgstr "Analogowe mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 +msgid "Analog Mono (Left)" +msgstr "Analogowe mono (lewy)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +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:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +msgid "Analog Stereo" +msgstr "Analogowe stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4462 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4463 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/bluez5/bluez5-device.c:2127 +msgid "Headset" +msgstr "Słuchawki z mikrofonem" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Speakerphone" +msgstr "Telefon głośnomówiący" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Multichannel" +msgstr "Wielokanałowe" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 2.1" +msgstr "Analogowe przestrzenne 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 3.0" +msgstr "Analogowe przestrzenne 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 3.1" +msgstr "Analogowe przestrzenne 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 4.0" +msgstr "Analogowe przestrzenne 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 4.1" +msgstr "Analogowe przestrzenne 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 5.0" +msgstr "Analogowe przestrzenne 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 5.1" +msgstr "Analogowe przestrzenne 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 6.0" +msgstr "Analogowe przestrzenne 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 6.1" +msgstr "Analogowe przestrzenne 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Analog Surround 7.0" +msgstr "Analogowe przestrzenne 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Analog Surround 7.1" +msgstr "Analogowe przestrzenne 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Stereo (IEC958)" +msgstr "Cyfrowe stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Cyfrowe przestrzenne 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Cyfrowe przestrzenne 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Cyfrowe przestrzenne 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Digital Stereo (HDMI)" +msgstr "Cyfrowe stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Cyfrowe przestrzenne 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 +msgid "Chat" +msgstr "Rozmowa" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 +msgid "Game" +msgstr "Gra" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +msgid "Analog Mono Duplex" +msgstr "Analogowy dupleks mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Analog Stereo Duplex" +msgstr "Analogowy dupleks stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Cyfrowy dupleks stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Multichannel Duplex" +msgstr "Dupleks wielokanałowy" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4633 +msgid "Stereo Duplex" +msgstr "Dupleks stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4634 +msgid "Mono Chat + 7.1 Surround" +msgstr "Rozmowa mono + przestrzenne 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4735 +#, c-format +msgid "%s Output" +msgstr "Wyjście %s" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4743 +#, c-format +msgid "%s Input" +msgstr "Wejście %s" + +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_avail() 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:1299 +#, c-format +msgid "" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_delay() 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:1346 +#, c-format +msgid "" +"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " +"%lu.\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr "" +"snd_pcm_avail_delay() 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:1389 +#, c-format +msgid "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_mmap_begin() 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:193 +msgid "Built-in Audio" +msgstr "Wbudowany dźwięk" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1806 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Bramka dźwięku (źródło A2DP i AG HSP/HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1834 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "Przesyłanie dźwięku do aparatów słuchowych (odpływ ASHA)" + +#: spa/plugins/bluez5/bluez5-device.c:1874 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1877 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1885 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1887 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1937 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1942 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Wejście o wysokiej dokładności (źródło BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1946 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1955 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1959 +msgid "High Fidelity Input (BAP Source)" +msgstr "Wejście o wysokiej dokładności (źródło BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1962 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:2008 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2128 +#: spa/plugins/bluez5/bluez5-device.c:2133 +#: spa/plugins/bluez5/bluez5-device.c:2140 +#: spa/plugins/bluez5/bluez5-device.c:2146 +#: spa/plugins/bluez5/bluez5-device.c:2152 +#: spa/plugins/bluez5/bluez5-device.c:2158 +#: spa/plugins/bluez5/bluez5-device.c:2164 +#: spa/plugins/bluez5/bluez5-device.c:2170 +#: spa/plugins/bluez5/bluez5-device.c:2176 +msgid "Handsfree" +msgstr "Zestaw głośnomówiący" + +#: spa/plugins/bluez5/bluez5-device.c:2134 +msgid "Handsfree (HFP)" +msgstr "Zestaw głośnomówiący (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2151 +msgid "Headphone" +msgstr "Słuchawki" + +#: spa/plugins/bluez5/bluez5-device.c:2157 +msgid "Portable" +msgstr "Przenośne" + +#: spa/plugins/bluez5/bluez5-device.c:2163 +msgid "Car" +msgstr "Samochód" + +#: spa/plugins/bluez5/bluez5-device.c:2169 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2175 +msgid "Phone" +msgstr "Telefon" + +#: spa/plugins/bluez5/bluez5-device.c:2182 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2183 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" diff --git a/po/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 , 2012. +# Rui Gouveia , 2012, 2015. +# Pedro Albuquerque , 2015. +# Juliano de Souza Camargo , 2020. +# Hugo Carvalho , 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 \n" +"Language-Team: Portuguese \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] \n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [options] \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 +# This file is distributed under the same license as the pipewire package. +# Fabian Affolter , 2008. +# Igor Pires Soares , 2009, 2012. +# Rafael Fontenelle , 2013-2021. +# Matheus Barbosa , 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 \n" +"Language-Team: Brazilian Portuguese \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] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opções] [|-]\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 , 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 \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] \n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [opțiuni] \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..b004144 --- /dev/null +++ b/po/ru.po @@ -0,0 +1,746 @@ +# Russian translation of pipewire. +# Copyright (C) 2010 pipewire +# This file is distributed under the same license as the pipewire package. +# +# Leonid Kurbatov , 2010, 2012. +# Alexander Potashev , 2014, 2019. +# Victor Ryzhykh , 2021. +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2023-05-28 10:45+0000\n" +"PO-Revision-Date: 2023-11-28 14:30+0300\n" +"Last-Translator: Aleksandr Melman \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.4.1\n" + +#: src/daemon/pipewire.c:26 +#, 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:141 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Туннель на %s%s%s" + +#: src/modules/module-fallback-sink.c:31 +msgid "Dummy Output" +msgstr "Холостой выход" + +#: src/modules/module-pulse-tunnel.c:844 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Туннель для %s@%s" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "Неизвестное устройство" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s на %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s по %s" + +#: src/tools/pw-cat.c:974 +#, c-format +msgid "" +"%s [options] [|-]\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:981 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Имя удаленного фоновой службы\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" +" -P --properties Задать свойства узла\n" +"\n" + +#: src/tools/pw-cat.c:999 +#, 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:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Режим проигрывания\n" +" -r, --record Режим записи\n" +" -m, --midi Режим MIDI\n" +" -d, --dsd Режим DSD\n" +" -o, --encoded Режим кодирования\n" +"\n" + +#: src/tools/pw-cli.c:2220 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [параметры] [команда]\n" +" -h, --help Показать справку\n" +" --version Показать версию\n" +" -d, --daemon Запустить в режиме фоновой службы " +"(По умолчанию false)\n" +" -r, --remote Имя удаленного фоновой службы\n" +" -m, --monitor Контроль активности\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:303 +msgid "Pro Audio" +msgstr "Pro Audio" + +#: spa/plugins/alsa/acp/acp.c:427 spa/plugins/alsa/acp/alsa-mixer.c:4648 +#: spa/plugins/bluez5/bluez5-device.c:1586 +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:1831 +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:1837 +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:1819 +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 Surround" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4748 +#, c-format +msgid "%s Output" +msgstr "%s выход" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4756 +#, 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." +msgstr[2] "" +"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." +msgstr[2] "" +"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." +msgstr[2] "" +"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:1597 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Адаптер аудиогарнитуры (приёмник A2DP и HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1622 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Воспроизведение высокого качества (приёмник A2DP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1625 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Дуплекс высокого качества (источник / приёмник A2DP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1633 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Воспроизведение высокого качества (приёмник A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1635 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Дуплекс высокого качества (источник / приёмник A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1677 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Воспроизведение высокого качества (приёмник BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1681 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Вход высокого качества (источник BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1685 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Дуплекс высокого качества (источник / приёмник BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1693 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Воспроизведение высокого качества (приёмник BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1696 +msgid "High Fidelity Input (BAP Source)" +msgstr "Вход высокого качества (источник BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1699 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Дуплекс высокого качества (источник / приёмник BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1735 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Гарнитура (HSP/HFP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1740 +msgid "Headset Head Unit (HSP/HFP)" +msgstr "Гарнитура (HSP/HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1820 +#: spa/plugins/bluez5/bluez5-device.c:1825 +#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:1838 +#: spa/plugins/bluez5/bluez5-device.c:1844 +#: spa/plugins/bluez5/bluez5-device.c:1850 +#: spa/plugins/bluez5/bluez5-device.c:1856 +#: spa/plugins/bluez5/bluez5-device.c:1862 +#: spa/plugins/bluez5/bluez5-device.c:1868 +msgid "Handsfree" +msgstr "Handsfree" + +#: spa/plugins/bluez5/bluez5-device.c:1826 +msgid "Handsfree (HFP)" +msgstr "Handsfree (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1843 +msgid "Headphone" +msgstr "Наушники" + +#: spa/plugins/bluez5/bluez5-device.c:1849 +msgid "Portable" +msgstr "Портативное устройство" + +#: spa/plugins/bluez5/bluez5-device.c:1855 +msgid "Car" +msgstr "Автомобильное устройство" + +#: spa/plugins/bluez5/bluez5-device.c:1861 +msgid "HiFi" +msgstr "Hi-Fi" + +#: spa/plugins/bluez5/bluez5-device.c:1867 +msgid "Phone" +msgstr "Телефон" + +#: spa/plugins/bluez5/bluez5-device.c:1874 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:1875 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" 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 , 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] \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 , 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 \n" +"Language-Team: Slovak \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] \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/sl.po b/po/sl.po new file mode 100644 index 0000000..de3f945 --- /dev/null +++ b/po/sl.po @@ -0,0 +1,766 @@ +# Slovenian translation for PipeWire. +# Copyright (C) 2024 PipeWire's COPYRIGHT HOLDER +# This file is distributed under the same license as the PipeWire package. +# +# Martin , 2024, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: PipeWire master\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2025-01-09 15:25+0000\n" +"PO-Revision-Date: 2025-01-23 09:23+0100\n" +"Last-Translator: Martin Srebotnjak \n" +"Language-Team: Slovenian \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n" +"%100<=4 ? 2 : 3);\n" +"X-Generator: Poedit 2.2.1\n" + +#: src/daemon/pipewire.c:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [možnosti]\n" +" -h, --help Pokaži to pomoč\n" +" -v, --verbose Povečaj opisnost za eno raven\n" +" --version Pokaži različico\n" +" -c, --config Naloži prilagoditev config (privzeto " +"%s)\n" +" -P —properties Določi lastnosti konteksta\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "Medijski sistem PipeWire" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "Zaženite medijski sistem PipeWire" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Prehod do %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Lažni izhod" + +#: src/modules/module-pulse-tunnel.c:760 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Prehod za %s@%s" + +#: src/modules/module-zeroconf-discover.c:320 +msgid "Unknown device" +msgstr "Neznana naprava" + +#: src/modules/module-zeroconf-discover.c:332 +#, c-format +msgid "%s on %s@%s" +msgstr "%s na %s@%s" + +#: src/modules/module-zeroconf-discover.c:336 +#, c-format +msgid "%s on %s" +msgstr "%s na %s" + +#: src/tools/pw-cat.c:973 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [možnosti] [|-]\n" +" -h, --help Pokaži to pomoč\n" +" --version Pokaži različico\n" +" -v, --verbose Omogoči podrobno opisane operacije\n" +"\n" +"\n" + +#: src/tools/pw-cat.c:980 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Ime oddaljenega demona\n" +" --media-type Nastavitev vrste medija (privzeto " +"%s)\n" +" --media-category Nastavi kategorijo predstavnosti " +"(privzeto %s)\n" +" --media-role Nastavi vlogo predstavnosti " +"(privzeto %s)\n" +" --target Nastavi serijsko ali ime ciljnega " +"vozlišča (privzeto %s)\n" +" 0 pomeni, da se ne poveže\n" +" --latency Nastavi zakasnitev vozlišča " +"(privzeto %s)\n" +" Xunit (enota = s, ms, us, ns)\n" +" ali neposredni vzorci (256)\n" +" Hitrost je enaka tisti v izvornih " +"datotekah\n" +" -P --properties Nastavi lastnosti vozlišča\n" +"\n" + +#: src/tools/pw-cat.c:998 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate Mera vzorčenja (zahtevano za rec) " +"(privzeto %u)\n" +" --channels Število kanalov (zahteva za " +"snemanje) (privzeto %u)\n" +" --channel-map Preslikava kanalov\n" +" Ena izmed: \"Stereo\", " +"\"surround-51\",... ali\n" +" seznam imen kanalov, ločen z " +"vejico: npr. \"FL,FR\"\n" +" --format Vzorčne oblike zapisa %s (zahtevano " +"za rec) (privzeto %s)\n" +" --volume Glasnost toka 0-1.0 (privzeto %.3f)\n" +" -q --quality Kakovost prevzorčenja (0 - 15) " +"(privzeto %d)\n" +" -a, --raw neobdelan način (RAW)\n" +"\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Način predvajanja\n" +" -r, --record Način snemanja\n" +" -m, --midi Midi način\n" +" -d, --dsd Način DSD\n" +" -o, --encoded Kodiran način\n" +"\n" + +#: src/tools/pw-cli.c:2306 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [možnosti] [ukaz]\n" +" -h, --help Pokaži to pomoč\n" +" --version Pokaži različico\n" +" -d, --daemon Začni kot zaledni proces (privzeto " +"false)\n" +" -r, --remote Ime oddaljenega zalednega procesa\n" +" -m, --monitor Spremljaj dejavnosti\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:347 +msgid "Pro Audio" +msgstr "Profesionalni zvok" + +#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1795 +msgid "Off" +msgstr "Izklopljeno" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +msgid "Input" +msgstr "Vhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +msgid "Docking Station Input" +msgstr "Vhod priklopne postaje" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "Mikrofon priklopne postaje" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "Linijski vhod priklopne postaje" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "Linijski vhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:2139 +msgid "Microphone" +msgstr "Mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "Sprednji mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "Zadnji mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "Zunanji mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "Notranji mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2662 +#: spa/plugins/alsa/acp/alsa-mixer.c:2748 +msgid "Radio" +msgstr "Radio" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2663 +#: spa/plugins/alsa/acp/alsa-mixer.c:2749 +msgid "Video" +msgstr "Video" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2664 +msgid "Automatic Gain Control" +msgstr "Samodejni nadzor ojačanja" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "Brez samodejnega nadzora ojačanja" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "Ojačitev" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "Brez ojačitve" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "Ojačevalnik" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "Brez ojačevalnika" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "Ojačitev nizkih tonov" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "Brez ojačitve nizkih tonov" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:2145 +msgid "Speaker" +msgstr "Zvočnik" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "Slušalke" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "Analogni vhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "Priklopni mikrofon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "Mikrofon s slušalkami" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "Analogni izhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "Slušalke 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "Mono izhod slušalk" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "Linijsk izhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "Analogni mono izhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "Govorniki" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2757 +msgid "HDMI / DisplayPort" +msgstr "HDMI / DisplayPort" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2758 +msgid "Digital Output (S/PDIF)" +msgstr "Digitalni izhod (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "Digitalni vhod (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +msgid "Multichannel Input" +msgstr "Večkanalni vhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +msgid "Multichannel Output" +msgstr "Večkanalni izhod" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "Vhod igre" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "Izhod klepeta" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "Vhod klepeta" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "Navidezni prostorski zvok 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +msgid "Analog Mono" +msgstr "Analogni mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 +msgid "Analog Mono (Left)" +msgstr "Analogni mono (levo)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Analog Mono (Right)" +msgstr "Analogni mono (desno)" + +#. Note: Not translated to "Analog Stereo Input", because the source +#. * name gets "Input" appended to it automatically, so adding "Input" +#. * here would lead to the source name to become "Analog Stereo Input +#. * Input". The same logic applies to analog-stereo-output, +#. * multichannel-input and multichannel-output. +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +msgid "Analog Stereo" +msgstr "Analogni stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4462 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4463 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/bluez5/bluez5-device.c:2127 +msgid "Headset" +msgstr "Slušalka" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Speakerphone" +msgstr "Zvočnik telefona" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Multichannel" +msgstr "Večkanalno" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 2.1" +msgstr "Analogni prostorski zvok 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 3.0" +msgstr "Analogni prostorski zvok 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 3.1" +msgstr "Analogni prostorski zvok 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 4.0" +msgstr "Analogni prostorski zvok 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 4.1" +msgstr "Analogni prostorski zvok 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 5.0" +msgstr "Analogni prostorski zvok 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 5.1" +msgstr "Analogni prostorski zvok 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 6.0" +msgstr "Analogni prostorski zvok 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 6.1" +msgstr "Analogni prostorski zvok 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Analog Surround 7.0" +msgstr "Analogni prostorski zvok 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Analog Surround 7.1" +msgstr "Analogni prostorski zvok 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Stereo (IEC958)" +msgstr "Digitalni stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Digitalni prostorski zvok 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Digitalni prostorski zvok 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Digitalni prostorski zvok 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Digital Stereo (HDMI)" +msgstr "Digitalni stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Digitalni prostorski zvok 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 +msgid "Chat" +msgstr "Klepet" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 +msgid "Game" +msgstr "Igra" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +msgid "Analog Mono Duplex" +msgstr "Analogni mono dupleks" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Analog Stereo Duplex" +msgstr "Analogni stereo dupleks" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Digitalni stereo dupleks (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Multichannel Duplex" +msgstr "Večkanalni dupleks" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4633 +msgid "Stereo Duplex" +msgstr "Stereo dupleks" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4634 +msgid "Mono Chat + 7.1 Surround" +msgstr "Mono klepet + prostorski zvok 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4735 +#, c-format +msgid "%s Output" +msgstr "Izhod %s" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4743 +#, c-format +msgid "%s Input" +msgstr "Vhod %s" + +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajt (%lu ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[1] "" +"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajta (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[2] "" +"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajti (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[3] "" +"snd_pcm_avail() je vrnil vrednost, ki je izjemno velika: %lu bajtov (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1299 +#, c-format +msgid "" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s" +"%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s" +"%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajt (%s%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[1] "" +"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajta (%s%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[2] "" +"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajti (%s%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[3] "" +"snd_pcm_delay() je vrnil vrednost, ki je izjemno velika: %li bajtov (%s%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1346 +#, c-format +msgid "" +"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " +"%lu.\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr "" +"snd_pcm_avail_delay() je vrnil nenavadne vrednosti: zakasnitev %lu je manjša " +"kot razpoložljiva %lu.\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1389 +#, c-format +msgid "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajt (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[1] "" +"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajta (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[2] "" +"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajti (%lu " +"ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." +msgstr[3] "" +"snd_pcm_mmap_begin() je vrnil vrednost, ki je izjemno velika: %lu bajtov " +"(%lu ms).\n" +"Najverjetneje je to napaka v gonilniku ALSA »%s«. O tej težavi obvestite " +"razvijalce ALSA." + +#: spa/plugins/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(neveljavno)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "Vgrajen zvok" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1806 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Zvožni prehod (vir A2DP in HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1834 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "Pretakanje zvoka za slušne aparate (ponor ASHA)" + +#: spa/plugins/bluez5/bluez5-device.c:1874 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Predvajanje visoke ločljivosti (ponor A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1877 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1885 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Predvajanje visoke ločljivosti (ponor A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1887 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1937 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Predvajanje visoke ločljivosti (ponor BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1942 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Vhod visoke ločljivosti (vir BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1946 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Dupleks visoke ločljivosti (vir/ponor BAP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1955 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Predvajanje visoke ločljivosti (ponor BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1959 +msgid "High Fidelity Input (BAP Source)" +msgstr "Vhod visoke ločljivosti (vir BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1962 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Dupleks visoke ločljivosti (vir/ponor BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:2008 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Naglavna enota slušalk (HSP/HFP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2128 +#: spa/plugins/bluez5/bluez5-device.c:2133 +#: spa/plugins/bluez5/bluez5-device.c:2140 +#: spa/plugins/bluez5/bluez5-device.c:2146 +#: spa/plugins/bluez5/bluez5-device.c:2152 +#: spa/plugins/bluez5/bluez5-device.c:2158 +#: spa/plugins/bluez5/bluez5-device.c:2164 +#: spa/plugins/bluez5/bluez5-device.c:2170 +#: spa/plugins/bluez5/bluez5-device.c:2176 +msgid "Handsfree" +msgstr "Prostoročno telefoniranje" + +#: spa/plugins/bluez5/bluez5-device.c:2134 +msgid "Handsfree (HFP)" +msgstr "Prostoročno telefoniranje (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2151 +msgid "Headphone" +msgstr "Slušalke" + +#: spa/plugins/bluez5/bluez5-device.c:2157 +msgid "Portable" +msgstr "Prenosna naprava" + +#: spa/plugins/bluez5/bluez5-device.c:2163 +msgid "Car" +msgstr "Avtomobil" + +#: spa/plugins/bluez5/bluez5-device.c:2169 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2175 +msgid "Phone" +msgstr "Telefon" + +#: spa/plugins/bluez5/bluez5-device.c:2182 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2183 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" diff --git a/po/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 (Игор Милетић) , 2009. +# Miloš Komarčević , 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ć \n" +"Language-Team: Serbian (sr) \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] \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ć) , 2009. +# Miloš Komarčević , 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ć \n" +"Language-Team: Serbian (sr) \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] \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..b867795 --- /dev/null +++ b/po/sv.po @@ -0,0 +1,733 @@ +# Swedish translation for pipewire. +# Copyright © 2008-2024 Free Software Foundation, Inc. +# This file is distributed under the same license as the pipewire package. +# Daniel Nylander , 2008, 2012. +# Josef Andersson , 2014, 2017. +# Anders Jonsson , 2021, 2022, 2023, 2024. +# +# 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: 2024-11-05 03:27+0000\n" +"PO-Revision-Date: 2024-11-07 21:52+0100\n" +"Last-Translator: Anders Jonsson \n" +"Language-Team: Swedish \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.5\n" + +#: src/daemon/pipewire.c:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [flaggor]\n" +" -h, --help Visa denna hjälp\n" +" -v, --verbose Öka utförligheten en nivå\n" +" --version Visa version\n" +" -c, --config Läs in konfig (standard %s)\n" +" -P --properties Ställ in kontextegenskaper\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:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "Tunnel till %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Attrapputgång" + +#: src/modules/module-pulse-tunnel.c:777 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Tunnel för %s@%s" + +#: src/modules/module-zeroconf-discover.c:320 +msgid "Unknown device" +msgstr "Okänd enhet" + +#: src/modules/module-zeroconf-discover.c:332 +#, c-format +msgid "%s on %s@%s" +msgstr "%s på %s@%s" + +#: src/modules/module-zeroconf-discover.c:336 +#, c-format +msgid "%s on %s" +msgstr "%s på %s" + +#: src/tools/pw-cat.c:973 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [flaggor] [|-]\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:980 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote 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åls serienummer eller namn\n" +" (standard %s), 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:998 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate 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" +" -a, --raw RAW-läge\n" +"\n" + +#: src/tools/pw-cat.c:1016 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Uppspelningsläge\n" +" -r, --record Inspelningsläge\n" +" -m, --midi Midiläge\n" +" -d, --dsd DSD-läge\n" +" -o, --encoded Kodat läge\n" +"\n" + +#: src/tools/pw-cli.c:2318 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [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" +" -m, --monitor Övervaka aktivitet\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Professionellt ljud" + +#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1705 +msgid "Off" +msgstr "Av" + +#: 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:2026 +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:2032 +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:4456 +msgid "Analog Mono" +msgstr "Analog mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Analog mono (vänster)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Analog stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Mono" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:2014 +msgid "Headset" +msgstr "Headset" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Högtalartelefon" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Multikanal" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Analog surround 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Analog surround 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Analog surround 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Analog surround 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Analog surround 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Analog surround 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Analog surround 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Analog surround 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Analog surround 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Analog surround 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Analog surround 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Digital stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Digital surround 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Digital surround 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Digital surround 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Digital stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Digital surround 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Chatt" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Spel" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Analog mono duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Analog stereo duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Digital stereo duplex (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Multikanalduplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "Stereo duplex" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Mono Chatt + 7.1 Surround" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s-utgång" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s-ingång" + +#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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:1297 +#, 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:1344 +#, 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:1387 +#, 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:193 +msgid "Built-in Audio" +msgstr "Inbyggt ljud" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1716 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1764 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "High fidelity-uppspelning (A2DP-utgång, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1767 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "High fidelity duplex (A2DP-källa/utgång, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1775 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "High fidelity-uppspelning (A2DP-utgång)" + +#: spa/plugins/bluez5/bluez5-device.c:1777 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "High fidelity duplex (A2DP-källa/utgång)" + +#: spa/plugins/bluez5/bluez5-device.c:1827 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "High fidelity-uppspelning (BAP-utgång, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "High fidelity-ingång (BAP-källa, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1836 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "High fidelity duplex (BAP-källa/utgång, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "High fidelity-uppspelning (BAP-utgång)" + +#: spa/plugins/bluez5/bluez5-device.c:1849 +msgid "High Fidelity Input (BAP Source)" +msgstr "High fidelity-ingång (BAP-källa)" + +#: spa/plugins/bluez5/bluez5-device.c:1852 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "High fidelity duplex (BAP-källa/utgång)" + +#: spa/plugins/bluez5/bluez5-device.c:1901 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2015 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2027 +#: spa/plugins/bluez5/bluez5-device.c:2033 +#: spa/plugins/bluez5/bluez5-device.c:2039 +#: spa/plugins/bluez5/bluez5-device.c:2045 +#: spa/plugins/bluez5/bluez5-device.c:2051 +#: spa/plugins/bluez5/bluez5-device.c:2057 +#: spa/plugins/bluez5/bluez5-device.c:2063 +msgid "Handsfree" +msgstr "Handsfree" + +#: spa/plugins/bluez5/bluez5-device.c:2021 +msgid "Handsfree (HFP)" +msgstr "Handsfree (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2038 +msgid "Headphone" +msgstr "Hörlurar" + +#: spa/plugins/bluez5/bluez5-device.c:2044 +msgid "Portable" +msgstr "Bärbar" + +#: spa/plugins/bluez5/bluez5-device.c:2050 +msgid "Car" +msgstr "Bil" + +#: spa/plugins/bluez5/bluez5-device.c:2056 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2062 +msgid "Phone" +msgstr "Telefon" + +#: spa/plugins/bluez5/bluez5-device.c:2069 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2070 +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 , 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 \n" +"Language-Team: Tamil \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] \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 , 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 \n" +"Language-Team: Telugu \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] \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..49eb6ea --- /dev/null +++ b/po/tr.po @@ -0,0 +1,706 @@ +# Turkish translation for PipeWire. +# Copyright (C) 2014-2024 PipeWire's COPYRIGHT HOLDER +# This file is distributed under the same license as the PipeWire package. +# +# Necdet Yücel , 2014. +# Kaan Özdinçer , 2014. +# Muhammet Kara , 2015, 2016, 2017. +# Oğuz Ersen , 2021-2022. +# Sabri Ünal , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PipeWire master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-25 03:43+0300\n" +"PO-Revision-Date: 2024-02-25 03:49+0300\n" +"Last-Translator: Sabri Ünal \n" +"Language-Team: Türkçe \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: Poedit 3.4.2\n" + +#: src/daemon/pipewire.c:26 +#, 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/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "PipeWire Ortam Sistemi" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "PipeWire Ortam Sistemini Başlat" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "%s%s%s tüneli" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Temsili Çıkış" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "%s@%s için tünel" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "Bilinmeyen aygıt" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s, %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s, %s" + +#: src/tools/pw-cat.c:991 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [seçenekler] [|-]\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: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 serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote Uzak art alan hizmeti 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 hedefi seri ya da adını 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: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 Ö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:1033 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback Çalma kipi\n" +" -r, --record Kayıt kipi\n" +" -m, --midi Midi kipi\n" +" -d, --dsd DSD kipi\n" +" -o, --encoded Kodlanmış kip\n" +"\n" + +#: src/tools/pw-cli.c:2252 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [seçenekler] [komut]\n" +" -h, --help Bu yardımı göster\n" +" --version Sürümü göster\n" +" -d, --daemon Art alan hizmeti olarak başlat " +"(Öntanımlı olarak yanlış)\n" +" -r, --remote Uzak arka plan programı adı\n" +" -m, --monitor Etkinliği izle\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "Profesyonel Ses" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "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:1989 +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:1995 +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:4456 +msgid "Analog Mono" +msgstr "Analog Tek Kanallı" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "Analog Tek Kanallı (Sol)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "Analog Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "Tek Kanallı" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "Kulaklık" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "Hoparlör" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "Çok kanallı" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "Analog Çevresel Ses 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "Analog Çevresel Ses 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "Analog Çevresel Ses 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "Analog Çevresel Ses 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "Analog Çevresel Ses 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "Analog Çevresel Ses 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "Analog Çevresel Ses 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "Analog Çevresel Ses 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "Analog Çevresel Ses 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "Analog Çevresel Ses 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "Analog Çevresel Ses 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "Sayısal Stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "Sayısal Stereo (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "Sayısal Çevresel Ses 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "Sohbet" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "Oyun" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "Analog Tek Kanallı İkili" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "Analog İkili Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "Sayısal İkili Stereo (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "Çok Kanallı İkili" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "İkili Stereo" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s Çıkışı" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s Girişi" + +#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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:1286 +#, 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:1333 +#, 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:1376 +#, 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:193 +msgid "Built-in Audio" +msgstr "Dahili Ses" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "Modem" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, 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:1763 +#, 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:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, 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:1828 +#, 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:1832 +#, 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:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Yüksek Kaliteli Çalma (BAP Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "Yüksek Kaliteli Giriş (BAP Kaynak)" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, 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:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "Ahizesiz" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "Ahizesiz (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "Kulaklık" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "Taşınabilir" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "Araba" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "Yüksek Kalite" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "Telefon" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..e251b82 --- /dev/null +++ b/po/uk.po @@ -0,0 +1,779 @@ +# Copyright (C) 2009 Free Software Foundation, Inc. +# This file is distributed under the same license as the pipewire package. +# +# Yuri Chornoivan , 2009-2021, 2022, 2023. +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issu" +"es\n" +"POT-Creation-Date: 2023-02-06 15:27+0000\n" +"PO-Revision-Date: 2023-02-11 17:42+0200\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \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:179 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:179 +#, 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:695 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "Тунель для %s@%s" + +#: src/modules/module-zeroconf-discover.c:335 +msgid "Unknown device" +msgstr "Невідомий пристрій" + +#: src/modules/module-zeroconf-discover.c:347 +#, c-format +msgid "%s on %s@%s" +msgstr "%s на %s@%s" + +#: src/modules/module-zeroconf-discover.c:351 +#, c-format +msgid "%s on %s" +msgstr "%s на %s" + +#: src/tools/pw-cat.c:940 +#, c-format +msgid "" +"%s [options] [|-]\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:947 +#, 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" +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote назва віддаленої фонової служби\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:965 +#, 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:982 +#| msgid "" +#| " -p, --playback Playback mode\n" +#| " -r, --record Recording mode\n" +#| " -m, --midi Midi mode\n" +#| " -d, --dsd DSD mode\n" +#| "\n" +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded\t\t\t Encoded mode\n" +"\n" +msgstr "" +" -p, --playback режим відтворення\n" +" -r, --record режим запису\n" +" -m, --midi режим MIDI\n" +" -d, --dsd режим DSD\n" +" -o, --encoded\t\t\t закодований режим\n" +"\n" + +#: src/tools/pw-cli.c:2236 +#, 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" +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [параметри] [команда]\n" +" -h, --help вивести довідку\n" +" --version вивести дані щодо версії\n" +" -d, --daemon запустити як фонову службу (типово, " +"false)\n" +" -r, --remote назва віддаленої фонової служби\n" +" -m, --monitor спостерігати за діями\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:323 +msgid "Pro Audio" +msgstr "Професійний звук" + +#: spa/plugins/alsa/acp/acp.c:447 spa/plugins/alsa/acp/alsa-mixer.c:4648 +#: spa/plugins/bluez5/bluez5-device.c:1303 +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:1536 +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:1542 +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:1524 +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." +msgstr[2] "" +"Функція 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." +msgstr[2] "" +"Функція 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." +msgstr[2] "" +"Функція 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:1314 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "Звуковий шлюз (джерело A2DP і HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1339 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "Високоточне відтворення (приймач A2DP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1342 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "Двобічний високоточний обмін (джерело/приймач A2DP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1350 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "Високоточне відтворення (приймач A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1352 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "Двобічний високоточний обмін (джерело/приймач A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:1391 +#, c-format +#| msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Високоточне відтворення (приймач BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1395 +#, c-format +#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Двобічний високоточний вхід (джерело BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1399 +#, c-format +#| msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Двобічний високоточний обмін (джерело/приймач BAP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1407 +#| msgid "High Fidelity Playback (A2DP Sink)" +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Високоточне відтворення (приймач BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1410 +#| msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgid "High Fidelity Input (BAP Source)" +msgstr "Двобічний високоточний вхід (джерело BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1413 +#| msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Двобічний високоточний обмін (джерело/приймач BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1441 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "Головний модуль гарнітури (HSP/HFP, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1446 +msgid "Headset Head Unit (HSP/HFP)" +msgstr "Головний модуль гарнітури (HSP/HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1525 +#: spa/plugins/bluez5/bluez5-device.c:1530 +#: spa/plugins/bluez5/bluez5-device.c:1537 +#: spa/plugins/bluez5/bluez5-device.c:1543 +#: spa/plugins/bluez5/bluez5-device.c:1549 +#: spa/plugins/bluez5/bluez5-device.c:1555 +#: spa/plugins/bluez5/bluez5-device.c:1561 +#: spa/plugins/bluez5/bluez5-device.c:1567 +#: spa/plugins/bluez5/bluez5-device.c:1573 +msgid "Handsfree" +msgstr "Hands-Free пристрій" + +#: spa/plugins/bluez5/bluez5-device.c:1531 +#| msgid "Handsfree" +msgid "Handsfree (HFP)" +msgstr "Hands-Free пристрій (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:1548 +msgid "Headphone" +msgstr "Навушники" + +#: spa/plugins/bluez5/bluez5-device.c:1554 +msgid "Portable" +msgstr "Портативна акустика" + +#: spa/plugins/bluez5/bluez5-device.c:1560 +msgid "Car" +msgstr "Автомобільна акустика" + +#: spa/plugins/bluez5/bluez5-device.c:1566 +msgid "HiFi" +msgstr "Hi-Fi" + +#: spa/plugins/bluez5/bluez5-device.c:1572 +msgid "Phone" +msgstr "Телефон" + +#: spa/plugins/bluez5/bluez5-device.c:1579 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:1580 +#| msgid "Bluetooth" +msgid "Bluetooth (HFP)" +msgstr "Bluetooth (HFP)" diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 0000000..1022f5d --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,704 @@ +# Simplified Chinese translation for PipeWire. +# Copyright (C) 2008 PULSEAUDIO COPYRIGHT HOLDER +# This file is distributed under the same license as the pipewire package. +# 闫丰刚 , 2008, 2009. +# Leah Liu , 2009, 2012. +# Cheng-Chia Tseng , 2010, 2012. +# Frank Hill , 2015. +# Mingye Wang (Arthur2e5) , 2015. +# lumingzh , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: pipewire.master-tx\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2024-09-09 16:36+0000\n" +"PO-Revision-Date: 2024-10-08 09:41+0800\n" +"Last-Translator: lumingzh \n" +"Language-Team: Chinese (China) \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: Gtranslator 47.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: src/daemon/pipewire.c:29 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" +msgstr "" +"%s [选项]\n" +" -h, --help 显示此帮助信息\n" +" -v, --verbose 增加一级的详尽程度\n" +" --version 显示版本\n" +" -c, --config 加载配置 (默认 %s)\n" +" -P --properties 设置上下文属性\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:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "至 %s%s%s 的隧道" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "虚拟输出" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "用于 %s@%s 的隧道" + +#: src/modules/module-zeroconf-discover.c:318 +msgid "Unknown device" +msgstr "未知设备" + +#: src/modules/module-zeroconf-discover.c:330 +#, c-format +msgid "%s on %s@%s" +msgstr "%2$s@%3$s 上的 %1$s" + +#: src/modules/module-zeroconf-discover.c:334 +#, c-format +msgid "%s on %s" +msgstr "%2$s 上的 %1$s" + +#: src/tools/pw-cat.c:996 +#, c-format +msgid "" +"%s [options] [|-]\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:1003 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +" -R, --remote 远程守护程序名\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" +" -P --properties 设置节点属性\n" +"\n" + +#: src/tools/pw-cat.c:1021 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +" -a, --raw RAW mode\n" +"\n" +msgstr "" +" --rate 采样率 (录制模式需要) (默认 %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" +" -a, --raw 原生模式\n" +"\n" + +#: src/tools/pw-cat.c:1039 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +" -p, --playback 回放模式\n" +" -r, --record 录制模式\n" +" -m, --midi Midi 模式\n" +" -d, --dsd DSD 模式\n" +" -o, --encoded 编码模式\n" +"\n" + +#: src/tools/pw-cli.c:2285 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [选项] [命令]\n" +" -h, --help 显示此帮助信息\n" +" --version 显示版本\n" +" -d, --daemon 以守护程序方式启动 (默认关闭)\n" +" -m, --monitor 监视器活动\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "专业音频" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "关" + +#: spa/plugins/alsa/acp/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:1989 +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:1995 +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:4456 +msgid "Analog Mono" +msgstr "模拟单声道" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "模拟单声道 (左声道)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +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:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "模拟立体声" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "单声道" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "立体声" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "耳机" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "扬声麦克风" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "多声道" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "模拟环绕 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "模拟环绕 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "模拟环绕 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "模拟环绕 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "模拟环绕 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "模拟环绕 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "模拟环绕 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "模拟环绕 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "模拟环绕 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "模拟环绕 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "模拟环绕 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "数字立体声 (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "数字环绕 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "数字环绕 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "数字环绕 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "数字立体声 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "数字环绕 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "语音" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "游戏" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "模拟单声道双工" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "模拟立体声双工" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "数字立体声双工 (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "多声道双工" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "模拟立体声双工" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "单声道语音 + 7.1 环绕声" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "%s 输出" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "%s 输入" + +#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"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:1297 +#, 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:1344 +#, 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:1387 +#, 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/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(无效)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "内置音频" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "调制解调器" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "高保真回放 (A2DP 信宿, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1763 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "高保真回放 (A2DP 信宿)" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "高保真双工 (A2DP 信源/信宿)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "高保真回放 (BAP 信宿, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1828 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "高保真输入 (BAP 信源, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "高保真双工 (BAP 信源/信宿, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "高保真回放 (BAP 信宿)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "高保真输入 (BAP 信源)" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "高保真双工 (BAP 信源/信宿)" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "头戴式耳机单元 (HSP/HFP, 编码 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "免手操作" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "免手操作 (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "头戴耳机" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "便携式" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "车内" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "高保真" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "电话" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "蓝牙" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "蓝牙 (HFP)" + +#~ msgid "Headset Head Unit (HSP/HFP)" +#~ msgstr "头戴式耳机单元 (HSP/HFP)" diff --git a/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 , 2010, 2012. +# pan93412 , 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 \n" +"Language-Team: Chinese \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] \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..6aa36df --- /dev/null +++ b/spa/examples/adapter-control.c @@ -0,0 +1,1066 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +/* + [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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static SPA_LOG_IMPL(default_log); + +#define MIN_LATENCY 1024 +#define CONTROL_BUFFER_SIZE 32768 + +#define DEFAULT_RAMP_SAMPLES (64*1*1024) +#define DEFAULT_RAMP_STEP_SAMPLES 200 + +#define DEFAULT_RAMP_TIME 2000 // 2 seconds +#define DEFAULT_RAMP_STEP_TIME 5000 // 5 milli seconds + +#define DEFAULT_DEVICE "hw:0,0" + +#define LINEAR "linear" +#define CUBIC "cubic" +#define DEFAULT_SCALE SPA_AUDIO_VOLUME_RAMP_LINEAR + +#define NON_NATIVE "non-native" +#define NATIVE "native" +#define DEFAULT_MODE NON_NATIVE + + +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; + + const char *alsa_device; + + const char *mode; + enum spa_audio_volume_ramp_scale scale; + + uint32_t volume_ramp_samples; + uint32_t volume_ramp_step_samples; + uint32_t volume_ramp_time; + uint32_t volume_ramp_step_time; + + bool running; + pthread_t thread; +}; + +static int load_handle (struct data *data, struct spa_handle **handle, const + char *lib, const char *name, struct spa_dict *info) +{ + 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, + info, 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; +} + +static int init_data(struct data *data) +{ + int res; + const char *str; + struct spa_handle *handle = NULL; + struct spa_dict_item items [2]; + struct spa_dict info; + 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); + + /* enable the debug messages in SPA */ + items [0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "true"); + info = SPA_DICT_INIT(items, 1); + if ((res = load_handle (data, &handle, "support/libspa-support.so", + SPA_NAME_SUPPORT_LOG, &info)) < 0) + return res; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Log, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data->log = iface; + 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, NULL)) < 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, NULL)) < 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 get_ramp_samples(struct data *data) +{ + int samples = -1; + if (data->volume_ramp_samples) + samples = data->volume_ramp_samples; + else if (data->volume_ramp_time) { + samples = (data->volume_ramp_time * 48000) / 1000; + } + if (!samples) + samples = -1; + + return samples; +} + +static int get_ramp_step_samples(struct data *data) +{ + int samples = -1; + if (data->volume_ramp_step_samples) + samples = data->volume_ramp_step_samples; + else if (data->volume_ramp_step_time) { + /* convert the step time which is in nano seconds to seconds */ + samples = (data->volume_ramp_step_time / 1000) * (48000 / 1000); + } + if (!samples) + samples = -1; + + return samples; +} + +static double get_volume_at_scale(struct data *data) +{ + if (data->scale == SPA_AUDIO_VOLUME_RAMP_LINEAR) + return data->volume_accum; + else if (data->scale == SPA_AUDIO_VOLUME_RAMP_CUBIC) + return (data->volume_accum * data->volume_accum * data->volume_accum); + + return 0.0; +} + +static int fade_in(struct data *data) +{ + printf("fading in\n"); + if (spa_streq (data->mode, NON_NATIVE)) { + struct spa_pod_builder b; + struct spa_pod_frame f[1]; + void *buffer = data->control_buffer->datas[0].data; + int ramp_samples = get_ramp_samples(data); + int ramp_step_samples = get_ramp_step_samples(data); + double step_size = ((double) ramp_step_samples / (double) ramp_samples); + uint32_t buffer_size = data->control_buffer->datas[0].maxsize; + data->control_buffer->datas[0].chunk[0].size = buffer_size; + + spa_pod_builder_init(&b, buffer, buffer_size); + spa_pod_builder_push_sequence(&b, &f[0], 0); + data->volume_offs = 0; + do { + // printf("volume level %f offset %d\n", get_volume_at_scale(data), data->volume_offs); + 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(get_volume_at_scale(data))); + data->volume_accum += step_size; + data->volume_offs += ramp_step_samples; + } while (data->volume_accum < 1.0); + spa_pod_builder_pop(&b, &f[0]); + } + else { + struct spa_pod_builder b; + struct spa_pod *props; + int res = 0; + uint8_t buffer[1024]; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, SPA_POD_Float(1.0), + SPA_PROP_volumeRampSamples, SPA_POD_Int(data->volume_ramp_samples), + SPA_PROP_volumeRampStepSamples, SPA_POD_Int(data->volume_ramp_step_samples), + SPA_PROP_volumeRampTime, SPA_POD_Int(data->volume_ramp_time), + SPA_PROP_volumeRampStepTime, SPA_POD_Int(data->volume_ramp_step_time), + SPA_PROP_volumeRampScale, SPA_POD_Id(data->scale)); + if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_Props, 0, props)) < 0) { + printf("can't call volramp set params %d\n", res); + return res; + } + } + + return 0; +} + +static int fade_out(struct data *data) +{ + printf("fading out\n"); + if (spa_streq (data->mode, NON_NATIVE)) { + struct spa_pod_builder b; + struct spa_pod_frame f[1]; + int ramp_samples = get_ramp_samples(data); + int ramp_step_samples = get_ramp_step_samples(data); + double step_size = ((double) ramp_step_samples / (double) ramp_samples); + + + 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; + + spa_pod_builder_init(&b, buffer, buffer_size); + spa_pod_builder_push_sequence(&b, &f[0], 0); + data->volume_offs = ramp_step_samples; + do { + // printf("volume level %f offset %d\n", get_volume_at_scale(data), data->volume_offs); + 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(get_volume_at_scale(data))); + data->volume_accum -= step_size; + data->volume_offs += ramp_step_samples; + } while (data->volume_accum > 0.0); + spa_pod_builder_pop(&b, &f[0]); + } else { + struct spa_pod_builder b; + uint8_t buffer[1024]; + struct spa_pod *props; + int res = 0; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, SPA_POD_Float(0.0), + SPA_PROP_volumeRampSamples, SPA_POD_Int(data->volume_ramp_samples), + SPA_PROP_volumeRampStepSamples, SPA_POD_Int(data->volume_ramp_step_samples), + SPA_PROP_volumeRampTime, SPA_POD_Int(data->volume_ramp_time), + SPA_PROP_volumeRampStepTime, SPA_POD_Int(data->volume_ramp_step_time), + SPA_PROP_volumeRampScale, SPA_POD_Id(data->scale)); + if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_Props, 0, props)) < 0) { + printf("can't call volramp set params %d\n", res); + return res; + } + } + + return 0; +} + +static void do_fade(struct data *data) +{ + if (spa_streq (data->mode, NON_NATIVE)) { + 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); + + if (spa_streq (data->mode, NON_NATIVE)) { + 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; + int runway = (get_ramp_samples(data) / 1024); + + /* 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 > (runway * 2)) + 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) +{ + 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; + float initial_volume = 0.0; + + 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; + } + printf("created source follower node %p\n", data->source_follower_node); + + /* 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 source 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; + } + printf("created source adapter node %p\n", data->source_node); + + /* 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; + } + printf("created sink follower node %p\n", data->sink_follower_node); + + /* 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; + } + printf("created sink adapter node %p\n", data->sink_node); + + /* 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(data->alsa_device), + 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; + } + printf("Selected (%s) alsa device\n", data->alsa_device); + + if (!data->start_fade_in) + initial_volume = 1.0; + + /* 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); + + if (spa_streq (data->mode, NON_NATIVE)) + 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)); + else + 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)); + + + 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; + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, SPA_POD_Float(initial_volume)); + if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_Props, 0, props)) < 0) { + printf("can't configure initial volume %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; + } + printf("set io buffers on port 0 of source node %p\n", data->source_node); + + + 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; + } + printf("set io buffers on port 0 of sink node %p\n", data->sink_node); + + /* set io position and clock on source and sink nodes */ + data->position.clock.target_rate = SPA_FRACTION(1, 48000); + data->position.clock.target_duration = 1024; + data->position.clock.rate = data->position.clock.target_rate; + data->position.clock.duration = data->position.clock.target_duration; + 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; + } + + if (spa_streq (data->mode, NON_NATIVE)) { + /* 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) { + printf("can't set format on source node: %d\n", res); + return res; + } + if ((res = spa_node_port_set_param(data->sink_node, + SPA_DIRECTION_INPUT, 0, SPA_PARAM_Format, 0, param)) < 0) { + printf("can't set format on source node: %d\n", res); + return res; + } + + if (spa_streq (data->mode, NON_NATIVE)) { + 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) { + printf("can't set format on control port of source node: %d\n", res); + 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, ¶m, &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; + printf("allocated and assigned buffer (%zu) to source node %p\n", buffer_size, data->source_node); + if ((res = spa_node_port_use_buffers(data->sink_node, + SPA_DIRECTION_INPUT, 0, 0, data->source_buffers, 1)) < 0) + return res; + printf("allocated and assigned buffers to sink node %p\n", data->sink_node); + + if (spa_streq (data->mode, NON_NATIVE)) { + /* 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; + printf("allocated and assigned control buffers(%d) to sink node %p\n", CONTROL_BUFFER_SIZE, data->sink_node); + } + + 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); + printf("Source node started\n"); + if ((res = spa_node_send_command(data->sink_node, &cmd)) < 0) + printf("got error %d\n", res); + printf("sink node started\n"); + + 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); +} + +static const char *getscale(uint32_t scale) +{ + const char *scale_s = NULL; + + if (scale == SPA_AUDIO_VOLUME_RAMP_LINEAR) + scale_s = LINEAR; + else if (scale == SPA_AUDIO_VOLUME_RAMP_CUBIC) + scale_s = CUBIC; + + return scale_s; +} +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" + " -d, --alsa-device ALSA device(\"aplay -l\" for more info) to play the samples on(default %s)\n" + " -m, --mode Volume Ramp Mode(\"NonNative\"(via Control Port) \"Native\" (via Volume Ramp Params of AudioAdapter plugin)) (default %s)\n" + " -s, --ramp-samples SPA_PROP_volumeRampSamples(Samples to ramp the volume over)(default %d)\n" + " -a, --ramp-step-samples SPA_PROP_volumeRampStepSamples(Step or incremental Samples to ramp the volume over)(default %d)\n" + " -t, --ramp-time SPA_PROP_volumeRampTime(Time to ramp the volume over in msec)(default %d)\n" + " -i, --ramp-step-time SPA_PROP_volumeRampStepTime(Step or incremental Time to ramp the volume over in nano sec)(default %d)\n" + " -c, --scale SPA_PROP_volumeRampScale(the scale or graph to used to ramp the volume)(\"linear\" or \"cubic\")(default %s)\n" + "examples:\n" + "adapter-control\n" + "-->when invoked with out any params, ramps volume with default values\n" + "adapter-control --ramp-samples=70000, rest of the parameters are defaults\n" + "-->ramps volume over 70000 samples(it is 1.45 seconds)\n" + "adapter-control --alsa-device=hw:0,0 --ramp-samples=70000\n" + "-->ramps volume on \"hw:0,0\" alsa device over 70000 samples\n" + "adapter-control --alsa-device=hw:0,0 --ramp-samples=70000 --mode=native\n" + "-->ramps volume on \"hw:0,0\" alsa device over 70000 samples in native mode\n" + "adapter-control --alsa-device=hw:0,0 --ramp-time=1000 --mode=native\n" + "-->ramps volume on \"hw:0,0\" alsa device over 1000 msec in native mode\n" + "adapter-control --alsa-device=hw:0,0 --ramp-time=1000 --ramp-step-time=5000 --mode=native\n" + "-->ramps volume on \"hw:0,0\" alsa device over 1000 msec in steps of 5000 nano seconds(5 msec)in native mode\n" + "adapter-control --alsa-device=hw:0,0 --ramp-samples=70000 --ramp-step-samples=200 --mode=native\n" + "-->ramps volume on \"hw:0,0\" alsa device over 70000 samples with a step size of 200 samples in native mode\n" + "adapter-control --alsa-device=hw:1,0 --scale=linear\n" + "-->ramps volume on \"hw:1,0\" in linear volume scale, one can leave choose to not use the linear scale here as it is the default\n" + "adapter-control --alsa-device=hw:1,0 --ramp-samples=70000 --scale=cubic\n" + "-->ramps volume on \"hw:1,0\" alsa device over 70000 samples deploying cubic volume scale\n" + "adapter-control --alsa-device=hw:1,0 --ramp-samples=70000 --mode=native --scale=cubic\n" + "-->ramps volume on \"hw:1,0\" alsa device over 70000 samples deploying cubic volume scale in native mode\n" + "adapter-control --alsa-device=hw:1,0 --ramp-time=3000 --scale=cubic --mode=native\n" + "-->ramps volume on \"hw:1,0\" alsa device over 3 seconds samples with a step size of 200 samples in native mode\n", + name, + DEFAULT_DEVICE, + DEFAULT_MODE, + DEFAULT_RAMP_SAMPLES, + DEFAULT_RAMP_STEP_SAMPLES, + DEFAULT_RAMP_TIME, + DEFAULT_RAMP_STEP_TIME, + getscale(DEFAULT_SCALE)); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res = 0, c; + + /* default values*/ + data.volume_ramp_samples = DEFAULT_RAMP_SAMPLES; + data.volume_ramp_step_samples = DEFAULT_RAMP_STEP_SAMPLES; + data.alsa_device = DEFAULT_DEVICE; + data.mode = DEFAULT_MODE; + data.scale = DEFAULT_SCALE; + + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "alsa-device", required_argument, NULL, 'd' }, + { "mode", required_argument, NULL, 'm' }, + { "ramp-samples", required_argument, NULL, 's' }, + { "ramp-time", required_argument, NULL, 't' }, + { "ramp-step-samples", required_argument, NULL, 'a' }, + { "ramp-step-time", required_argument, NULL, 'i' }, + { "scale", required_argument, NULL, 'c' }, + { NULL, 0, NULL, 0} + }; + + setlocale(LC_ALL, ""); + + while ((c = getopt_long(argc, argv, "hdmstiac:", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(&data, argv[0], false); + return 0; + case 'm': + if (!spa_streq (optarg, NATIVE) && !spa_streq (optarg, NON_NATIVE)) + printf("Invalid Mode(\"%s\"), using default(\"%s\")\n", optarg, DEFAULT_MODE); + else + data.mode = optarg; + break; + case 'c': + if (!spa_streq (optarg, LINEAR) && !spa_streq (optarg, CUBIC)) + printf("Invalid Scale(\"%s\"), using default(\"%s\")\n", optarg, + getscale(DEFAULT_SCALE)); + else + if (spa_streq (optarg, LINEAR)) + data.scale = SPA_AUDIO_VOLUME_RAMP_LINEAR; + else if (spa_streq (optarg, CUBIC)) + data.scale = SPA_AUDIO_VOLUME_RAMP_CUBIC; + break; + case 'd': + data.alsa_device = optarg; + break; + case 's': + data.volume_ramp_samples = atoi(optarg); + break; + case 't': + data.volume_ramp_time = atoi(optarg); + if (!data.volume_ramp_step_time) + data.volume_ramp_step_time = DEFAULT_RAMP_STEP_TIME; + data.volume_ramp_samples = 0; + data.volume_ramp_step_samples = 0; + break; + case 'a': + data.volume_ramp_step_samples = atoi(optarg); + break; + case 'i': + data.volume_ramp_step_time = atoi(optarg); + break; + default: + show_help(&data, argv[0], true); + return -1; + } + } + + + /* 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)) < 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; + } + + printf("using %s mode\n", data.mode); + if (data.volume_ramp_samples && data.volume_ramp_step_samples) + printf("using %d samples with a step size of %d samples to ramp volume at %s scale\n", + data.volume_ramp_samples, data.volume_ramp_step_samples, getscale(data.scale)); + else if (data.volume_ramp_time && data.volume_ramp_step_time) + printf("using %d msec with a step size of %d msec to ramp volume at %s scale\n", + data.volume_ramp_time, (data.volume_ramp_step_time/1000), getscale(data.scale)); + + 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..f4755fa --- /dev/null +++ b/spa/examples/example-control.c @@ -0,0 +1,540 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + +#include + +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; +} + +static 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..d6e9f80 --- /dev/null +++ b/spa/examples/local-libcamera.c @@ -0,0 +1,516 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* @author Raghavendra Rao Sidlagatta */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Example using libspa-libcamera, with only \ref api_spa + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 640 +#define HEIGHT 480 + +static SPA_LOG_IMPL(default_log); + +#define MAX_BUFFERS 8 +#define LOOP_TIMEOUT_MS 100 +#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, PROT_READ, + MAP_PRIVATE, datas[0].fd, datas[0].mapoffset); + if (map == MAP_FAILED) + return -errno; + sdata = map; + } else if (datas[0].type == SPA_DATA_MemPtr) { + map = NULL; + sdata = datas[0].data; + } else + return -EIO; + + sstride = datas[0].chunk->stride; + + for (i = 0; i < HEIGHT; i++) { + src = ((uint8_t *) sdata + i * sstride); + dst = ((uint8_t *) ddata + i * dstride); + memcpy(dst, src, SPA_MIN(sstride, dstride)); + } + SDL_UnlockTexture(data->texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (map) + munmap(map, datas[0].maxsize); + } + + if ((res = spa_node_process(data->source)) < 0) + printf("got process error %d\n", res); + + return 0; +} + +static const struct spa_node_callbacks source_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = on_source_ready, +}; + +static int make_nodes(struct data *data, 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(struct data *data) +{ + int res; + struct spa_command cmd; + SDL_Event event; + + printf("starting...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); + + data->running = true; + + while (data->running) { + // must be called from the thread that created the renderer + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + data->running = false; + break; + } + } + + // small timeout to make sure we don't starve the SDL loop + spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS); + } + + printf("pausing...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + 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); + loop(&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..46f224e --- /dev/null +++ b/spa/examples/local-v4l2.c @@ -0,0 +1,512 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Example using libspa-v4l2, with only \ref api_spa + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static SPA_LOG_IMPL(default_log); + +#define MAX_BUFFERS 8 +#define LOOP_TIMEOUT_MS 100 + +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, PROT_READ, + MAP_PRIVATE, datas[0].fd, datas[0].mapoffset); + if (map == MAP_FAILED) + return -errno; + sdata = map; + } else if (datas[0].type == SPA_DATA_MemPtr) { + map = NULL; + sdata = datas[0].data; + } else + return -EIO; + + sstride = datas[0].chunk->stride; + + for (i = 0; i < 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); + } + + 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(struct data *data) +{ + int res; + struct spa_command cmd; + SDL_Event event; + + printf("starting...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); + + data->running = true; + + while (data->running) { + // must be called from the thread that created the renderer + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + data->running = false; + break; + } + } + + // small timeout to make sure we don't starve the SDL loop + spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS); + } + + printf("pausing...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + 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); + loop(&data); + spa_loop_control_leave(data.control); + + SDL_DestroyRenderer(data.renderer); + + return 0; +} diff --git a/spa/examples/local-videotestsrc.c b/spa/examples/local-videotestsrc.c new file mode 100644 index 0000000..3743e24 --- /dev/null +++ b/spa/examples/local-videotestsrc.c @@ -0,0 +1,535 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* @author Raghavendra Rao Sidlagatta */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Example using libspa-videotestsrc, with only \ref api_spa + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WIDTH 640 +#define HEIGHT 480 + +static SPA_LOG_IMPL(default_log); + +#define MAX_BUFFERS 8 +#define LOOP_TIMEOUT_MS 100 +#define USE_BUFFER true + +struct buffer { + struct spa_buffer buffer; + struct spa_meta metas[1]; + struct spa_meta_header header; + struct spa_data datas[1]; + struct spa_chunk chunks[1]; + SDL_Texture *texture; +}; + +struct data { + const char *plugin_dir; + struct spa_log *log; + struct spa_system *system; + struct spa_loop *loop; + struct spa_loop_control *control; + struct spa_loop_utils *loop_utils; + + struct spa_support support[6]; + uint32_t n_support; + + struct spa_node *source; + struct spa_hook listener; + struct spa_io_buffers source_output[1]; + + SDL_Renderer *renderer; + SDL_Window *window; + SDL_Texture *texture; + + bool use_buffer; + + bool running; + pthread_t thread; + + struct spa_buffer *bp[MAX_BUFFERS]; + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + + char *path = NULL; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) { + return -ENOMEM; + } + if ((hnd = dlopen(path, RTLD_NOW)) == NULL) { + printf("can't load %s: %s\n", path, dlerror()); + free(path); + return -errno; + } + free(path); + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + return -errno; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + NULL, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + return res; + } + return 0; + } + return -EBADF; +} + +static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name) +{ + struct spa_handle *handle = NULL; + void *iface; + int res; + + if ((res = load_handle(data, &handle, lib, name)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + *node = iface; + return 0; +} + +static int on_source_ready(void *_data, int status) +{ + struct data *data = _data; + int res; + struct buffer *b; + void *sdata, *ddata; + int sstride, dstride; + int i; + uint8_t *src, *dst; + struct spa_data *datas; + struct spa_io_buffers *io = &data->source_output[0]; + + if (io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= MAX_BUFFERS) + return -EINVAL; + + b = &data->buffers[io->buffer_id]; + io->status = SPA_STATUS_NEED_DATA; + + datas = b->buffer.datas; + + if (b->texture) { + SDL_Texture *texture = b->texture; + + SDL_UnlockTexture(texture); + if (SDL_RenderClear(data->renderer) < 0) { + fprintf(stderr, "Couldn't render clear: %s\n", SDL_GetError()); + return -EIO; + } + if (SDL_RenderCopy(data->renderer, texture, NULL, NULL) < 0) { + fprintf(stderr, "Couldn't render copy: %s\n", SDL_GetError()); + return -EIO; + } + SDL_RenderPresent(data->renderer); + + if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + + datas[0].data = sdata; + } else { + uint8_t *map; + + if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + sdata = datas[0].data; + if (datas[0].type == SPA_DATA_MemFd || + datas[0].type == SPA_DATA_DmaBuf) { + map = mmap(NULL, datas[0].maxsize, PROT_READ, + MAP_PRIVATE, datas[0].fd, datas[0].mapoffset); + if (map == MAP_FAILED) + return -errno; + sdata = map; + } else if (datas[0].type == SPA_DATA_MemPtr) { + map = NULL; + sdata = datas[0].data; + } else + return -EIO; + + sstride = datas[0].chunk->stride; + + for (i = 0; i < HEIGHT; i++) { + src = ((uint8_t *) sdata + i * sstride); + dst = ((uint8_t *) ddata + i * dstride); + memcpy(dst, src, SPA_MIN(sstride, dstride)); + } + SDL_UnlockTexture(data->texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (map) + munmap(map, datas[0].maxsize); + } + + if ((res = spa_node_process(data->source)) < 0) + printf("got process error %d\n", res); + + return 0; +} + +static const struct spa_node_callbacks source_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = on_source_ready, +}; + +static int make_nodes(struct data *data, uint32_t pattern) +{ + int res; + struct spa_pod *props; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[256]; + uint32_t index; + + if ((res = + make_node(data, &data->source, + "videotestsrc/libspa-videotestsrc.so", + "videotestsrc")) < 0) { + printf("can't create videotestsrc: %d\n", res); + return res; + } + + spa_node_set_callbacks(data->source, &source_callbacks, data); + + index = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_enum_params_sync(data->source, SPA_PARAM_Props, + &index, NULL, &props, &b)) == 1) { + spa_debug_pod(0, NULL, props); + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_patternType, SPA_POD_Int(pattern)); + + if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0) + printf("got set_props error %d\n", res); + + return res; +} + +static int setup_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + + data->bp[i] = &b->buffer; + + b->texture = NULL; + + b->buffer.metas = b->metas; + b->buffer.n_metas = 1; + b->buffer.datas = b->datas; + b->buffer.n_datas = 1; + + b->header.flags = 0; + b->header.seq = 0; + b->header.pts = 0; + b->header.dts_offset = 0; + b->metas[0].type = SPA_META_Header; + b->metas[0].data = &b->header; + b->metas[0].size = sizeof(b->header); + + b->datas[0].type = SPA_DATA_DmaBuf; + b->datas[0].flags = 0; + b->datas[0].fd = -1; + b->datas[0].mapoffset = 0; + b->datas[0].maxsize = 0; + b->datas[0].data = NULL; + b->datas[0].chunk = &b->chunks[0]; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = 0; + b->datas[0].chunk->stride = 0; + } + data->n_buffers = MAX_BUFFERS; + return 0; +} + +static int sdl_alloc_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + SDL_Texture *texture; + void *ptr; + int stride; + + texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_RGB24, + SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT); + if (!texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -ENOMEM; + } + if (SDL_LockTexture(texture, NULL, &ptr, &stride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + b->texture = texture; + + b->datas[0].type = SPA_DATA_DmaBuf; + b->datas[0].maxsize = stride * HEIGHT; + b->datas[0].data = ptr; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = stride * HEIGHT; + b->datas[0].chunk->stride = stride; + } + return 0; +} + +static int negotiate_formats(struct data *data) +{ + int res; + struct spa_pod *format; + uint8_t buffer[256]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + data->source_output[0] = SPA_IO_BUFFERS_INIT; + + if ((res = + spa_node_port_set_io(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_IO_Buffers, + &data->source_output[0], sizeof(data->source_output[0]))) < 0) + return res; + + format = spa_format_video_raw_build(&b, 0, + &SPA_VIDEO_INFO_RAW_INIT( + .format = SPA_VIDEO_FORMAT_RGB, + .size = SPA_RECTANGLE(WIDTH, HEIGHT), + .framerate = SPA_FRACTION(25,1))); + + if ((res = spa_node_port_set_param(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, + format)) < 0) + return res; + + + setup_buffers(data); + + if (data->use_buffer) { + if ((res = sdl_alloc_buffers(data)) < 0) + return res; + + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, 0, + data->bp, data->n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + } else { + unsigned int n_buffers; + + data->texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_RGB24, + SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT); + if (!data->texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -1; + } + n_buffers = MAX_BUFFERS; + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_NODE_BUFFERS_FLAG_ALLOC, + data->bp, n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + data->n_buffers = n_buffers; + } + return 0; +} + +static void loop(struct data *data) +{ + int res; + struct spa_command cmd; + SDL_Event event; + + printf("starting...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); + + data->running = true; + + while (data->running) { + // must be called from the thread that created the renderer + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + data->running = false; + break; + } + } + + // small timeout to make sure we don't starve the SDL loop + spa_loop_control_iterate(data->control, LOOP_TIMEOUT_MS); + } + + printf("pausing...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + uint32_t pattern = 0; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data.plugin_dir = str; + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data.system = iface; + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data.system); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data.system); + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.loop = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.control = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopUtils, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.loop_utils = iface; + + data.use_buffer = USE_BUFFER; + + data.log = &default_log.log; + + if ((str = getenv("SPA_DEBUG"))) + data.log->level = atoi(str); + + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data.loop); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data.loop); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, data.loop_utils); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("can't initialize SDL: %s\n", SDL_GetError()); + return -1; + } + + if (SDL_CreateWindowAndRenderer + (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) { + printf("can't create window: %s\n", SDL_GetError()); + return -1; + } + + if (argv[1] != NULL) + pattern = atoi(argv[1]); + + if ((res = make_nodes(&data, pattern)) < 0) { + printf("can't make nodes: %d\n", res); + return -1; + } + + if ((res = negotiate_formats(&data)) < 0) { + printf("can't negotiate nodes: %d\n", res); + return -1; + } + + spa_loop_control_enter(data.control); + loop(&data); + spa_loop_control_leave(data.control); + + SDL_DestroyRenderer(data.renderer); + + return 0; +} diff --git a/spa/examples/meson.build b/spa/examples/meson.build new file mode 100644 index 0000000..8d0fc49 --- /dev/null +++ b/spa/examples/meson.build @@ -0,0 +1,38 @@ +# Examples, in order from simple to complicated +spa_examples = [ + 'adapter-control', + 'example-control', + 'local-libcamera', + 'local-v4l2', + 'local-videotestsrc', +] + +if not get_option('examples').allowed() or not get_option('spa-plugins').allowed() + subdir_done() +endif + +spa_examples_extra_deps = { + 'local-v4l2': [sdl_dep], + 'local-videotestsrc': [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-private/spa-private/dbus-helpers.h b/spa/include-private/spa-private/dbus-helpers.h new file mode 100644 index 0000000..55a664d --- /dev/null +++ b/spa/include-private/spa-private/dbus-helpers.h @@ -0,0 +1,72 @@ +/* Spa DBus helpers */ +/* SPDX-FileCopyrightText: Copyright © 2023 PipeWire authors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PRIVATE_DBUS_HELPERS_H +#define SPA_PRIVATE_DBUS_HELPERS_H + +#include + +#include + +#include + +static inline void cancel_and_unref(DBusPendingCall **pp) +{ + DBusPendingCall *pending_call = spa_steal_ptr(*pp); + + if (pending_call) { + dbus_pending_call_cancel(pending_call); + dbus_pending_call_unref(pending_call); + } +} + +static inline DBusMessage *steal_reply_and_unref(DBusPendingCall **pp) +{ + DBusPendingCall *pending_call = spa_steal_ptr(*pp); + + DBusMessage *reply = dbus_pending_call_steal_reply(pending_call); + dbus_pending_call_unref(pending_call); + + return reply; +} + +SPA_DEFINE_AUTOPTR_CLEANUP(DBusMessage, DBusMessage, { + spa_clear_ptr(*thing, dbus_message_unref); +}) + +static inline bool reply_with_error(DBusConnection *conn, + DBusMessage *reply_to, + const char *error_name, const char *error_message) +{ + spa_autoptr(DBusMessage) reply = dbus_message_new_error(reply_to, error_name, error_message); + + return reply && dbus_connection_send(conn, reply, NULL); +} + +static inline DBusPendingCall *send_with_reply(DBusConnection *conn, + DBusMessage *m, + DBusPendingCallNotifyFunction callback, void *user_data) +{ + DBusPendingCall *pending_call; + + if (!dbus_connection_send_with_reply(conn, m, &pending_call, DBUS_TIMEOUT_USE_DEFAULT)) + return NULL; + + if (!pending_call) + return NULL; + + if (!dbus_pending_call_set_notify(pending_call, callback, user_data, NULL)) { + dbus_pending_call_cancel(pending_call); + dbus_pending_call_unref(pending_call); + return NULL; + } + + return pending_call; +} + +SPA_DEFINE_AUTO_CLEANUP(DBusError, DBusError, { + dbus_error_free(thing); +}) + +#endif /* SPA_PRIVATE_DBUS_HELPERS_H */ diff --git a/spa/include/meson.build b/spa/include/meson.build new file mode 100644 index 0000000..0ab86ea --- /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..dc8f4cc --- /dev/null +++ b/spa/include/spa/buffer/alloc.h @@ -0,0 +1,338 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BUFFER_ALLOC_H +#define SPA_BUFFER_ALLOC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#ifndef SPA_API_BUFFER_ALLOC + #ifdef SPA_API_IMPL + #define SPA_API_BUFFER_ALLOC SPA_API_IMPL + #else + #define SPA_API_BUFFER_ALLOC static inline + #endif +#endif + +/** + * \addtogroup spa_buffer + * \{ + */ + +/** 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. + * */ +SPA_API_BUFFER_ALLOC int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info, + uint32_t n_metas, struct spa_meta metas[], + uint32_t n_datas, struct spa_data datas[], + uint32_t data_aligns[]) +{ + 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 + * || | ... | 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 + * ||| | ... | 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 + * || | ... | + * || +------------------------------+ + * +->| struct spa_chunk | memory for n_datas chunks + * | | uint32_t offset | + * | | uint32_t size | + * | | int32_t stride | + * | | int32_t dummy | + * | | ... chunks | + * | +------------------------------+ + * +>| data | memory for n_datas data, aligned + * | ... 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 + */ +SPA_API_BUFFER_ALLOC 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. + * + */ +SPA_API_BUFFER_ALLOC int +spa_buffer_alloc_layout_array(struct spa_buffer_alloc_info *info, + uint32_t n_buffers, struct spa_buffer *buffers[], + void *skel_mem, void *data_mem) +{ + 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. + * + */ +SPA_API_BUFFER_ALLOC struct spa_buffer ** +spa_buffer_alloc_array(uint32_t n_buffers, uint32_t flags, + uint32_t n_metas, struct spa_meta metas[], + uint32_t n_datas, struct spa_data datas[], + uint32_t data_aligns[]) +{ + + struct spa_buffer **buffers; + struct spa_buffer_alloc_info info = { flags | SPA_BUFFER_ALLOC_FLAG_INLINE_ALL, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + void *skel; + + spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns); + + 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..03fd13b --- /dev/null +++ b/spa/include/spa/buffer/buffer.h @@ -0,0 +1,129 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BUFFER_H +#define SPA_BUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_BUFFER + #ifdef SPA_API_IMPL + #define SPA_API_BUFFER SPA_API_IMPL + #else + #define SPA_API_BUFFER static inline + #endif +#endif + +/** \defgroup spa_buffer Buffers + * + * Buffers describe the data and metadata that is exchanged between + * 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, /**< memfd, mmap to get to memory. */ + SPA_DATA_DmaBuf, /**< fd to dmabuf memory. This might not be readily + * mappable (unless the MAPPABLE flag is set) and should + * normally be handled with DMABUF apis. */ + SPA_DATA_MemId, /**< memory is identified with an id. The actual memory + * can be obtained in some other way and can be identified + * with this id. */ + SPA_DATA_SyncObj, /**< a syncobj, usually requires a spa_meta_sync_timeline metadata + * with timeline points. */ + + _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) +#define SPA_DATA_FLAG_MAPPABLE (1u<<3) /**< data is mappable with simple mmap/munmap. Some memory + * types are not simply mappable (DmaBuf) unless explicitly + * specified with this flag. */ + uint32_t flags; /**< data flags */ + int64_t fd; /**< optional fd for data */ + uint32_t mapoffset; /**< offset to map fd at, this is page aligned */ + 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 */ +SPA_API_BUFFER 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; +} + +SPA_API_BUFFER void *spa_buffer_find_meta_data(const struct spa_buffer *b, uint32_t type, size_t size) +{ + struct spa_meta *m; + if ((m = spa_buffer_find_meta(b, type)) && m->size >= size) + 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..b484cfb --- /dev/null +++ b/spa/include/spa/buffer/meta.h @@ -0,0 +1,203 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_META_H +#define SPA_META_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_META + #ifdef SPA_API_IMPL + #define SPA_API_META SPA_API_IMPL + #else + #define SPA_API_META static inline + #endif +#endif + +/** + * \addtogroup spa_buffer + * \{ + */ + +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_SyncTimeline, /**< struct spa_meta_sync_timeline */ + + _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 */ +}; + +SPA_API_META void *spa_meta_first(const struct spa_meta *m) { + return m->data; +} + +SPA_API_META void *spa_meta_end(const struct spa_meta *m) { + return SPA_PTROFF(m->data,m->size,void); +} +#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; +}; + +SPA_API_META bool spa_meta_region_is_valid(const struct spa_meta_region *m) { + return m->region.size.width != 0 && m->region.size.height != 0; +} + +/** 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)++) + +/** + * 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. */ +}; + +SPA_API_META bool spa_meta_bitmap_is_valid(const struct spa_meta_bitmap *m) { + return m->format != 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. */ +}; + +SPA_API_META bool spa_meta_cursor_is_valid(const struct spa_meta_cursor *m) { + return m->id != 0; +} + +/** a timed set of events associated with the buffer */ +struct spa_meta_control { + struct spa_pod_sequence sequence; +}; + +/** 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 + * buffer 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 */ +}; + +/** + * A timeline point for explicit sync + * + * Metadata to describe the time on the timeline when the buffer + * can be acquired and when it can be reused. + * + * This metadata will require negotiation of 2 extra fds for the acquire + * and release timelines respectively. One way to achieve this is to place + * this metadata as SPA_PARAM_BUFFERS_metaType when negotiating a buffer + * layout with 2 extra fds. + */ +struct spa_meta_sync_timeline { + uint32_t flags; + uint32_t padding; + uint64_t acquire_point; /**< the timeline acquire point, this is when the data + * can be accessed. */ + uint64_t release_point; /**< the timeline release point, this timeline point should + * be signaled when the data is no longer accessed. */ +}; + +/** + * \} + */ + +#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..cdfe527 --- /dev/null +++ b/spa/include/spa/buffer/type-info.h @@ -0,0 +1,92 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BUFFER_TYPES_H +#define SPA_BUFFER_TYPES_H + +/** + * \addtogroup spa_buffer + * \{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#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 }, + { SPA_DATA_SyncObj, SPA_TYPE_Int, SPA_TYPE_INFO_DATA_BASE "SyncObj", 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 ":" + +/* VideoTransform meta */ +#define SPA_TYPE_INFO_META_Transformation SPA_TYPE_INFO_ENUM_BASE "Meta:Transformation" +#define SPA_TYPE_INFO_META_TRANSFORMATION_BASE SPA_TYPE_INFO_META_Transformation ":" + +static const struct spa_type_info spa_type_meta_videotransform_type[] = { + { SPA_META_TRANSFORMATION_None, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "None", NULL }, + { SPA_META_TRANSFORMATION_90, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "90", NULL }, + { SPA_META_TRANSFORMATION_180, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "180", NULL }, + { SPA_META_TRANSFORMATION_270, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "270", NULL }, + { SPA_META_TRANSFORMATION_Flipped, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "Flipped", NULL }, + { SPA_META_TRANSFORMATION_Flipped90, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "Flipped90", NULL }, + { SPA_META_TRANSFORMATION_Flipped180, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "Flipped180", NULL }, + { SPA_META_TRANSFORMATION_Flipped270, SPA_TYPE_Int, SPA_TYPE_INFO_META_TRANSFORMATION_BASE "Flipped270", NULL }, + { 0, 0, NULL, NULL }, +}; + +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 }, + { SPA_META_SyncTimeline, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "SyncTimeline", 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..1c1ec81 --- /dev/null +++ b/spa/include/spa/control/control.h @@ -0,0 +1,45 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_CONTROL_H +#define SPA_CONTROL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_control Control + * Control type declarations + */ + +/** + * \addtogroup spa_control + * \{ + */ + +/** Different Control types */ +enum spa_control_type { + SPA_CONTROL_Invalid, + SPA_CONTROL_Properties, /**< SPA_TYPE_OBJECT_Props */ + SPA_CONTROL_Midi, /**< spa_pod_bytes with raw midi data (deprecated, use SPA_CONTROL_UMP) */ + SPA_CONTROL_OSC, /**< spa_pod_bytes with an OSC packet */ + SPA_CONTROL_UMP, /**< spa_pod_bytes with raw UMP (universal MIDI packet) + * data. The UMP 32 bit words are stored in native endian + * format. */ + + _SPA_CONTROL_LAST, /**< not part of ABI */ +}; + +/** + * \} + */ + +#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..6c7bd6a --- /dev/null +++ b/spa/include/spa/control/type-info.h @@ -0,0 +1,42 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_CONTROL_TYPES_H +#define SPA_CONTROL_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_control + * \{ + */ + +#include +#include +#include + +/* 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 }, + { SPA_CONTROL_UMP, SPA_TYPE_Int, SPA_TYPE_INFO_CONTROL_BASE "UMP", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_CONTROL_TYPES_H */ diff --git a/spa/include/spa/control/ump-utils.h b/spa/include/spa/control/ump-utils.h new file mode 100644 index 0000000..9f57efb --- /dev/null +++ b/spa/include/spa/control/ump-utils.h @@ -0,0 +1,230 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + + +#ifndef SPA_CONTROL_UMP_UTILS_H +#define SPA_CONTROL_UMP_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_CONTROL_UMP_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_CONTROL_UMP_UTILS SPA_API_IMPL + #else + #define SPA_API_CONTROL_UMP_UTILS static inline + #endif +#endif +/** + * \addtogroup spa_control + * \{ + */ + +SPA_API_CONTROL_UMP_UTILS size_t spa_ump_message_size(uint8_t message_type) +{ + static const uint32_t ump_sizes[] = { + [0x0] = 1, /* Utility messages */ + [0x1] = 1, /* System messages */ + [0x2] = 1, /* MIDI 1.0 messages */ + [0x3] = 2, /* 7bit SysEx messages */ + [0x4] = 2, /* MIDI 2.0 messages */ + [0x5] = 4, /* 8bit data message */ + [0x6] = 1, + [0x7] = 1, + [0x8] = 2, + [0x9] = 2, + [0xa] = 2, + [0xb] = 3, + [0xc] = 3, + [0xd] = 4, /* Flexible data messages */ + [0xe] = 4, + [0xf] = 4, /* Stream messages */ + }; + return ump_sizes[message_type & 0xf]; +} + +SPA_API_CONTROL_UMP_UTILS int spa_ump_to_midi(uint32_t *ump, size_t ump_size, + uint8_t *midi, size_t midi_maxsize) +{ + int size = 0; + + if (ump_size < 4) + return 0; + if (midi_maxsize < 8) + return -ENOSPC; + + switch (ump[0] >> 28) { + case 0x1: /* System Real Time and System Common Messages (except System Exclusive) */ + midi[size++] = (ump[0] >> 16) & 0xff; + if (midi[0] >= 0xf1 && midi[0] <= 0xf3) { + midi[size++] = (ump[0] >> 8) & 0x7f; + if (midi[0] == 0xf2) + midi[size++] = ump[0] & 0x7f; + } + break; + case 0x2: /* MIDI 1.0 Channel Voice Messages */ + midi[size++] = (ump[0] >> 16); + midi[size++] = (ump[0] >> 8); + if (midi[0] < 0xc0 || midi[0] > 0xdf) + midi[size++] = (ump[0]); + break; + case 0x3: /* Data Messages (including System Exclusive) */ + { + uint8_t status, i, bytes; + + if (ump_size < 8) + return 0; + + status = (ump[0] >> 20) & 0xf; + bytes = SPA_CLAMP((ump[0] >> 16) & 0xf, 0u, 6u); + + if (status == 0 || status == 1) + midi[size++] = 0xf0; + for (i = 0 ; i < bytes; i++) + /* ump[0] >> 8 | ump[0] | ump[1] >> 24 | ump[1] >>16 ... */ + midi[size++] = ump[(i+2)/4] >> ((5-i)%4 * 8); + if (status == 0 || status == 3) + midi[size++] = 0xf7; + break; + } + case 0x4: /* MIDI 2.0 Channel Voice Messages */ + if (ump_size < 8) + return 0; + midi[size++] = (ump[0] >> 16) | 0x80; + if (midi[0] < 0xc0 || midi[0] > 0xdf) + midi[size++] = (ump[0] >> 8) & 0x7f; + midi[size++] = (ump[1] >> 25); + break; + + case 0x0: /* Utility Messages */ + case 0x5: /* Data Messages */ + default: + return 0; + } + return size; +} + +SPA_API_CONTROL_UMP_UTILS int spa_ump_from_midi(uint8_t **midi, size_t *midi_size, + uint32_t *ump, size_t ump_maxsize, uint8_t group, uint64_t *state) +{ + int size = 0; + uint32_t i, prefix = group << 24, to_consume = 0, bytes; + uint8_t status, *m = (*midi), end; + + if (*midi_size < 1) + return 0; + if (ump_maxsize < 16) + return -ENOSPC; + + status = m[0]; + + /* SysEx */ + if (*state == 0) { + if (status == 0xf0) + *state = 1; /* sysex start */ + else if (status == 0xf7) + *state = 2; /* sysex continue */ + } + if (*state & 3) { + prefix |= 0x30000000; + if (status & 0x80) { + m++; + to_consume++; + } + bytes = SPA_CLAMP(*midi_size - to_consume, 0u, 7u); + if (bytes > 0) { + end = m[bytes-1]; + if (end & 0x80) { + bytes--; /* skip terminator */ + to_consume++; + } + else + end = 0xf0; /* pretend there is a continue terminator */ + + bytes = SPA_CLAMP(bytes, 0u, 6u); + to_consume += bytes; + + if (end == 0xf7) { + if (*state == 2) { + /* continue and done */ + prefix |= 0x3 << 20; + *state = 0; + } + } else if (*state == 1) { + /* first packet but not finished */ + prefix |= 0x1 << 20; + *state = 2; /* sysex continue */ + } else { + /* continue and not finished */ + prefix |= 0x2 << 20; + } + ump[size++] = prefix | bytes << 16; + ump[size++] = 0; + for (i = 0 ; i < bytes; i++) + /* ump[0] |= (m[0] & 0x7f) << 8 + * ump[0] |= (m[1] & 0x7f) + * ump[1] |= (m[2] & 0x7f) << 24 + * ... */ + ump[(i+2)/4] |= (m[i] & 0x7f) << ((5-i)%4 * 8); + } + } else { + /* regular messages */ + switch (status) { + case 0x80 ... 0x8f: + case 0x90 ... 0x9f: + case 0xa0 ... 0xaf: + case 0xb0 ... 0xbf: + case 0xe0 ... 0xef: + to_consume = 3; + prefix |= 0x20000000; + break; + case 0xc0 ... 0xdf: + to_consume = 2; + prefix |= 0x20000000; + break; + case 0xf2: + to_consume = 3; + prefix = 0x10000000; + break; + case 0xf1: case 0xf3: + to_consume = 2; + prefix = 0x10000000; + break; + case 0xf4 ... 0xff: + to_consume = 1; + prefix = 0x10000000; + break; + default: + return -EIO; + } + if (*midi_size < to_consume) { + to_consume = *midi_size; + } else { + prefix |= status << 16; + if (to_consume > 1) + prefix |= (m[1] & 0x7f) << 8; + if (to_consume > 2) + prefix |= (m[2] & 0x7f); + ump[size++] = prefix; + } + } + (*midi_size) -= to_consume; + (*midi) += to_consume; + + return size * 4; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_CONTROL_UMP_UTILS_H */ diff --git a/spa/include/spa/debug/buffer.h b/spa/include/spa/debug/buffer.h new file mode 100644 index 0000000..eea48ae --- /dev/null +++ b/spa/include/spa/debug/buffer.h @@ -0,0 +1,121 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 +#include +#include +#include + +#ifndef SPA_API_DEBUG_BUFFER + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_BUFFER SPA_API_IMPL + #else + #define SPA_API_DEBUG_BUFFER static inline + #endif +#endif + +SPA_API_DEBUG_BUFFER int spa_debugc_buffer(struct spa_debug_context *ctx, int indent, const struct spa_buffer *buffer) +{ + uint32_t i; + + 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; +} + +SPA_API_DEBUG_BUFFER 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..13002f6 --- /dev/null +++ b/spa/include/spa/debug/context.h @@ -0,0 +1,71 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_CONTEXT_H +#define SPA_DEBUG_CONTEXT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include +/** + * \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 + + +#ifndef SPA_API_DEBUG_CONTEXT + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_CONTEXT SPA_API_IMPL + #else + #define SPA_API_DEBUG_CONTEXT static inline + #endif +#endif + +struct spa_debug_context { + void (*log) (struct spa_debug_context *ctx, const char *fmt, ...) SPA_PRINTF_FUNC(2, 3); +}; + +#define spa_debugc(_c,_fmt,...) (_c)?((_c)->log((_c),_fmt, ## __VA_ARGS__)):(void)spa_debug(_fmt, ## __VA_ARGS__) + +SPA_API_DEBUG_CONTEXT void spa_debugc_error_location(struct spa_debug_context *c, + struct spa_error_location *loc) +{ + int i, skip = loc->col > 80 ? loc->col - 40 : 0, lc = loc->col-skip-1; + char buf[80]; + + for (i = 0; (size_t)i < sizeof(buf)-1 && (size_t)(i + skip) < loc->len; i++) { + char ch = loc->location[i + skip]; + if (ch == '\n' || ch == '\0') + break; + buf[i] = isspace(ch) ? ' ' : ch; + } + buf[i] = '\0'; + spa_debugc(c, "line:%6d | %s%s", loc->line, skip ? "..." : "", buf); + for (i = 0; buf[i]; i++) + buf[i] = i < lc ? '-' : i == lc ? '^' : ' '; + spa_debugc(c, "column:%4d |-%s%s", loc->col, skip ? "---" : "", buf); +} + +/** + * \} + */ + +#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..5657b2d --- /dev/null +++ b/spa/include/spa/debug/dict.h @@ -0,0 +1,50 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_DICT_H +#define SPA_DEBUG_DICT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include + +#ifndef SPA_API_DEBUG_DICT + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_DICT SPA_API_IMPL + #else + #define SPA_API_DEBUG_DICT static inline + #endif +#endif + +SPA_API_DEBUG_DICT int spa_debugc_dict(struct spa_debug_context *ctx, int indent, const struct spa_dict *dict) +{ + const struct spa_dict_item *item; + spa_debugc(ctx, "%*sflags:%08x n_items:%d", indent, "", dict->flags, dict->n_items); + spa_dict_for_each(item, dict) { + spa_debugc(ctx, "%*s %s = \"%s\"", indent, "", item->key, item->value); + } + return 0; +} + +SPA_API_DEBUG_DICT 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/file.h b/spa/include/spa/debug/file.h new file mode 100644 index 0000000..17ce46b --- /dev/null +++ b/spa/include/spa/debug/file.h @@ -0,0 +1,70 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_FILE_H +#define SPA_DEBUG_FILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/** + * \addtogroup spa_debug + * \{ + */ + +#ifndef SPA_API_DEBUG_FILE + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_FILE SPA_API_IMPL + #else + #define SPA_API_DEBUG_FILE static inline + #endif +#endif + +struct spa_debug_file_ctx { + struct spa_debug_context ctx; + FILE *f; +}; + +SPA_PRINTF_FUNC(2,3) +SPA_API_DEBUG_FILE void spa_debug_file_log(struct spa_debug_context *ctx, const char *fmt, ...) +{ + struct spa_debug_file_ctx *c = SPA_CONTAINER_OF(ctx, struct spa_debug_file_ctx, ctx); + va_list args; + va_start(args, fmt); + vfprintf(c->f, fmt, args); fputc('\n', c->f); + va_end(args); +} + +#define SPA_DEBUG_FILE_INIT(_f) \ + (struct spa_debug_file_ctx){ { spa_debug_file_log }, _f, } + +#define spa_debug_file_error_location(f,loc,fmt,...) \ +({ \ + struct spa_debug_file_ctx c = SPA_DEBUG_FILE_INIT(f); \ + if (fmt) spa_debugc(&c.ctx, fmt, __VA_ARGS__); \ + spa_debugc_error_location(&c.ctx, loc); \ +}) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_FILE_H */ diff --git a/spa/include/spa/debug/format.h b/spa/include/spa/debug/format.h new file mode 100644 index 0000000..ad7f204 --- /dev/null +++ b/spa/include/spa/debug/format.h @@ -0,0 +1,223 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_FORMAT_H +#define SPA_DEBUG_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_DEBUG_FORMAT + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_FORMAT SPA_API_IMPL + #else + #define SPA_API_DEBUG_FORMAT static inline + #endif +#endif + + +SPA_API_DEBUG_FORMAT int +spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + + 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; +} + +SPA_API_DEBUG_FORMAT 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; +} + +SPA_API_DEBUG_FORMAT int spa_debugc_format(struct spa_debug_context *ctx, int indent, + const struct spa_type_info *info, const struct spa_pod *format) +{ + const char *media_type; + 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; +} + +SPA_API_DEBUG_FORMAT int spa_debug_format(int indent, + const struct spa_type_info *info, const struct spa_pod *format) +{ + return spa_debugc_format(NULL, indent, info, format); +} +/** + * \} + */ + +#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..05c3bd5 --- /dev/null +++ b/spa/include/spa/debug/log.h @@ -0,0 +1,111 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_LOG_H +#define SPA_DEBUG_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_DEBUG_LOG + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_LOG SPA_API_IMPL + #else + #define SPA_API_DEBUG_LOG static inline + #endif +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +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) +SPA_API_DEBUG_LOG void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...) +{ + struct spa_debug_log_ctx *c = SPA_CONTAINER_OF(ctx, struct spa_debug_log_ctx, ctx); + va_list args; + 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); \ +}) + +#define spa_debug_log_dict(l,lev,indent,dict) \ +({ \ + 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_dict(&c.ctx, indent, dict); \ +}) + +#define spa_debug_log_error_location(l,lev,loc,fmt,...) \ +({ \ + 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))) { \ + if (fmt) spa_debugc(&c.ctx, fmt, __VA_ARGS__); \ + spa_debugc_error_location(&c.ctx, loc); \ + } \ +}) + +/** + * \} + */ + +#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..9666294 --- /dev/null +++ b/spa/include/spa/debug/mem.h @@ -0,0 +1,59 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_MEM_H +#define SPA_DEBUG_MEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_debug + * \{ + */ + +#include + +#ifndef SPA_API_DEBUG_MEM + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_MEM SPA_API_IMPL + #else + #define SPA_API_DEBUG_MEM static inline + #endif +#endif + +SPA_API_DEBUG_MEM int spa_debugc_mem(struct spa_debug_context *ctx, int indent, const void *data, size_t size) +{ + const uint8_t *t = (const uint8_t*)data; + char buffer[512]; + 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; +} + +SPA_API_DEBUG_MEM 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..baa273f --- /dev/null +++ b/spa/include/spa/debug/node.h @@ -0,0 +1,55 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_NODE_H +#define SPA_DEBUG_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_DEBUG_NODE + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_NODE SPA_API_IMPL + #else + #define SPA_API_DEBUG_NODE static inline + #endif +#endif + +SPA_API_DEBUG_NODE int spa_debugc_port_info(struct spa_debug_context *ctx, int indent, const struct spa_port_info *info) +{ + spa_debugc(ctx, "%*s" "struct spa_port_info %p:", indent, "", info); + spa_debugc(ctx, "%*s" " flags: \t%08" PRIx64, indent, "", info->flags); + 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; +} + +SPA_API_DEBUG_NODE int spa_debug_port_info(int indent, const struct spa_port_info *info) +{ + return spa_debugc_port_info(NULL, indent, info); +} +/** + * \} + */ + + +#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..9db6f4b --- /dev/null +++ b/spa/include/spa/debug/pod.h @@ -0,0 +1,215 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_POD_H +#define SPA_DEBUG_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include +#include +#include + +#ifndef SPA_API_DEBUG_POD + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_POD SPA_API_IMPL + #else + #define SPA_API_DEBUG_POD static inline + #endif +#endif + +SPA_API_DEBUG_POD int +spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + 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 %" PRIu32 "x%" PRIu32 "", indent, "", r->width, r->height); + break; + } + case SPA_TYPE_Fraction: + { + struct spa_fraction *f = (struct spa_fraction *)body; + spa_debugc(ctx, "%*s" "Fraction %" PRIu32 "/%" PRIu32 "", 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; +} + +SPA_API_DEBUG_POD int spa_debugc_pod(struct spa_debug_context *ctx, int indent, + const struct spa_type_info *info, const struct spa_pod *pod) +{ + return spa_debugc_pod_value(ctx, indent, info ? info : SPA_TYPE_ROOT, + SPA_POD_TYPE(pod), + SPA_POD_BODY(pod), + SPA_POD_BODY_SIZE(pod)); +} + +SPA_API_DEBUG_POD int +spa_debug_pod_value(int indent, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + return spa_debugc_pod_value(NULL, indent, info, type, body, size); +} + +SPA_API_DEBUG_POD int spa_debug_pod(int indent, + const struct spa_type_info *info, const struct spa_pod *pod) +{ + return spa_debugc_pod(NULL, indent, info, pod); +} +/** + * \} + */ + + +#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..d7ca836 --- /dev/null +++ b/spa/include/spa/debug/types.h @@ -0,0 +1,113 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEBUG_TYPES_H +#define SPA_DEBUG_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include + +#include + +#ifndef SPA_API_DEBUG_TYPES + #ifdef SPA_API_IMPL + #define SPA_API_DEBUG_TYPES SPA_API_IMPL + #else + #define SPA_API_DEBUG_TYPES static inline + #endif +#endif + + +SPA_API_DEBUG_TYPES const struct spa_type_info *spa_debug_type_find(const struct spa_type_info *info, uint32_t type) +{ + const struct spa_type_info *res; + + 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; +} + +SPA_API_DEBUG_TYPES const char *spa_debug_type_short_name(const char *name) +{ + return spa_type_short_name(name); +} + +SPA_API_DEBUG_TYPES const char *spa_debug_type_find_name(const struct spa_type_info *info, uint32_t type) +{ + if ((info = spa_debug_type_find(info, type)) == NULL) + return NULL; + return info->name; +} + +SPA_API_DEBUG_TYPES const char *spa_debug_type_find_short_name(const struct spa_type_info *info, uint32_t type) +{ + const char *str; + if ((str = spa_debug_type_find_name(info, type)) == NULL) + return NULL; + return spa_debug_type_short_name(str); +} + +SPA_API_DEBUG_TYPES uint32_t spa_debug_type_find_type(const struct spa_type_info *info, const char *name) +{ + if (info == NULL) + info = SPA_TYPE_ROOT; + + 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; +} + +SPA_API_DEBUG_TYPES const struct spa_type_info *spa_debug_type_find_short(const struct spa_type_info *info, const char *name) +{ + while (info && info->name) { + if (strcmp(spa_debug_type_short_name(info->name), name) == 0) + 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; +} + +SPA_API_DEBUG_TYPES uint32_t spa_debug_type_find_type_short(const struct spa_type_info *info, const char *name) +{ + if ((info = spa_debug_type_find_short(info, name)) == NULL) + return SPA_ID_INVALID; + return info->type; +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_NODE_H */ diff --git a/spa/include/spa/filter-graph/filter-graph.h b/spa/include/spa/filter-graph/filter-graph.h new file mode 100644 index 0000000..05904c7 --- /dev/null +++ b/spa/include/spa/filter-graph/filter-graph.h @@ -0,0 +1,150 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_FILTER_GRAPH_H +#define SPA_FILTER_GRAPH_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +#ifndef SPA_API_FILTER_GRAPH + #ifdef SPA_API_IMPL + #define SPA_API_FILTER_GRAPH SPA_API_IMPL + #else + #define SPA_API_FILTER_GRAPH static inline + #endif +#endif + +/** \defgroup spa_filter_graph Filter Graph + * a graph of filters + */ + +/** + * \addtogroup spa_filter_graph + * \{ + */ + +/** + * A graph of filters + */ +#define SPA_TYPE_INTERFACE_FilterGraph SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph" + +#define SPA_VERSION_FILTER_GRAPH 0 +struct spa_filter_graph { struct spa_interface iface; }; + +struct spa_filter_graph_info { + uint32_t n_inputs; + uint32_t n_outputs; + +#define SPA_FILTER_GRAPH_CHANGE_MASK_FLAGS (1u<<0) +#define SPA_FILTER_GRAPH_CHANGE_MASK_PROPS (1u<<1) + uint64_t change_mask; + + uint64_t flags; + struct spa_dict *props; +}; + +struct spa_filter_graph_events { +#define SPA_VERSION_FILTER_GRAPH_EVENTS 0 + uint32_t version; + + void (*info) (void *object, const struct spa_filter_graph_info *info); + + void (*apply_props) (void *object, enum spa_direction direction, const struct spa_pod *props); + + void (*props_changed) (void *object, enum spa_direction direction); +}; + +struct spa_filter_graph_methods { +#define SPA_VERSION_FILTER_GRAPH_METHODS 0 + uint32_t version; + + int (*add_listener) (void *object, + struct spa_hook *listener, + const struct spa_filter_graph_events *events, + void *data); + + int (*enum_prop_info) (void *object, uint32_t idx, struct spa_pod_builder *b, + struct spa_pod **param); + int (*get_props) (void *object, struct spa_pod_builder *b, struct spa_pod **props); + int (*set_props) (void *object, enum spa_direction direction, const struct spa_pod *props); + + int (*activate) (void *object, const struct spa_dict *props); + int (*deactivate) (void *object); + + int (*reset) (void *object); + + int (*process) (void *object, const void *in[], void *out[], uint32_t n_samples); +}; + +SPA_API_FILTER_GRAPH int spa_filter_graph_add_listener(struct spa_filter_graph *object, + struct spa_hook *listener, + const struct spa_filter_graph_events *events, void *data) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, add_listener, 0, listener, + events, data); +} + +SPA_API_FILTER_GRAPH int spa_filter_graph_enum_prop_info(struct spa_filter_graph *object, + uint32_t idx, struct spa_pod_builder *b, struct spa_pod **param) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, enum_prop_info, 0, idx, b, param); +} +SPA_API_FILTER_GRAPH int spa_filter_graph_get_props(struct spa_filter_graph *object, + struct spa_pod_builder *b, struct spa_pod **props) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, get_props, 0, b, props); +} + +SPA_API_FILTER_GRAPH int spa_filter_graph_set_props(struct spa_filter_graph *object, + enum spa_direction direction, const struct spa_pod *props) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, set_props, 0, direction, props); +} + +SPA_API_FILTER_GRAPH int spa_filter_graph_activate(struct spa_filter_graph *object, const struct spa_dict *props) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, activate, 0, props); +} +SPA_API_FILTER_GRAPH int spa_filter_graph_deactivate(struct spa_filter_graph *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, deactivate, 0); +} + +SPA_API_FILTER_GRAPH int spa_filter_graph_reset(struct spa_filter_graph *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, reset, 0); +} + +SPA_API_FILTER_GRAPH int spa_filter_graph_process(struct spa_filter_graph *object, + const void *in[], void *out[], uint32_t n_samples) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_filter_graph, &object->iface, process, 0, in, out, n_samples); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_FILTER_GRAPH_H */ + diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h new file mode 100644 index 0000000..537e6e7 --- /dev/null +++ b/spa/include/spa/graph/graph.h @@ -0,0 +1,355 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_GRAPH_H +#define SPA_GRAPH_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_graph Graph + * Node graph + */ + +/** + * \addtogroup spa_graph + * \{ + */ + +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_GRAPH + #ifdef SPA_API_IMPL + #define SPA_API_GRAPH SPA_API_IMPL + #else + #define SPA_API_GRAPH static inline + #endif +#endif + + +#ifndef spa_debug +#define spa_debug(...) +#endif + +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 */ +}; + +SPA_API_GRAPH 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) (SPA_ATOMIC_DEC(s->pending) == 0) + +SPA_API_GRAPH 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)) + 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 */ +}; + +SPA_API_GRAPH 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; +} + +SPA_API_GRAPH 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; +} + +SPA_API_GRAPH int spa_graph_finish(struct spa_graph *graph) +{ + spa_debug("graph %p finish", graph); + if (graph->parent) + return spa_graph_node_trigger(graph->parent); + return 0; +} +SPA_API_GRAPH int spa_graph_link_signal_node(void *data) +{ + struct spa_graph_node *node = (struct spa_graph_node *)data; + spa_debug("node %p call process", node); + return spa_graph_node_process(node); +} + +SPA_API_GRAPH int spa_graph_link_signal_graph(void *data) +{ + struct spa_graph_node *node = (struct spa_graph_node *)data; + return spa_graph_finish(node->graph); +} + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH void spa_graph_link_remove(struct spa_graph_link *link) +{ + link->state->required--; + spa_debug("link %p state %p remove %d", link, link->state, link->state->required); + spa_list_remove(&link->link); +} + +SPA_API_GRAPH void +spa_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state) +{ + spa_list_init(&node->ports[SPA_DIRECTION_INPUT]); + 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); +} + + +SPA_API_GRAPH int spa_graph_node_impl_sub_process(void *data SPA_UNUSED, struct spa_graph_node *node) +{ + struct spa_graph *graph = node->subgraph; + spa_debug("node %p: sub process %p", node, graph); + return spa_graph_run(graph); +} + +static const struct spa_graph_node_callbacks spa_graph_node_sub_impl_default = { + .version = SPA_VERSION_GRAPH_NODE_CALLBACKS, + .process = spa_graph_node_impl_sub_process, +}; + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH void spa_graph_node_remove(struct spa_graph_node *node) +{ + spa_debug("node %p remove from graph %p, state %p required %d", + node, node->graph, node->state, node->state->required); + spa_graph_link_remove(&node->graph_link); + node->state->required--; + spa_list_remove(&node->link); +} + + +SPA_API_GRAPH 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; +} + +SPA_API_GRAPH 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); +} + +SPA_API_GRAPH void spa_graph_port_remove(struct spa_graph_port *port) +{ + spa_debug("port %p remove", port); + spa_list_remove(&port->link); +} + +SPA_API_GRAPH void +spa_graph_port_link(struct spa_graph_port *out, struct spa_graph_port *in) +{ + spa_debug("port %p link to %p %p %p", out, in, in->node, in->node->state); + out->peer = in; + in->peer = out; +} + +SPA_API_GRAPH 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; + } +} + +SPA_API_GRAPH int spa_graph_node_impl_process(void *data, struct spa_graph_node *node) +{ + struct spa_node *n = (struct spa_node *)data; + struct spa_graph_state *state = node->state; + + 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; +} + +SPA_API_GRAPH int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node SPA_UNUSED, + uint32_t port_id, uint32_t buffer_id) +{ + struct spa_node *n = (struct spa_node *)data; + return spa_node_port_reuse_buffer(n, port_id, buffer_id); +} + +static const struct spa_graph_node_callbacks spa_graph_node_impl_default = { + .version = 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..48626a2 --- /dev/null +++ b/spa/include/spa/interfaces/audio/aec.h @@ -0,0 +1,147 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#ifndef SPA_AUDIO_AEC_H +#define SPA_AUDIO_AEC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SPA_API_AUDIO_AEC + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_AEC SPA_API_IMPL + #else + #define SPA_API_AUDIO_AEC static inline + #endif +#endif + +#define SPA_TYPE_INTERFACE_AUDIO_AEC SPA_TYPE_INFO_INTERFACE_BASE "Audio:AEC" + +#define SPA_VERSION_AUDIO_AEC 1 +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 3 + 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); + + /* version 1:3 */ + int (*init2) (void *object, const struct spa_dict *args, + struct spa_audio_info_raw *play_info, + struct spa_audio_info_raw *rec_info, + struct spa_audio_info_raw *out_info); +}; + +SPA_API_AUDIO_AEC int spa_audio_aec_add_listener(struct spa_audio_aec *object, + struct spa_hook *listener, + const struct spa_audio_aec_events *events, + void *data) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, add_listener, 0, listener, events, data); +} + +SPA_API_AUDIO_AEC int spa_audio_aec_init(struct spa_audio_aec *object, + const struct spa_dict *args, const struct spa_audio_info_raw *info) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, init, 0, args, info); +} +SPA_API_AUDIO_AEC int spa_audio_aec_run(struct spa_audio_aec *object, + const float *rec[], const float *play[], float *out[], uint32_t n_samples) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, run, 0, rec, play, out, n_samples); +} +SPA_API_AUDIO_AEC int spa_audio_aec_set_props(struct spa_audio_aec *object, const struct spa_dict *args) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, set_props, 0, args); +} +SPA_API_AUDIO_AEC int spa_audio_aec_activate(struct spa_audio_aec *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, activate, 1); +} +SPA_API_AUDIO_AEC int spa_audio_aec_deactivate(struct spa_audio_aec *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, deactivate, 1); +} +SPA_API_AUDIO_AEC int spa_audio_aec_enum_props(struct spa_audio_aec *object, + int index, struct spa_pod_builder* builder) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, enum_props, 2, index, builder); +} +SPA_API_AUDIO_AEC int spa_audio_aec_get_params(struct spa_audio_aec *object, + struct spa_pod_builder* builder) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, get_params, 2, builder); +} +SPA_API_AUDIO_AEC int spa_audio_aec_set_params(struct spa_audio_aec *object, + const struct spa_pod *args) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, set_params, 2, args); +} +SPA_API_AUDIO_AEC int spa_audio_aec_init2(struct spa_audio_aec *object, + const struct spa_dict *args, + struct spa_audio_info_raw *play_info, + struct spa_audio_info_raw *rec_info, + struct spa_audio_info_raw *out_info) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_audio_aec, &object->iface, init2, 3, args, play_info, rec_info, out_info); +} + +#ifdef __cplusplus +} /* extern "C" */ +#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..73b4a94 --- /dev/null +++ b/spa/include/spa/monitor/device.h @@ -0,0 +1,310 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEVICE_H +#define SPA_DEVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#ifndef SPA_API_DEVICE + #ifdef SPA_API_IMPL + #define SPA_API_DEVICE SPA_API_IMPL + #else + #define SPA_API_DEVICE static inline + #endif +#endif + +/** + * \defgroup spa_device Device + * + * 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); +}; + +SPA_API_DEVICE int spa_device_add_listener(struct spa_device *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, add_listener, 0, + listener, events, data); + +} +SPA_API_DEVICE int spa_device_sync(struct spa_device *object, int seq) +{ + return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, sync, 0, + seq); +} +SPA_API_DEVICE int spa_device_enum_params(struct spa_device *object, int seq, + uint32_t id, uint32_t index, uint32_t max, + const struct spa_pod *filter) +{ + return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, enum_params, 0, + seq, id, index, max, filter); +} +SPA_API_DEVICE int spa_device_set_param(struct spa_device *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, set_param, 0, + id, flags, param); +} + +#define SPA_KEY_DEVICE_ENUM_API "device.enum.api" /**< the api used to discover this + * device */ +#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" */ +#define SPA_KEY_DEVICE_DEVIDS "device.devids" /**< space separated list of device ids (dev_t) of the + * underlying device(s) if applicable */ +/** + * \} + */ + +#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..8955f81 --- /dev/null +++ b/spa/include/spa/monitor/event.h @@ -0,0 +1,43 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_EVENT_DEVICE_H +#define SPA_EVENT_DEVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \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..e43a17b --- /dev/null +++ b/spa/include/spa/monitor/type-info.h @@ -0,0 +1,47 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEVICE_TYPE_INFO_H +#define SPA_DEVICE_TYPE_INFO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +/** + * \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..4337ece --- /dev/null +++ b/spa/include/spa/monitor/utils.h @@ -0,0 +1,94 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DEVICE_UTILS_H +#define SPA_DEVICE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_DEVICE_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_DEVICE_UTILS SPA_API_IMPL + #else + #define SPA_API_DEVICE_UTILS static inline + #endif +#endif + +/** + * \addtogroup spa_device + * \{ + */ + +struct spa_result_device_params_data { + struct spa_pod_builder *builder; + struct spa_result_device_params data; +}; + +SPA_API_DEVICE_UTILS void spa_result_func_device_params(void *data, int seq SPA_UNUSED, int res SPA_UNUSED, + uint32_t type SPA_UNUSED, const void *result) +{ + struct spa_result_device_params_data *d = + (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); +} + +SPA_API_DEVICE_UTILS int spa_device_enum_params_sync(struct spa_device *device, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_device_params_data data = { builder, {0}}; + struct spa_hook listener = {{0}, {0}, 0, 0}; + static const struct spa_device_events device_events = { + .version = SPA_VERSION_DEVICE_EVENTS, + .info = NULL, + .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..24c81a5 --- /dev/null +++ b/spa/include/spa/node/command.h @@ -0,0 +1,53 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_COMMAND_NODE_H +#define SPA_COMMAND_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +/* 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..b975a7b --- /dev/null +++ b/spa/include/spa/node/event.h @@ -0,0 +1,44 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_EVENT_NODE_H +#define SPA_EVENT_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +/* 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..c1c725e --- /dev/null +++ b/spa/include/spa/node/io.h @@ -0,0 +1,368 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_IO_H +#define SPA_IO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include +#include + +/** 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 (currently not used in PipeWire) */ + SPA_IO_Clock, /**< area to update clock information, struct spa_io_clock */ + SPA_IO_Latency, /**< latency reporting, struct spa_io_latency (currently not used in + * PipeWire). \see spa_param_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 (currently not used in PipeWire) */ + SPA_IO_AsyncBuffers, /**< async area to exchange buffers, struct spa_io_async_buffers */ +}; + +/** + * 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. + * + * Driver nodes are supposed to update the contents of \ref SPA_IO_Clock before + * signaling the start of a graph cycle. These updated clock values become + * visible to other nodes in \ref SPA_IO_Position. Non-driver nodes do + * not need to update the contents of their \ref SPA_IO_Clock. + * + * The host generally gives each node a separate \ref spa_io_clock in \ref + * SPA_IO_Clock, so that updates made by the driver are not visible in the + * contents of \ref SPA_IO_Clock of other nodes. Instead, \ref SPA_IO_Position + * is used to look up the current graph time. + * + * A node is a driver when \ref spa_io_clock.id in \ref SPA_IO_Clock and + * \ref spa_io_position.clock.id in \ref SPA_IO_Position are the same. + */ +struct spa_io_clock { +#define SPA_IO_CLOCK_FLAG_FREEWHEEL (1u<<0) /* graph is freewheeling */ +#define SPA_IO_CLOCK_FLAG_XRUN_RECOVER (1u<<1) /* recovering from xrun */ +#define SPA_IO_CLOCK_FLAG_LAZY (1u<<2) /* lazy scheduling */ +#define SPA_IO_CLOCK_FLAG_NO_RATE (1u<<3) /* the rate of the clock is only approximately. + * it is recommended to use the nsec as a clock source. + * The rate_diff contains the measured inaccuracy. */ + uint32_t flags; /**< Clock flags */ + uint32_t id; /**< Unique clock id, set by host application */ + char name[64]; /**< Clock name prefixed with API, set by node when it receives + * \ref SPA_IO_Clock. 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 + * (CLOCK_MONOTONIC). This fields reflects a real time instant + * in the past. The value may have jitter. */ + struct spa_fraction rate; /**< Rate for position/duration/delay/xrun */ + uint64_t position; /**< Current position, in samples @ \ref rate */ + uint64_t duration; /**< Duration of current cycle, in samples @ \ref rate */ + int64_t delay; /**< Delay between position and hardware, in samples @ \ref rate */ + double rate_diff; /**< Rate difference between clock and monotonic time, as a ratio of + * clock speeds. */ + uint64_t next_nsec; /**< Estimated next wakeup time in nanoseconds. + * This time is a logical start time of the next cycle, and + * is not necessarily in the future. + */ + + struct spa_fraction target_rate; /**< Target rate of next cycle */ + uint64_t target_duration; /**< Target duration of next cycle */ + uint32_t target_seq; /**< Seq counter. must be equal at start and + * end of read and lower bit must be 0 */ + uint32_t cycle; /**< incremented each time the graph is started */ + uint64_t xrun; /**< Estimated accumulated xrun duration */ +}; + +/* 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 + * + * Currently not used in PipeWire. Instead, \see spa_param_latency + */ +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 */ + double bar_start_tick; + double ticks_per_beat; + uint32_t padding[4]; +}; + +/** 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 in \ref SPA_IO_Position, and the contents of \ref + * spa_io_position.clock contain the clock updates made by the driving node in + * the graph in its \ref SPA_IO_Clock. Also, \ref spa_io_position.clock.id + * will contain the clock id of the driving node in the graph. + * + * The position clock indicates the logical start time of the current graph + * cycle. + * + * 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. + * + * It is usually set on the nodes that process resampled data, by + * the component (audioadapter) that handles resampling between graph + * and node rates. The \a flags and \a rate fields may be modified by the node. + * + * The node can request a correction to the resampling rate in its process(), by setting + * \ref SPA_IO_RATE_MATCH_ACTIVE on \a flags, and setting \a rate to the desired rate + * correction. Usually the rate is obtained from DLL or other adaptive mechanism that + * e.g. drives the node buffer fill level toward a specific value. + * + * When resampling to (graph->node) direction, the number of samples produced + * by the resampler varies on each cycle, as the rates are not commensurate. + * + * When resampling to (node->graph) direction, the number of samples consumed by the + * resampler varies. Node output ports in process() should produce \a size number of + * samples to match what the resampler needs to produce one graph quantum of output + * samples. + * + * Resampling filters introduce processing delay, given by \a delay and \a delay_frac, in + * samples at node rate. The delay varies on each cycle e.g. when resampling between + * noncommensurate rates. + * + * The first sample output (graph->node) or consumed (node->graph) by the resampler is + * offset by \a delay + \a delay_frac / 1e9 node samples relative to the nominal graph + * cycle start position: + * + * \code{.unparsed} + * first_resampled_sample_nsec = + * first_original_sample_nsec + * - (rate_match->delay * SPA_NSEC_PER_SEC + rate_match->delay_frac) / node_rate + * \endcode + */ +struct spa_io_rate_match { + uint32_t delay; /**< resampling delay, in samples at + * node rate */ + uint32_t size; /**< requested input size for resampler */ + double rate; /**< rate for resampler (set by node) */ +#define SPA_IO_RATE_MATCH_FLAG_ACTIVE (1 << 0) + uint32_t flags; /**< extra flags (set by node) */ + int32_t delay_frac; /**< resampling delay fractional part, + * in units of nanosamples (1/10^9 sample) at node rate */ + uint32_t padding[6]; +}; + +/** async buffers */ +struct spa_io_async_buffers { + struct spa_io_buffers buffers[2]; /**< async buffers, writers write to current (cycle+1)&1, + * readers read from (cycle)&1 */ +}; + +/** + * \} + */ + +#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..fea7b78 --- /dev/null +++ b/spa/include/spa/node/keys.h @@ -0,0 +1,48 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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_DESCRIPTION "node.description" /**< localized human readable node one-line + * description. Ex. "Foobar USB Headset" */ +#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 */ +#define SPA_KEY_PORT_IGNORE_LATENCY "port.ignore-latency" /**< latency ignored by peers */ +#define SPA_KEY_PORT_GROUP "port.group" /**< the port group this port belongs to */ + + +/** + * \} + */ + +#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..ae9f635 --- /dev/null +++ b/spa/include/spa/node/node.h @@ -0,0 +1,767 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_NODE + #ifdef SPA_API_IMPL + #define SPA_API_NODE SPA_API_IMPL + #else + #define SPA_API_NODE static inline + #endif +#endif + + +#define SPA_TYPE_INTERFACE_Node SPA_TYPE_INFO_INTERFACE_BASE "Node" + +#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. + * + * When SPA_ID_INVALID is given as the port_id, the node will reply with + * the params that would be returned for a new port in the given direction. + * + * 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 or SPA_ID_INVALID + * \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. The node muse be paused + * or the port SPA_IO_Buffers area is NULL when this function is called with + * a param that changes the processing state (like a format change). + * + * \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. The node muse be paused + * or the port SPA_IO_Buffers area is NULL when this function is called. + * + * \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. + * + * This function can be called when the node is running and the node + * must be prepared to handle changes in io areas while running. This + * is normally done by synchronizing the port io updates with the + * data processing loop. + * + * \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); +}; + + +SPA_API_NODE int spa_node_add_listener(struct spa_node *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, add_listener, 0, + listener, events, data); +} +SPA_API_NODE int spa_node_set_callbacks(struct spa_node *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_callbacks, 0, + callbacks, data); +} +SPA_API_NODE int spa_node_sync(struct spa_node *object, int seq) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, sync, 0, + seq); +} +SPA_API_NODE int spa_node_enum_params(struct spa_node *object, int seq, + uint32_t id, uint32_t start, uint32_t max, + const struct spa_pod *filter) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, enum_params, 0, + seq, id, start, max, filter); +} +SPA_API_NODE int spa_node_set_param(struct spa_node *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_param, 0, + id, flags, param); +} +SPA_API_NODE int spa_node_set_io(struct spa_node *object, + uint32_t id, void *data, size_t size) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, set_io, 0, + id, data, size); +} +SPA_API_NODE int spa_node_send_command(struct spa_node *object, + const struct spa_command *command) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, send_command, 0, + command); +} +SPA_API_NODE int spa_node_add_port(struct spa_node *object, + enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, add_port, 0, + direction, port_id, props); +} +SPA_API_NODE int spa_node_remove_port(struct spa_node *object, + enum spa_direction direction, uint32_t port_id) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, remove_port, 0, + direction, port_id); +} +SPA_API_NODE int spa_node_port_enum_params(struct spa_node *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t max, + const struct spa_pod *filter) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_enum_params, 0, + seq, direction, port_id, id, start, max, filter); +} +SPA_API_NODE int spa_node_port_set_param(struct spa_node *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_set_param, 0, + direction, port_id, id, flags, param); +} +SPA_API_NODE int spa_node_port_use_buffers(struct spa_node *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_use_buffers, 0, + direction, port_id, flags, buffers, n_buffers); +} +SPA_API_NODE int spa_node_port_set_io(struct spa_node *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_set_io, 0, + direction, port_id, id, data, size); +} + +SPA_API_NODE int spa_node_port_reuse_buffer(struct spa_node *object, uint32_t port_id, uint32_t buffer_id) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, port_reuse_buffer, 0, + port_id, buffer_id); +} +SPA_API_NODE int spa_node_port_reuse_buffer_fast(struct spa_node *object, uint32_t port_id, uint32_t buffer_id) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_node, &object->iface, port_reuse_buffer, 0, + port_id, buffer_id); +} +SPA_API_NODE int spa_node_process(struct spa_node *object) +{ + return spa_api_method_r(int, -ENOTSUP, spa_node, &object->iface, process, 0); +} +SPA_API_NODE int spa_node_process_fast(struct spa_node *object) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_node, &object->iface, process, 0); +} + +/** + * \} + */ + +#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..5b95634 --- /dev/null +++ b/spa/include/spa/node/type-info.h @@ -0,0 +1,88 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_NODE_TYPES_H +#define SPA_NODE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +#include +#include +#include + +#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 }, + { SPA_IO_AsyncBuffers, SPA_TYPE_Int, SPA_TYPE_INFO_IO_BASE "AsyncBuffers", 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..b7724e9 --- /dev/null +++ b/spa/include/spa/node/utils.h @@ -0,0 +1,146 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_NODE_UTILS_H +#define SPA_NODE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +#include + +#ifndef SPA_API_NODE_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_NODE_UTILS SPA_API_IMPL + #else + #define SPA_API_NODE_UTILS static inline + #endif +#endif + +struct spa_result_node_params_data { + struct spa_pod_builder *builder; + struct spa_result_node_params data; +}; + +SPA_API_NODE_UTILS void spa_result_func_node_params(void *data, + int seq SPA_UNUSED, int res SPA_UNUSED, uint32_t type SPA_UNUSED, const void *result) +{ + struct spa_result_node_params_data *d = + (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); +} + +SPA_API_NODE_UTILS int spa_node_enum_params_sync(struct spa_node *node, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_node_params_data data = { builder, {0}}; + struct spa_hook listener = {{0}, {0}, 0, 0}; + static const struct spa_node_events node_events = { + .version = SPA_VERSION_NODE_EVENTS, + .info = NULL, + .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; +} + +SPA_API_NODE_UTILS int spa_node_port_enum_params_sync(struct spa_node *node, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_node_params_data data = { builder, {0}}; + struct spa_hook listener = {{0}, {0}, 0, 0}; + static const struct spa_node_events node_events = { + .version = SPA_VERSION_NODE_EVENTS, + .info = NULL, + .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; \ + spa_callbacks_call_fast_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..6e50473 --- /dev/null +++ b/spa/include/spa/param/audio/aac-types.h @@ -0,0 +1,43 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AAC_TYPES_H +#define SPA_AUDIO_AAC_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_param + * \{ + */ + +#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..01f226b --- /dev/null +++ b/spa/include/spa/param/audio/aac-utils.h @@ -0,0 +1,78 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AAC_UTILS_H +#define SPA_AUDIO_AAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_AAC_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_AAC_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_AAC_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_AAC_UTILS int +spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_aac *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_AAC_UTILS struct spa_pod * +spa_format_audio_aac_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_aac *info) +{ + 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..0bc1dea --- /dev/null +++ b/spa/include/spa/param/audio/aac.h @@ -0,0 +1,56 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AAC_H +#define SPA_AUDIO_AAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..898a84e --- /dev/null +++ b/spa/include/spa/param/audio/alac-utils.h @@ -0,0 +1,70 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_ALAC_UTILS_H +#define SPA_AUDIO_ALAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_ALAC_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_ALAC_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_ALAC_UTILS static inline + #endif +#endif + + +SPA_API_AUDIO_ALAC_UTILS int +spa_format_audio_alac_parse(const struct spa_pod *format, struct spa_audio_info_alac *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_ALAC_UTILS struct spa_pod * +spa_format_audio_alac_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_alac *info) +{ + 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..6e6f19f --- /dev/null +++ b/spa/include/spa/param/audio/alac.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_ALAC_H +#define SPA_AUDIO_ALAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..9ab4c9e --- /dev/null +++ b/spa/include/spa/param/audio/amr-types.h @@ -0,0 +1,37 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AMR_TYPES_H +#define SPA_AUDIO_AMR_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_param + * \{ + */ + +#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..cfe6aa5 --- /dev/null +++ b/spa/include/spa/param/audio/amr-utils.h @@ -0,0 +1,74 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AMR_UTILS_H +#define SPA_AUDIO_AMR_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_AMR_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_AMR_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_AMR_UTILS static inline + #endif +#endif + + +SPA_API_AUDIO_AMR_UTILS int +spa_format_audio_amr_parse(const struct spa_pod *format, struct spa_audio_info_amr *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_AMR_UTILS struct spa_pod * +spa_format_audio_amr_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_amr *info) +{ + 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..d00125a --- /dev/null +++ b/spa/include/spa/param/audio/amr.h @@ -0,0 +1,41 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AMR_H +#define SPA_AUDIO_AMR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..d05c596 --- /dev/null +++ b/spa/include/spa/param/audio/ape-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_APE_UTILS_H +#define SPA_AUDIO_APE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_APE_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_APE_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_APE_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_APE_UTILS int +spa_format_audio_ape_parse(const struct spa_pod *format, struct spa_audio_info_ape *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_APE_UTILS struct spa_pod * +spa_format_audio_ape_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_ape *info) +{ + 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..03f787f --- /dev/null +++ b/spa/include/spa/param/audio/ape.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_APE_H +#define SPA_AUDIO_APE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..1105415 --- /dev/null +++ b/spa/include/spa/param/audio/compressed.h @@ -0,0 +1,19 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Asymptotic Inc. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_COMPRESSED_H +#define SPA_AUDIO_COMPRESSED_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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..3f7065b --- /dev/null +++ b/spa/include/spa/param/audio/dsd-utils.h @@ -0,0 +1,89 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DSD_UTILS_H +#define SPA_AUDIO_DSD_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_DSD_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_DSD_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_DSD_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_DSD_UTILS int +spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_dsd *info) +{ + struct spa_pod *position = NULL; + 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; +} + +SPA_API_AUDIO_DSD_UTILS struct spa_pod * +spa_format_audio_dsd_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_dsd *info) +{ + 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..53678d4 --- /dev/null +++ b/spa/include/spa/param/audio/dsd.h @@ -0,0 +1,61 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DSD_H +#define SPA_AUDIO_DSD_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \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..af107f1 --- /dev/null +++ b/spa/include/spa/param/audio/dsp-utils.h @@ -0,0 +1,64 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DSP_UTILS_H +#define SPA_AUDIO_DSP_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_DSP_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_DSP_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_DSP_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_DSP_UTILS int +spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_dsp *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format)); + return res; +} + +SPA_API_AUDIO_DSP_UTILS struct spa_pod * +spa_format_audio_dsp_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_dsp *info) +{ + 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..592f25c --- /dev/null +++ b/spa/include/spa/param/audio/dsp.h @@ -0,0 +1,33 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DSP_H +#define SPA_AUDIO_DSP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..bc3d8af --- /dev/null +++ b/spa/include/spa/param/audio/flac-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_FLAC_UTILS_H +#define SPA_AUDIO_FLAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_FLAC_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_FLAC_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_FLAC_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_FLAC_UTILS int +spa_format_audio_flac_parse(const struct spa_pod *format, struct spa_audio_info_flac *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_FLAC_UTILS struct spa_pod * +spa_format_audio_flac_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_flac *info) +{ + 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..f213e3f --- /dev/null +++ b/spa/include/spa/param/audio/flac.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_FLAC_H +#define SPA_AUDIO_FLAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..4ff40fa --- /dev/null +++ b/spa/include/spa/param/audio/format-utils.h @@ -0,0 +1,130 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_AUDIO_FORMAT_UTILS_H +#define SPA_PARAM_AUDIO_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_FORMAT_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_FORMAT_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_FORMAT_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_FORMAT_UTILS int +spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info) +{ + int res; + + 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; +} + +SPA_API_AUDIO_FORMAT_UTILS struct spa_pod * +spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info *info) +{ + 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..0619de3 --- /dev/null +++ b/spa/include/spa/param/audio/format.h @@ -0,0 +1,62 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_AUDIO_FORMAT_H +#define SPA_PARAM_AUDIO_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; + struct spa_audio_info_ape opus; + } 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..adcffdc --- /dev/null +++ b/spa/include/spa/param/audio/iec958-types.h @@ -0,0 +1,60 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_IEC958_TYPES_H +#define SPA_AUDIO_IEC958_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_IEC958_TYPES + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_IEC958_TYPES SPA_API_IMPL + #else + #define SPA_API_AUDIO_IEC958_TYPES static inline + #endif +#endif + +#define SPA_TYPE_INFO_AudioIEC958Codec SPA_TYPE_INFO_ENUM_BASE "AudioIEC958Codec" +#define SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE SPA_TYPE_INFO_AudioIEC958Codec ":" + +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 }, +}; + +SPA_API_AUDIO_IEC958_TYPES uint32_t spa_type_audio_iec958_codec_from_short_name(const char *name) +{ + return spa_type_from_short_name(name, spa_type_audio_iec958_codec, SPA_AUDIO_IEC958_CODEC_UNKNOWN); +} +SPA_API_AUDIO_IEC958_TYPES const char * spa_type_audio_iec958_codec_to_short_name(uint32_t type) +{ + return spa_type_to_short_name(type, spa_type_audio_iec958_codec, "UNKNOWN"); +} +/** + * \} + */ + +#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..1c4ec10 --- /dev/null +++ b/spa/include/spa/param/audio/iec958-utils.h @@ -0,0 +1,68 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_IEC958_UTILS_H +#define SPA_AUDIO_IEC958_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_IEC958_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_IEC958_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_IEC958_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_IEC958_UTILS int +spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_info_iec958 *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_IEC958_UTILS struct spa_pod * +spa_format_audio_iec958_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_iec958 *info) +{ + 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..0547518 --- /dev/null +++ b/spa/include/spa/param/audio/iec958.h @@ -0,0 +1,49 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_IEC958_H +#define SPA_AUDIO_IEC958_H + +#include + +#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..545ceae --- /dev/null +++ b/spa/include/spa/param/audio/layout.h @@ -0,0 +1,170 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_LAYOUT_H +#define SPA_AUDIO_LAYOUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ +#include + +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..a7ba22a --- /dev/null +++ b/spa/include/spa/param/audio/mp3-types.h @@ -0,0 +1,39 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_MP3_TYPES_H +#define SPA_AUDIO_MP3_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_param + * \{ + */ + +#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..481000e --- /dev/null +++ b/spa/include/spa/param/audio/mp3-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_MP3_UTILS_H +#define SPA_AUDIO_MP3_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_MP3_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_MP3_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_MP3_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_MP3_UTILS int +spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_mp3 *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_MP3_UTILS struct spa_pod * +spa_format_audio_mp3_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_mp3 *info) +{ + 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..86be21a --- /dev/null +++ b/spa/include/spa/param/audio/mp3.h @@ -0,0 +1,42 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_MP3_H +#define SPA_AUDIO_MP3_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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/opus.h b/spa/include/spa/param/audio/opus.h new file mode 100644 index 0000000..cdd0e6c --- /dev/null +++ b/spa/include/spa/param/audio/opus.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_OPUS_H +#define SPA_AUDIO_OPUS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +struct spa_audio_info_opus { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_OPUS_INIT(...) ((struct spa_audio_info_opus) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_OPUS_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..79e9651 --- /dev/null +++ b/spa/include/spa/param/audio/ra-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RA_UTILS_H +#define SPA_AUDIO_RA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_RA_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_RA_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_RA_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_RA_UTILS int +spa_format_audio_ra_parse(const struct spa_pod *format, struct spa_audio_info_ra *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_RA_UTILS struct spa_pod * +spa_format_audio_ra_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_ra *info) +{ + 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..b784ab5 --- /dev/null +++ b/spa/include/spa/param/audio/ra.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RA_H +#define SPA_AUDIO_RA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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-json.h b/spa/include/spa/param/audio/raw-json.h new file mode 100644 index 0000000..07f0e0c --- /dev/null +++ b/spa/include/spa/param/audio/raw-json.h @@ -0,0 +1,105 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RAW_JSON_H +#define SPA_AUDIO_RAW_JSON_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_RAW_JSON + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_RAW_JSON SPA_API_IMPL + #else + #define SPA_API_AUDIO_RAW_JSON static inline + #endif +#endif + +SPA_API_AUDIO_RAW_JSON int +spa_audio_parse_position(const char *str, size_t len, + uint32_t *position, uint32_t *n_channels) +{ + struct spa_json iter; + char v[256]; + uint32_t channels = 0; + + if (spa_json_begin_array_relax(&iter, str, len) <= 0) + return 0; + + while (spa_json_get_string(&iter, v, sizeof(v)) > 0 && + channels < SPA_AUDIO_MAX_CHANNELS) { + position[channels++] = spa_type_audio_channel_from_short_name(v); + } + *n_channels = channels; + return channels; +} + +SPA_API_AUDIO_RAW_JSON int +spa_audio_info_raw_update(struct spa_audio_info_raw *info, const char *key, const char *val, bool force) +{ + uint32_t v; + if (spa_streq(key, SPA_KEY_AUDIO_FORMAT)) { + if (force || info->format == 0) + info->format = (enum spa_audio_format)spa_type_audio_format_from_short_name(val); + } else if (spa_streq(key, SPA_KEY_AUDIO_RATE)) { + if (spa_atou32(val, &v, 0) && (force || info->rate == 0)) + info->rate = v; + } else if (spa_streq(key, SPA_KEY_AUDIO_CHANNELS)) { + if (spa_atou32(val, &v, 0) && (force || info->channels == 0)) + info->channels = SPA_MIN(v, SPA_AUDIO_MAX_CHANNELS); + } else if (spa_streq(key, SPA_KEY_AUDIO_POSITION)) { + if (force || info->channels == 0) { + if (spa_audio_parse_position(val, strlen(val), info->position, &info->channels) > 0) + SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + } + } + return 0; +} + +SPA_API_AUDIO_RAW_JSON int SPA_SENTINEL +spa_audio_info_raw_init_dict_keys(struct spa_audio_info_raw *info, + const struct spa_dict *defaults, + const struct spa_dict *dict, ...) +{ + spa_zero(*info); + SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + if (dict) { + const char *val, *key; + va_list args; + va_start(args, dict); + while ((key = va_arg(args, const char *))) { + if ((val = spa_dict_lookup(dict, key)) == NULL) + continue; + spa_audio_info_raw_update(info, key, val, true); + } + va_end(args); + } + if (defaults) { + const struct spa_dict_item *it; + spa_dict_for_each(it, defaults) + spa_audio_info_raw_update(info, it->key, it->value, false); + } + return 0; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RAW_JSON_H */ diff --git a/spa/include/spa/param/audio/raw-types.h b/spa/include/spa/param/audio/raw-types.h new file mode 100644 index 0000000..9aa9591 --- /dev/null +++ b/spa/include/spa/param/audio/raw-types.h @@ -0,0 +1,286 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RAW_TYPES_H +#define SPA_AUDIO_RAW_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_AUDIO_RAW_TYPES + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_RAW_TYPES SPA_API_IMPL + #else + #define SPA_API_AUDIO_RAW_TYPES static inline + #endif +#endif + +#define SPA_TYPE_INFO_AudioFormat SPA_TYPE_INFO_ENUM_BASE "AudioFormat" +#define SPA_TYPE_INFO_AUDIO_FORMAT_BASE SPA_TYPE_INFO_AudioFormat ":" + +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 }, +}; + +SPA_API_AUDIO_RAW_TYPES uint32_t spa_type_audio_format_from_short_name(const char *name) +{ + return spa_type_from_short_name(name, spa_type_audio_format, SPA_AUDIO_FORMAT_UNKNOWN); +} +SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_format_to_short_name(uint32_t type) +{ + return spa_type_to_short_name(type, spa_type_audio_format, "UNKNOWN"); +} + +#define SPA_TYPE_INFO_AudioFlags SPA_TYPE_INFO_FLAGS_BASE "AudioFlags" +#define SPA_TYPE_INFO_AUDIO_FLAGS_BASE SPA_TYPE_INFO_AudioFlags ":" + +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 }, +}; + +SPA_API_AUDIO_RAW_TYPES uint32_t spa_type_audio_channel_from_short_name(const char *name) +{ + return spa_type_from_short_name(name, spa_type_audio_channel, SPA_AUDIO_CHANNEL_UNKNOWN); +} +SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_channel_to_short_name(uint32_t type) +{ + return spa_type_to_short_name(type, spa_type_audio_channel, "UNK"); +} + + +/** + * \} + */ + +#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..178e3dd --- /dev/null +++ b/spa/include/spa/param/audio/raw-utils.h @@ -0,0 +1,85 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RAW_UTILS_H +#define SPA_AUDIO_RAW_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_RAW_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_RAW_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_RAW_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_RAW_UTILS int +spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info) +{ + struct spa_pod *position = NULL; + 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; +} + +SPA_API_AUDIO_RAW_UTILS struct spa_pod * +spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_raw *info) +{ + 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..8bed3f8 --- /dev/null +++ b/spa/include/spa/param/audio/raw.h @@ -0,0 +1,301 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_RAW_H +#define SPA_AUDIO_RAW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +/** + * \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, +}; + +enum spa_audio_volume_ramp_scale { + SPA_AUDIO_VOLUME_RAMP_INVALID, + SPA_AUDIO_VOLUME_RAMP_LINEAR, + SPA_AUDIO_VOLUME_RAMP_CUBIC, +}; + +/** 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..8a3aa49 --- /dev/null +++ b/spa/include/spa/param/audio/type-info.h @@ -0,0 +1,15 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_TYPES_H +#define SPA_AUDIO_TYPES_H + +#include +#include +#include +#include +#include +#include + +#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..bc901e6 --- /dev/null +++ b/spa/include/spa/param/audio/vorbis-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_VORBIS_UTILS_H +#define SPA_AUDIO_VORBIS_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_VORBIS_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_VORBIS_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_VORBIS_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_VORBIS_UTILS int +spa_format_audio_vorbis_parse(const struct spa_pod *format, struct spa_audio_info_vorbis *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_VORBIS_UTILS struct spa_pod * +spa_format_audio_vorbis_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_vorbis *info) +{ + 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..e3ab490 --- /dev/null +++ b/spa/include/spa/param/audio/vorbis.h @@ -0,0 +1,34 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_VORBIS_H +#define SPA_AUDIO_VORBIS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..40f9d66 --- /dev/null +++ b/spa/include/spa/param/audio/wma-types.h @@ -0,0 +1,42 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_WMA_TYPES_H +#define SPA_AUDIO_WMA_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_param + * \{ + */ + +#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..ca15f7d --- /dev/null +++ b/spa/include/spa/param/audio/wma-utils.h @@ -0,0 +1,82 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_WMA_UTILS_H +#define SPA_AUDIO_WMA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#ifndef SPA_API_AUDIO_WMA_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_WMA_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_WMA_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_WMA_UTILS int +spa_format_audio_wma_parse(const struct spa_pod *format, struct spa_audio_info_wma *info) +{ + int res; + 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; +} + +SPA_API_AUDIO_WMA_UTILS struct spa_pod * +spa_format_audio_wma_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_wma *info) +{ + 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..dd1e53a --- /dev/null +++ b/spa/include/spa/param/audio/wma.h @@ -0,0 +1,52 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_WMA_H +#define SPA_AUDIO_WMA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_param + * \{ + */ + +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..c95e22d --- /dev/null +++ b/spa/include/spa/param/bluetooth/audio.h @@ -0,0 +1,60 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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_AAC_ELD, + 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, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + + /* HFP */ + SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100, + SPA_BLUETOOTH_AUDIO_CODEC_MSBC, + SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, + + /* BAP */ + SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200, + + /* ASHA */ + SPA_BLUETOOTH_AUDIO_CODEC_G722 = 0x300, +}; + +/** + * \} + */ + +#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..7d9cd36 --- /dev/null +++ b/spa/include/spa/param/bluetooth/type-info.h @@ -0,0 +1,63 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUETOOTH_TYPES_H +#define SPA_BLUETOOTH_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#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_AAC_ELD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aac_eld", 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_OPUS_G, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_g", 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_SWB, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3_swb", NULL }, + + { SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL }, + + { SPA_BLUETOOTH_AUDIO_CODEC_G722, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "g722", NULL }, + + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#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..4ca45cb --- /dev/null +++ b/spa/include/spa/param/buffers-types.h @@ -0,0 +1,71 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_BUFFERS_TYPES_H +#define SPA_PARAM_BUFFERS_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#include + +#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 }, + { SPA_PARAM_BUFFERS_metaType, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "metaType", 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..9c157ae --- /dev/null +++ b/spa/include/spa/param/buffers.h @@ -0,0 +1,53 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_BUFFERS_H +#define SPA_PARAM_BUFFERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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 (flags choice Int, mask of enum spa_data_type) */ + SPA_PARAM_BUFFERS_metaType, /**< required meta data types (Int, mask of enum spa_meta_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..8daaa10 --- /dev/null +++ b/spa/include/spa/param/format-types.h @@ -0,0 +1,178 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_FORMAT_TYPES_H +#define SPA_PARAM_FORMAT_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#include +#include +#include + +#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 }, + { SPA_MEDIA_SUBTYPE_opus, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "opus", 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 ":" + +#define SPA_TYPE_INFO_FormatControl SPA_TYPE_INFO_FORMAT_BASE "Control" +#define SPA_TYPE_INFO_FORMAT_CONTROL_BASE SPA_TYPE_INFO_FormatControl ":" + +static const struct spa_type_info spa_type_format[] = { + { SPA_FORMAT_START, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE, spa_type_param, }, + + { 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 }, + + { SPA_FORMAT_CONTROL_types, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_CONTROL_BASE "types", spa_type_control }, + { 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..27fc5f5 --- /dev/null +++ b/spa/include/spa/param/format-utils.h @@ -0,0 +1,46 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_FORMAT_UTILS_H +#define SPA_PARAM_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#ifndef SPA_API_FORMAT_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_FORMAT_UTILS SPA_API_IMPL + #else + #define SPA_API_FORMAT_UTILS static inline + #endif +#endif + +SPA_API_FORMAT_UTILS int +spa_format_parse(const struct spa_pod *format, uint32_t *media_type, uint32_t *media_subtype) +{ + return spa_pod_parse_object(format, + 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..eb3b851 --- /dev/null +++ b/spa/include/spa/param/format.h @@ -0,0 +1,159 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_FORMAT_H +#define SPA_PARAM_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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_opus, /** since 0.3.68 */ + + 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, + SPA_FORMAT_CONTROL_types, /**< possible control types (flags choice Int, + * mask of enum spa_control_type) */ +}; + +#define SPA_KEY_FORMAT_DSP "format.dsp" /**< a predefined DSP format, + * 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..883375f --- /dev/null +++ b/spa/include/spa/param/latency-types.h @@ -0,0 +1,55 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_LATENCY_TYPES_H +#define SPA_PARAM_LATENCY_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#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..45f817e --- /dev/null +++ b/spa/include/spa/param/latency-utils.h @@ -0,0 +1,182 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_LATENCY_UTILS_H +#define SPA_PARAM_LATENCY_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#include +#include +#include + +#ifndef SPA_API_LATENCY_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_LATENCY_UTILS SPA_API_IMPL + #else + #define SPA_API_LATENCY_UTILS static inline + #endif +#endif + +SPA_API_LATENCY_UTILS int +spa_latency_info_compare(const struct spa_latency_info *a, const struct spa_latency_info *b) +{ + if (a->min_quantum == b->min_quantum && + 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; +} + +SPA_API_LATENCY_UTILS void +spa_latency_info_combine_start(struct spa_latency_info *info, enum spa_direction direction) +{ + *info = SPA_LATENCY_INFO(direction, + .min_quantum = FLT_MAX, + .max_quantum = FLT_MIN, + .min_rate = INT32_MAX, + .max_rate = INT32_MIN, + .min_ns = INT64_MAX, + .max_ns = INT64_MIN); +} +SPA_API_LATENCY_UTILS void +spa_latency_info_combine_finish(struct spa_latency_info *info) +{ + if (info->min_quantum == FLT_MAX) + info->min_quantum = 0; + if (info->max_quantum == FLT_MIN) + info->max_quantum = 0; + if (info->min_rate == INT32_MAX) + info->min_rate = 0; + if (info->max_rate == INT32_MIN) + info->max_rate = 0; + if (info->min_ns == INT64_MAX) + info->min_ns = 0; + if (info->max_ns == INT64_MIN) + info->max_ns = 0; +} + +SPA_API_LATENCY_UTILS int +spa_latency_info_combine(struct spa_latency_info *info, const struct spa_latency_info *other) +{ + if (info->direction != other->direction) + 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; +} + +SPA_API_LATENCY_UTILS 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; +} + +SPA_API_LATENCY_UTILS struct spa_pod * +spa_latency_build(struct spa_pod_builder *builder, uint32_t id, const struct spa_latency_info *info) +{ + return (struct spa_pod *)spa_pod_builder_add_object(builder, + 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)); +} + +SPA_API_LATENCY_UTILS 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; +} + +SPA_API_LATENCY_UTILS struct spa_pod * +spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_process_latency_info *info) +{ + 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)); +} + +SPA_API_LATENCY_UTILS 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; +} + +SPA_API_LATENCY_UTILS int +spa_process_latency_info_compare(const struct spa_process_latency_info *a, + const struct spa_process_latency_info *b) +{ + if (a->quantum == b->quantum && + a->rate == b->rate && + a->ns == b->ns) + return 0; + return 1; +} + +/** + * \} + */ + +#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..4087941 --- /dev/null +++ b/spa/include/spa/param/latency.h @@ -0,0 +1,91 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_LATENY_H +#define SPA_PARAM_LATENY_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** + * Properties for SPA_TYPE_OBJECT_ParamLatency + * + * The latency indicates: + * + * - for playback: time delay between start of a graph cycle, and the rendering of + * the first sample of that cycle in audio output. + * + * - for capture: time delay between start of a graph cycle, and the first sample + * of that cycle having occurred in audio input. + * + * For physical output/input, the latency is intended to correspond to the + * rendering/capture of physical audio, including hardware internal rendering delay. + * + * The latency values are adjusted by \ref SPA_PROP_latencyOffsetNsec or + * SPA_PARAM_ProcessLatency, if present. (e.g. for ALSA this is used to adjust for + * the internal hardware latency). + */ +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 graph rate */ + SPA_PARAM_LATENCY_maxRate, /**< max latency (Int) relative to graph 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; + int32_t min_rate; + int32_t max_rate; + int64_t min_ns; + int64_t max_ns; +}; + +#define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ }) + +/** + * Properties for SPA_TYPE_OBJECT_ParamProcessLatency + * + * The processing latency indicates logical time delay between a sample in an input port, + * and a corresponding sample in an output port, relative to the graph time. + */ +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 graph rate */ + SPA_PARAM_PROCESS_LATENCY_ns, /**< latency (Long) in nanoseconds */ +}; + +/** Helper structure for managing process latency objects */ +struct spa_process_latency_info { + float quantum; + int32_t rate; + int64_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..ebb8d98 --- /dev/null +++ b/spa/include/spa/param/param-types.h @@ -0,0 +1,101 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_TYPES_H +#define SPA_PARAM_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +/* 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 }, + { SPA_PARAM_Tag, SPA_TYPE_OBJECT_ParamTag, SPA_TYPE_INFO_PARAM_ID_BASE "Tag", 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 + +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_int_array[] = { + { SPA_PROP_START, SPA_TYPE_Int, SPA_TYPE_INFO_BASE "intArray", NULL, }, + { 0, 0, NULL, NULL }, +}; + +static const struct spa_type_info spa_type_prop_channel_map[] = { + { SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_BASE "channelMap", spa_type_audio_channel, }, + { 0, 0, NULL, NULL }, +}; + +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..51c442c --- /dev/null +++ b/spa/include/spa/param/param.h @@ -0,0 +1,88 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 + +/** 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 */ + SPA_PARAM_Tag, /**< tag reporting, a SPA_TYPE_OBJECT_ParamTag. Since 0.3.79 */ +}; + +/** 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 +#include +#include +#include + +/** + * \} + */ + +#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..2eabb78 --- /dev/null +++ b/spa/include/spa/param/port-config-types.h @@ -0,0 +1,53 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PORT_CONFIG_TYPES_H +#define SPA_PARAM_PORT_CONFIG_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#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..7e05eda --- /dev/null +++ b/spa/include/spa/param/port-config.h @@ -0,0 +1,46 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PORT_CONFIG_H +#define SPA_PARAM_PORT_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +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, /**< (Id enum spa_direction) 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..3373d64 --- /dev/null +++ b/spa/include/spa/param/profile-types.h @@ -0,0 +1,45 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROFILE_TYPES_H +#define SPA_PARAM_PROFILE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#include + +#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..00468f3 --- /dev/null +++ b/spa/include/spa/param/profile.h @@ -0,0 +1,52 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROFILE_H +#define SPA_PARAM_PROFILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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..57f3f36 --- /dev/null +++ b/spa/include/spa/param/profiler-types.h @@ -0,0 +1,41 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROFILER_TYPES_H +#define SPA_PARAM_PROFILER_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#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, }, + { SPA_PROFILER_followerClock, SPA_TYPE_Struct, SPA_TYPE_INFO_PROFILER_BASE "followerClock", 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..36af0fc --- /dev/null +++ b/spa/include/spa/param/profiler.h @@ -0,0 +1,93 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROFILER_H +#define SPA_PARAM_PROFILER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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, + * Int : transport_state, + * Int : clock cycle, + * Long : xrun duration)) */ + 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, + * Int : xrun_count)) */ + + 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, + * Int : xrun_count)) */ + SPA_PROFILER_followerClock, /**< follower clock information + * (Struct( + * Int : clock id, + * String: clock name, + * Long : clock nsec, + * Fraction : clock rate, + * Long : clock position, + * Long : clock duration, + * Long : clock delay, + * Double : clock rate_diff, + * Long : clock next_nsec, + * Long : xrun duration)) */ + SPA_PROFILER_START_CUSTOM = 0x1000000, +}; + +/** + * \} + */ + +#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..e661259 --- /dev/null +++ b/spa/include/spa/param/props-types.h @@ -0,0 +1,104 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROPS_TYPES_H +#define SPA_PARAM_PROPS_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#include + +/** 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_volumeRampSamples, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampSamples", NULL }, + { SPA_PROP_volumeRampStepSamples, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepSamples", NULL }, + { SPA_PROP_volumeRampTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampTime", NULL }, + { SPA_PROP_volumeRampStepTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepTime", NULL }, + { SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", NULL }, + + { SPA_PROP_brightness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL }, + { SPA_PROP_contrast, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL }, + { SPA_PROP_saturation, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "saturation", NULL }, + { SPA_PROP_hue, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "hue", NULL }, + { SPA_PROP_gamma, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "gamma", NULL }, + { SPA_PROP_exposure, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "exposure", NULL }, + { SPA_PROP_gain, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "gain", NULL }, + { SPA_PROP_sharpness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "sharpness", NULL }, + + { SPA_PROP_params, SPA_TYPE_Struct, SPA_TYPE_INFO_PROPS_BASE "params", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** 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..a7a2e4c --- /dev/null +++ b/spa/include/spa/param/props.h @@ -0,0 +1,127 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_PROPS_H +#define SPA_PARAM_PROPS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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 no attenutation */ + SPA_PROP_mute, /**< mute (Bool) */ + SPA_PROP_patternType, + SPA_PROP_ditherType, + SPA_PROP_truncate, + SPA_PROP_channelVolumes, /**< a volume array, one (linear) volume per channel + * (Array of Float). 0.0 is silence, 1.0 is + * without attenuation. This is the effective + * volume that is applied. It can result + * in a hardware volume and software volume + * (see softVolumes) */ + 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 (linear) volume per + * channel (Array of Float) */ + SPA_PROP_latencyOffsetNsec, /**< delay adjustment */ + SPA_PROP_softMute, /**< mute (Bool) applied in software */ + SPA_PROP_softVolumes, /**< a volume array, one (linear) volume per channel + * (Array of Float). 0.0 is silence, 1.0 is without + * attenuation. This is the volume applied in + * software, there might be a part applied in + * hardware. */ + + SPA_PROP_iec958Codecs, /**< enabled IEC958 (S/PDIF) codecs, + * (Array (Id enum spa_audio_iec958_codec) */ + SPA_PROP_volumeRampSamples, /**< Samples to ramp the volume over */ + SPA_PROP_volumeRampStepSamples, /**< Step or incremental Samples to ramp + * the volume over */ + SPA_PROP_volumeRampTime, /**< Time in millisec to ramp the volume over */ + SPA_PROP_volumeRampStepTime, /**< Step or incremental Time in nano seconds + * to ramp the */ + SPA_PROP_volumeRampScale, /**< the scale or graph to used to ramp the + * volume */ + + 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..78ced49 --- /dev/null +++ b/spa/include/spa/param/route-types.h @@ -0,0 +1,51 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_ROUTE_TYPES_H +#define SPA_PARAM_ROUTE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#include + +#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_Array, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profiles", spa_type_prop_int_array, }, + { SPA_PARAM_ROUTE_props, SPA_TYPE_OBJECT_Props, SPA_TYPE_INFO_PARAM_ROUTE_BASE "props", NULL, }, + { SPA_PARAM_ROUTE_devices, SPA_TYPE_Array, SPA_TYPE_INFO_PARAM_ROUTE_BASE "devices", spa_type_prop_int_array, }, + { SPA_PARAM_ROUTE_profile, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profile", NULL, }, + { SPA_PARAM_ROUTE_save, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_ROUTE_BASE "save", NULL, }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#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..d73880c --- /dev/null +++ b/spa/include/spa/param/route.h @@ -0,0 +1,49 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_ROUTE_H +#define SPA_PARAM_ROUTE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** 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/tag-types.h b/spa/include/spa/param/tag-types.h new file mode 100644 index 0000000..573fb4a --- /dev/null +++ b/spa/include/spa/param/tag-types.h @@ -0,0 +1,39 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_TAG_TYPES_H +#define SPA_PARAM_TAG_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#define SPA_TYPE_INFO_PARAM_Tag SPA_TYPE_INFO_PARAM_BASE "Tag" +#define SPA_TYPE_INFO_PARAM_TAG_BASE SPA_TYPE_INFO_PARAM_Tag ":" + +static const struct spa_type_info spa_type_param_tag[] = { + { SPA_PARAM_TAG_START, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_TAG_BASE, spa_type_param, }, + { SPA_PARAM_TAG_direction, SPA_TYPE_Id, SPA_TYPE_INFO_PARAM_TAG_BASE "direction", spa_type_direction, }, + { SPA_PARAM_TAG_info, SPA_TYPE_Struct, SPA_TYPE_INFO_PARAM_TAG_BASE "info", NULL, }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_TAG_TYPES_H */ diff --git a/spa/include/spa/param/tag-utils.h b/spa/include/spa/param/tag-utils.h new file mode 100644 index 0000000..ba8a952 --- /dev/null +++ b/spa/include/spa/param/tag-utils.h @@ -0,0 +1,151 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_TAG_UTILS_H +#define SPA_PARAM_TAG_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#include +#include +#include +#include + +#ifndef SPA_API_TAG_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_TAG_UTILS SPA_API_IMPL + #else + #define SPA_API_TAG_UTILS static inline + #endif +#endif + +SPA_API_TAG_UTILS int +spa_tag_compare(const struct spa_pod *a, const struct spa_pod *b) +{ + return ((a == b) || (a && b && SPA_POD_SIZE(a) == SPA_POD_SIZE(b) && + memcmp(a, b, SPA_POD_SIZE(b)) == 0)) ? 0 : 1; +} + +SPA_API_TAG_UTILS int +spa_tag_parse(const struct spa_pod *tag, struct spa_tag_info *info, void **state) +{ + int res; + const struct spa_pod_object *obj = (const struct spa_pod_object*)tag; + const struct spa_pod_prop *first, *start, *cur; + + spa_zero(*info); + + if ((res = spa_pod_parse_object(tag, + SPA_TYPE_OBJECT_ParamTag, NULL, + SPA_PARAM_TAG_direction, SPA_POD_Id(&info->direction))) < 0) + return res; + + first = spa_pod_prop_first(&obj->body); + start = *state ? spa_pod_prop_next((struct spa_pod_prop*)*state) : first; + + res = 0; + for (cur = start; spa_pod_prop_is_inside(&obj->body, obj->pod.size, cur); + cur = spa_pod_prop_next(cur)) { + if (cur->key == SPA_PARAM_TAG_info) { + info->info = &cur->value; + *state = (void*)cur; + return 1; + } + } + return 0; +} + +SPA_API_TAG_UTILS int +spa_tag_info_parse(const struct spa_tag_info *info, struct spa_dict *dict, struct spa_dict_item *items) +{ + struct spa_pod_parser prs; + uint32_t n, n_items; + const char *key, *value; + struct spa_pod_frame f[1]; + + spa_pod_parser_pod(&prs, info->info); + if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 || + spa_pod_parser_get_int(&prs, (int32_t*)&n_items) < 0) + return -EINVAL; + + if (items == NULL) { + dict->n_items = n_items; + return 0; + } + n_items = SPA_MIN(dict->n_items, n_items); + + for (n = 0; n < n_items; n++) { + if (spa_pod_parser_get(&prs, + SPA_POD_String(&key), + SPA_POD_String(&value), + NULL) < 0) + break; + items[n].key = key; + items[n].value = value; + } + dict->items = items; + spa_pod_parser_pop(&prs, &f[0]); + return 0; +} + +SPA_API_TAG_UTILS void +spa_tag_build_start(struct spa_pod_builder *builder, struct spa_pod_frame *f, + uint32_t id, enum spa_direction direction) +{ + spa_pod_builder_push_object(builder, f, SPA_TYPE_OBJECT_ParamTag, id); + spa_pod_builder_add(builder, + SPA_PARAM_TAG_direction, SPA_POD_Id(direction), + 0); +} + +SPA_API_TAG_UTILS void +spa_tag_build_add_info(struct spa_pod_builder *builder, const struct spa_pod *info) +{ + spa_pod_builder_add(builder, + SPA_PARAM_TAG_info, SPA_POD_Pod(info), + 0); +} + +SPA_API_TAG_UTILS void +spa_tag_build_add_dict(struct spa_pod_builder *builder, const struct spa_dict *dict) +{ + uint32_t i, n_items; + struct spa_pod_frame f; + + n_items = dict ? dict->n_items : 0; + + spa_pod_builder_prop(builder, SPA_PARAM_TAG_info, SPA_POD_PROP_FLAG_HINT_DICT); + spa_pod_builder_push_struct(builder, &f); + spa_pod_builder_int(builder, n_items); + for (i = 0; i < n_items; i++) { + spa_pod_builder_string(builder, dict->items[i].key); + spa_pod_builder_string(builder, dict->items[i].value); + } + spa_pod_builder_pop(builder, &f); +} + +SPA_API_TAG_UTILS struct spa_pod * +spa_tag_build_end(struct spa_pod_builder *builder, struct spa_pod_frame *f) +{ + return (struct spa_pod*)spa_pod_builder_pop(builder, f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_TAG_UTILS_H */ diff --git a/spa/include/spa/param/tag.h b/spa/include/spa/param/tag.h new file mode 100644 index 0000000..8e36ce5 --- /dev/null +++ b/spa/include/spa/param/tag.h @@ -0,0 +1,46 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_TAG_H +#define SPA_PARAM_TAG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_ParamTag */ +enum spa_param_tag { + SPA_PARAM_TAG_START, + SPA_PARAM_TAG_direction, /**< direction, input/output (Id enum spa_direction) */ + SPA_PARAM_TAG_info, /**< Struct( + * Int: n_items + * (String: key + * String: value)* + * ) */ +}; + +/** helper structure for managing tag objects */ +struct spa_tag_info { + enum spa_direction direction; + const struct spa_pod *info; +}; + +#define SPA_TAG_INFO(dir,...) ((struct spa_tag_info) { .direction = (dir), ## __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_TAG_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..fee2d03 --- /dev/null +++ b/spa/include/spa/param/type-info.h @@ -0,0 +1,19 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_TYPE_INFO_H +#define SPA_PARAM_TYPE_INFO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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..0fc8daf --- /dev/null +++ b/spa/include/spa/param/video/chroma.h @@ -0,0 +1,44 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..9a65bf0 --- /dev/null +++ b/spa/include/spa/param/video/color.h @@ -0,0 +1,105 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..6e76309 --- /dev/null +++ b/spa/include/spa/param/video/dsp-utils.h @@ -0,0 +1,76 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_DSP_UTILS_H +#define SPA_VIDEO_DSP_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_VIDEO_DSP_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_DSP_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_DSP_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_DSP_UTILS int +spa_format_video_dsp_parse(const struct spa_pod *format, + struct spa_video_info_dsp *info) +{ + info->flags = SPA_VIDEO_FLAG_NONE; + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) != NULL) { + info->flags |= SPA_VIDEO_FLAG_MODIFIER; + if ((mod_prop->flags & SPA_POD_PROP_FLAG_DONT_FIXATE) == SPA_POD_PROP_FLAG_DONT_FIXATE) + info->flags |= SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + } + + 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_API_VIDEO_DSP_UTILS struct spa_pod * +spa_format_video_dsp_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info_dsp *info) +{ + 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_prop(builder, + SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); + spa_pod_builder_long(builder, info->modifier); + } + 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..3200dea --- /dev/null +++ b/spa/include/spa/param/video/dsp.h @@ -0,0 +1,35 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_DSP_H +#define SPA_VIDEO_DSP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +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..670da5a --- /dev/null +++ b/spa/include/spa/param/video/encoded.h @@ -0,0 +1,11 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_ENCODED_H +#define SPA_VIDEO_ENCODED_H + +#include +#include + +#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..9efbef6 --- /dev/null +++ b/spa/include/spa/param/video/format-utils.h @@ -0,0 +1,73 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_VIDEO_FORMAT_UTILS_H +#define SPA_PARAM_VIDEO_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_VIDEO_FORMAT_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_FORMAT_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_FORMAT_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_FORMAT_UTILS int +spa_format_video_parse(const struct spa_pod *format, struct spa_video_info *info) +{ + int res; + + 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; +} + +SPA_API_VIDEO_FORMAT_UTILS struct spa_pod * +spa_format_video_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info *info) +{ + 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..7fa992c --- /dev/null +++ b/spa/include/spa/param/video/format.h @@ -0,0 +1,41 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PARAM_VIDEO_FORMAT_H +#define SPA_PARAM_VIDEO_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +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..fa69332 --- /dev/null +++ b/spa/include/spa/param/video/h264-utils.h @@ -0,0 +1,78 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_H264_UTILS_H +#define SPA_VIDEO_H264_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_VIDEO_H264_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_H264_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_H264_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_H264_UTILS int +spa_format_video_h264_parse(const struct spa_pod *format, + struct spa_video_info_h264 *info) +{ + 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)); +} + +SPA_API_VIDEO_H264_UTILS struct spa_pod * +spa_format_video_h264_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info_h264 *info) +{ + 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..33ddffc --- /dev/null +++ b/spa/include/spa/param/video/h264.h @@ -0,0 +1,48 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_H264_H +#define SPA_VIDEO_H264_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +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..f1aa27a --- /dev/null +++ b/spa/include/spa/param/video/mjpg-utils.h @@ -0,0 +1,70 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_MJPG_UTILS_H +#define SPA_VIDEO_MJPG_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_VIDEO_MJPG_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_MJPG_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_MJPG_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_MJPG_UTILS int +spa_format_video_mjpg_parse(const struct spa_pod *format, + struct spa_video_info_mjpg *info) +{ + 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_API_VIDEO_MJPG_UTILS struct spa_pod * +spa_format_video_mjpg_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info_mjpg *info) +{ + 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..fb85daa --- /dev/null +++ b/spa/include/spa/param/video/mjpg.h @@ -0,0 +1,33 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_MJPG_H +#define SPA_VIDEO_MJPG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +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..34d4742 --- /dev/null +++ b/spa/include/spa/param/video/multiview.h @@ -0,0 +1,111 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 + * metadata 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 metadata */ + /* 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 a 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..bca0c8d --- /dev/null +++ b/spa/include/spa/param/video/raw-types.h @@ -0,0 +1,162 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_RAW_TYPES_H +#define SPA_VIDEO_RAW_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ +#include +#include + +#ifndef SPA_API_VIDEO_RAW_TYPES + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_RAW_TYPES SPA_API_IMPL + #else + #define SPA_API_VIDEO_RAW_TYPES static inline + #endif +#endif + +#define SPA_TYPE_INFO_VideoFormat SPA_TYPE_INFO_ENUM_BASE "VideoFormat" +#define SPA_TYPE_INFO_VIDEO_FORMAT_BASE SPA_TYPE_INFO_VideoFormat ":" + +static const struct spa_type_info spa_type_video_format[] = { + { SPA_VIDEO_FORMAT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UNKNOWN", NULL }, + { SPA_VIDEO_FORMAT_ENCODED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ENCODED", NULL }, + { SPA_VIDEO_FORMAT_I420, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", NULL }, + { SPA_VIDEO_FORMAT_YV12, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", NULL }, + { SPA_VIDEO_FORMAT_YUY2, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", NULL }, + { 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 }, +}; + +SPA_API_VIDEO_RAW_TYPES uint32_t spa_type_video_format_from_short_name(const char *name) +{ + return spa_type_from_short_name(name, spa_type_video_format, SPA_VIDEO_FORMAT_UNKNOWN); +} +SPA_API_VIDEO_RAW_TYPES const char * spa_type_video_format_to_short_name(uint32_t type) +{ + return spa_type_to_short_name(type, spa_type_video_format, "UNKNOWN"); +} + +#define SPA_TYPE_INFO_VideoFlags SPA_TYPE_INFO_FLAGS_BASE "VideoFlags" +#define SPA_TYPE_INFO_VIDEO_FLAGS_BASE SPA_TYPE_INFO_VideoFlags ":" + +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..8a5a277 --- /dev/null +++ b/spa/include/spa/param/video/raw-utils.h @@ -0,0 +1,128 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_RAW_UTILS_H +#define SPA_VIDEO_RAW_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_VIDEO_RAW_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_RAW_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_RAW_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_RAW_UTILS int +spa_format_video_raw_parse(const struct spa_pod *format, + struct spa_video_info_raw *info) +{ + info->flags = SPA_VIDEO_FLAG_NONE; + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) != NULL) { + info->flags |= SPA_VIDEO_FLAG_MODIFIER; + if ((mod_prop->flags & SPA_POD_PROP_FLAG_DONT_FIXATE) == SPA_POD_PROP_FLAG_DONT_FIXATE) + info->flags |= SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + } + + 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)); +} + +SPA_API_VIDEO_RAW_UTILS struct spa_pod * +spa_format_video_raw_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info_raw *info) +{ + 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_prop(builder, + SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); + spa_pod_builder_long(builder, info->modifier); + } + 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..84f78ad --- /dev/null +++ b/spa/include/spa/param/video/raw.h @@ -0,0 +1,202 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_RAW_H +#define SPA_VIDEO_RAW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +#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 */ + SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED = (1 << 3), /**< format modifier was not fixated yet */ +}; + +/** + * 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..04e00c9 --- /dev/null +++ b/spa/include/spa/param/video/type-info.h @@ -0,0 +1,10 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_TYPES_H +#define SPA_VIDEO_TYPES_H + +#include + +#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..553f755 --- /dev/null +++ b/spa/include/spa/pod/builder.h @@ -0,0 +1,698 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 + +#include +#include +#include + +#ifndef SPA_API_POD_BUILDER + #ifdef SPA_API_IMPL + #define SPA_API_POD_BUILDER SPA_API_IMPL + #else + #define SPA_API_POD_BUILDER static inline + #endif +#endif + +struct spa_pod_builder_state { + uint32_t offset; +#define SPA_POD_BUILDER_FLAG_BODY (1<<0) +#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, {0,0,NULL},{NULL,NULL}}) + +SPA_API_POD_BUILDER void +spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builder_state *state) +{ + *state = builder->state; +} + +SPA_API_POD_BUILDER void +spa_pod_builder_set_callbacks(struct spa_pod_builder *builder, + const struct spa_pod_builder_callbacks *callbacks, void *data) +{ + builder->callbacks = SPA_CALLBACKS_INIT(callbacks, data); +} + +SPA_API_POD_BUILDER void +spa_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_state *state) +{ + struct spa_pod_frame *f; + uint32_t size = builder->state.offset - state->offset; + builder->state = *state; + for (f = builder->state.frame; f ; f = f->parent) + f->pod.size -= size; +} + +SPA_API_POD_BUILDER void spa_pod_builder_init(struct spa_pod_builder *builder, void *data, uint32_t size) +{ + *builder = SPA_POD_BUILDER_INIT(data, size); +} + +SPA_API_POD_BUILDER 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; +} + +SPA_API_POD_BUILDER struct spa_pod * +spa_pod_builder_frame(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + if (frame->offset + SPA_POD_SIZE(&frame->pod) <= builder->size) + return SPA_PTROFF(builder->data, frame->offset, struct spa_pod); + return NULL; +} + +SPA_API_POD_BUILDER 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; +} + +SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, const void *data, uint32_t size) +{ + int res = 0; + struct spa_pod_frame *f; + uint32_t offset = builder->state.offset; + size_t data_offset = -1; + + if (offset + size > builder->size) { + /* data could be inside the data we will realloc */ + if (spa_ptrinside(builder->data, builder->size, data, size, NULL)) + data_offset = SPA_PTRDIFF(data, builder->data); + + res = -ENOSPC; + if (offset <= builder->size) + spa_callbacks_call_res(&builder->callbacks, + struct spa_pod_builder_callbacks, res, + overflow, 0, offset + size); + } + if (res == 0 && data) { + if (data_offset != (size_t) -1) + data = SPA_PTROFF(builder->data, data_offset, const void); + + memcpy(SPA_PTROFF(builder->data, offset, void), data, size); + } + + builder->state.offset += size; + + for (f = builder->state.frame; f ; f = f->parent) + f->pod.size += size; + + return res; +} + +SPA_API_POD_BUILDER int spa_pod_builder_pad(struct spa_pod_builder *builder, uint32_t size) +{ + uint64_t zeroes = 0; + size = SPA_ROUND_UP_N(size, 8) - size; + return size ? spa_pod_builder_raw(builder, &zeroes, size) : 0; +} + +SPA_API_POD_BUILDER int +spa_pod_builder_raw_padded(struct spa_pod_builder *builder, const void *data, uint32_t size) +{ + int r, res = spa_pod_builder_raw(builder, data, size); + if ((r = spa_pod_builder_pad(builder, size)) < 0) + res = r; + return res; +} + +SPA_API_POD_BUILDER 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; +} + +SPA_API_POD_BUILDER 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) + +SPA_API_POD_BUILDER int spa_pod_builder_none(struct spa_pod_builder *builder) +{ + const struct spa_pod p = SPA_POD_INIT_None(); + return spa_pod_builder_primitive(builder, &p); +} + +SPA_API_POD_BUILDER int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_t size, uint32_t type) +{ + const struct spa_pod p = SPA_POD_INIT(size,type); + SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST); + 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 }) + +SPA_API_POD_BUILDER int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val) +{ + const struct spa_pod_bool p = SPA_POD_INIT_Bool(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Id(val) ((struct spa_pod_id){ { sizeof(uint32_t), SPA_TYPE_Id }, (val), 0 }) + +SPA_API_POD_BUILDER int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t val) +{ + const struct spa_pod_id p = SPA_POD_INIT_Id(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Int(val) ((struct spa_pod_int){ { sizeof(int32_t), SPA_TYPE_Int }, (val), 0 }) + +SPA_API_POD_BUILDER int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t val) +{ + const struct spa_pod_int p = SPA_POD_INIT_Int(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Long(val) ((struct spa_pod_long){ { sizeof(int64_t), SPA_TYPE_Long }, (val) }) + +SPA_API_POD_BUILDER int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t val) +{ + const struct spa_pod_long p = SPA_POD_INIT_Long(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Float(val) ((struct spa_pod_float){ { sizeof(float), SPA_TYPE_Float }, (val), 0 }) + +SPA_API_POD_BUILDER int spa_pod_builder_float(struct spa_pod_builder *builder, float val) +{ + const struct spa_pod_float p = SPA_POD_INIT_Float(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Double(val) ((struct spa_pod_double){ { sizeof(double), SPA_TYPE_Double }, (val) }) + +SPA_API_POD_BUILDER int spa_pod_builder_double(struct spa_pod_builder *builder, double val) +{ + const struct spa_pod_double p = SPA_POD_INIT_Double(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_String(len) ((struct spa_pod_string){ { (len), SPA_TYPE_String } }) + +SPA_API_POD_BUILDER 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; +} + +SPA_API_POD_BUILDER int +spa_pod_builder_string_len(struct spa_pod_builder *builder, const char *str, uint32_t len) +{ + const struct spa_pod_string p = SPA_POD_INIT_String(len+1); + 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; +} + +SPA_API_POD_BUILDER int spa_pod_builder_string(struct spa_pod_builder *builder, const char *str) +{ + uint32_t len = str ? strlen(str) : 0; + return spa_pod_builder_string_len(builder, str ? str : "", len); +} + +#define SPA_POD_INIT_Bytes(len) ((struct spa_pod_bytes){ { (len), SPA_TYPE_Bytes } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len) +{ + const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(len); + 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; +} +SPA_API_POD_BUILDER 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) } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const void *val) +{ + const struct spa_pod_pointer p = SPA_POD_INIT_Pointer(type, val); + 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) }) + +SPA_API_POD_BUILDER int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd) +{ + const struct spa_pod_fd p = SPA_POD_INIT_Fd(fd); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Rectangle(val) ((struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, (val) }) + +SPA_API_POD_BUILDER int +spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint32_t height) +{ + const struct spa_pod_rectangle p = SPA_POD_INIT_Rectangle(SPA_RECTANGLE(width, height)); + 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) }) + +SPA_API_POD_BUILDER int +spa_pod_builder_fraction(struct spa_pod_builder *builder, uint32_t num, uint32_t denom) +{ + const struct spa_pod_fraction p = SPA_POD_INIT_Fraction(SPA_FRACTION(num, denom)); + return spa_pod_builder_primitive(builder, &p.pod); +} + +SPA_API_POD_BUILDER int +spa_pod_builder_push_array(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + const struct spa_pod_array p = + { {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; +} + +SPA_API_POD_BUILDER int +spa_pod_builder_array(struct spa_pod_builder *builder, + uint32_t child_size, uint32_t child_type, uint32_t n_elems, const void *elems) +{ + 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__ } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_frame *frame, + uint32_t type, uint32_t flags) +{ + 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 } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + const struct spa_pod_struct p = SPA_POD_INIT_Struct(0); + 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__ }) + +SPA_API_POD_BUILDER int +spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_frame *frame, + uint32_t type, uint32_t id) +{ + 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) } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t flags) +{ + const struct { uint32_t key; uint32_t flags; } p = { key, flags }; + 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 } }) + +SPA_API_POD_BUILDER int +spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_frame *frame, uint32_t unit) +{ + const struct spa_pod_sequence p = + 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; +} + +SPA_API_POD_BUILDER int +spa_pod_builder_control(struct spa_pod_builder *builder, uint32_t offset, uint32_t type) +{ + const struct { uint32_t offset; uint32_t type; } p = { offset, type }; + return spa_pod_builder_raw(builder, &p, sizeof(p)); +} + +SPA_API_POD_BUILDER 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, (float)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) + +SPA_API_POD_BUILDER 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; +} + +SPA_API_POD_BUILDER 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 */ +SPA_API_POD_BUILDER 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..330f56e --- /dev/null +++ b/spa/include/spa/pod/command.h @@ -0,0 +1,49 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_COMMAND_H +#define SPA_COMMAND_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \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..5089875 --- /dev/null +++ b/spa/include/spa/pod/compare.h @@ -0,0 +1,174 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_COMPARE_H +#define SPA_POD_COMPARE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef SPA_API_POD_COMPARE + #ifdef SPA_API_IMPL + #define SPA_API_POD_COMPARE SPA_API_IMPL + #else + #define SPA_API_POD_COMPARE static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +SPA_API_POD_COMPARE int spa_pod_compare_value(uint32_t type, const void *r1, const void *r2, uint32_t size) +{ + switch (type) { + case SPA_TYPE_None: + return 0; + case SPA_TYPE_Bool: + return SPA_CMP(!!*(int32_t *)r1, !!*(int32_t *)r2); + case SPA_TYPE_Id: + return SPA_CMP(*(uint32_t *)r1, *(uint32_t *)r2); + case SPA_TYPE_Int: + return SPA_CMP(*(int32_t *)r1, *(int32_t *)r2); + case SPA_TYPE_Long: + return SPA_CMP(*(int64_t *)r1, *(int64_t *)r2); + case SPA_TYPE_Float: + return SPA_CMP(*(float *)r1, *(float *)r2); + case SPA_TYPE_Double: + return SPA_CMP(*(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; + uint64_t n1, n2; + n1 = ((uint64_t) f1->num) * f2->denom; + n2 = ((uint64_t) f2->num) * f1->denom; + return SPA_CMP(n1, n2); + } + default: + break; + } + return 0; +} + +SPA_API_POD_COMPARE 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..e9998cd --- /dev/null +++ b/spa/include/spa/pod/dynamic.h @@ -0,0 +1,75 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_DYNAMIC_H +#define SPA_POD_DYNAMIC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_POD_DYNAMIC + #ifdef SPA_API_IMPL + #define SPA_API_POD_DYNAMIC SPA_API_IMPL + #else + #define SPA_API_POD_DYNAMIC static inline + #endif +#endif + +struct spa_pod_dynamic_builder { + struct spa_pod_builder b; + void *data; + 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, *new_data; + + if (old_data == d->data) + d->b.data = NULL; + if ((new_data = realloc(d->b.data, new_size)) == NULL) + return -errno; + if (old_data == d->data && new_data != old_data && old_size > 0) + memcpy(new_data, old_data, old_size); + d->b.data = new_data; + d->b.size = new_size; + return 0; +} + +SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_builder *builder, + void *data, uint32_t size, uint32_t extend) +{ + static const struct spa_pod_builder_callbacks spa_pod_dynamic_builder_callbacks = { + .version = 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; +} + +SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder) +{ + if (builder->data != builder->b.data) + free(builder->b.data); +} + +SPA_DEFINE_AUTO_CLEANUP(spa_pod_dynamic_builder, struct spa_pod_dynamic_builder, { + spa_pod_dynamic_builder_clean(thing); +}) + +#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..c631bd3 --- /dev/null +++ b/spa/include/spa/pod/event.h @@ -0,0 +1,48 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_EVENT_H +#define SPA_EVENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \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..2cfc7bf --- /dev/null +++ b/spa/include/spa/pod/filter.h @@ -0,0 +1,473 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_FILTER_H +#define SPA_POD_FILTER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef SPA_API_POD_FILTER + #ifdef SPA_API_IMPL + #define SPA_API_POD_FILTER SPA_API_IMPL + #else + #define SPA_API_POD_FILTER static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +SPA_API_POD_FILTER 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; +} + +SPA_API_POD_FILTER int spa_pod_filter_flags_value(struct spa_pod_builder *b, + uint32_t type, const void *r1, const void *r2, uint32_t size SPA_UNUSED) +{ + switch (type) { + 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; +} + +SPA_API_POD_FILTER int spa_pod_filter_is_step_of(uint32_t type, const void *r1, + const void *r2, uint32_t size SPA_UNUSED) +{ + switch (type) { + 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; +} + +SPA_API_POD_FILTER int +spa_pod_filter_prop(struct spa_pod_builder *b, + const struct spa_pod_prop *p1, + const struct spa_pod_prop *p2) +{ + const struct spa_pod *v1, *v2; + struct spa_pod_choice *nc, dummy; + 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); + /* write to dummy value when builder overflows. We don't want to error + * because overflowing is a way to determine the required buffer size. */ + if (nc == NULL) + nc = &dummy; + + /* 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; +} + +SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, + const struct spa_pod *pod, uint32_t pod_size, + const struct spa_pod *filter, uint32_t filter_size) +{ + 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; +} + +SPA_API_POD_FILTER 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..1bd5699 --- /dev/null +++ b/spa/include/spa/pod/iter.h @@ -0,0 +1,477 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_ITER_H +#define SPA_POD_ITER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#ifndef SPA_API_POD_ITER + #ifdef SPA_API_IMPL + #define SPA_API_POD_ITER SPA_API_IMPL + #else + #define SPA_API_POD_ITER static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +struct spa_pod_frame { + struct spa_pod pod; + struct spa_pod_frame *parent; + uint32_t offset; + uint32_t flags; +}; + +SPA_API_POD_ITER bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter) +{ + size_t remaining; + + return spa_ptr_type_inside(pod, size, iter, struct spa_pod, &remaining) && + remaining >= SPA_POD_BODY_SIZE(iter); +} + +SPA_API_POD_ITER void *spa_pod_next(const void *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_SIZE(iter), 8), void); +} + +SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_first(const struct spa_pod_object_body *body) +{ + return SPA_PTROFF(body, sizeof(struct spa_pod_object_body), struct spa_pod_prop); +} + +SPA_API_POD_ITER bool spa_pod_prop_is_inside(const struct spa_pod_object_body *body, + uint32_t size, const struct spa_pod_prop *iter) +{ + size_t remaining; + + return spa_ptr_type_inside(body, size, iter, struct spa_pod_prop, &remaining) && + remaining >= iter->value.size; +} + +SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_next(const struct spa_pod_prop *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(iter), 8), struct spa_pod_prop); +} + +SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_first(const struct spa_pod_sequence_body *body) +{ + return SPA_PTROFF(body, sizeof(struct spa_pod_sequence_body), struct spa_pod_control); +} + +SPA_API_POD_ITER bool spa_pod_control_is_inside(const struct spa_pod_sequence_body *body, + uint32_t size, const struct spa_pod_control *iter) +{ + size_t remaining; + + return spa_ptr_type_inside(body, size, iter, struct spa_pod_control, &remaining) && + remaining >= iter->value.size; +} + +SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_next(const struct spa_pod_control *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(iter), 8), struct spa_pod_control); +} + +#define SPA_POD_ARRAY_BODY_FOREACH(body, _size, iter) \ + for ((iter) = (__typeof__(iter))SPA_PTROFF((body), sizeof(struct spa_pod_array_body), void); \ + (body)->child.size > 0 && spa_ptrinside(body, _size, iter, (body)->child.size, NULL); \ + (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); \ + (body)->child.size > 0 && spa_ptrinside(body, _size, iter, (body)->child.size, NULL); \ + (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) + + +SPA_API_POD_ITER void *spa_pod_from_data(void *data, size_t maxsize, off_t offset, size_t size) +{ + void *pod; + if (size < sizeof(struct spa_pod) || offset + size > maxsize) + return NULL; + pod = SPA_PTROFF(data, offset, void); + if (SPA_POD_SIZE(pod) > size) + return NULL; + return pod; +} + +SPA_API_POD_ITER int spa_pod_is_none(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_None); +} + +SPA_API_POD_ITER int spa_pod_is_bool(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Bool && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); +} + +SPA_API_POD_ITER 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; +} + +SPA_API_POD_ITER int spa_pod_is_id(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Id && SPA_POD_BODY_SIZE(pod) >= sizeof(uint32_t)); +} + +SPA_API_POD_ITER int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value) +{ + if (!spa_pod_is_id(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_id, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_int(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Int && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); +} + +SPA_API_POD_ITER int spa_pod_get_int(const struct spa_pod *pod, int32_t *value) +{ + if (!spa_pod_is_int(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_int, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_long(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Long && SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); +} + +SPA_API_POD_ITER int spa_pod_get_long(const struct spa_pod *pod, int64_t *value) +{ + if (!spa_pod_is_long(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_long, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_float(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Float && SPA_POD_BODY_SIZE(pod) >= sizeof(float)); +} + +SPA_API_POD_ITER 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; +} + +SPA_API_POD_ITER int spa_pod_is_double(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Double && SPA_POD_BODY_SIZE(pod) >= sizeof(double)); +} + +SPA_API_POD_ITER 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; +} + +SPA_API_POD_ITER int spa_pod_is_string(const struct spa_pod *pod) +{ + const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + return (SPA_POD_TYPE(pod) == SPA_TYPE_String && + SPA_POD_BODY_SIZE(pod) > 0 && + s[SPA_POD_BODY_SIZE(pod)-1] == '\0'); +} + +SPA_API_POD_ITER int spa_pod_get_string(const struct spa_pod *pod, const char **value) +{ + if (!spa_pod_is_string(pod)) + return -EINVAL; + *value = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen, char *dest) +{ + const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + if (!spa_pod_is_string(pod) || maxlen < 1) + return -EINVAL; + strncpy(dest, s, maxlen-1); + dest[maxlen-1]= '\0'; + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_bytes(const struct spa_pod *pod) +{ + return SPA_POD_TYPE(pod) == SPA_TYPE_Bytes; +} + +SPA_API_POD_ITER int spa_pod_get_bytes(const struct spa_pod *pod, const void **value, uint32_t *len) +{ + if (!spa_pod_is_bytes(pod)) + return -EINVAL; + *value = (const void *)SPA_POD_CONTENTS(struct spa_pod_bytes, pod); + *len = SPA_POD_BODY_SIZE(pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_pointer(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Pointer && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_pointer_body)); +} + +SPA_API_POD_ITER int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type, const void **value) +{ + if (!spa_pod_is_pointer(pod)) + return -EINVAL; + *type = ((struct spa_pod_pointer*)pod)->body.type; + *value = ((struct spa_pod_pointer*)pod)->body.value; + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_fd(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Fd && + SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); +} + +SPA_API_POD_ITER int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value) +{ + if (!spa_pod_is_fd(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_fd, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_rectangle(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Rectangle && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_rectangle)); +} + +SPA_API_POD_ITER int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_rectangle *value) +{ + if (!spa_pod_is_rectangle(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_rectangle, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_fraction(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Fraction && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_fraction)); +} + +SPA_API_POD_ITER int spa_pod_get_fraction(const struct spa_pod *pod, struct spa_fraction *value) +{ + spa_return_val_if_fail(spa_pod_is_fraction(pod), -EINVAL); + *value = SPA_POD_VALUE(struct spa_pod_fraction, pod); + return 0; +} + +SPA_API_POD_ITER int spa_pod_is_bitmap(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Bitmap && + SPA_POD_BODY_SIZE(pod) >= sizeof(uint8_t)); +} + +SPA_API_POD_ITER int spa_pod_is_array(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Array && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_array_body)); +} + +SPA_API_POD_ITER void *spa_pod_get_array(const struct spa_pod *pod, uint32_t *n_values) +{ + spa_return_val_if_fail(spa_pod_is_array(pod), NULL); + *n_values = SPA_POD_ARRAY_N_VALUES(pod); + return SPA_POD_ARRAY_VALUES(pod); +} + +SPA_API_POD_ITER uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t type, + void *values, uint32_t max_values) +{ + uint32_t n_values; + 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; +} + +SPA_API_POD_ITER int spa_pod_is_choice(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Choice && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_choice_body)); +} + +SPA_API_POD_ITER struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint32_t *n_vals, uint32_t *choice) +{ + if (pod->type == SPA_TYPE_Choice) { + *n_vals = SPA_POD_CHOICE_N_VALUES(pod); + 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; + } +} + +SPA_API_POD_ITER int spa_pod_is_struct(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Struct); +} + +SPA_API_POD_ITER int spa_pod_is_object(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Object && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_object_body)); +} + +SPA_API_POD_ITER bool spa_pod_is_object_type(const struct spa_pod *pod, uint32_t type) +{ + return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_TYPE(pod) == type); +} + +SPA_API_POD_ITER bool spa_pod_is_object_id(const struct spa_pod *pod, uint32_t id) +{ + return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_ID(pod) == id); +} + +SPA_API_POD_ITER int spa_pod_is_sequence(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Sequence && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_sequence_body)); +} + +SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_object_find_prop(const struct spa_pod_object *pod, + const struct spa_pod_prop *start, uint32_t key) +{ + const struct spa_pod_prop *first, *res; + + 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; +} + +SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_find_prop(const struct spa_pod *pod, + const struct spa_pod_prop *start, uint32_t key) +{ + if (!spa_pod_is_object(pod)) + return NULL; + return spa_pod_object_find_prop((const struct spa_pod_object *)pod, start, key); +} + +SPA_API_POD_ITER int spa_pod_object_fixate(struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) { + 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; +} + +SPA_API_POD_ITER int spa_pod_fixate(struct spa_pod *pod) +{ + if (!spa_pod_is_object(pod)) + return -EINVAL; + return spa_pod_object_fixate((struct spa_pod_object *)pod); +} + +SPA_API_POD_ITER int spa_pod_object_is_fixated(const struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) { + if (res->value.type == SPA_TYPE_Choice && + ((struct spa_pod_choice*)&res->value)->body.type != SPA_CHOICE_None) + return 0; + } + return 1; +} + +SPA_API_POD_ITER int spa_pod_object_has_props(const struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) + return 1; + return 0; +} + +SPA_API_POD_ITER 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..d2aa206 --- /dev/null +++ b/spa/include/spa/pod/parser.h @@ -0,0 +1,582 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_PARSER_H +#define SPA_POD_PARSER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +#ifndef SPA_API_POD_PARSER + #ifdef SPA_API_IMPL + #define SPA_API_POD_PARSER SPA_API_IMPL + #else + #define SPA_API_POD_PARSER static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +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, {0,0,NULL}}) + +SPA_API_POD_PARSER void spa_pod_parser_init(struct spa_pod_parser *parser, + const void *data, uint32_t size) +{ + *parser = SPA_POD_PARSER_INIT(data, size); +} + +SPA_API_POD_PARSER void spa_pod_parser_pod(struct spa_pod_parser *parser, + const struct spa_pod *pod) +{ + spa_pod_parser_init(parser, pod, SPA_POD_SIZE(pod)); +} + +SPA_API_POD_PARSER void +spa_pod_parser_get_state(struct spa_pod_parser *parser, struct spa_pod_parser_state *state) +{ + *state = parser->state; +} + +SPA_API_POD_PARSER void +spa_pod_parser_reset(struct spa_pod_parser *parser, struct spa_pod_parser_state *state) +{ + parser->state = *state; +} + +SPA_API_POD_PARSER struct spa_pod * +spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t size) +{ + /* Cast to uint64_t to avoid wraparound. Add 8 for the pod itself. */ + 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; +} + +SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame) +{ + return SPA_PTROFF(parser->data, frame->offset, struct spa_pod); +} + +SPA_API_POD_PARSER void spa_pod_parser_push(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, const struct spa_pod *pod, uint32_t offset) +{ + frame->pod = *pod; + frame->offset = offset; + frame->parent = parser->state.frame; + frame->flags = parser->state.flags; + parser->state.frame = frame; +} + +SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser) +{ + struct spa_pod_frame *f = parser->state.frame; + uint32_t size = f ? f->offset + SPA_POD_SIZE(&f->pod) : parser->size; + return spa_pod_parser_deref(parser, parser->state.offset, size); +} + +SPA_API_POD_PARSER void spa_pod_parser_advance(struct spa_pod_parser *parser, const struct spa_pod *pod) +{ + parser->state.offset += SPA_ROUND_UP_N(SPA_POD_SIZE(pod), 8); +} + +SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser) +{ + struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod) + spa_pod_parser_advance(parser, pod); + return pod; +} + +SPA_API_POD_PARSER 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; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_bool(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_id(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_int(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_long(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_float(struct spa_pod_parser *parser, float *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_float(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_double(struct spa_pod_parser *parser, double *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_double(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_string(struct spa_pod_parser *parser, const char **value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_string(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const void **value, uint32_t *len) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_bytes(pod, value, len)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint32_t *type, const void **value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_pointer(pod, type, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_fd(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, struct spa_rectangle *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_rectangle(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, struct spa_fraction *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_fraction(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct spa_pod **value) +{ + struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod == NULL) + return -EPIPE; + *value = pod; + spa_pod_parser_advance(parser, pod); + return 0; +} +SPA_API_POD_PARSER int spa_pod_parser_push_struct(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + const struct spa_pod *pod = spa_pod_parser_current(parser); + 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; +} + +SPA_API_POD_PARSER int spa_pod_parser_push_object(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, uint32_t type, uint32_t *id) +{ + const struct spa_pod *pod = spa_pod_parser_current(parser); + 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; +} + +SPA_API_POD_PARSER 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) + +SPA_API_POD_PARSER int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list args) +{ + struct spa_pod_frame *f = parser->state.frame; + uint32_t ftype = f ? f->pod.type : (uint32_t)SPA_TYPE_Struct; + const struct spa_pod_prop *prop = NULL; + int count = 0; + + do { + bool optional; + const struct spa_pod *pod = NULL; + const char *format; + + if (f && 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; +} + +SPA_API_POD_PARSER 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..f7627f4 --- /dev/null +++ b/spa/include/spa/pod/pod.h @@ -0,0 +1,226 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_H +#define SPA_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \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..a30e114 --- /dev/null +++ b/spa/include/spa/pod/vararg.h @@ -0,0 +1,93 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_VARARG_H +#define SPA_POD_VARARG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +/** + * \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..ce8551e --- /dev/null +++ b/spa/include/spa/support/cpu.h @@ -0,0 +1,209 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_CPU_H +#define SPA_CPU_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +#ifndef SPA_API_CPU + #ifdef SPA_API_IMPL + #define SPA_API_CPU SPA_API_IMPL + #else + #define SPA_API_CPU static inline + #endif +#endif + +/** \defgroup spa_cpu CPU + * Querying CPU properties + */ + +/** + * \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) + +/* RISCV specific */ +#define SPA_CPU_FLAG_RISCV_V (1 << 0) + +#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) + +SPA_API_CPU const char *spa_cpu_vm_type_to_string(uint32_t vm_type) +{ + switch(vm_type) { + case SPA_CPU_VM_NONE: + return NULL; + case SPA_CPU_VM_KVM: + return "kvm"; + case SPA_CPU_VM_QEMU: + return "qemu"; + case SPA_CPU_VM_BOCHS: + return "bochs"; + case SPA_CPU_VM_XEN: + return "xen"; + case SPA_CPU_VM_UML: + return "uml"; + case SPA_CPU_VM_VMWARE: + return "vmware"; + case SPA_CPU_VM_ORACLE: + return "oracle"; + case SPA_CPU_VM_MICROSOFT: + return "microsoft"; + case SPA_CPU_VM_ZVM: + return "zvm"; + case SPA_CPU_VM_PARALLELS: + return "parallels"; + case SPA_CPU_VM_BHYVE: + return "bhyve"; + case SPA_CPU_VM_QNX: + return "qnx"; + case SPA_CPU_VM_ACRN: + return "acrn"; + case SPA_CPU_VM_POWERVM: + return "powervm"; + case SPA_CPU_VM_OTHER: + return "other"; + default: + return "unknown"; + } +} + +/** + * 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); +}; + +SPA_API_CPU uint32_t spa_cpu_get_flags(struct spa_cpu *c) +{ + return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_flags, 0); +} +SPA_API_CPU int spa_cpu_force_flags(struct spa_cpu *c, uint32_t flags) +{ + return spa_api_method_r(int, -ENOTSUP, spa_cpu, &c->iface, force_flags, 0, flags); +} +SPA_API_CPU uint32_t spa_cpu_get_count(struct spa_cpu *c) +{ + return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_count, 0); +} +SPA_API_CPU uint32_t spa_cpu_get_max_align(struct spa_cpu *c) +{ + return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_max_align, 0); +} +SPA_API_CPU uint32_t spa_cpu_get_vm_type(struct spa_cpu *c) +{ + return spa_api_method_r(uint32_t, 0, spa_cpu, &c->iface, get_vm_type, 1); +} +SPA_API_CPU int spa_cpu_zero_denormals(struct spa_cpu *c, bool enable) +{ + return spa_api_method_r(int, -ENOTSUP, spa_cpu, &c->iface, zero_denormals, 2, enable); +} + +/** keys can be given when initializing the cpu handle */ +#define SPA_KEY_CPU_FORCE "cpu.force" /**< force cpu flags */ +#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..3908bfe --- /dev/null +++ b/spa/include/spa/support/dbus.h @@ -0,0 +1,150 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DBUS_H +#define SPA_DBUS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#ifndef SPA_API_DBUS + #ifdef SPA_API_IMPL + #define SPA_API_DBUS SPA_API_IMPL + #else + #define SPA_API_DBUS static inline + #endif +#endif + +/** \defgroup spa_dbus DBus + * DBus communication + */ + +/** + * \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); +}; + +/** \copydoc spa_dbus_connection.get + * \sa spa_dbus_connection.get */ +SPA_API_DBUS void *spa_dbus_connection_get(struct spa_dbus_connection *conn) +{ + return spa_api_func_r(void *, NULL, conn, get, 0); +} +/** \copydoc spa_dbus_connection.destroy + * \sa spa_dbus_connection.destroy */ +SPA_API_DBUS void spa_dbus_connection_destroy(struct spa_dbus_connection *conn) +{ + spa_api_func_v(conn, destroy, 0); +} +/** \copydoc spa_dbus_connection.add_listener + * \sa spa_dbus_connection.add_listener */ +SPA_API_DBUS void spa_dbus_connection_add_listener(struct spa_dbus_connection *conn, + struct spa_hook *listener, + const struct spa_dbus_connection_events *events, + void *data) +{ + spa_api_func_v(conn, add_listener, 1, listener, events, data); +} + +struct spa_dbus_methods { +#define SPA_VERSION_DBUS_METHODS 0 + 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 + */ +SPA_API_DBUS struct spa_dbus_connection * +spa_dbus_get_connection(struct spa_dbus *dbus, enum spa_dbus_type type) +{ + return spa_api_method_r(struct spa_dbus_connection *, NULL, + spa_dbus, &dbus->iface, get_connection, 0, type); +} + +/** + * \} + */ + +#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..3b25887 --- /dev/null +++ b/spa/include/spa/support/i18n.h @@ -0,0 +1,87 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_I18N_H +#define SPA_I18N_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_I18N + #ifdef SPA_API_IMPL + #define SPA_API_I18N SPA_API_IMPL + #else + #define SPA_API_I18N static inline + #endif +#endif + +/** \defgroup spa_i18n I18N + * Gettext interface + */ + +/** + * \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) +SPA_API_I18N const char * +spa_i18n_text(struct spa_i18n *i18n, const char *msgid) +{ + return spa_api_method_null_r(const char *, msgid, spa_i18n, i18n, &i18n->iface, + text, 0, msgid); +} + +SPA_API_I18N const char * +spa_i18n_ntext(struct spa_i18n *i18n, const char *msgid, + const char *msgid_plural, unsigned long int n) +{ + return spa_api_method_null_r(const char *, n == 1 ? msgid : msgid_plural, + spa_i18n, i18n, &i18n->iface, ntext, 0, msgid, msgid_plural, n); +} + +/** + * \} + */ + +#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..8132d05 --- /dev/null +++ b/spa/include/spa/support/log-impl.h @@ -0,0 +1,124 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_LOG_IMPL_H +#define SPA_LOG_IMPL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** + * \addtogroup spa_log + * \{ + */ + +static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object SPA_UNUSED, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + 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 SPA_UNUSED, struct spa_log_topic *topic SPA_UNUSED) +{ + /* 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, \ + spa_log_impl_topic_init, \ + } } + +#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..e144144 --- /dev/null +++ b/spa/include/spa/support/log.h @@ -0,0 +1,394 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_LOG_H +#define SPA_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +#ifndef SPA_API_LOG + #ifdef SPA_API_IMPL + #define SPA_API_LOG SPA_API_IMPL + #else + #define SPA_API_LOG static inline + #endif +#endif + +/** \defgroup spa_log Log + * Logging interface + */ + +/** + * \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; +}; + +/** + * Enumeration of log topics in a plugin + * + * \since 1.1.0 + */ +struct spa_log_topic_enum { +#define SPA_VERSION_LOG_TOPIC_ENUM 0 + uint32_t version; + /** Array of pointers to log topics */ + struct spa_log_topic * const * const topics; + /** End of topics array */ + struct spa_log_topic * const * const topics_end; +}; + + +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. + * + * \deprecated + * Plugin host should obtain log topics from \ref SPA_LOG_TOPIC_ENUM_NAME + * and update them itself. + * + * \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)} + +SPA_API_LOG void spa_log_topic_init(struct spa_log *log, struct spa_log_topic *topic) +{ + if (SPA_UNLIKELY(!log)) + return; + + spa_interface_call(&log->iface, struct spa_log_methods, topic_init, 1, topic); +} + +SPA_API_LOG bool spa_log_level_topic_enabled(const struct spa_log *log, + const struct spa_log_topic *topic, + enum spa_log_level level) +{ + enum spa_log_level max_level; + + if (SPA_UNLIKELY(!log)) + return false; + + if (topic && topic->has_custom_level) + max_level = topic->level; + else + max_level = log->level; + + return level <= max_level; +} + +/* Transparently calls to version 0 log if v1 is not supported */ +#define spa_log_logt(l,lev,topic,...) \ +({ \ + struct spa_log *_l = l; \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \ + struct spa_interface *_if = &_l->iface; \ + if (!spa_interface_call(_if, \ + struct spa_log_methods, 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 */ +SPA_PRINTF_FUNC(7, 0) +SPA_API_LOG void spa_log_logtv(struct spa_log *l, enum spa_log_level level, + const struct spa_log_topic *topic, const char *file, int line, + const char *func, const char *fmt, va_list args) +{ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(l, topic, level))) { + struct spa_interface *i = &l->iface; + if (!spa_interface_call(i, + struct spa_log_methods, logtv, 1, + level, topic, + file, line, func, fmt, args)) + spa_interface_call(i, + struct spa_log_methods, logv, 0, + level, file, line, func, fmt, args); + } +} + +#define spa_logt_lev(l,lev,t,...) \ + spa_log_logt(l,lev,t,__FILE__,__LINE__,__func__,__VA_ARGS__) + +#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 + + +/** + * Name of the symbol indicating a \ref spa_log_topic_enum enumerating + * the static log topics in a plugin, + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_ENUM_NAME "spa_log_topic_enum" + +/** + * Define the symbol for \ref SPA_LOG_TOPIC_ENUM_NAME + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_ENUM_DEFINE(s, e) \ + SPA_EXPORT struct spa_log_topic_enum spa_log_topic_enum = (struct spa_log_topic_enum) { \ + .version = SPA_VERSION_LOG_TOPIC_ENUM, \ + .topics = (s), \ + .topics_end = (e), \ + } + +/** + * Magically register a statically defined \ref spa_log_topic into + * the log topic enumeration for a plugin. + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_REGISTER(v) \ + __attribute__((used)) __attribute__((retain)) \ + __attribute__((section("spa_log_topic"))) __attribute__((aligned(__alignof__(struct spa_log_topic *)))) \ + static struct spa_log_topic * const spa_log_topic_export_##v = &v + +/** + * Define and magically register a \ref spa_log_topic + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_DEFINE(var,name) \ + struct spa_log_topic var = SPA_LOG_TOPIC(SPA_VERSION_LOG_TOPIC, name); \ + SPA_LOG_TOPIC_REGISTER(var) + +/** + * Define and magically register a \ref spa_log_topic with static scope + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_DEFINE_STATIC(var,name) \ + static struct spa_log_topic var = SPA_LOG_TOPIC(SPA_VERSION_LOG_TOPIC, name); \ + SPA_LOG_TOPIC_REGISTER(var) + +/** + * Do \ref SPA_LOG_TOPIC_ENUM_DEFINE for the auto-registered + * \ref spa_log_topic in the plugin. + * + * \since 1.1.0 + */ +#define SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED \ + extern struct spa_log_topic * const __start_spa_log_topic[]; \ + extern struct spa_log_topic * const __stop_spa_log_topic[]; \ + SPA_LOG_TOPIC_ENUM_DEFINE(__start_spa_log_topic, __stop_spa_log_topic) + +/** \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, set to "force" to enable + * colors even when not logging to a terminal */ +#define SPA_KEY_LOG_FILE "log.file" /**< log to the specified file instead of + * stderr. */ +#define SPA_KEY_LOG_TIMESTAMP "log.timestamp" /**< log timestamp type (local, realtime, monotonic, monotonic-raw). + * boolean true means local. */ +#define SPA_KEY_LOG_LINE "log.line" /**< log file and line numbers */ +#define SPA_KEY_LOG_PATTERNS "log.patterns" /**< Spa:String:JSON array of [ {"pattern" : level}, ... ] */ + +/** + * \} + */ + +#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..520a465 --- /dev/null +++ b/spa/include/spa/support/loop.h @@ -0,0 +1,441 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_LOOP_H +#define SPA_LOOP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +#ifndef SPA_API_LOOP + #ifdef SPA_API_IMPL + #define SPA_API_LOOP SPA_API_IMPL + #else + #define SPA_API_LOOP static inline + #endif +#endif + +/** \defgroup spa_loop Loop + * Event loop interface + */ + +/** + * \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 1 +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. Must be called from the loop's own thread. + * + * \param[in] object The callbacks data. + * \param[in] source The source. + * \return 0 on success, negative errno-style value on failure. + */ + int (*add_source) (void *object, + struct spa_source *source); + + /** Update the source io mask. Must be called from the loop's own thread. + * + * \param[in] object The callbacks data. + * \param[in] source The source. + * \return 0 on success, negative errno-style value on failure. + */ + int (*update_source) (void *object, + struct spa_source *source); + + /** Remove a source from the loop. Must be called from the loop's own thread. + * + * \param[in] object The callbacks data. + * \param[in] source The source. + * \return 0 on success, negative errno-style value on failure. + */ + int (*remove_source) (void *object, + struct spa_source *source); + + /** Invoke a function in the context of this loop. + * May be called from any thread and multiple threads at the same time. + * If called from the loop's thread, all callbacks previously queued with + * invoke() will be run synchronously, which might cause unexpected + * reentrancy problems. + * + * \param[in] object The callbacks data. + * \param func The function to be invoked. + * \param seq An opaque sequence number. This will be made + * available to func. + * \param[in] data Data that will be copied into the internal ring buffer and made + * available to func. Because this data is copied, it is okay to + * pass a pointer to a local variable, but do not pass a pointer to + * an object that has identity. + * \param size The size of data to copy. + * \param block If \true, do not return until func has been called. Otherwise, + * returns immediately. Passing \true does not risk a deadlock because + * the data thread is never allowed to wait on any other thread. + * \param user_data An opaque pointer passed to func. + * \return `-EPIPE` if the internal ring buffer filled up, + * if block is \false, 0 if seq was SPA_ID_INVALID or + * seq with the ASYNC flag set + * or the return value of func otherwise. */ + int (*invoke) (void *object, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + bool block, + void *user_data); +}; + +SPA_API_LOOP int spa_loop_add_source(struct spa_loop *object, struct spa_source *source) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop, &object->iface, add_source, 0, source); +} +SPA_API_LOOP int spa_loop_update_source(struct spa_loop *object, struct spa_source *source) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop, &object->iface, update_source, 0, source); +} +SPA_API_LOOP int spa_loop_remove_source(struct spa_loop *object, struct spa_source *source) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop, &object->iface, remove_source, 0, source); +} +SPA_API_LOOP int spa_loop_invoke(struct spa_loop *object, + spa_invoke_func_t func, uint32_t seq, const void *data, + size_t size, bool block, void *user_data) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop, &object->iface, invoke, 0, func, seq, data, + size, block, user_data); +} + +/** Control hooks. These hooks can't be removed from their + * callbacks and must be removed from a safe place (when the loop + * 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); +}; + +SPA_API_LOOP void spa_loop_control_hook_before(struct spa_hook_list *l) +{ + struct spa_hook *h; + spa_list_for_each_reverse(h, &l->list, link) + spa_callbacks_call_fast(&h->cb, struct spa_loop_control_hooks, before, 0); +} + +SPA_API_LOOP void spa_loop_control_hook_after(struct spa_hook_list *l) +{ + struct spa_hook *h; + spa_list_for_each(h, &l->list, link) + spa_callbacks_call_fast(&h->cb, struct spa_loop_control_hooks, after, 0); +} + +/** + * Control an event loop + * + * The event loop control function provide API to run the event loop. + * + * The below (pseudo)code is a minimal example outlining the use of the loop + * control: + * \code{.c} + * spa_loop_control_enter(loop); + * while (running) { + * spa_loop_control_iterate(loop, -1); + * } + * spa_loop_control_leave(loop); + * \endcode + * + * It is also possible to add the loop to an existing event loop by using the + * spa_loop_control_get_fd() call. This fd will become readable when activity + * has been detected on the sources in the loop. spa_loop_control_iterate() with + * a 0 timeout should be called to process the pending sources. + * + * spa_loop_control_enter() and spa_loop_control_leave() should be called once + * from the thread that will run the iterate() function. + */ +struct spa_loop_control_methods { + /* the version of this structure. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_LOOP_CONTROL_METHODS 1 + uint32_t version; + + /** get the loop fd + * \param object the control to query + * + * Get the fd of this loop control. This fd will be readable when a + * source in the loop has activity. The user should call iterate() + * with a 0 timeout to schedule one iteration of the loop and dispatch + * the sources. + * \return the fd of the loop + */ + int (*get_fd) (void *object); + + /** Add a hook + * \param object 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 object the control + * + * This function should be called before calling iterate and is + * typically used to capture the thread that this loop will run in. + * It should ideally be called once from the thread that will run + * the loop. + */ + void (*enter) (void *object); + /** Leave a loop + * \param object the control + * + * It should ideally be called once after calling iterate when the loop + * will no longer be iterated from the thread that called enter(). + */ + void (*leave) (void *object); + + /** 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); + + /** Check context of the loop + * \param ctrl the control + * + * This function will check if the current thread is currently the + * one that did the enter call. Since version 1:1. + * + * returns 1 on success, 0 or negative errno value on error. + */ + int (*check) (void *object); +}; + +SPA_API_LOOP int spa_loop_control_get_fd(struct spa_loop_control *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, get_fd, 0); +} +SPA_API_LOOP void spa_loop_control_add_hook(struct spa_loop_control *object, + struct spa_hook *hook, const struct spa_loop_control_hooks *hooks, + void *data) +{ + spa_api_method_v(spa_loop_control, &object->iface, add_hook, 0, + hook, hooks, data); +} +SPA_API_LOOP void spa_loop_control_enter(struct spa_loop_control *object) +{ + spa_api_method_v(spa_loop_control, &object->iface, enter, 0); +} +SPA_API_LOOP void spa_loop_control_leave(struct spa_loop_control *object) +{ + spa_api_method_v(spa_loop_control, &object->iface, leave, 0); +} +SPA_API_LOOP int spa_loop_control_iterate(struct spa_loop_control *object, + int timeout) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, iterate, 0, timeout); +} +SPA_API_LOOP int spa_loop_control_iterate_fast(struct spa_loop_control *object, + int timeout) +{ + return spa_api_method_fast_r(int, -ENOTSUP, + spa_loop_control, &object->iface, iterate, 0, timeout); +} +SPA_API_LOOP int spa_loop_control_check(struct spa_loop_control *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, check, 1); +} + +typedef void (*spa_source_io_func_t) (void *data, int fd, uint32_t mask); +typedef void (*spa_source_idle_func_t) (void *data); +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); +}; + +SPA_API_LOOP struct spa_source * +spa_loop_utils_add_io(struct spa_loop_utils *object, int fd, uint32_t mask, + bool close, spa_source_io_func_t func, void *data) +{ + return spa_api_method_r(struct spa_source *, NULL, + spa_loop_utils, &object->iface, add_io, 0, fd, mask, close, func, data); +} +SPA_API_LOOP int spa_loop_utils_update_io(struct spa_loop_utils *object, + struct spa_source *source, uint32_t mask) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_utils, &object->iface, update_io, 0, source, mask); +} +SPA_API_LOOP struct spa_source * +spa_loop_utils_add_idle(struct spa_loop_utils *object, bool enabled, + spa_source_idle_func_t func, void *data) +{ + return spa_api_method_r(struct spa_source *, NULL, + spa_loop_utils, &object->iface, add_idle, 0, enabled, func, data); +} +SPA_API_LOOP int spa_loop_utils_enable_idle(struct spa_loop_utils *object, + struct spa_source *source, bool enabled) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_utils, &object->iface, enable_idle, 0, source, enabled); +} +SPA_API_LOOP struct spa_source * +spa_loop_utils_add_event(struct spa_loop_utils *object, spa_source_event_func_t func, void *data) +{ + return spa_api_method_r(struct spa_source *, NULL, + spa_loop_utils, &object->iface, add_event, 0, func, data); +} +SPA_API_LOOP int spa_loop_utils_signal_event(struct spa_loop_utils *object, + struct spa_source *source) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_utils, &object->iface, signal_event, 0, source); +} +SPA_API_LOOP struct spa_source * +spa_loop_utils_add_timer(struct spa_loop_utils *object, spa_source_timer_func_t func, void *data) +{ + return spa_api_method_r(struct spa_source *, NULL, + spa_loop_utils, &object->iface, add_timer, 0, func, data); +} +SPA_API_LOOP int spa_loop_utils_update_timer(struct spa_loop_utils *object, + struct spa_source *source, struct timespec *value, + struct timespec *interval, bool absolute) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_utils, &object->iface, update_timer, 0, source, + value, interval, absolute); +} +SPA_API_LOOP struct spa_source * +spa_loop_utils_add_signal(struct spa_loop_utils *object, int signal_number, + spa_source_signal_func_t func, void *data) +{ + return spa_api_method_r(struct spa_source *, NULL, + spa_loop_utils, &object->iface, add_signal, 0, + signal_number, func, data); +} +SPA_API_LOOP void spa_loop_utils_destroy_source(struct spa_loop_utils *object, + struct spa_source *source) +{ + spa_api_method_v(spa_loop_utils, &object->iface, destroy_source, 0, source); +} + +/** + * \} + */ + +#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..9540853 --- /dev/null +++ b/spa/include/spa/support/plugin-loader.h @@ -0,0 +1,81 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PLUGIN_LOADER_H +#define SPA_PLUGIN_LOADER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_PLUGIN_LOADER + #ifdef SPA_API_IMPL + #define SPA_API_PLUGIN_LOADER SPA_API_IMPL + #else + #define SPA_API_PLUGIN_LOADER static inline + #endif +#endif + +/** \defgroup spa_plugin_loader Plugin Loader + * SPA plugin loader + */ + +/** + * \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); +}; + +SPA_API_PLUGIN_LOADER struct spa_handle * +spa_plugin_loader_load(struct spa_plugin_loader *loader, const char *factory_name, const struct spa_dict *info) +{ + return spa_api_method_null_r(struct spa_handle *, NULL, spa_plugin_loader, loader, &loader->iface, + load, 0, factory_name, info); +} + +SPA_API_PLUGIN_LOADER int +spa_plugin_loader_unload(struct spa_plugin_loader *loader, struct spa_handle *handle) +{ + return spa_api_method_null_r(int, -1, spa_plugin_loader, loader, &loader->iface, + unload, 0, handle); +} + +/** + * \} + */ + +#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..576c195 --- /dev/null +++ b/spa/include/spa/support/plugin.h @@ -0,0 +1,247 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_PLUGIN_H +#define SPA_PLUGIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +#ifndef SPA_API_PLUGIN + #ifdef SPA_API_IMPL + #define SPA_API_PLUGIN SPA_API_IMPL + #else + #define SPA_API_PLUGIN static inline + #endif +#endif + +/** + * \defgroup spa_handle Plugin Handle + * SPA plugin handle and factory interfaces + */ + +/** + * \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 iface result to hold the interface. + * \return 0 on success + * -ENOTSUP when there are no interfaces + * -EINVAL when handle or info is NULL + */ + int (*get_interface) (struct spa_handle *handle, const char *type, void **iface); + /** + * 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); +}; + +SPA_API_PLUGIN int +spa_handle_get_interface(struct spa_handle *object, + const char *type, void **iface) +{ + return spa_api_func_r(int, -ENOTSUP, object, get_interface, 0, type, iface); +} +SPA_API_PLUGIN int +spa_handle_clear(struct spa_handle *object) +{ + return spa_api_func_r(int, -ENOTSUP, object, clear, 0); +} + +/** + * This structure lists the information about available interfaces on + * 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 */ +SPA_API_PLUGIN 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); +}; + +SPA_API_PLUGIN size_t +spa_handle_factory_get_size(const struct spa_handle_factory *object, + const struct spa_dict *params) +{ + return spa_api_func_r(size_t, 0, object, get_size, 1, params); +} +SPA_API_PLUGIN int +spa_handle_factory_init(const struct spa_handle_factory *object, + struct spa_handle *handle, const struct spa_dict *info, + const struct spa_support *support, uint32_t n_support) +{ + return spa_api_func_r(int, -ENOTSUP, object, init, 1, handle, info, + support, n_support); +} +SPA_API_PLUGIN int +spa_handle_factory_enum_interface_info(const struct spa_handle_factory *object, + const struct spa_interface_info **info, uint32_t *index) +{ + return spa_api_func_r(int, -ENOTSUP, object, enum_interface_info, 1, + info, index); +} + +/** + * The function signature of the entry point in a plugin. + * + * \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..aa140c9 --- /dev/null +++ b/spa/include/spa/support/system.h @@ -0,0 +1,218 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_SYSTEM_H +#define SPA_SYSTEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct itimerspec; + +#include +#include +#include + +#include +#include + +#ifndef SPA_API_SYSTEM + #ifdef SPA_API_IMPL + #define SPA_API_SYSTEM SPA_API_IMPL + #else + #define SPA_API_SYSTEM static inline + #endif +#endif + +/** \defgroup spa_system System + * I/O, clock, polling, timer, and signal interfaces + */ + +/** + * \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); +}; + +SPA_API_SYSTEM ssize_t spa_system_read(struct spa_system *object, int fd, void *buf, size_t count) +{ + return spa_api_method_fast_r(ssize_t, -ENOTSUP, spa_system, &object->iface, read, 0, fd, buf, count); +} +SPA_API_SYSTEM ssize_t spa_system_write(struct spa_system *object, int fd, const void *buf, size_t count) +{ + return spa_api_method_fast_r(ssize_t, -ENOTSUP, spa_system, &object->iface, write, 0, fd, buf, count); +} +#define spa_system_ioctl(object,fd,request,...) \ + spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, ioctl, 0, fd, request, ##__VA_ARGS__) + +SPA_API_SYSTEM int spa_system_close(struct spa_system *object, int fd) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, close, 0, fd); +} +SPA_API_SYSTEM int spa_system_clock_gettime(struct spa_system *object, + int clockid, struct timespec *value) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, clock_gettime, 0, clockid, value); +} +SPA_API_SYSTEM int spa_system_clock_getres(struct spa_system *object, + int clockid, struct timespec *res) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, clock_getres, 0, clockid, res); +} + +SPA_API_SYSTEM int spa_system_pollfd_create(struct spa_system *object, int flags) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_create, 0, flags); +} +SPA_API_SYSTEM int spa_system_pollfd_add(struct spa_system *object, int pfd, int fd, uint32_t events, void *data) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_add, 0, pfd, fd, events, data); +} +SPA_API_SYSTEM int spa_system_pollfd_mod(struct spa_system *object, int pfd, int fd, uint32_t events, void *data) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_mod, 0, pfd, fd, events, data); +} +SPA_API_SYSTEM int spa_system_pollfd_del(struct spa_system *object, int pfd, int fd) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_del, 0, pfd, fd); +} +SPA_API_SYSTEM int spa_system_pollfd_wait(struct spa_system *object, int pfd, + struct spa_poll_event *ev, int n_ev, int timeout) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_wait, 0, pfd, ev, n_ev, timeout); +} + +SPA_API_SYSTEM int spa_system_timerfd_create(struct spa_system *object, int clockid, int flags) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_create, 0, clockid, flags); +} + +SPA_API_SYSTEM int spa_system_timerfd_settime(struct spa_system *object, + int fd, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_settime, 0, + fd, flags, new_value, old_value); +} + +SPA_API_SYSTEM int spa_system_timerfd_gettime(struct spa_system *object, + int fd, struct itimerspec *curr_value) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_gettime, 0, + fd, curr_value); +} +SPA_API_SYSTEM int spa_system_timerfd_read(struct spa_system *object, int fd, uint64_t *expirations) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, timerfd_read, 0, + fd, expirations); +} + +SPA_API_SYSTEM int spa_system_eventfd_create(struct spa_system *object, int flags) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_create, 0, flags); +} +SPA_API_SYSTEM int spa_system_eventfd_write(struct spa_system *object, int fd, uint64_t count) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_write, 0, + fd, count); +} +SPA_API_SYSTEM int spa_system_eventfd_read(struct spa_system *object, int fd, uint64_t *count) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, eventfd_read, 0, + fd, count); +} + +SPA_API_SYSTEM int spa_system_signalfd_create(struct spa_system *object, int signal, int flags) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, signalfd_create, 0, + signal, flags); +} + +SPA_API_SYSTEM int spa_system_signalfd_read(struct spa_system *object, int fd, int *signal) +{ + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, signalfd_read, 0, + fd, signal); +} + +/** + * \} + */ + +#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..b69cb68 --- /dev/null +++ b/spa/include/spa/support/thread.h @@ -0,0 +1,129 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_THREAD_H +#define SPA_THREAD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +#ifndef SPA_API_THREAD + #ifdef SPA_API_IMPL + #define SPA_API_THREAD SPA_API_IMPL + #else + #define SPA_API_THREAD static inline + #endif +#endif + +/** \defgroup spa_thread Thread + * Threading utility interfaces + */ + +/** + * \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 */ +SPA_API_THREAD struct spa_thread *spa_thread_utils_create(struct spa_thread_utils *o, + const struct spa_dict *props, void *(*start_routine)(void*), void *arg) +{ + return spa_api_method_r(struct spa_thread *, NULL, + spa_thread_utils, &o->iface, create, 0, + props, start_routine, arg); +} + +/** \copydoc spa_thread_utils_methods.join + * \sa spa_thread_utils_methods.join */ +SPA_API_THREAD int spa_thread_utils_join(struct spa_thread_utils *o, + struct spa_thread *thread, void **retval) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_thread_utils, &o->iface, join, 0, + thread, retval); +} + +/** \copydoc spa_thread_utils_methods.get_rt_range + * \sa spa_thread_utils_methods.get_rt_range */ +SPA_API_THREAD int spa_thread_utils_get_rt_range(struct spa_thread_utils *o, + const struct spa_dict *props, int *min, int *max) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_thread_utils, &o->iface, get_rt_range, 0, + props, min, max); +} + +/** \copydoc spa_thread_utils_methods.acquire_rt + * \sa spa_thread_utils_methods.acquire_rt */ +SPA_API_THREAD int spa_thread_utils_acquire_rt(struct spa_thread_utils *o, + struct spa_thread *thread, int priority) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_thread_utils, &o->iface, acquire_rt, 0, + thread, priority); +} + +/** \copydoc spa_thread_utils_methods.drop_rt + * \sa spa_thread_utils_methods.drop_rt */ +SPA_API_THREAD int spa_thread_utils_drop_rt(struct spa_thread_utils *o, + struct spa_thread *thread) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_thread_utils, &o->iface, drop_rt, 0, thread); +} + +#define SPA_KEY_THREAD_NAME "thread.name" /* the thread name */ +#define SPA_KEY_THREAD_STACK_SIZE "thread.stack-size" /* the stack size of the thread */ +#define SPA_KEY_THREAD_AFFINITY "thread.affinity" /* array of CPUs for this thread */ +#define SPA_KEY_THREAD_CREATOR "thread.creator" /* platform specific thread creator function */ + +/** + * \} + */ + +#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..5239d39 --- /dev/null +++ b/spa/include/spa/utils/ansi.h @@ -0,0 +1,94 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Red Hat, Inc. */ +/* SPDX-License-Identifier: MIT */ + +#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/atomic.h b/spa/include/spa/utils/atomic.h new file mode 100644 index 0000000..549420b --- /dev/null +++ b/spa/include/spa/utils/atomic.h @@ -0,0 +1,35 @@ +/* Atomic operations */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ATOMIC_H +#define SPA_ATOMIC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define SPA_ATOMIC_CAS(v,ov,nv) \ +({ \ + __typeof__(v) __ov = (ov); \ + __atomic_compare_exchange_n(&(v), &__ov, (nv), \ + 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); \ +}) + +#define SPA_ATOMIC_DEC(s) __atomic_sub_fetch(&(s), 1, __ATOMIC_SEQ_CST) +#define SPA_ATOMIC_INC(s) __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST) +#define SPA_ATOMIC_LOAD(s) __atomic_load_n(&(s), __ATOMIC_SEQ_CST) +#define SPA_ATOMIC_STORE(s,v) __atomic_store_n(&(s), (v), __ATOMIC_SEQ_CST) +#define SPA_ATOMIC_XCHG(s,v) __atomic_exchange_n(&(s), (v), __ATOMIC_SEQ_CST) + +#define SPA_SEQ_WRITE(s) SPA_ATOMIC_INC(s) +#define SPA_SEQ_WRITE_SUCCESS(s1,s2) ((s1) + 1 == (s2) && ((s2) & 1) == 0) + +#define SPA_SEQ_READ(s) SPA_ATOMIC_LOAD(s) +#define SPA_SEQ_READ_SUCCESS(s1,s2) ((s1) == (s2) && ((s2) & 1) == 0) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_ATOMIC_H */ diff --git a/spa/include/spa/utils/cleanup.h b/spa/include/spa/utils/cleanup.h new file mode 100644 index 0000000..23fb8e1 --- /dev/null +++ b/spa/include/spa/utils/cleanup.h @@ -0,0 +1,124 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2023 PipeWire authors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_CLEANUP_H +#define SPA_UTILS_CLEANUP_H + +#define spa_exchange(var, new_value) \ +__extension__ ({ \ + __typeof__(var) *_ptr_ = &(var); \ + __typeof__(var) _old_value_ = *_ptr_; \ + *_ptr_ = (new_value); \ + _old_value_; \ +}) + +/* ========================================================================== */ + +#if __GNUC__ >= 10 || defined(__clang__) +#define spa_steal_ptr(ptr) ((__typeof__(*(ptr)) *) spa_exchange((ptr), NULL)) +#else +#define spa_steal_ptr(ptr) spa_exchange((ptr), NULL) +#endif + +#define spa_clear_ptr(ptr, destructor) \ +__extension__ ({ \ + __typeof__(ptr) _old_value = spa_steal_ptr(ptr); \ + if (_old_value) \ + destructor(_old_value); \ + (void) 0; \ +}) + +/* ========================================================================== */ + +#include +#include + +#define spa_steal_fd(fd) spa_exchange((fd), -1) + +#define spa_clear_fd(fd) \ +__extension__ ({ \ + int _old_value = spa_steal_fd(fd), _res = 0; \ + if (_old_value >= 0) \ + _res = close(_old_value); \ + _res; \ +}) + +/* ========================================================================== */ + +#if defined(__has_attribute) && __has_attribute(__cleanup__) + +#define spa_cleanup(func) __attribute__((__cleanup__(func))) + +#define SPA_DEFINE_AUTO_CLEANUP(name, type, ...) \ +typedef __typeof__(type) _spa_auto_cleanup_type_ ## name; \ +static inline void _spa_auto_cleanup_func_ ## name (__typeof__(type) *thing) \ +{ \ + int _save_errno = errno; \ + __VA_ARGS__ \ + errno = _save_errno; \ +} + +#define spa_auto(name) \ + spa_cleanup(_spa_auto_cleanup_func_ ## name) \ + _spa_auto_cleanup_type_ ## name + +#define SPA_DEFINE_AUTOPTR_CLEANUP(name, type, ...) \ +typedef __typeof__(type) * _spa_autoptr_cleanup_type_ ## name; \ +static inline void _spa_autoptr_cleanup_func_ ## name (__typeof__(type) **thing) \ +{ \ + int _save_errno = errno; \ + __VA_ARGS__ \ + errno = _save_errno; \ +} + +#define spa_autoptr(name) \ + spa_cleanup(_spa_autoptr_cleanup_func_ ## name) \ + _spa_autoptr_cleanup_type_ ## name + +/* ========================================================================== */ + +#include + +static inline void _spa_autofree_cleanup_func(void *p) +{ + int save_errno = errno; + free(*(void **) p); + errno = save_errno; +} +#define spa_autofree spa_cleanup(_spa_autofree_cleanup_func) + +/* ========================================================================== */ + +static inline void _spa_autoclose_cleanup_func(int *fd) +{ + int save_errno = errno; + spa_clear_fd(*fd); + errno = save_errno; +} +#define spa_autoclose spa_cleanup(_spa_autoclose_cleanup_func) + +/* ========================================================================== */ + +#include + +SPA_DEFINE_AUTOPTR_CLEANUP(FILE, FILE, { + spa_clear_ptr(*thing, fclose); +}) + +/* ========================================================================== */ + +#include + +SPA_DEFINE_AUTOPTR_CLEANUP(DIR, DIR, { + spa_clear_ptr(*thing, closedir); +}) + +#else + +#define SPA_DEFINE_AUTO_CLEANUP(name, type, ...) +#define SPA_DEFINE_AUTOPTR_CLEANUP(name, type, ...) + +#endif + +#endif /* SPA_UTILS_CLEANUP_H */ diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h new file mode 100644 index 0000000..1c1a73a --- /dev/null +++ b/spa/include/spa/utils/defs.h @@ -0,0 +1,466 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_DEFS_H +#define SPA_UTILS_DEFS_H + +#ifdef __cplusplus +extern "C" { +# if __cplusplus >= 201103L +# define SPA_STATIC_ASSERT_IMPL(expr, msg, ...) static_assert(expr, msg) +# define SPA_ALIGNOF alignof +# endif +#elif __STDC_VERSION__ >= 202311L +# define SPA_STATIC_ASSERT_IMPL(expr, msg, ...) static_assert(expr, msg) +# define SPA_ALIGNOF alignof +#else +# include +# if __STDC_VERSION__ >= 201112L +# define SPA_STATIC_ASSERT_IMPL(expr, msg, ...) _Static_assert(expr, msg) +# define SPA_ALIGNOF _Alignof +# endif +#endif +#ifndef SPA_STATIC_ASSERT_IMPL +#define SPA_STATIC_ASSERT_IMPL(expr, ...) \ + ((void)sizeof(struct { int spa_static_assertion_failed : 2 * !!(expr) - 1; })) +#endif +#ifndef SPA_ALIGNOF +#define SPA_ALIGNOF __alignof__ +#endif + +#define SPA_STATIC_ASSERT(expr, ...) SPA_STATIC_ASSERT_IMPL(expr, ## __VA_ARGS__, "`" #expr "` evaluated to false") + +#define SPA_CONCAT_NOEXPAND(a, b) a ## b +#define SPA_CONCAT(a, b) SPA_CONCAT_NOEXPAND(a, b) + +#include +#include +#include +#include +#include +#include + +/** + * \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; (ptr) < (arr) + SPA_N_ELEMENTS(arr); (ptr)++) + +#define SPA_FOR_EACH_ELEMENT_VAR(arr, var) \ + for (__typeof__((arr)[0])* var = arr; (var) < (arr) + SPA_N_ELEMENTS(arr); (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_CLAMPD(v,low,high) \ +({ \ + fmin(fmax(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; \ +}) + +/** 3-way comparison. NaN > NaN and NaN > finite numbers */ +#define SPA_CMP(a, b) \ +({ \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + (_a > _b) ? 1 : (_a == _b) ? 0 : (_a < _b) ? -1 \ + : (_a == _a) ? -1 : (_b == _b) ? 1 \ + : 1; \ +}) + +/** + * 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_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 + +#ifndef SPA_API_IMPL +#define SPA_API_PROTO static inline +#define SPA_API_IMPL static inline +#endif + +#ifndef SPA_API_UTILS_DEFS + #ifdef SPA_API_IMPL + #define SPA_API_UTILS_DEFS SPA_API_IMPL + #else + #define SPA_API_UTILS_DEFS static inline + #endif +#endif + + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +#define SPA_RESTRICT restrict +#elif defined(__GNUC__) && __GNUC__ >= 4 +#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(val,num,denom) \ +({ \ + uint64_t _val = (val); \ + uint64_t _denom = (denom); \ + (uint32_t)(((_val) * (num)) / (_denom)); \ +}) + +#define SPA_SCALE32_UP(val,num,denom) \ +({ \ + uint64_t _val = (val); \ + uint64_t _denom = (denom); \ + (uint32_t)(((_val) * (num) + (_denom)-1) / (_denom)); \ +}) + + +#define SPA_PTR_ALIGNMENT(p,align) ((uintptr_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 + +SPA_API_UTILS_DEFS bool spa_ptrinside(const void *p1, size_t s1, const void *p2, size_t s2, + size_t *remaining) +{ + if (SPA_LIKELY((uintptr_t)p1 <= (uintptr_t)p2 && s2 <= s1 && + (uintptr_t)p2 - (uintptr_t)p1 <= s1 - s2)) { + if (remaining != NULL) + *remaining = ((uintptr_t)p1 + s1) - ((uintptr_t)p2 + s2); + return true; + } else { + if (remaining != NULL) + *remaining = 0; + return false; + } +} + +SPA_API_UTILS_DEFS bool spa_ptr_inside_and_aligned(const void *p1, size_t s1, + const void *p2, size_t s2, size_t align, + size_t *remaining) +{ + if (SPA_IS_ALIGNED(p2, align)) { + return spa_ptrinside(p1, s1, p2, s2, remaining); + } else { + if (remaining != NULL) + *remaining = 0; + return false; + } +} + +#define spa_ptr_type_inside(p1, s1, p2, type, remaining) \ + spa_ptr_inside_and_aligned(p1, s1, p2, sizeof(type), SPA_ALIGNOF(type), remaining) + +#define SPA_PTR_TO_INT(p) ((int) ((intptr_t) (p))) +#define SPA_INT_TO_PTR(u) ((void*) ((intptr_t) (u))) + +#define SPA_STRINGIFY_1(...) #__VA_ARGS__ +#define SPA_STRINGIFY(...) SPA_STRINGIFY_1(__VA_ARGS__) + +struct spa_error_location { + int line; + int col; + size_t len; + const char *location; + const char *reason; +}; + +#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..c88a833 --- /dev/null +++ b/spa/include/spa/utils/dict.h @@ -0,0 +1,113 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DICT_H +#define SPA_DICT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +#ifndef SPA_API_DICT + #ifdef SPA_API_IMPL + #define SPA_API_DICT SPA_API_IMPL + #else + #define SPA_API_DICT static inline + #endif +#endif + +/** + * \defgroup spa_dict Dictionary + * Dictionary data structure + */ + +/** + * \addtogroup spa_dict + * \{ + */ + +struct spa_dict_item { + const char *key; + const char *value; +}; + +#define SPA_DICT_ITEM(key,value) ((struct spa_dict_item) { (key), (value) }) +#define SPA_DICT_ITEM_INIT(key,value) SPA_DICT_ITEM(key,value) + +struct spa_dict { +#define SPA_DICT_FLAG_SORTED (1<<0) /**< items are sorted */ + uint32_t flags; + uint32_t n_items; + const struct spa_dict_item *items; +}; + +#define SPA_DICT(items,n_items) ((struct spa_dict) { 0, (n_items), (items) }) +#define SPA_DICT_ARRAY(items) SPA_DICT((items),SPA_N_ELEMENTS(items)) +#define SPA_DICT_ITEMS(...) SPA_DICT_ARRAY(((struct spa_dict_item[]) { __VA_ARGS__})) + +#define SPA_DICT_INIT(items,n_items) SPA_DICT(items,n_items) +#define SPA_DICT_INIT_ARRAY(items) SPA_DICT_ARRAY(items) + +#define spa_dict_for_each(item, dict) \ + for ((item) = (dict)->items; \ + (item) < &(dict)->items[(dict)->n_items]; \ + (item)++) + +SPA_API_DICT int spa_dict_item_compare(const void *i1, const void *i2) +{ + const struct spa_dict_item *it1 = (const struct spa_dict_item *)i1, + *it2 = (const struct spa_dict_item *)i2; + return strcmp(it1->key, it2->key); +} + +SPA_API_DICT void spa_dict_qsort(struct spa_dict *dict) +{ + if (dict->n_items > 0) + qsort((void*)dict->items, dict->n_items, sizeof(struct spa_dict_item), + spa_dict_item_compare); + SPA_FLAG_SET(dict->flags, SPA_DICT_FLAG_SORTED); +} + +SPA_API_DICT const struct spa_dict_item *spa_dict_lookup_item(const struct spa_dict *dict, + const char *key) +{ + const struct spa_dict_item *item; + + 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; +} + +SPA_API_DICT const char *spa_dict_lookup(const struct spa_dict *dict, const char *key) +{ + const struct spa_dict_item *item = spa_dict_lookup_item(dict, key); + return item ? item->value : NULL; +} + +/** + * \} + */ + +#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..7b8fd20 --- /dev/null +++ b/spa/include/spa/utils/dll.h @@ -0,0 +1,61 @@ +/* Simple DLL */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_DLL_H +#define SPA_DLL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#ifndef SPA_API_DLL + #ifdef SPA_API_IMPL + #define SPA_API_DLL SPA_API_IMPL + #else + #define SPA_API_DLL static inline + #endif +#endif + +#define SPA_DLL_BW_MAX 0.128 +#define SPA_DLL_BW_MIN 0.016 + +struct spa_dll { + double bw; + double z1, z2, z3; + double w0, w1, w2; +}; + +SPA_API_DLL void spa_dll_init(struct spa_dll *dll) +{ + dll->bw = 0.0; + dll->z1 = dll->z2 = dll->z3 = 0.0; +} + +SPA_API_DLL void spa_dll_set_bw(struct spa_dll *dll, double bw, unsigned period, unsigned rate) +{ + double w = 2 * M_PI * bw * period / rate; + dll->w0 = 1.0 - exp (-20.0 * w); + dll->w1 = w * 1.5 / period; + dll->w2 = w / 1.5; + dll->bw = bw; +} + +SPA_API_DLL double spa_dll_update(struct spa_dll *dll, double err) +{ + dll->z1 += dll->w0 * (dll->w1 * err - dll->z1); + dll->z2 += dll->w0 * (dll->z1 - dll->z2); + 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/endian.h b/spa/include/spa/utils/endian.h new file mode 100644 index 0000000..2d002d4 --- /dev/null +++ b/spa/include/spa/utils/endian.h @@ -0,0 +1,26 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ENDIAN_H +#define SPA_ENDIAN_H + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#include +#define bswap_16 bswap16 +#define bswap_32 bswap32 +#define bswap_64 bswap64 +#elif defined(_MSC_VER) && defined(_WIN32) +#include +#define __LITTLE_ENDIAN 1234 +#define __BIG_ENDIAN 4321 +#define __BYTE_ORDER __LITTLE_ENDIAN +#define bswap_16 _byteswap_ushort +#define bswap_32 _byteswap_ulong +#define bswap_64 _byteswap_uint64 +#else +#include +#include +#endif + +#endif /* SPA_ENDIAN_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..881ffd8 --- /dev/null +++ b/spa/include/spa/utils/enum-types.h @@ -0,0 +1,49 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ENUM_TYPES_H +#define SPA_ENUM_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_types + * \{ + */ + +#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 } +}; + +#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..dbbb019 --- /dev/null +++ b/spa/include/spa/utils/hook.h @@ -0,0 +1,546 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_HOOK_H +#define SPA_HOOK_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_HOOK + #ifdef SPA_API_IMPL + #define SPA_API_HOOK SPA_API_IMPL + #else + #define SPA_API_HOOK static inline + #endif +#endif + +/** \defgroup spa_interfaces Interfaces + * + * \brief Generic implementation of implementation-independent interfaces + * + * 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; \ +}) + +#define spa_callbacks_call_fast(callbacks,type,method,vers,...) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + (_f->method)((callbacks)->data, ## __VA_ARGS__); \ + true; \ +}) + + +/** + * 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; \ +}) +#define spa_callbacks_call_fast_res(callbacks,type,res,method,vers,...) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + res = (_f->method)((callbacks)->data, ## __VA_ARGS__); \ +}) + +/** + * 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__) + +#define spa_interface_call_fast(iface,method_type,method,vers,...) \ + spa_callbacks_call_fast(&(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__) + +#define spa_interface_call_fast_res(iface,method_type,res,method,vers,...) \ + spa_callbacks_call_fast_res(&(iface)->cb,method_type,res,method,vers,##__VA_ARGS__) + + +#define spa_api_func_v(o,method,version,...) \ +({ \ + if (SPA_LIKELY(SPA_CALLBACK_CHECK(o,method,version))) \ + ((o)->method)(o, ##__VA_ARGS__); \ +}) +#define spa_api_func_r(rtype,def,o,method,version,...) \ +({ \ + rtype _res = def; \ + if (SPA_LIKELY(SPA_CALLBACK_CHECK(o,method,version))) \ + _res = ((o)->method)(o, ##__VA_ARGS__); \ + _res; \ +}) +#define spa_api_func_fast(o,method,...) \ +({ \ + ((o)->method)(o, ##__VA_ARGS__); \ +}) + +#define spa_api_method_v(type,o,method,version,...) \ +({ \ + struct spa_interface *_i = o; \ + spa_interface_call(_i, struct type ##_methods, \ + method, version, ##__VA_ARGS__); \ +}) +#define spa_api_method_r(rtype,def,type,o,method,version,...) \ +({ \ + rtype _res = def; \ + struct spa_interface *_i = o; \ + spa_interface_call_res(_i, struct type ##_methods, \ + _res, method, version, ##__VA_ARGS__); \ + _res; \ +}) +#define spa_api_method_null_v(type,co,o,method,version,...) \ +({ \ + struct type *_co = co; \ + if (SPA_LIKELY(_co != NULL)) { \ + struct spa_interface *_i = o; \ + spa_interface_call(_i, struct type ##_methods, \ + method, version, ##__VA_ARGS__); \ + } \ +}) +#define spa_api_method_null_r(rtype,def,type,co,o,method,version,...) \ +({ \ + rtype _res = def; \ + struct type *_co = co; \ + if (SPA_LIKELY(_co != NULL)) { \ + struct spa_interface *_i = o; \ + spa_interface_call_res(_i, struct type ##_methods, \ + _res, method, version, ##__VA_ARGS__); \ + } \ + _res; \ +}) +#define spa_api_method_fast_v(type,o,method,version,...) \ +({ \ + struct spa_interface *_i = o; \ + spa_interface_call_fast(_i, struct type ##_methods, \ + method, version, ##__VA_ARGS__); \ +}) +#define spa_api_method_fast_r(rtype,def,type,o,method,version,...) \ +({ \ + rtype _res = def; \ + struct spa_interface *_i = o; \ + spa_interface_call_fast_res(_i, struct type ##_methods, \ + _res, method, version, ##__VA_ARGS__); \ + _res; \ +}) + +/** + * \} + */ + +/** \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*/ +SPA_API_HOOK void spa_hook_list_init(struct spa_hook_list *list) +{ + spa_list_init(&list->list); +} + +SPA_API_HOOK bool spa_hook_list_is_empty(struct spa_hook_list *list) +{ + return spa_list_is_empty(&list->list); +} + +/** Append a hook. */ +SPA_API_HOOK 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 */ +SPA_API_HOOK 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 */ +SPA_API_HOOK 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 */ +SPA_API_HOOK void spa_hook_list_clean(struct spa_hook_list *list) +{ + struct spa_hook *h; + spa_list_consume(h, &list->list, link) + spa_hook_remove(h); +} + +SPA_API_HOOK 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); +} + +SPA_API_HOOK 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-core.h b/spa/include/spa/utils/json-core.h new file mode 100644 index 0000000..31bf772 --- /dev/null +++ b/spa/include/spa/utils/json-core.h @@ -0,0 +1,635 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_JSON_H +#define SPA_UTILS_JSON_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef SPA_API_JSON + #ifdef SPA_API_IMPL + #define SPA_API_JSON SPA_API_IMPL + #else + #define SPA_API_JSON static inline + #endif +#endif + +/** \defgroup spa_json JSON + * Relaxed JSON variant parsing + */ + +/** + * \addtogroup spa_json + * \{ + */ + +/* a simple JSON compatible tokenizer */ +struct spa_json { + const char *cur; + const char *end; + struct spa_json *parent; +#define SPA_JSON_ERROR_FLAG 0x100 + uint32_t state; + uint32_t depth; +}; + +#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), NULL, 0, 0 }) + +SPA_API_JSON void spa_json_init(struct spa_json * iter, const char *data, size_t size) +{ + *iter = SPA_JSON_INIT(data, size); +} +#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 }) + +SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub) +{ + *sub = SPA_JSON_ENTER(iter); +} + +#define SPA_JSON_SAVE(iter) ((struct spa_json) { (iter)->cur, (iter)->end, NULL, (iter)->state, 0 }) + +SPA_API_JSON void spa_json_save(struct spa_json * iter, struct spa_json * save) +{ + *save = SPA_JSON_SAVE(iter); +} + +#define SPA_JSON_START(iter,p) ((struct spa_json) { (p), (iter)->end, NULL, 0, 0 }) + +SPA_API_JSON void spa_json_start(struct spa_json * iter, struct spa_json * sub, const char *pos) +{ + *sub = SPA_JSON_START(iter,pos); +} + +/** Get the next token. \a value points to the token and the return value + * is the length. Returns -1 on parse error, 0 on end of input. */ +SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) +{ + int utf8_remain = 0, err = 0; + enum { + __NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC, __COMMENT, + __ARRAY_FLAG = 0x10, /* in array context */ + __PREV_ARRAY_FLAG = 0x20, /* depth=0 array context flag */ + __KEY_FLAG = 0x40, /* inside object key */ + __SUB_FLAG = 0x80, /* not at top-level */ + __FLAGS = 0xff0, + __ERROR_SYSTEM = SPA_JSON_ERROR_FLAG, + __ERROR_INVALID_ARRAY_SEPARATOR, + __ERROR_EXPECTED_OBJECT_KEY, + __ERROR_EXPECTED_OBJECT_VALUE, + __ERROR_TOO_DEEP_NESTING, + __ERROR_EXPECTED_ARRAY_CLOSE, + __ERROR_EXPECTED_OBJECT_CLOSE, + __ERROR_MISMATCHED_BRACKET, + __ERROR_ESCAPE_NOT_ALLOWED, + __ERROR_CHARACTERS_NOT_ALLOWED, + __ERROR_INVALID_ESCAPE, + __ERROR_INVALID_STATE, + __ERROR_UNFINISHED_STRING, + }; + uint64_t array_stack[8] = {0}; /* array context flags of depths 1...512 */ + + *value = iter->cur; + + if (iter->state & SPA_JSON_ERROR_FLAG) + return -1; + + for (; iter->cur < iter->end; iter->cur++) { + unsigned char cur = (unsigned char)*iter->cur; + uint32_t flag; + +#define _SPA_ERROR(reason) { err = __ERROR_ ## reason; goto error; } + again: + flag = iter->state & __FLAGS; + switch (iter->state & ~__FLAGS) { + case __NONE: + flag &= ~(__KEY_FLAG | __PREV_ARRAY_FLAG); + iter->state = __STRUCT | flag; + iter->depth = 0; + goto again; + case __STRUCT: + switch (cur) { + case '\0': case '\t': case ' ': case '\r': case '\n': case ',': + continue; + case ':': case '=': + if (flag & __ARRAY_FLAG) + _SPA_ERROR(INVALID_ARRAY_SEPARATOR); + if (!(flag & __KEY_FLAG)) + _SPA_ERROR(EXPECTED_OBJECT_KEY); + iter->state |= __SUB_FLAG; + continue; + case '#': + iter->state = __COMMENT | flag; + continue; + case '"': + if (flag & __KEY_FLAG) + flag |= __SUB_FLAG; + if (!(flag & __ARRAY_FLAG)) + SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG)); + *value = iter->cur; + iter->state = __STRING | flag; + continue; + case '[': case '{': + if (!(flag & __ARRAY_FLAG)) { + /* At top-level we may be either in object context + * or in single-item context, and then we need to + * accept array/object here. + */ + if ((iter->state & __SUB_FLAG) && !(flag & __KEY_FLAG)) + _SPA_ERROR(EXPECTED_OBJECT_KEY); + SPA_FLAG_CLEAR(flag, __KEY_FLAG); + } + iter->state = __STRUCT | __SUB_FLAG | flag; + SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, cur == '['); + + /* We need to remember previous array state across calls + * for depth=0, so store that in state. Others bits go to + * temporary stack. + */ + if (iter->depth == 0) { + SPA_FLAG_UPDATE(iter->state, __PREV_ARRAY_FLAG, flag & __ARRAY_FLAG); + } else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) { + uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f); + SPA_FLAG_UPDATE(array_stack[(iter->depth-1) >> 6], mask, flag & __ARRAY_FLAG); + } else { + /* too deep */ + _SPA_ERROR(TOO_DEEP_NESTING); + } + + *value = iter->cur; + if (++iter->depth > 1) + continue; + iter->cur++; + return 1; + case '}': case ']': + if ((flag & __ARRAY_FLAG) && cur != ']') + _SPA_ERROR(EXPECTED_ARRAY_CLOSE); + if (!(flag & __ARRAY_FLAG) && cur != '}') + _SPA_ERROR(EXPECTED_OBJECT_CLOSE); + if (flag & __KEY_FLAG) { + /* incomplete key-value pair */ + _SPA_ERROR(EXPECTED_OBJECT_VALUE); + } + iter->state = __STRUCT | __SUB_FLAG | flag; + if (iter->depth == 0) { + if (iter->parent) + iter->parent->cur = iter->cur; + else + _SPA_ERROR(MISMATCHED_BRACKET); + return 0; + } + --iter->depth; + if (iter->depth == 0) { + SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, flag & __PREV_ARRAY_FLAG); + } else if (((iter->depth-1) >> 6) < SPA_N_ELEMENTS(array_stack)) { + uint64_t mask = 1ULL << ((iter->depth-1) & 0x3f); + SPA_FLAG_UPDATE(iter->state, __ARRAY_FLAG, + SPA_FLAG_IS_SET(array_stack[(iter->depth-1) >> 6], mask)); + } else { + /* too deep */ + _SPA_ERROR(TOO_DEEP_NESTING); + } + continue; + case '\\': + /* disallow bare escape */ + _SPA_ERROR(ESCAPE_NOT_ALLOWED); + default: + /* allow bare ascii */ + if (!(cur >= 32 && cur <= 126)) + _SPA_ERROR(CHARACTERS_NOT_ALLOWED); + if (flag & __KEY_FLAG) + flag |= __SUB_FLAG; + if (!(flag & __ARRAY_FLAG)) + SPA_FLAG_UPDATE(flag, __KEY_FLAG, !(flag & __KEY_FLAG)); + *value = iter->cur; + iter->state = __BARE | flag; + } + continue; + case __BARE: + switch (cur) { + case '\0': + case '\t': case ' ': case '\r': case '\n': + case '"': case '#': + case ':': case ',': case '=': case ']': case '}': + iter->state = __STRUCT | flag; + if (iter->depth > 0) + goto again; + return iter->cur - *value; + case '\\': + /* disallow bare escape */ + _SPA_ERROR(ESCAPE_NOT_ALLOWED); + default: + /* allow bare ascii */ + if (cur >= 32 && cur <= 126) + continue; + } + _SPA_ERROR(CHARACTERS_NOT_ALLOWED); + case __STRING: + switch (cur) { + case '\\': + iter->state = __ESC | flag; + continue; + case '"': + iter->state = __STRUCT | flag; + if (iter->depth > 0) + continue; + return ++iter->cur - *value; + case 240 ... 247: + utf8_remain++; + SPA_FALLTHROUGH; + case 224 ... 239: + utf8_remain++; + SPA_FALLTHROUGH; + case 192 ... 223: + utf8_remain++; + iter->state = __UTF8 | flag; + continue; + default: + if (cur >= 32 && cur <= 127) + continue; + } + _SPA_ERROR(CHARACTERS_NOT_ALLOWED); + case __UTF8: + switch (cur) { + case 128 ... 191: + if (--utf8_remain == 0) + iter->state = __STRING | flag; + continue; + } + _SPA_ERROR(CHARACTERS_NOT_ALLOWED); + case __ESC: + switch (cur) { + case '"': case '\\': case '/': case 'b': case 'f': + case 'n': case 'r': case 't': case 'u': + iter->state = __STRING | flag; + continue; + } + _SPA_ERROR(INVALID_ESCAPE); + case __COMMENT: + switch (cur) { + case '\n': case '\r': + iter->state = __STRUCT | flag; + } + break; + default: + _SPA_ERROR(INVALID_STATE); + } + + } + if (iter->depth != 0 || iter->parent) + _SPA_ERROR(MISMATCHED_BRACKET); + + switch (iter->state & ~__FLAGS) { + case __STRING: case __UTF8: case __ESC: + /* string/escape not closed */ + _SPA_ERROR(UNFINISHED_STRING); + case __COMMENT: + /* trailing comment */ + return 0; + } + + if ((iter->state & __SUB_FLAG) && (iter->state & __KEY_FLAG)) { + /* incomplete key-value pair */ + _SPA_ERROR(EXPECTED_OBJECT_VALUE); + } + + if ((iter->state & ~__FLAGS) != __STRUCT) { + iter->state = __STRUCT | (iter->state & __FLAGS); + return iter->cur - *value; + } + return 0; +#undef _SPA_ERROR + +error: + iter->state = err; + while (iter->parent) { + if (iter->parent->state & SPA_JSON_ERROR_FLAG) + break; + iter->parent->state = err; + iter->parent->cur = iter->cur; + iter = iter->parent; + } + return -1; +} + +/** + * Return if there was a parse error, and its possible location. + * + * \since 1.1.0 + */ +SPA_API_JSON bool spa_json_get_error(struct spa_json *iter, const char *start, + struct spa_error_location *loc) +{ + static const char *reasons[] = { + "System error", + "Invalid array separator", + "Expected object key", + "Expected object value", + "Too deep nesting", + "Expected array close bracket", + "Expected object close brace", + "Mismatched bracket", + "Escape not allowed", + "Character not allowed", + "Invalid escape", + "Invalid state", + "Unfinished string", + "Expected key separator", + }; + + if (!(iter->state & SPA_JSON_ERROR_FLAG)) + return false; + + if (loc) { + int linepos = 1, colpos = 1, code; + const char *p, *l; + + for (l = p = start; p && p != iter->cur; ++p) { + if (*p == '\n') { + linepos++; + colpos = 1; + l = p+1; + } else { + colpos++; + } + } + code = SPA_CLAMP(iter->state & 0xff, 0u, SPA_N_ELEMENTS(reasons)-1); + loc->line = linepos; + loc->col = colpos; + loc->location = l; + loc->len = SPA_PTRDIFF(iter->end, loc->location) / sizeof(char); + loc->reason = code == 0 ? strerror(errno) : reasons[code]; + } + return true; +} + +SPA_API_JSON int spa_json_is_container(const char *val, int len) +{ + return len > 0 && (*val == '{' || *val == '['); +} + +/* object */ +SPA_API_JSON int spa_json_is_object(const char *val, int len) +{ + return len > 0 && *val == '{'; +} + +/* array */ +SPA_API_JSON bool spa_json_is_array(const char *val, int len) +{ + return len > 0 && *val == '['; +} + +/* null */ +SPA_API_JSON bool spa_json_is_null(const char *val, int len) +{ + return len == 4 && strncmp(val, "null", 4) == 0; +} + +/* float */ +SPA_API_JSON int spa_json_parse_float(const char *val, int len, float *result) +{ + char buf[96]; + char *end; + int pos; + + if (len <= 0 || len >= (int)sizeof(buf)) + return 0; + + for (pos = 0; pos < len; ++pos) { + switch (val[pos]) { + case '+': case '-': case '0' ... '9': case '.': case 'e': case 'E': break; + default: return 0; + } + } + + memcpy(buf, val, len); + buf[len] = '\0'; + + *result = spa_strtof(buf, &end); + return len > 0 && end == buf + len; +} + +SPA_API_JSON bool spa_json_is_float(const char *val, int len) +{ + float dummy; + return spa_json_parse_float(val, len, &dummy); +} + +SPA_API_JSON char *spa_json_format_float(char *str, int size, float val) +{ + if (SPA_UNLIKELY(!isnormal(val))) { + if (isinf(val)) + val = signbit(val) ? FLT_MIN : FLT_MAX; + else + val = 0.0f; + } + return spa_dtoa(str, size, val); +} + +/* int */ +SPA_API_JSON int spa_json_parse_int(const char *val, int len, int *result) +{ + char buf[64]; + char *end; + + if (len <= 0 || len >= (int)sizeof(buf)) + return 0; + + memcpy(buf, val, len); + buf[len] = '\0'; + + *result = strtol(buf, &end, 0); + return len > 0 && end == buf + len; +} +SPA_API_JSON bool spa_json_is_int(const char *val, int len) +{ + int dummy; + return spa_json_parse_int(val, len, &dummy); +} + +/* bool */ +SPA_API_JSON bool spa_json_is_true(const char *val, int len) +{ + return len == 4 && strncmp(val, "true", 4) == 0; +} + +SPA_API_JSON bool spa_json_is_false(const char *val, int len) +{ + return len == 5 && strncmp(val, "false", 5) == 0; +} + +SPA_API_JSON bool spa_json_is_bool(const char *val, int len) +{ + return spa_json_is_true(val, len) || spa_json_is_false(val, len); +} + +SPA_API_JSON int spa_json_parse_bool(const char *val, int len, bool *result) +{ + if ((*result = spa_json_is_true(val, len))) + return 1; + if (!(*result = !spa_json_is_false(val, len))) + return 1; + return -1; +} + +/* string */ +SPA_API_JSON bool spa_json_is_string(const char *val, int len) +{ + return len > 1 && *val == '"'; +} + +SPA_API_JSON int spa_json_parse_hex(const char *p, int num, uint32_t *res) +{ + int i; + *res = 0; + for (i = 0; i < num; i++) { + char v = p[i]; + if (v >= '0' && v <= '9') + v = v - '0'; + else if (v >= 'a' && v <= 'f') + v = v - 'a' + 10; + else if (v >= 'A' && v <= 'F') + v = v - 'A' + 10; + else + return -1; + *res = (*res << 4) | v; + } + return 1; +} + +SPA_API_JSON int spa_json_parse_stringn(const char *val, int len, char *result, int maxlen) +{ + const char *p; + if (maxlen <= len) + return -ENOSPC; + if (!spa_json_is_string(val, len)) { + if (result != val) + memmove(result, val, len); + result += len; + } else { + for (p = val+1; p < val + len; p++) { + if (*p == '\\') { + p++; + if (*p == 'n') + *result++ = '\n'; + else if (*p == 'r') + *result++ = '\r'; + else if (*p == 'b') + *result++ = '\b'; + else if (*p == 't') + *result++ = '\t'; + else if (*p == 'f') + *result++ = '\f'; + else if (*p == 'u') { + uint8_t prefix[] = { 0, 0xc0, 0xe0, 0xf0 }; + uint32_t idx, n, v, cp, enc[] = { 0x80, 0x800, 0x10000 }; + if (val + len - p < 5 || + spa_json_parse_hex(p+1, 4, &cp) < 0) { + *result++ = *p; + continue; + } + p += 4; + + if (cp >= 0xd800 && cp <= 0xdbff) { + if (val + len - p < 7 || + p[1] != '\\' || p[2] != 'u' || + spa_json_parse_hex(p+3, 4, &v) < 0 || + v < 0xdc00 || v > 0xdfff) + continue; + p += 6; + cp = 0x010000 + (((cp & 0x3ff) << 10) | (v & 0x3ff)); + } else if (cp >= 0xdc00 && cp <= 0xdfff) + continue; + + for (idx = 0; idx < 3; idx++) + if (cp < enc[idx]) + break; + for (n = idx; n > 0; n--, cp >>= 6) + result[n] = (cp | 0x80) & 0xbf; + *result++ = (cp | prefix[idx]) & 0xff; + result += idx; + } else + *result++ = *p; + } else if (*p == '\"') { + break; + } else + *result++ = *p; + } + } + *result = '\0'; + return 1; +} + +SPA_API_JSON int spa_json_parse_string(const char *val, int len, char *result) +{ + return spa_json_parse_stringn(val, len, result, len+1); +} + +SPA_API_JSON int spa_json_encode_string(char *str, int size, const char *val) +{ + int len = 0; + static const char hex[] = { "0123456789abcdef" }; +#define __PUT(c) { if (len < size) *str++ = c; len++; } + __PUT('"'); + while (*val) { + switch (*val) { + case '\n': + __PUT('\\'); __PUT('n'); + break; + case '\r': + __PUT('\\'); __PUT('r'); + break; + case '\b': + __PUT('\\'); __PUT('b'); + break; + case '\t': + __PUT('\\'); __PUT('t'); + break; + case '\f': + __PUT('\\'); __PUT('f'); + break; + case '\\': + case '"': + __PUT('\\'); __PUT(*val); + break; + default: + if (*val > 0 && *val < 0x20) { + __PUT('\\'); __PUT('u'); + __PUT('0'); __PUT('0'); + __PUT(hex[((*val)>>4)&0xf]); __PUT(hex[(*val)&0xf]); + } else { + __PUT(*val); + } + break; + } + val++; + } + __PUT('"'); + __PUT('\0'); +#undef __PUT + return len-1; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_H */ diff --git a/spa/include/spa/utils/json-pod.h b/spa/include/spa/utils/json-pod.h new file mode 100644 index 0000000..22f2b28 --- /dev/null +++ b/spa/include/spa/utils/json-pod.h @@ -0,0 +1,182 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_JSON_POD_H +#define SPA_UTILS_JSON_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#ifndef SPA_API_JSON_POD + #ifdef SPA_API_IMPL + #define SPA_API_JSON_POD SPA_API_IMPL + #else + #define SPA_API_JSON_POD static inline + #endif +#endif + +/** \defgroup spa_json_pod JSON to POD + * JSON to POD conversion + */ + +/** + * \addtogroup spa_json_pod + * \{ + */ + +SPA_API_JSON_POD int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags, uint32_t id, + const struct spa_type_info *info, struct spa_json *iter, const char *value, int len) +{ + const struct spa_type_info *ti; + 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 ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { + const struct spa_type_info *pi; + 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; + } + if (l < 0) + return l; + 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; + if (l < 0) + return l; + 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, (uint32_t)val); + break; + case SPA_TYPE_Int: + spa_pod_builder_int(b, (int32_t)val); + break; + case SPA_TYPE_Long: + spa_pod_builder_long(b, (int64_t)val); + break; + case SPA_TYPE_Struct: + if (spa_json_is_int(value, len)) + spa_pod_builder_int(b, (int32_t)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; +} + +SPA_API_JSON_POD int spa_json_to_pod_checked(struct spa_pod_builder *b, uint32_t flags, + const struct spa_type_info *info, const char *value, int len, + struct spa_error_location *loc) +{ + struct spa_json iter; + const char *val; + int res; + + if (loc) + spa_zero(*loc); + + if ((res = spa_json_begin(&iter, value, len, &val)) <= 0) + goto error; + + res = spa_json_to_pod_part(b, flags, info->type, info, &iter, val, len); + +error: + if (res < 0 && loc) + spa_json_get_error(&iter, value, loc); + return res; +} + +SPA_API_JSON_POD int spa_json_to_pod(struct spa_pod_builder *b, uint32_t flags, + const struct spa_type_info *info, const char *value, int len) +{ + return spa_json_to_pod_checked(b, flags, info, value, len, NULL); +} + +/** + * \} + */ + +#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..a36554d --- /dev/null +++ b/spa/include/spa/utils/json.h @@ -0,0 +1,222 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_JSON_UTILS_H +#define SPA_UTILS_JSON_UTILS_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +#include + +#ifndef SPA_API_JSON_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_JSON_UTILS SPA_API_IMPL + #else + #define SPA_API_JSON_UTILS static inline + #endif +#endif + +/** \defgroup spa_json_utils JSON Utils + * Relaxed JSON variant parsing Utils + */ + +/** + * \addtogroup spa_json + * \{ + */ + +SPA_API_JSON_UTILS int spa_json_begin(struct spa_json * iter, const char *data, size_t size, const char **val) +{ + spa_json_init(iter, data, size); + return spa_json_next(iter, val); +} + +/* float */ +SPA_API_JSON_UTILS 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 len; + return spa_json_parse_float(value, len, res); +} + +/* int */ +SPA_API_JSON_UTILS int spa_json_get_int(struct spa_json *iter, int *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return len; + return spa_json_parse_int(value, len, res); +} + +/* bool */ +SPA_API_JSON_UTILS int spa_json_get_bool(struct spa_json *iter, bool *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return len; + return spa_json_parse_bool(value, len, res); +} + +/* string */ +SPA_API_JSON_UTILS int spa_json_get_string(struct spa_json *iter, char *res, int maxlen) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return len; + return spa_json_parse_stringn(value, len, res, maxlen); +} + + +SPA_API_JSON_UTILS int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return len; + if (!spa_json_is_container(value, len)) + return -EPROTO; + if (*value != type) + return -EINVAL; + spa_json_enter(iter, sub); + return 1; +} + +SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter, + const char *data, size_t size, char type, bool relax) +{ + int res; + spa_json_init(iter, data, size); + res = spa_json_enter_container(iter, iter, type); + if (res == -EPROTO && relax) + spa_json_init(iter, data, size); + else if (res <= 0) + return res; + return 1; +} +/** + * Return length of container at current position, starting at \a value. + * + * \return Length of container including {} or [], or 0 on error. + */ +SPA_API_JSON_UTILS int spa_json_container_len(struct spa_json *iter, const char *value, int len SPA_UNUSED) +{ + const char *val; + struct spa_json sub; + int res; + spa_json_enter(iter, &sub); + while ((res = spa_json_next(&sub, &val)) > 0); + if (res < 0) + return 0; + return sub.cur + 1 - value; +} + +/* object */ +SPA_API_JSON_UTILS int spa_json_enter_object(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '{'); +} +SPA_API_JSON_UTILS int spa_json_begin_object_relax(struct spa_json * iter, const char *data, size_t size) +{ + return spa_json_begin_container(iter, data, size, '{', true); +} +SPA_API_JSON_UTILS int spa_json_begin_object(struct spa_json * iter, const char *data, size_t size) +{ + return spa_json_begin_container(iter, data, size, '{', false); +} + +SPA_API_JSON_UTILS int spa_json_object_next(struct spa_json *iter, char *key, int maxkeylen, const char **value) +{ + int res1, res2; + while (true) { + res1 = spa_json_get_string(iter, key, maxkeylen); + if (res1 <= 0 && res1 != -ENOSPC) + return res1; + res2 = spa_json_next(iter, value); + if (res2 <= 0 || res1 != -ENOSPC) + return res2; + } +} + +SPA_API_JSON_UTILS int spa_json_object_find(struct spa_json *iter, const char *key, const char **value) +{ + struct spa_json obj = SPA_JSON_SAVE(iter); + int res, len = strlen(key) + 3; + char k[len]; + + while ((res = spa_json_object_next(&obj, k, len, value)) > 0) + if (spa_streq(k, key)) + return res; + return -ENOENT; +} + +SPA_API_JSON_UTILS int spa_json_str_object_find(const char *obj, size_t obj_len, + const char *key, char *value, size_t maxlen) +{ + struct spa_json iter; + int l; + const char *v; + + if (spa_json_begin_object(&iter, obj, obj_len) <= 0) + return -EINVAL; + if ((l = spa_json_object_find(&iter, key, &v)) <= 0) + return l; + return spa_json_parse_stringn(v, l, value, maxlen); +} + +/* array */ +SPA_API_JSON_UTILS int spa_json_enter_array(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '['); +} +SPA_API_JSON_UTILS int spa_json_begin_array_relax(struct spa_json * iter, const char *data, size_t size) +{ + return spa_json_begin_container(iter, data, size, '[', true); +} +SPA_API_JSON_UTILS int spa_json_begin_array(struct spa_json * iter, const char *data, size_t size) +{ + return spa_json_begin_container(iter, data, size, '[', false); +} + +#define spa_json_make_str_array_unpack(maxlen,type,conv) \ +{ \ + struct spa_json iter; \ + char v[maxlen]; \ + uint32_t count = 0; \ + if (spa_json_begin_array_relax(&iter, arr, arr_len) <= 0) \ + return -EINVAL; \ + while (spa_json_get_string(&iter, v, sizeof(v)) > 0 && count < max) \ + values[count++] = conv(v); \ + return count; \ +} + +SPA_API_JSON_UTILS int spa_json_str_array_uint32(const char *arr, size_t arr_len, + uint32_t *values, size_t max) +{ + spa_json_make_str_array_unpack(32,uint32_t, atoi); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_UTILS_H */ diff --git a/spa/include/spa/utils/keys.h b/spa/include/spa/utils/keys.h new file mode 100644 index 0000000..12a8cfa --- /dev/null +++ b/spa/include/spa/utils/keys.h @@ -0,0 +1,151 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 */ +#define SPA_KEY_API_ALSA_BIND_CTLS "api.alsa.bind-ctls" /**< alsa controls to bind as params */ +#define SPA_KEY_API_ALSA_SPLIT_ENABLE "api.alsa.split-enable" /**< For UCM devices with split PCMs, don't split to + * multiple PCMs using alsa-lib plugins, but instead + * add api.alsa.split properties to emitted nodes + * with PCM splitting information. + */ +#define SPA_KEY_API_ALSA_SPLIT_PARENT "api.alsa.split.parent" /**< PCM is UCM SplitPCM parent PCM, + * to be opened with SplitPCM set. + */ + +/** info from alsa card_info */ +#define SPA_KEY_API_ALSA_CARD_ID "api.alsa.card.id" /**< id from card_info */ +#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 */ + +#define SPA_KEY_API_ALSA_SPLIT_POSITION "api.alsa.split.position" /**< (SPA JSON list) If present, this is a + * virtual device corresponding to a subset of + * channels in an underlying PCM, listed in this + * property. The \ref SPA_KEY_API_ALSA_PATH + * contains the underlying split PCM. */ +#define SPA_KEY_API_ALSA_SPLIT_HW_POSITION \ + "api.alsa.split.hw-position" /**< (SPA JSON list) Channel map of the + * underlying split PCM. */ + +/** keys for v4l2 api */ +#define SPA_KEY_API_V4L2 "api.v4l2" /**< key for the v4l2 api */ +#define SPA_KEY_API_V4L2_PATH "api.v4l2.path" /**< v4l2 device path as can be + * 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" */ +#define SPA_KEY_API_LIBCAMERA_ROTATION "api.libcamera.rotation" /**< rotation of the camera: + * "0", "90", "180" or "270" */ + +/** 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..60c89f2 --- /dev/null +++ b/spa/include/spa/utils/list.h @@ -0,0 +1,156 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_LIST_H +#define SPA_LIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#ifndef SPA_API_LIST + #ifdef SPA_API_IMPL + #define SPA_API_LIST SPA_API_IMPL + #else + #define SPA_API_LIST static inline + #endif +#endif + +/** + * \defgroup spa_list List + * Doubly linked list data structure + */ + +/** + * \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) }) + +SPA_API_LIST void spa_list_init(struct spa_list *list) +{ + *list = SPA_LIST_INIT(list); +} + +SPA_API_LIST int spa_list_is_initialized(struct spa_list *list) +{ + return !!list->prev; +} + +#define spa_list_is_empty(l) ((l)->next == (l)) + +SPA_API_LIST 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; +} + +SPA_API_LIST 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; +} + +SPA_API_LIST 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..a3f5190 --- /dev/null +++ b/spa/include/spa/utils/names.h @@ -0,0 +1,152 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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_CONVERT_DUMMY "video.convert.dummy" /**< a dummy converter as fallback for the + * videoadapter node */ +#define SPA_NAME_VIDEO_ADAPT "video.adapt" /**< combination of a node and a + * video.convert. */ +/** keys for alsa factory names */ +#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_DEVICE "api.alsa.compress.offload.device" /**< an alsa Device interface for + * compressed audio */ +#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. */ +#define SPA_NAME_API_VULKAN_BLIT_FILTER \ + "api.vulkan.blit.filter" /**< a vulkan blit filter. */ +#define SPA_NAME_API_VULKAN_BLIT_DSP_FILTER \ + "api.vulkan.blit.dsp-filter" /**< a vulkan blit dsp-filter. */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_NAMES_H */ diff --git a/spa/include/spa/utils/ratelimit.h b/spa/include/spa/utils/ratelimit.h new file mode 100644 index 0000000..2af2c26 --- /dev/null +++ b/spa/include/spa/utils/ratelimit.h @@ -0,0 +1,53 @@ +/* Ratelimit */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_RATELIMIT_H +#define SPA_RATELIMIT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#ifndef SPA_API_RATELIMIT + #ifdef SPA_API_IMPL + #define SPA_API_RATELIMIT SPA_API_IMPL + #else + #define SPA_API_RATELIMIT static inline + #endif +#endif + +struct spa_ratelimit { + uint64_t interval; + uint64_t begin; + unsigned burst; + unsigned n_printed; + unsigned n_suppressed; +}; + +SPA_API_RATELIMIT int spa_ratelimit_test(struct spa_ratelimit *r, uint64_t now) +{ + unsigned suppressed = 0; + if (r->begin + r->interval < now) { + suppressed = r->n_suppressed; + r->begin = now; + r->n_printed = 0; + r->n_suppressed = 0; + } else if (r->n_printed >= r->burst) { + r->n_suppressed++; + return -1; + } + r->n_printed++; + return suppressed; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_RATELIMIT_H */ diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h new file mode 100644 index 0000000..312a6bb --- /dev/null +++ b/spa/include/spa/utils/result.h @@ -0,0 +1,62 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 + +#include + +#ifndef SPA_API_RESULT + #ifdef SPA_API_IMPL + #define SPA_API_RESULT SPA_API_IMPL + #else + #define SPA_API_RESULT static inline + #endif +#endif + + +#define SPA_ASYNC_BIT (1 << 30) +#define SPA_ASYNC_SEQ_MASK (SPA_ASYNC_BIT - 1) +#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)) + +SPA_API_RESULT const char *spa_strerror(int err) +{ + int _err = -(err); + if (SPA_RESULT_IS_ASYNC(err)) + _err = EINPROGRESS; + return 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..e8c5d62 --- /dev/null +++ b/spa/include/spa/utils/ringbuffer.h @@ -0,0 +1,176 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 + +#include + +#ifndef SPA_API_RINGBUFFER + #ifdef SPA_API_IMPL + #define SPA_API_RINGBUFFER SPA_API_IMPL + #else + #define SPA_API_RINGBUFFER static inline + #endif +#endif + +/** + * A ringbuffer type. + */ +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 + */ +SPA_API_RINGBUFFER 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 + */ +SPA_API_RINGBUFFER 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. + */ +SPA_API_RINGBUFFER int32_t spa_ringbuffer_get_read_index(struct spa_ringbuffer *rbuf, uint32_t *index) +{ + *index = __atomic_load_n(&rbuf->readindex, __ATOMIC_RELAXED); + return (int32_t) (__atomic_load_n(&rbuf->writeindex, __ATOMIC_ACQUIRE) - *index); +} + +/** + * 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 + */ +SPA_API_RINGBUFFER void +spa_ringbuffer_read_data(struct spa_ringbuffer *rbuf SPA_UNUSED, + const void *buffer, uint32_t size, + uint32_t offset, void *data, uint32_t len) +{ + 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 + */ +SPA_API_RINGBUFFER 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. + */ +SPA_API_RINGBUFFER int32_t spa_ringbuffer_get_write_index(struct spa_ringbuffer *rbuf, uint32_t *index) +{ + *index = __atomic_load_n(&rbuf->writeindex, __ATOMIC_RELAXED); + return (int32_t) (*index - __atomic_load_n(&rbuf->readindex, __ATOMIC_ACQUIRE)); +} + +/** + * 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 + */ +SPA_API_RINGBUFFER void +spa_ringbuffer_write_data(struct spa_ringbuffer *rbuf SPA_UNUSED, + void *buffer, uint32_t size, + uint32_t offset, const void *data, uint32_t len) +{ + 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 + */ +SPA_API_RINGBUFFER 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..060ef7d --- /dev/null +++ b/spa/include/spa/utils/string.h @@ -0,0 +1,404 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Red Hat, Inc. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_STRING_H +#define SPA_UTILS_STRING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#include + +#ifndef SPA_API_STRING + #ifdef SPA_API_IMPL + #define SPA_API_STRING SPA_API_IMPL + #else + #define SPA_API_STRING static inline + #endif +#endif + +/** + * \defgroup spa_string String handling + * String handling utilities + */ + +/** + * \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. + * + */ +SPA_API_STRING 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. + */ +SPA_API_STRING bool spa_strneq(const char *s1, const char *s2, size_t len) +{ + return SPA_LIKELY(s1 && s2) ? strncmp(s1, s2, len) == 0 : s1 == s2; +} + + +/** + * \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. + */ +SPA_API_STRING 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. + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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) +SPA_API_STRING 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) +SPA_API_STRING 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. + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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. + */ +SPA_API_STRING 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 + */ +SPA_API_STRING 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; +} + +SPA_API_STRING 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; +}; + +SPA_API_STRING void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t maxsize) +{ + buf->buffer = buffer; + buf->maxsize = maxsize; + buf->pos = 0; + if (maxsize > 0) + buf->buffer[0] = '\0'; +} + +SPA_PRINTF_FUNC(2, 3) +SPA_API_STRING 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..9ee2f3a --- /dev/null +++ b/spa/include/spa/utils/type-info.h @@ -0,0 +1,95 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_TYPE_INFO_H +#define SPA_TYPE_INFO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_types + * \{ + */ + +#ifndef SPA_TYPE_ROOT +#define SPA_TYPE_ROOT spa_types +#endif + + +#include +#include + +#include +#include +#include +#include + +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 }, + { SPA_TYPE_OBJECT_ParamTag, SPA_TYPE_Object, SPA_TYPE_INFO_PARAM_Tag, spa_type_param_tag }, + + { 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..88de2c6 --- /dev/null +++ b/spa/include/spa/utils/type.h @@ -0,0 +1,184 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_TYPE_H +#define SPA_TYPE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#ifndef SPA_API_TYPE + #ifdef SPA_API_IMPL + #define SPA_API_TYPE SPA_API_IMPL + #else + #define SPA_API_TYPE static inline + #endif +#endif + +/** \defgroup spa_types Types + * Data type information enumerations + */ + +/** + * \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_ParamTag, + _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; +}; + +SPA_API_TYPE bool spa_type_is_a(const char *type, const char *parent) +{ + return type != NULL && parent != NULL && strncmp(type, parent, strlen(parent)) == 0; +} + +SPA_API_TYPE const char *spa_type_short_name(const char *name) +{ + const char *h; + if ((h = strrchr(name, ':')) != NULL) + name = h + 1; + return name; +} + +SPA_API_TYPE uint32_t spa_type_from_short_name(const char *name, + const struct spa_type_info *info, uint32_t unknown) +{ + int i; + for (i = 0; info[i].name; i++) { + if (spa_streq(name, spa_type_short_name(info[i].name))) + return info[i].type; + } + return unknown; +} +SPA_API_TYPE const char * spa_type_to_name(uint32_t type, + const struct spa_type_info *info, const char *unknown) +{ + int i; + for (i = 0; info[i].name; i++) { + if (info[i].type == type) + return info[i].name; + } + return unknown; +} + +SPA_API_TYPE const char * spa_type_to_short_name(uint32_t type, + const struct spa_type_info *info, const char *unknown) +{ + const char *n = spa_type_to_name(type, info, unknown); + return n ? spa_type_short_name(n) : NULL; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_TYPE_H */ diff --git a/spa/lib/lib.c b/spa/lib/lib.c new file mode 100644 index 0000000..e2acb9c --- /dev/null +++ b/spa/lib/lib.c @@ -0,0 +1,161 @@ + +#define SPA_API_IMPL SPA_EXPORT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + + + + diff --git a/spa/lib/meson.build b/spa/lib/meson.build new file mode 100644 index 0000000..a12c304 --- /dev/null +++ b/spa/lib/meson.build @@ -0,0 +1,6 @@ +spa_lib = shared_library('spa', + [ 'lib.c' ], + include_directories : [ configinc ], + dependencies : [ spa_dep, pthread_lib, mathlib ], + install : true, + install_dir : spa_plugindir ) diff --git a/spa/meson.build b/spa/meson.build new file mode 100644 index 0000000..9faaae4 --- /dev/null +++ b/spa/meson.build @@ -0,0 +1,137 @@ +#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'), + include_directories('include-private'), + ], + 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') + +jack_dep = dependency('jack', version : '>= 1.9.10', required: get_option('jack')) +summary({'JACK2': jack_dep.found()}, bool_yn: true, section: 'Backend') + +if get_option('spa-plugins').allowed() + udevrulesdir = get_option('udevrulesdir') + if udevrulesdir == '' + # absolute path, otherwise meson prepends the prefix + udevrulesdir = '/usr/lib/udev/rules.d' + endif + + # plugin-specific dependencies + alsa_dep = dependency('alsa', version : '>=1.2.6', required: get_option('alsa')) + summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend') + + if alsa_dep.version().version_compare('>=1.2.10') + cdata.set('HAVE_ALSA_UMP', true) + endif + + bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5')) + bluez_gio_dep = dependency('gio-2.0', required : get_option('bluez5')) + bluez_gio_unix_dep = dependency('gio-unix-2.0', required : get_option('bluez5')) + bluez_glib2_dep = dependency('glib-2.0', required : get_option('bluez5')) + sbc_dep = dependency('sbc', required: get_option('bluez5')) + summary({'SBC': sbc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + bluez5_deps = [ mathlib, dbus_dep, sbc_dep, bluez_dep, bluez_glib2_dep, bluez_gio_dep, bluez_gio_unix_dep ] + bluez_deps_found = get_option('bluez5').allowed() + foreach dep: bluez5_deps + if get_option('bluez5').enabled() and not dep.found() + error('bluez5 enabled, but dependency not found: ' + dep.name()) + endif + bluez_deps_found = bluez_deps_found and dep.found() + endforeach + summary({'Bluetooth audio': bluez_deps_found}, bool_yn: true, section: 'Backend') + if bluez_deps_found + 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') + if get_option('bluez5-codec-opus').enabled() and not opus_dep.found() + error('bluez5-codec-opus enabled, but opus dependency not found') + endif + 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 + cdata.set('HAVE_LC3', get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()) + g722_codec_option = get_option('bluez5-codec-g722') + summary({'G722': g722_codec_option.allowed()}, bool_yn: true, section: 'Bluetooth audio codecs') + cdata.set('HAVE_G722', g722_codec_option.allowed()) + endif + + have_vulkan = false + vulkan_dep = dependency('vulkan', version : '>= 1.2.170', required: get_option('vulkan')) + if vulkan_dep.found() + have_vulkan = cc.has_header('vulkan/vulkan.h', dependencies : vulkan_dep) + assert((not get_option('vulkan').enabled()) or have_vulkan, 'Vulkan headers are missing') + endif + summary({'Vulkan': have_vulkan}, bool_yn: true, section: 'Misc dependencies') + + libcamera_dep = dependency('libcamera', version: '>= 0.2.0', required: get_option('libcamera')) + summary({'libcamera': libcamera_dep.found()}, bool_yn: true, section: 'Backend') + + compress_offload_option = get_option('compress-offload') + summary({'Compress-Offload': compress_offload_option.allowed()}, bool_yn: true, section: 'Backend') + cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', compress_offload_option.allowed()) + + # common dependencies + libudev_dep = dependency('libudev', required: get_option('udev').enabled()) + cdata.set('HAVE_LIBUDEV', libudev_dep.found()) + summary({'Udev': libudev_dep.found()}, bool_yn: true, section: 'Backend') + + libmysofa_dep = dependency('libmysofa', required : get_option('libmysofa')) + summary({'libmysofa': libmysofa_dep.found()}, bool_yn: true, section: 'filter-graph') + + lilv_lib = dependency('lilv-0', required: get_option('lv2')) + summary({'lilv (for lv2 plugins)': lilv_lib.found()}, bool_yn: true, section: 'filter-graph') + + ebur128_lib = dependency('libebur128', required: get_option('ebur128').enabled()) + summary({'EBUR128': ebur128_lib.found()}, bool_yn: true, section: 'filter-graph') + + cdata.set('HAVE_SPA_PLUGINS', true) + subdir('plugins') +endif + +subdir('tools') +subdir('tests') +subdir('examples') +subdir('lib') diff --git a/spa/plugins/aec/aec-null.c b/spa/plugins/aec/aec-null.c new file mode 100644 index 0000000..3161c2f --- /dev/null +++ b/spa/plugins/aec/aec-null.c @@ -0,0 +1,157 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +struct impl { + struct spa_handle handle; + struct spa_audio_aec aec; + struct spa_log *log; + + struct spa_hook_list hooks_list; + + uint32_t channels; +}; + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..74255aa --- /dev/null +++ b/spa/plugins/aec/aec-webrtc.cpp @@ -0,0 +1,451 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_WEBRTC +#include +#include +#include +#else +#include +#endif + +struct impl_data { + struct spa_handle handle; + struct spa_audio_aec aec; + + struct spa_log *log; +#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1) + std::unique_ptr apm; +#elif defined(HAVE_WEBRTC2) + rtc::scoped_refptr apm; +#endif + spa_audio_info_raw rec_info; + spa_audio_info_raw out_info; + spa_audio_info_raw play_info; + std::unique_ptr play_buffer, rec_buffer, out_buffer; +}; + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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; +} + +#ifdef HAVE_WEBRTC +/* [ f0 f1 f2 ] */ +static int parse_point(struct spa_json *it, float (&f)[3]) +{ + struct spa_json arr; + int i, res; + + if (spa_json_enter_array(it, &arr) <= 0) + return -EINVAL; + + for (i = 0; i < 3; i++) { + if ((res = spa_json_get_float(&arr, &f[i])) <= 0) + return -EINVAL; + } + return 0; +} + +/* [ point1 point2 ... ] */ +static int parse_mic_geometry(struct impl_data *impl, const char *mic_geometry, + std::vector& geometry) +{ + int res; + size_t i; + struct spa_json it[1]; + + if (spa_json_begin_array(&it[0], mic_geometry, strlen(mic_geometry)) <= 0) { + spa_log_error(impl->log, "Error: webrtc.mic-geometry expects an array"); + return -EINVAL; + } + + for (i = 0; i < geometry.size(); i++) { + float f[3]; + + if ((res = parse_point(&it[0], f)) < 0) { + spa_log_error(impl->log, "Error: can't parse webrtc.mic-geometry points: %d", res); + return res; + } + + spa_log_info(impl->log, "mic %zd position: (%g %g %g)", i, f[0], f[1], f[2]); + geometry[i].c[0] = f[0]; + geometry[i].c[1] = f[1]; + geometry[i].c[2] = f[2]; + } + return 0; +} +#endif + +static int webrtc_init2(void *object, const struct spa_dict *args, + struct spa_audio_info_raw *rec_info, struct spa_audio_info_raw *out_info, + struct spa_audio_info_raw *play_info) +{ + auto impl = static_cast(object); + int res; + + 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); +#if defined(HAVE_WEBRTC) + bool extended_filter = webrtc_get_spa_bool(args, "webrtc.extended_filter", true); + bool delay_agnostic = webrtc_get_spa_bool(args, "webrtc.delay_agnostic", true); + // Disable experimental flags by default + bool experimental_agc = webrtc_get_spa_bool(args, "webrtc.experimental_agc", false); + bool experimental_ns = webrtc_get_spa_bool(args, "webrtc.experimental_ns", false); + + bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true); + bool beamforming = webrtc_get_spa_bool(args, "webrtc.beamforming", false); +#elif defined(HAVE_WEBRTC1) + bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true); + bool transient_suppression = webrtc_get_spa_bool(args, "webrtc.transient_suppression", true); +#endif + // Note: AGC seems to mess up with Agnostic Delay Detection, especially with speech, + // result in very poor performance, disable by default + bool gain_control = webrtc_get_spa_bool(args, "webrtc.gain_control", false); + + // FIXME: Intelligibility enhancer is not currently supported + // This filter will modify playback buffer (when calling ProcessReverseStream), but now + // playback buffer modifications are discarded. + +#if defined(HAVE_WEBRTC) + webrtc::Config config; + config.Set(new webrtc::ExtendedFilter(extended_filter)); + config.Set(new webrtc::DelayAgnostic(delay_agnostic)); + config.Set(new webrtc::ExperimentalAgc(experimental_agc)); + config.Set(new webrtc::ExperimentalNs(experimental_ns)); + + if (beamforming) { + std::vector geometry(rec_info->channels); + const char *mic_geometry, *target_direction; + + /* The beamformer gives a single mono channel */ + out_info->channels = 1; + out_info->position[0] = SPA_AUDIO_CHANNEL_MONO; + + if ((mic_geometry = spa_dict_lookup(args, "webrtc.mic-geometry")) == NULL) { + spa_log_error(impl->log, "Error: webrtc.beamforming requires webrtc.mic-geometry"); + return -EINVAL; + } + + if ((res = parse_mic_geometry(impl, mic_geometry, geometry)) < 0) + return res; + + if ((target_direction = spa_dict_lookup(args, "webrtc.target-direction")) != NULL) { + webrtc::SphericalPointf direction(0.0f, 0.0f, 0.0f); + struct spa_json it; + float f[3]; + + spa_json_init(&it, target_direction, strlen(target_direction)); + if (parse_point(&it, f) < 0) { + spa_log_error(impl->log, "Error: can't parse target-direction %s", + target_direction); + return -EINVAL; + } + + direction.s[0] = f[0]; + direction.s[1] = f[1]; + direction.s[2] = f[2]; + + config.Set(new webrtc::Beamforming(true, geometry, direction)); + } else { + config.Set(new webrtc::Beamforming(true, geometry)); + } + } +#elif defined(HAVE_WEBRTC1) + webrtc::AudioProcessing::Config config; + config.echo_canceller.enabled = true; + config.pipeline.multi_channel_capture = rec_info->channels > 1; + config.pipeline.multi_channel_render = play_info->channels > 1; + // FIXME: Example code enables both gain controllers, but that seems sus + config.gain_controller1.enabled = gain_control; + config.gain_controller1.mode = webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital; + config.gain_controller1.analog_level_minimum = 0; + config.gain_controller1.analog_level_maximum = 255; + config.gain_controller2.enabled = gain_control; + config.high_pass_filter.enabled = high_pass_filter; + config.noise_suppression.enabled = noise_suppression; + config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; + // FIXME: expose pre/postamp gain + config.transient_suppression.enabled = transient_suppression; + config.voice_detection.enabled = voice_detection; +#elif defined(HAVE_WEBRTC2) + webrtc::AudioProcessing::Config config; + config.pipeline.multi_channel_capture = rec_info->channels > 1; + config.pipeline.multi_channel_render = play_info->channels > 1; + // FIXME: Example code enables both gain controllers, but that seems sus + config.gain_controller1.enabled = gain_control; + config.gain_controller1.mode = webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital; + config.gain_controller2.enabled = gain_control; + config.high_pass_filter.enabled = high_pass_filter; + config.noise_suppression.enabled = noise_suppression; + config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; + // FIXME: expose pre/postamp gain +#endif + +#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1) + webrtc::ProcessingConfig pconfig = {{ + webrtc::StreamConfig(rec_info->rate, rec_info->channels, false), /* input stream */ + webrtc::StreamConfig(out_info->rate, out_info->channels, false), /* output stream */ + webrtc::StreamConfig(play_info->rate, play_info->channels, false), /* reverse input stream */ + webrtc::StreamConfig(play_info->rate, play_info->channels, false), /* reverse output stream */ + }}; +#elif defined(HAVE_WEBRTC2) + webrtc::ProcessingConfig pconfig = {{ + webrtc::StreamConfig(rec_info->rate, rec_info->channels), /* input stream */ + webrtc::StreamConfig(out_info->rate, out_info->channels), /* output stream */ + webrtc::StreamConfig(play_info->rate, play_info->channels), /* reverse input stream */ + webrtc::StreamConfig(play_info->rate, play_info->channels), /* reverse output stream */ + }}; +#endif + +#if defined(HAVE_WEBRTC) + auto apm = std::unique_ptr(webrtc::AudioProcessing::Create(config)); +#elif defined(HAVE_WEBRTC1) + auto apm = std::unique_ptr(webrtc::AudioProcessingBuilder().Create()); + apm->ApplyConfig(config); +#elif defined(HAVE_WEBRTC2) + auto apm = webrtc::AudioProcessingBuilder().Create(); + apm->ApplyConfig(config); +#endif + + if ((res = apm->Initialize(pconfig)) != webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Error initialising webrtc audio processing module: %d", res); + return -EINVAL; + } + +#ifdef HAVE_WEBRTC + 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 suppression 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); +#endif + impl->apm = std::move(apm); + impl->rec_info = *rec_info; + impl->out_info = *out_info; + impl->play_info = *play_info; + impl->play_buffer = std::make_unique(play_info->channels); + impl->rec_buffer = std::make_unique(rec_info->channels); + impl->out_buffer = std::make_unique(out_info->channels); + return 0; +} + +static int webrtc_init(void *object, const struct spa_dict *args, + const struct spa_audio_info_raw *info) +{ + int res; + struct spa_audio_info_raw rec_info = *info; + struct spa_audio_info_raw out_info = *info; + struct spa_audio_info_raw play_info = *info; + res = webrtc_init2(object, args, &rec_info, &out_info, &play_info); + if (rec_info.channels != out_info.channels) + res = -EINVAL; + return res; +} + +static int webrtc_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples) +{ + auto impl = static_cast(object); + int res; + +#if defined(HAVE_WEBRTC) || defined(HAVE_WEBRTC1) + webrtc::StreamConfig play_config = + webrtc::StreamConfig(impl->play_info.rate, impl->play_info.channels, false); + webrtc::StreamConfig rec_config = + webrtc::StreamConfig(impl->rec_info.rate, impl->rec_info.channels, false); + webrtc::StreamConfig out_config = + webrtc::StreamConfig(impl->out_info.rate, impl->out_info.channels, false); +#elif defined(HAVE_WEBRTC2) + webrtc::StreamConfig play_config = + webrtc::StreamConfig(impl->play_info.rate, impl->play_info.channels); + webrtc::StreamConfig rec_config = + webrtc::StreamConfig(impl->rec_info.rate, impl->rec_info.channels); + webrtc::StreamConfig out_config = + webrtc::StreamConfig(impl->out_info.rate, impl->out_info.channels); +#endif + unsigned int num_blocks = n_samples * 1000 / impl->play_info.rate / 10; + + if (n_samples * 1000 / impl->play_info.rate % 10 != 0) { + spa_log_error(impl->log, "Buffers must be multiples of 10ms in length (currently %u samples)", n_samples); + return -EINVAL; + } + + for (size_t i = 0; i < num_blocks; i ++) { + for (size_t j = 0; j < impl->play_info.channels; j++) + impl->play_buffer[j] = const_cast(play[j]) + play_config.num_frames() * i; + for (size_t j = 0; j < impl->rec_info.channels; j++) + impl->rec_buffer[j] = const_cast(rec[j]) + rec_config.num_frames() * i; + for (size_t j = 0; j < impl->out_info.channels; j++) + impl->out_buffer[j] = out[j] + out_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 ((res = impl->apm->ProcessReverseStream(impl->play_buffer.get(), + play_config, play_config, impl->play_buffer.get())) != + webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Processing reverse stream failed: %d", res); + } + + // Extra delay introduced by multiple frames + impl->apm->set_stream_delay_ms((num_blocks - 1) * 10); + + if ((res = impl->apm->ProcessStream(impl->rec_buffer.get(), + rec_config, out_config, impl->out_buffer.get())) != + webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Processing stream failed: %d", res); + } + } + return 0; +} + +static const struct spa_audio_aec_methods impl_aec = { + .version = SPA_VERSION_AUDIO_AEC_METHODS, + .add_listener = NULL, + .init = webrtc_init, + .run = webrtc_run, + .init2 = webrtc_init2, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + auto impl = reinterpret_cast(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(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(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, +}; + +extern "C" { +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; +} + +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..9ef3d53 --- /dev/null +++ b/spa/plugins/alsa/90-pipewire-alsa.rules @@ -0,0 +1,216 @@ +# 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 . + +SUBSYSTEM!="sound", GOTO="pipewire_end" +ACTION!="change", GOTO="pipewire_end" +KERNEL!="card*", GOTO="pipewire_end" +SUBSYSTEMS=="usb", GOTO="pipewire_check_usb" +SUBSYSTEMS=="firewire", GOTO="pipewire_firewire_quirk" +SUBSYSTEMS=="pci", GOTO="pipewire_check_pci" + +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:0038 is for the Astro Mixamp Pro TR +ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0038", 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..c124011 --- /dev/null +++ b/spa/plugins/alsa/acp-tool.c @@ -0,0 +1,776 @@ +/* ALSA card profile test */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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) +{ + static const char * const levels[] = { "E", "W", "N", "I", "D", "T" }; + const char *level_str = levels[SPA_CLAMP(level, 0, (int)SPA_N_ELEMENTS(levels) - 1)]; + + if (file) { + const char *p = strrchr(file, '/'); + if (p) + file = p + 1; + } + + fprintf(stderr, "%s %16s:%-5d ", level_str, file ? file : "", line); + while (level-- > 1) + fprintf(stderr, " "); + 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: 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: 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: 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: missing\n"); + return -EINVAL; + } + dev_id = atoi(argv[1]); + vol = (float)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: 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.2f); +} + +static int cmd_dec_volume(struct data *data, const struct command *cmd, int argc, char *argv[]) +{ + return adjust_volume(data, cmd, argc, argv, -0.2f); +} + +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: 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: 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: 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", "", "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", "", "spr", "Activate a profile", cmd_set_profile }, + { "list-ports", "[id]", "lp", "List ports", cmd_list_ports }, + { "set-port", "", "sp", "Activate a port", cmd_set_port }, + { "list-devices", "[id]", "ld", "List available devices", cmd_list_devices }, + { "get-volume", "", "gv", "Get volume from device", cmd_get_volume }, + { "set-volume", " ", "v", "Set volume on device", cmd_set_volume }, + { "inc-volume", "", "v+", "Increase volume on device", cmd_inc_volume }, + { "dec-volume", "", "v-", "Decrease volume on device", cmd_dec_volume }, + { "get-mute", "", "gm", "Get mute state from device", cmd_get_mute }, + { "set-mute", " ", "sm", "Set mute on device", cmd_set_mute }, + { "toggle-mute", "", "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; + struct spa_json it; + + acp_set_log_func(log_func, data); + acp_set_log_level(data->verbose); + + items[n_items++] = ACP_DICT_ITEM_INIT("use-ucm", "true"); + items[n_items++] = ACP_DICT_ITEM_INIT("verbose", data->verbose ? "true" : "false"); + + if (spa_json_begin_object_relax(&it, data->properties, strlen(data->properties)) > 0) { + char key[1024]; + const char *value; + int len; + struct spa_error_location loc; + while ((len = spa_json_object_next(&it, key, sizeof(key), &value)) > 0) { + char *k = alloca(strlen(key) + 1); + char *v = alloca(len + 1); + memcpy(k, key, strlen(key) + 1); + spa_json_parse_stringn(value, len, v, len + 1); + items[n_items++] = ACP_DICT_ITEM_INIT(k, v); + if (n_items >= SPA_N_ELEMENTS(items)) + break; + } + if (spa_json_get_error(&it, data->properties, &loc)) { + struct spa_debug_context *c = NULL; + spa_debugc(c, "invalid --properties: %s", loc.reason); + spa_debugc_error_location(c, &loc); + return -EINVAL; + } + } + props = ACP_DICT_INIT(items, n_items); + + 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 = { .properties = strdup("") }; + + 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': + free(data.properties); + 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..63a32ea --- /dev/null +++ b/spa/plugins/alsa/acp/acp.c @@ -0,0 +1,2215 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "acp.h" +#include "alsa-mixer.h" +#include "alsa-ucm.h" + +#include +#include +#include + +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); + } + if (m->split) { + char pos[2048]; + struct spa_strbuf b; + int i; + + spa_strbuf_init(&b, pos, sizeof(pos)); + spa_strbuf_append(&b, "["); + for (i = 0; i < m->split->channels; ++i) + spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), m->split->idx[i]); + spa_strbuf_append(&b, "]"); + pa_proplist_sets(dev->proplist, "api.alsa.split.position", pos); + + spa_strbuf_init(&b, pos, sizeof(pos)); + spa_strbuf_append(&b, "["); + for (i = 0; i < m->split->hw_channels; ++i) + spa_strbuf_append(&b, "%sAUX%d", ((i == 0) ? "" : ","), i); + spa_strbuf_append(&b, "]"); + pa_proplist_sets(dev->proplist, "api.alsa.split.hw-position", pos); + } + pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name); + pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description); + pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index); + 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 const char *find_best_verb(pa_card *impl) +{ + const char *res = NULL; + unsigned prio = 0; + pa_alsa_ucm_verb *verb; + PA_LLIST_FOREACH(verb, impl->ucm.verbs) { + if (verb->priority >= prio) + res = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + } + return res; +} + +static int add_pro_profile(pa_card *impl, uint32_t index) +{ + snd_ctl_t *ctl_hndl; + int err, dev, count = 0, n_capture = 0, n_playback = 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; + uint32_t idx; + + if (impl->use_ucm) { + const char *verb = find_best_verb(impl); + if (verb == NULL) + return -ENOTSUP; + if ((err = snd_use_case_set(impl->ucm.ucm_mgr, "_verb", verb)) < 0) { + pa_log_error("error setting verb: %s", snd_strerror(err)); + return err; + } + } + + ss.format = PA_SAMPLE_S32LE; + ss.rate = impl->rate; + ss.channels = impl->pro_channels; + + 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, 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_proplist_setf(m->output_proplist, "device.profile.pro", "true"); + 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); + n_playback++; + } + 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, 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_proplist_setf(m->input_proplist, "device.profile.pro", "true"); + 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); + n_capture++; + } + pa_idxset_put(ap->input_mappings, m, NULL); + free(name); + } + } + snd_ctl_close(ctl_hndl); + + if (n_capture == 1 && n_playback == 1) { + PA_IDXSET_FOREACH(m, ap->output_mappings, idx) { + pa_proplist_setf(m->output_proplist, "node.group", "pro-audio-%u", index); + pa_proplist_setf(m->output_proplist, "node.link-group", "pro-audio-%u", index); + pa_proplist_setf(m->output_proplist, "api.alsa.auto-link", "true"); + pa_proplist_setf(m->output_proplist, "api.alsa.disable-tsched", "true"); + } + PA_IDXSET_FOREACH(m, ap->input_mappings, idx) { + pa_proplist_setf(m->input_proplist, "node.group", "pro-audio-%u", index); + pa_proplist_setf(m->input_proplist, "node.link-group", "pro-audio-%u", index); + pa_proplist_setf(m->input_proplist, "api.alsa.auto-link", "true"); + pa_proplist_setf(m->input_proplist, "api.alsa.disable-tsched", "true"); + } + } + return 0; +} + +static bool contains_string(const char *arr, const char *str) +{ + struct spa_json it[1]; + char v[256]; + + if (arr == NULL || str == NULL) + return false; + + if (spa_json_begin_array_relax(&it[0], arr, strlen(arr)) <= 0) + return false; + + while (spa_json_get_string(&it[0], v, sizeof(v)) > 0) { + if (spa_streq(v, str)) + return true; + } + return false; +} + +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; + const char *arr; + bool broken_ucm = false; + + 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_device) { + pa_alsa_ucm_add_port(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 (m->split && m->split->broken) + broken_ucm = true; + } + } + + 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_device) { + pa_alsa_ucm_add_port(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); + + if (m->split && m->split->broken) + broken_ucm = true; + } + } + cp->n_devices = pa_dynarray_size(&ap->out.devices); + cp->devices = ap->out.devices.array.data; + pa_hashmap_put(impl->profiles, ap->name, cp); + } + + + /* Add a conspicuous notice if there are errors in the UCM profile */ + if (broken_ucm) { + const char *desc; + char *new_desc = NULL; + + desc = pa_proplist_gets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION); + if (!desc) + desc = ""; + new_desc = spa_aprintf(_("%s [ALSA UCM error]"), desc); + pa_log_notice("Errors in ALSA UCM profile for card %s", desc); + if (new_desc) + pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION, new_desc); + free(new_desc); + } + + 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); + } + arr = pa_proplist_gets(impl->proplist, "api.acp.hidden-ports"); + PA_HASHMAP_FOREACH(dp, impl->ports, state) { + if (contains_string(arr, dp->name)) + dp->port.flags |= ACP_PORT_HIDDEN; + 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); + arr = pa_proplist_gets(impl->proplist, "api.acp.hidden-profiles"); + PA_HASHMAP_FOREACH(cp, impl->profiles, state) { + if (contains_string(arr, cp->name)) + cp->flags |= ACP_PROFILE_HIDDEN; + 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), *elem; + 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; + + pa_assert(_elem); + elem = *_elem; +#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 void acp_iec958_codec_mask_to_json(uint64_t codecs, char *buf, size_t maxsize) +{ + struct spa_strbuf b; + const struct spa_type_info *info; + + spa_strbuf_init(&b, buf, maxsize); + for (info = spa_type_audio_iec958_codec; info->name; ++info) + if ((codecs & (1ULL << info->type)) && info->type != SPA_AUDIO_IEC958_CODEC_UNKNOWN) + spa_strbuf_append(&b, "%s\"%s\"", (b.pos ? "," : "["), + spa_type_audio_iec958_codec_to_short_name(info->type)); + if (b.pos) + spa_strbuf_append(&b, "]"); +} + +void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize) +{ + struct spa_strbuf b; + + spa_strbuf_init(&b, buf, maxsize); + spa_strbuf_append(&b, "["); + for (size_t i = 0; i < n_codecs; ++i) + spa_strbuf_append(&b, "%s\"%s\"", (i ? "," : ""), + spa_type_audio_iec958_codec_to_short_name(codecs[i])); + spa_strbuf_append(&b, "]"); +} + +size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs) +{ + struct spa_json it; + char v[256]; + size_t n_codecs = 0; + + if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0) + return 0; + + while (spa_json_get_string(&it, v, sizeof(v)) > 0) { + uint32_t type = spa_type_audio_iec958_codec_from_short_name(v); + if (type != SPA_AUDIO_IEC958_CODEC_UNKNOWN) + codecs[n_codecs++] = type; + if (n_codecs >= max_codecs) + break; + } + + return n_codecs; +} + +static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) +{ + pa_card *impl = snd_mixer_elem_get_callback_private(melem); + snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem; + int device, i; + const char *old_monitor_name, *old_iec958_codec_list; + pa_device_port *p; + pa_hdmi_eld eld; + bool changed = false; + + pa_assert(_elem); + elem = *_elem; + device = snd_hctl_elem_get_device(elem); + + 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)); + + // Strip trailing whitespace from monitor_name (primarily an NVidia driver bug for now) + for (i = strlen(eld.monitor_name) - 1; i >= 0; i--) { + if (eld.monitor_name[i] == '\n' || eld.monitor_name[i] == '\r' || eld.monitor_name[i] == '\t' || + eld.monitor_name[i] == ' ') + eld.monitor_name[i] = 0; + else + break; + } + + 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); + } + + old_iec958_codec_list = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); + if (eld.iec958_codecs == 0) { + changed |= old_iec958_codec_list != NULL; + pa_proplist_unset(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); + } else { + char codecs[512]; + acp_iec958_codec_mask_to_json(eld.iec958_codecs, codecs, sizeof(codecs)); + changed |= (old_iec958_codec_list == NULL) || (!spa_streq(old_iec958_codec_list, codecs)); + pa_proplist_sets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED, codecs); + } + + pa_proplist_as_dict(p->proplist, &p->port.props); + + if (changed && mask != 0 && impl->events && impl->events->props_changed) + 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 (SPA_FLAG_IS_SET(p->flags, ACP_PROFILE_HIDDEN)) + continue; + + 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->ucm_context) { + if (!dev->active_port) + return 0; + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); + if (pa_alsa_ucm_port_device_status(data) <= 0) + return 0; + } + + 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; + + if (v != &dev->real_volume) + dev->real_volume = *v; + + if (dev->ucm_context) { + if (!dev->active_port) + return; + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); + if (pa_alsa_ucm_port_device_status(data) <= 0) + return; + } + + 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->ucm_context) { + if (!dev->active_port) + return 0; + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); + if (pa_alsa_ucm_port_device_status(data) <= 0) + return 0; + } + + 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->ucm_context) { + if (!dev->active_port) + return; + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(dev->active_port); + if (pa_alsa_ucm_port_device_status(data) <= 0) + return; + } + + 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 = (float)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)) < 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; + + if (!impl->disable_mixer_path) + pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted); + } else { + pa_alsa_ucm_port_data *data; + + data = PA_DEVICE_PORT_DATA(dev->active_port); + + /* Now activate volume controls, if any */ + if (data->path) { + dev->mixer_path = data->path; + if (!impl->disable_mixer_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 */ + if (!impl->disable_mixer_path) + 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; + const char *codecs; + pa_device_port *p; + void *state = NULL; + 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++) { + 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; + + while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) { + codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); + if (codecs) { + dev->device.n_codecs = acp_iec958_codecs_from_json(codecs, dev->device.codecs, + ACP_N_ELEMENTS(dev->device.codecs)); + break; + } + } + + return 0; +} + +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; + + np = (pa_alsa_profile*)profiles[new_index]; + if (SPA_FLAG_IS_SET(np->profile.flags, ACP_PROFILE_HIDDEN)) + return -EINVAL; + + op = old_index != ACP_INVALID_INDEX ? (pa_alsa_profile*)profiles[old_index] : NULL; + 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) { + if (np->profile.flags & ACP_PROFILE_OFF) { + if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, NULL, op)) < 0) + return res; + } else if (np->profile.flags & ACP_PROFILE_PRO) { + const char *verb = find_best_verb(impl); + if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, NULL, op)) < 0) + return res; + if ((res = snd_use_case_set(impl->ucm.ucm_mgr, "_verb", verb)) < 0) { + pa_log_error("error setting verb: %s", snd_strerror(res)); + return res; + } + } else if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, np, op)) < 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_device) { + pa_alsa_ucm_add_port(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_device) { + pa_alsa_ucm_add_port(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); +} + +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; + impl->pro_channels = 64; + + 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.disable-mixer-path")) != NULL) + impl->disable_mixer_path = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL) + impl->ignore_dB = spa_atob(s); + if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL) + 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); + if ((s = acp_dict_lookup(props, "api.acp.pro-channels")) != NULL) + impl->pro_channels = atoi(s); + if ((s = acp_dict_lookup(props, "api.alsa.split-enable")) != NULL) + impl->ucm.split_enable = spa_atob(s); + } + +#if SND_LIB_VERSION < 0x10207 + if (impl->ucm.split_enable) + pa_log_info("alsa-lib too old for PipeWire-side UCM SplitPCM"); + + impl->ucm.split_enable = false; /* API addition in 1.2.7 */ +#endif + + impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE; + impl->ucm.default_sample_spec.rate = impl->rate; + impl->ucm.default_sample_spec.channels = 2; + 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 = -EEXIST; + 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"; + + init_eld_ctls(impl); + + profile_index = acp_card_find_best_profile_index(&impl->card, profile); + acp_card_set_profile(&impl->card, profile_index, 0); + + 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; + pa_card *impl = d->card; + + 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 && !impl->disable_mixer_path) + 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 (SPA_FLAG_IS_SET(p->flags, ACP_PORT_HIDDEN)) + continue; + + 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; + if (SPA_FLAG_IS_SET(p->port.flags, ACP_PORT_HIDDEN)) + 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); + + res = pa_alsa_ucm_set_port(d->ucm_context, p); + sync_mixer(d, p); + } 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] = (float)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..4b9f2c4 --- /dev/null +++ b/spa/plugins/alsa/acp/acp.h @@ -0,0 +1,310 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef ACP_H +#define ACP_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#include +#include +#include +#include +#include + +#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)++) + +static inline 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 (strcmp(key, it->key) == 0) + return it->value; + return NULL; +} + +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. + */ +#define ACP_KEY_IEC958_CODECS_DETECTED "iec958.codecs.detected" + /**< A list of IEC958 passthrough formats which have been auto-detected as being + * supported by a given node. This only serves as a hint, as the auto-detected + * values may be incorrect and/or might change, e.g. when external devices such + * as receivers are powered on or off. + */ + +struct acp_device; + +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 */ +#define ACP_PORT_HIDDEN (1<<2) + 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) +#define ACP_DEVICE_HIDDEN (1<<5) + 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 */ +#define ACP_PROFILE_HIDDEN (1<<4) /* don't show the 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); + +void acp_iec958_codecs_to_json(const uint32_t *codecs, size_t n_codecs, char *buf, size_t maxsize); +size_t acp_iec958_codecs_from_json(const char *str, uint32_t *codecs, size_t max_codecs); + +#ifdef __cplusplus +} +#endif + +#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..1f494ee --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-mixer.c @@ -0,0 +1,5360 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + +#include "config.h" + +#include +#include +#include + +#include + +#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 = (long) (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; +} + +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; + + fn = get_data_path(paths_dir, "paths", fname); + + pa_log_info("Loading path config: %s", fn); + + 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_xfree(m->split); + + 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) { + pa_strbuf *sb; + uint32_t idx; + pa_alsa_mapping *m; + + sb = pa_strbuf_new(); + + if (p->output_mappings) + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (!pa_strbuf_isempty(sb)) + pa_strbuf_puts(sb, " + "); + + pa_strbuf_printf(sb, _("%s Output"), m->description); + } + + if (p->input_mappings) + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (!pa_strbuf_isempty(sb)) + pa_strbuf_puts(sb, " + "); + + pa_strbuf_printf(sb, _("%s Input"), m->description); + } + + p->description = pa_strbuf_to_string_free(sb); + } + + 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) { + pa_strbuf *buf; + unsigned long i, nsteps; + + pa_assert(db_fix->min_step <= db_fix->max_step); + nsteps = db_fix->max_step - db_fix->min_step + 1; + + buf = pa_strbuf_new(); + for (i = 0; i < nsteps; ++i) + pa_strbuf_printf(buf, "[%li]:%0.2f ", i + db_fix->min_step, db_fix->db_values[i] / 100.0); + + db_values = pa_strbuf_to_string_free(buf); + } + + 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); +} + +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 = get_data_path(NULL, "profile-sets", fname ? fname : "default.conf"); + + pa_log_info("Loading profile set: %s", fn); + + if ((r = access(fn, R_OK)) != 0) { + if (fname != NULL) { + pa_log_warn("profile-set '%s' can't be accessed: %m", fn); + fn = get_data_path(NULL, "profile-sets", "default.conf"); + 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, 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..cbfac4a --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-mixer.h @@ -0,0 +1,464 @@ +#ifndef fooalsamixerhfoo +#define fooalsamixerhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + +#include + +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; + + pa_alsa_ucm_split *split; + + 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; + + /* ucm device context */ + pa_alsa_ucm_profile_context ucm_context; + + 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..73466f0 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -0,0 +1,2966 @@ +/*** + This file is part of PulseAudio. + + Copyright 2011 Wolfson Microelectronics PLC + Author Margarita Olaya + Copyright 2012 Feng Wei , 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 . + +***/ + +#include "config.h" + +#include +#include +#include +#include + +#ifdef HAVE_VALGRIND_MEMCHECK_H +#include +#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 *device); +static void ucm_port_data_free(pa_device_port *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 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; +} + +static PA_PRINTF_FUNC(2,3) const char *ucm_get_string(snd_use_case_mgr_t *uc_mgr, const char *fmt, ...) +{ + char *id; + const char *value; + va_list args; + int err; + + va_start(args, fmt); + id = pa_vsprintf_malloc(fmt, args); + va_end(args); + err = snd_use_case_get(uc_mgr, id, &value); + if (err >= 0) + pa_log_debug("Got %s: %s", id, value); + pa_xfree(id); + if (err < 0) { + errno = -err; + return NULL; + } + return value; +} + +static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd_use_case_mgr_t *uc_mgr, const char *prefix) { + pa_alsa_ucm_split *split; + const char *value; + const char *device_name; + int i; + uint32_t hw_channels; + const char *pcm_name; + const char *rule_name; + + if (spa_streq(prefix, "Playback")) + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK); + else + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (!pcm_name) + pcm_name = ""; + + device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); + if (!device_name) + return NULL; + + value = ucm_get_string(uc_mgr, "%sChannels/%s", prefix, device_name); + if (pa_atou(value, &hw_channels) < 0) + return NULL; + + split = pa_xnew0(pa_alsa_ucm_split, 1); + + for (i = 0; i < PA_CHANNELS_MAX; i++) { + uint32_t idx; + snd_pcm_chmap_t *map; + + value = ucm_get_string(uc_mgr, "%sChannel%d/%s", prefix, i, device_name); + if (pa_atou(value, &idx) < 0) + break; + + if (idx >= hw_channels) { + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannel%d=%d >= %sChannels=%d", + pcm_name, device_name, prefix, i, idx, prefix, hw_channels); + split->broken = true; + } + + value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name); + if (!value) { + rule_name = "ChannelPos"; + goto fail; + } + + map = snd_pcm_chmap_parse_string(value); + if (!map) { + rule_name = "ChannelPos value"; + goto fail; + } + + if (map->channels == 1) { + pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)", + prefix, (int)idx, device_name, i, value, map->pos[0]); + split->idx[i] = idx; + split->pos[i] = map->pos[0]; + free(map); + } else { + free(map); + rule_name = "channel map parsing"; + goto fail; + } + } + + if (i == 0) { + pa_xfree(split); + return NULL; + } + + split->channels = i; + split->hw_channels = hw_channels; + return split; + +fail: + pa_log_warn("Invalid SplitPCM ALSA UCM %s for device %s (%s)", rule_name, pcm_name, device_name); + pa_xfree(split); + return NULL; +} + +/* Create a property list for this ucm device */ +static int ucm_get_device_property( + pa_alsa_ucm_device *device, + 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); + } + + device->playback_split = ucm_get_split_channels(device, uc_mgr, "Playback"); + device->capture_split = ucm_get_split_channels(device, uc_mgr, "Capture"); + + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) { + /* get priority from static table */ + for (i = 0; dev_info[i].id; i++) { + 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); + + device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + if (n_confdev <= 0) + pa_log_debug("No %s for device %s", "_conflictingdevs", device_name); + else { + 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); + + device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + if (n_suppdev <= 0) + pa_log_debug("No %s for device %s", "_supporteddevs", device_name); + else { + 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, + pa_alsa_ucm_verb *verb, + const char *modifier_name) { + const char *value; + char *id; + int i; + const char **devices; + int n_confdev, n_suppdev; + + 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); + n_confdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + modifier->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + if (n_confdev <= 0) + pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name); + else { + ucm_add_devices_to_idxset(modifier->conflicting_devices, NULL, verb->devices, devices, n_confdev); + snd_use_case_free_list(devices, n_confdev); + } + + id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name); + n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + modifier->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + if (n_suppdev <= 0) + pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name); + else { + ucm_add_devices_to_idxset(modifier->supported_devices, NULL, verb->devices, devices, n_suppdev); + snd_use_case_free_list(devices, n_suppdev); + } + + 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 long ucm_device_status(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + char *devstatus; + long status = 0; + + if (!ucm->active_verb) { + pa_log_error("Failed to get status for UCM device %s: no UCM verb set", dev_name); + return -1; + } + + devstatus = pa_sprintf_malloc("_devstatus/%s", dev_name); + if (snd_use_case_geti(ucm->ucm_mgr, devstatus, &status) < 0) { + pa_log_debug("Failed to get status for UCM device %s", dev_name); + status = -1; + } + pa_xfree(devstatus); + + return status; +} + +static int ucm_device_disable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + if (!ucm->active_verb) { + pa_log_error("Failed to disable UCM device %s: no UCM verb set", dev_name); + return -1; + } + + /* If any of dev's conflicting devices is enabled, trying to disable + * dev gives an error despite the fact that it's already disabled. + * Check that dev is enabled to avoid this error. */ + if (ucm_device_status(ucm, dev) == 0) { + pa_log_debug("UCM device %s is already disabled", dev_name); + return 0; + } + + pa_log_debug("Disabling 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); + return -1; + } + + return 0; +} + +static int ucm_device_enable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *dev) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + if (!ucm->active_verb) { + pa_log_error("Failed to enable UCM device %s: no UCM verb set", dev_name); + return -1; + } + + /* We don't need to enable devices that are already enabled */ + if (ucm_device_status(ucm, dev) > 0) { + pa_log_debug("UCM device %s is already enabled", dev_name); + return 0; + } + + pa_log_debug("Enabling UCM device %s", dev_name); + if (snd_use_case_set(ucm->ucm_mgr, "_enadev", dev_name) < 0) { + pa_log("Failed to enable UCM device %s", dev_name); + return -1; + } + + 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 long ucm_modifier_status(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + char *modstatus; + long status = 0; + + if (!ucm->active_verb) { + pa_log_error("Failed to get status for UCM modifier %s: no UCM verb set", mod_name); + return -1; + } + + modstatus = pa_sprintf_malloc("_modstatus/%s", mod_name); + if (snd_use_case_geti(ucm->ucm_mgr, modstatus, &status) < 0) { + pa_log_debug("Failed to get status for UCM modifier %s", mod_name); + status = -1; + } + pa_xfree(modstatus); + + return status; +} + +static int ucm_modifier_disable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + if (!ucm->active_verb) { + pa_log_error("Failed to disable UCM modifier %s: no UCM verb set", mod_name); + return -1; + } + + /* We don't need to disable modifiers that are already disabled */ + if (ucm_modifier_status(ucm, mod) == 0) { + pa_log_debug("UCM modifier %s is already disabled", mod_name); + return 0; + } + + pa_log_debug("Disabling 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); + return -1; + } + + return 0; +} + +static int ucm_modifier_enable(pa_alsa_ucm_config *ucm, pa_alsa_ucm_modifier *mod) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + if (!ucm->active_verb) { + pa_log_error("Failed to enable UCM modifier %s: no UCM verb set", mod_name); + return -1; + } + + /* We don't need to enable modifiers that are already enabled */ + if (ucm_modifier_status(ucm, mod) > 0) { + pa_log_debug("UCM modifier %s is already enabled", mod_name); + return 0; + } + + pa_log_debug("Enabling 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); + return -1; + } + + 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(pa_alsa_ucm_device *dev, const char *role_name, const char *role, bool is_sink) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + const char *sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); + const char *source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (is_sink && sink) + add_role_to_device(dev, dev_name, role_name, role); + else if (!is_sink && source) + add_role_to_device(dev, dev_name, role_name, role); +} + +static char *modifier_name_to_role(const char *mod_name, bool *is_sink) { + char *sub = NULL, *tmp, *pos; + + *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); + + pos = sub; + while (pos && *pos == ' ') pos++; + + if (!pos || !*pos) { + pa_xfree(sub); + pa_log_warn("Can't match media roles for modifier %s", mod_name); + return NULL; + } + + tmp = pos; + + do { + *tmp = tolower(*tmp); + } while (*(++tmp)); + + tmp = pa_xstrdup(pos); + pa_xfree(sub); + return tmp; +} + +static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, const char *mod_name) { + pa_alsa_ucm_device *dev; + bool is_sink = false; + char *sub = NULL; + const char *role_name; + uint32_t idx; + + 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; + PA_IDXSET_FOREACH(dev, modifier->supported_devices, idx) { + /* 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(dev, role_name, sub, is_sink); + } +} + +static void append_lost_relationship(pa_alsa_ucm_device *dev) { + uint32_t idx; + pa_alsa_ucm_device *d; + + PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) + 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)); + + PA_IDXSET_FOREACH(d, dev->supported_devices, idx) + 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; + const char *split_prefix = ucm->split_enable ? "<<>>" : ""; + + /* support multiple card instances, address card directly by index */ + card_name = pa_sprintf_malloc("%shw:%i", split_prefix, card_index); + if (card_name == NULL) + return -PA_ALSA_ERR_UNSPECIFIED; + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + char *ucm_card_name; + + /* fallback longname: is UCM available for this card ? */ + pa_xfree(card_name); + err = snd_card_get_name(card_index, &ucm_card_name); + if (err < 0) { + pa_log("Card can't get card_name from card_index %d", card_index); + err = -PA_ALSA_ERR_UNSPECIFIED; + goto name_fail; + } + card_name = pa_sprintf_malloc("%s%s", split_prefix, ucm_card_name); + free(ucm_card_name); + if (card_name == NULL) { + err = -PA_ALSA_ERR_UNSPECIFIED; + goto name_fail; + } + + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + 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 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; +} + +static void ucm_verb_set_split_leaders(pa_alsa_ucm_verb *verb) { + pa_alsa_ucm_device *d, *d2; + + /* Set first virtual device in each split HW PCM as the split leader */ + + PA_LLIST_FOREACH(d, verb->devices) { + if (d->playback_split) + d->playback_split->leader = true; + if (d->capture_split) + d->capture_split->leader = true; + } + + PA_LLIST_FOREACH(d, verb->devices) { + const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK); + const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (d->playback_split) { + if (!sink) + d->playback_split->leader = false; + + if (d->playback_split->leader) { + PA_LLIST_FOREACH(d2, verb->devices) { + const char *sink2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SINK); + + if (d == d2 || !d2->playback_split || !sink || !sink2 || !pa_streq(sink, sink2)) + continue; + d2->playback_split->leader = false; + } + } + } + + if (d->capture_split) { + if (!source) + d->capture_split->leader = false; + + if (d->capture_split->leader) { + PA_LLIST_FOREACH(d2, verb->devices) { + const char *source2 = pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (d == d2 || !d2->capture_split || !source || !source2 || !pa_streq(source, source2)) + continue; + d2->capture_split->leader = false; + } + } + } + } +} + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) { + pa_alsa_ucm_device *d; + pa_alsa_ucm_modifier *mod; + 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); + } + + ucm_verb_set_split_leaders(verb); + + /* 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, verb, 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, 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; + void *state; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + dev = data->device; + data->eld_device = dev->eld_device; + if (data->eld_mixer_device_name) + pa_xfree(data->eld_mixer_device_name); + data->eld_mixer_device_name = pa_xstrdup(dev->eld_mixer_device_name); + } +} + +static void update_mixer_paths(pa_hashmap *ports, const char *verb_name) { + 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", verb_name, port->name); + data = PA_DEVICE_PORT_DATA(port); + data->path = pa_hashmap_get(data->paths, verb_name); + } +} + +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 *verb_name, *mdev; + void *state, *state2; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + + dev = data->device; + mdev = get_mixer_device(dev, is_sink); + 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(verb_name, 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, verb_name); + } 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, verb_name); + } else + pa_log_debug("Set up h/w %s using '%s' for %s:%s", path->has_volume ? "volume" : "mute", + path->name, verb_name, 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 proplist_set_icon_name( + pa_proplist *proplist, + pa_device_port_type_t type, + bool is_sink) { + const char *icon; + + if (is_sink) { + switch (type) { + case PA_DEVICE_PORT_TYPE_HEADPHONES: + icon = "audio-headphones"; + break; + case PA_DEVICE_PORT_TYPE_HDMI: + icon = "video-display"; + break; + case PA_DEVICE_PORT_TYPE_SPEAKER: + default: + icon = "audio-speakers"; + break; + } + } else { + switch (type) { + case PA_DEVICE_PORT_TYPE_HEADSET: + icon = "audio-headset"; + break; + case PA_DEVICE_PORT_TYPE_MIC: + default: + icon = "audio-input-microphone"; + break; + } + } + + pa_proplist_sets(proplist, "device.icon_name", icon); +} + +static char *devset_name(pa_idxset *devices, const char *sep) { + int i = 0; + int num = pa_idxset_size(devices); + pa_alsa_ucm_device *sorted[num], *dev; + char *dev_names = NULL; + char *tmp = NULL; + uint32_t idx; + + PA_IDXSET_FOREACH(dev, devices, idx) { + sorted[i] = dev; + i++; + } + + /* Sort by alphabetical order so as to have a deterministic naming scheme */ + qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp); + + for (i = 0; i < num; i++) { + dev = sorted[i]; + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + if (!dev_names) { + dev_names = pa_xstrdup(dev_name); + } else { + tmp = pa_sprintf_malloc("%s%s%s", dev_names, sep, dev_name); + pa_xfree(dev_names); + dev_names = tmp; + } + } + + return dev_names; +} + +PA_UNUSED static char *devset_description(pa_idxset *devices, const char *sep) { + int i = 0; + int num = pa_idxset_size(devices); + pa_alsa_ucm_device *sorted[num], *dev; + char *dev_descs = NULL; + char *tmp = NULL; + uint32_t idx; + + PA_IDXSET_FOREACH(dev, devices, idx) { + sorted[i] = dev; + i++; + } + + /* Sort by alphabetical order to match devset_name() */ + qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp); + + for (i = 0; i < num; i++) { + dev = sorted[i]; + const char *dev_desc = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + + if (!dev_descs) { + dev_descs = pa_xstrdup(dev_desc); + } else { + tmp = pa_sprintf_malloc("%s%s%s", dev_descs, sep, dev_desc); + pa_xfree(dev_descs); + dev_descs = tmp; + } + } + + return dev_descs; +} + +/* If invert is true, uses 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. */ +static unsigned devset_playback_priority(pa_idxset *devices, bool invert) { + pa_alsa_ucm_device *dev; + uint32_t idx; + double priority = 0; + + PA_IDXSET_FOREACH(dev, devices, idx) { + if (dev->playback_priority > 0 && invert) + priority += 1.0 / dev->playback_priority; + else + priority += dev->playback_priority; + } + + if (priority > 0 && invert) + return (unsigned)(1.0 / priority); + + return (unsigned) priority; +} + +static unsigned devset_capture_priority(pa_idxset *devices, bool invert) { + pa_alsa_ucm_device *dev; + uint32_t idx; + double priority = 0; + + PA_IDXSET_FOREACH(dev, devices, idx) { + if (dev->capture_priority > 0 && invert) + priority += 1.0 / dev->capture_priority; + else + priority += dev->capture_priority; + } + + if (priority > 0 && invert) + return (unsigned)(1.0 / priority); + + return (unsigned) priority; +} + +static void ucm_add_port_props( + pa_device_port *port, + bool is_sink) +{ + proplist_set_icon_name(port->proplist, port->type, is_sink); +} + +void pa_alsa_ucm_add_port( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_device_port *port; + unsigned priority; + char *name, *desc; + const char *dev_name; + const char *direction; + const char *verb_name; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_volume *vol; + pa_alsa_jack *jack; + pa_device_port_type_t type; + void *state; + + dev = context->ucm_device; + if (!dev) + return; + + 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 = pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION)); + priority = is_sink ? dev->playback_priority : dev->capture_priority; + jack = ucm_get_jack(context->ucm, dev); + type = dev->type; + + 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, dev); + 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); + ucm_add_port_props(port, is_sink); + } + + data = PA_DEVICE_PORT_DATA(port); + PA_HASHMAP_FOREACH_KV(verb_name, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) { + if (pa_hashmap_get(data->paths, verb_name)) + continue; + pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem, + is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT); + if (!path) + pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem); + else { + if (vol->master_elem) { + pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false); + e->switch_use = PA_ALSA_SWITCH_MUTE; + e->volume_use = PA_ALSA_VOLUME_MERGE; + } + + pa_hashmap_put(data->paths, pa_xstrdup(verb_name), path); + + /* Add path also to already created empty path set */ + if (is_sink) + pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + else + pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + } + } + + 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); + } + + /* ELD devices */ + set_eld_devices(ports); +} + +static bool devset_supports_device(pa_idxset *devices, pa_alsa_ucm_device *dev) { + const char *sink, *sink2, *source, *source2; + pa_alsa_ucm_device *d; + uint32_t idx; + + pa_assert(devices); + pa_assert(dev); + + /* Can add anything to empty group */ + if (pa_idxset_isempty(devices)) + return true; + + /* Device already selected */ + if (pa_idxset_contains(devices, dev)) + return true; + + /* No conflicting device must already be selected */ + if (!pa_idxset_isdisjoint(devices, dev->conflicting_devices)) + return false; + + /* No already selected device must be unsupported */ + if (!pa_idxset_isempty(dev->supported_devices)) + if (!pa_idxset_issubset(devices, dev->supported_devices)) + return false; + + sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE); + + PA_IDXSET_FOREACH(d, devices, idx) { + /* Must not be unsupported by any selected device */ + if (!pa_idxset_isempty(d->supported_devices)) + if (!pa_idxset_contains(d->supported_devices, dev)) + return false; + + /* PlaybackPCM must not be the same as any selected device, except when both split */ + sink2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK); + if (sink && sink2 && pa_streq(sink, sink2)) { + if (!(dev->playback_split && d->playback_split)) + return false; + } + + /* CapturePCM must not be the same as any selected device, except when both split */ + source2 = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (source && source2 && pa_streq(source, source2)) { + if (!(dev->capture_split && d->capture_split)) + return false; + } + } + + return true; +} + +/* Iterates nonempty subsets of UCM devices that can be simultaneously + * used, including subsets of previously returned subsets. At start, + * *state should be NULL. It's not safe to modify the devices argument + * until iteration ends. The returned idxsets must be freed by the + * caller. */ +static pa_idxset *iterate_device_subsets(pa_idxset *devices, void **state) { + uint32_t idx; + pa_alsa_ucm_device *dev; + + pa_assert(devices); + pa_assert(state); + + if (*state == NULL) { + /* First iteration, start adding from first device */ + *state = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + dev = pa_idxset_first(devices, &idx); + + } else { + /* Backtrack the most recent device we added and skip it */ + dev = pa_idxset_steal_last(*state, NULL); + pa_idxset_get_by_data(devices, dev, &idx); + if (dev) + dev = pa_idxset_next(devices, &idx); + } + + /* Try adding devices we haven't decided on yet */ + for (; dev; dev = pa_idxset_next(devices, &idx)) { + if (devset_supports_device(*state, dev)) + pa_idxset_put(*state, dev, NULL); + } + + if (pa_idxset_isempty(*state)) { + /* No more choices to backtrack on, therefore no more subsets to + * return after this. Don't return the empty set, instead clean + * up and end iteration. */ + pa_idxset_free(*state, NULL); + *state = NULL; + return NULL; + } + + return pa_idxset_copy(*state, NULL); +} + +/* This a wrapper around iterate_device_subsets() that only returns the + * biggest possible groups and not any of their subsets. */ +static pa_idxset *iterate_maximal_device_subsets(pa_idxset *devices, void **state) { + uint32_t idx; + pa_alsa_ucm_device *dev; + pa_idxset *subset = NULL; + + pa_assert(devices); + pa_assert(state); + + while (subset == NULL && (subset = iterate_device_subsets(devices, state))) { + /* Skip this group if it's incomplete, by checking if we can add any + * other device. If we can, this iteration is a subset of another + * group that we already returned or eventually return. */ + PA_IDXSET_FOREACH(dev, devices, idx) { + if (subset && !pa_idxset_contains(subset, dev) && devset_supports_device(subset, dev)) { + pa_idxset_free(subset, NULL); + subset = NULL; + } + } + } + + return subset; +} + +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( + 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) { + + 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_port(*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-verb paths from ports if probing them + * fails. The path for the current verb 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 (context->ucm->active_verb) { + const char *verb_name; + verb_name = pa_proplist_gets(context->ucm->active_verb->proplist, PA_ALSA_PROP_UCM_NAME); + update_mixer_paths(*p, verb_name); + } + + /* then set property PA_PROP_DEVICE_INTENDED_ROLES */ + merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES)); + + dev = context->ucm_device; + if (dev) { + const char *roles = pa_proplist_gets(dev->proplist, role_name); + tmp = merge_roles(merged_roles, roles); + pa_xfree(merged_roles); + merged_roles = tmp; + } + + mod = context->ucm_modifier; + if (mod) { + 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, pa_alsa_profile *new_profile, pa_alsa_profile *old_profile) { + int ret = 0; + const char *verb_name, *profile_name; + pa_alsa_ucm_verb *verb; + pa_alsa_mapping *map; + uint32_t idx; + + if (new_profile == old_profile) + return 0; + + if (new_profile == NULL) { + verb = NULL; + profile_name = SND_USE_CASE_VERB_INACTIVE; + verb_name = SND_USE_CASE_VERB_INACTIVE; + } else { + verb = new_profile->ucm_context.verb; + profile_name = new_profile->name; + verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + } + + pa_log_info("Set profile to %s", profile_name); + if (ucm->active_verb != verb) { + /* change verb */ + pa_log_info("Set UCM verb to %s", verb_name); + if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) { + pa_log("Failed to set verb %s", verb_name); + ret = -1; + } + + } else if (ucm->active_verb) { + /* Disable modifiers not in new profile. Has to be done before + * devices, because _dismod fails if a modifier's supported + * devices are disabled. */ + PA_IDXSET_FOREACH(map, old_profile->input_mappings, idx) + if (new_profile && !pa_idxset_contains(new_profile->input_mappings, map)) + if (map->ucm_context.ucm_modifier && ucm_modifier_disable(ucm, map->ucm_context.ucm_modifier) < 0) + ret = -1; + + PA_IDXSET_FOREACH(map, old_profile->output_mappings, idx) + if (new_profile && !pa_idxset_contains(new_profile->output_mappings, map)) + if (map->ucm_context.ucm_modifier && ucm_modifier_disable(ucm, map->ucm_context.ucm_modifier) < 0) + ret = -1; + + /* Disable devices not in new profile */ + PA_IDXSET_FOREACH(map, old_profile->input_mappings, idx) + if (new_profile && !pa_idxset_contains(new_profile->input_mappings, map)) + if (map->ucm_context.ucm_device && ucm_device_disable(ucm, map->ucm_context.ucm_device) < 0) + ret = -1; + + PA_IDXSET_FOREACH(map, old_profile->output_mappings, idx) + if (new_profile && !pa_idxset_contains(new_profile->output_mappings, map)) + if (map->ucm_context.ucm_device && ucm_device_disable(ucm, map->ucm_context.ucm_device) < 0) + ret = -1; + } + ucm->active_verb = verb; + + update_mixer_paths(card->ports, verb_name); + + return ret; +} + +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port) { + pa_alsa_ucm_config *ucm; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_port_data *data; + const char *dev_name, *ucm_dev_name; + + pa_assert(context && context->ucm); + + ucm = context->ucm; + pa_assert(ucm->ucm_mgr); + + data = PA_DEVICE_PORT_DATA(port); + dev = data->device; + pa_assert(dev); + + if (context->ucm_device) { + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + ucm_dev_name = pa_proplist_gets(context->ucm_device->proplist, PA_ALSA_PROP_UCM_NAME); + if (!pa_streq(dev_name, ucm_dev_name)) { + pa_log_error("Failed to set port %s with wrong UCM context: %s", dev_name, ucm_dev_name); + return -1; + } + } + + return ucm_device_enable(ucm, dev); +} + +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; + + m->ucm_context.ucm_device = device; + + 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; + + proplist_set_icon_name(m->proplist, device->type, is_sink); + + mdev = get_mixer_device(device, is_sink); + if (mdev) + pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev); +} + +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; + + m->ucm_context.ucm_modifier = modifier; + + 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 *ucm_name, bool is_sink) { + pa_alsa_mapping *m; + char *mapping_name; + + mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, ucm_name, 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 const struct { + enum snd_pcm_chmap_position pos; + pa_channel_position_t channel; +} chmap_info[] = { + [SND_CHMAP_MONO] = { SND_CHMAP_MONO, PA_CHANNEL_POSITION_MONO }, + [SND_CHMAP_FL] = { SND_CHMAP_FL, PA_CHANNEL_POSITION_FRONT_LEFT }, + [SND_CHMAP_FR] = { SND_CHMAP_FR, PA_CHANNEL_POSITION_FRONT_RIGHT }, + [SND_CHMAP_RL] = { SND_CHMAP_RL, PA_CHANNEL_POSITION_REAR_LEFT }, + [SND_CHMAP_RR] = { SND_CHMAP_RR, PA_CHANNEL_POSITION_REAR_RIGHT }, + [SND_CHMAP_FC] = { SND_CHMAP_FC, PA_CHANNEL_POSITION_FRONT_CENTER }, + [SND_CHMAP_LFE] = { SND_CHMAP_LFE, PA_CHANNEL_POSITION_LFE }, + [SND_CHMAP_SL] = { SND_CHMAP_SL, PA_CHANNEL_POSITION_SIDE_LEFT }, + [SND_CHMAP_SR] = { SND_CHMAP_SR, PA_CHANNEL_POSITION_SIDE_RIGHT }, + [SND_CHMAP_RC] = { SND_CHMAP_RC, PA_CHANNEL_POSITION_REAR_CENTER }, + [SND_CHMAP_FLC] = { SND_CHMAP_FLC, PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER }, + [SND_CHMAP_FRC] = { SND_CHMAP_FRC, PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER }, + /* XXX: missing channel positions, mapped to aux... */ + /* [SND_CHMAP_RLC] = { SND_CHMAP_RLC, PA_CHANNEL_POSITION_REAR_LEFT_OF_CENTER }, */ + /* [SND_CHMAP_RRC] = { SND_CHMAP_RRC, PA_CHANNEL_POSITION_REAR_RIGHT_OF_CENTER }, */ + /* [SND_CHMAP_FLW] = { SND_CHMAP_FLW, PA_CHANNEL_POSITION_FRONT_LEFT_WIDE }, */ + /* [SND_CHMAP_FRW] = { SND_CHMAP_FRW, PA_CHANNEL_POSITION_FRONT_RIGHT_WIDE }, */ + /* [SND_CHMAP_FLH] = { SND_CHMAP_FLH, PA_CHANNEL_POSITION_FRONT_LEFT_HIGH }, */ + /* [SND_CHMAP_FCH] = { SND_CHMAP_FCH, PA_CHANNEL_POSITION_FRONT_CENTER_HIGH }, */ + /* [SND_CHMAP_FRH] = { SND_CHMAP_FRH, PA_CHANNEL_POSITION_FRONT_RIGHT_HIGH }, */ + [SND_CHMAP_TC] = { SND_CHMAP_TC, PA_CHANNEL_POSITION_TOP_CENTER }, + [SND_CHMAP_TFL] = { SND_CHMAP_TFL, PA_CHANNEL_POSITION_TOP_FRONT_LEFT }, + [SND_CHMAP_TFR] = { SND_CHMAP_TFR, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT }, + [SND_CHMAP_TFC] = { SND_CHMAP_TFC, PA_CHANNEL_POSITION_TOP_FRONT_CENTER }, + [SND_CHMAP_TRL] = { SND_CHMAP_TRL, PA_CHANNEL_POSITION_TOP_REAR_LEFT }, + [SND_CHMAP_TRR] = { SND_CHMAP_TRR, PA_CHANNEL_POSITION_TOP_REAR_RIGHT }, + [SND_CHMAP_TRC] = { SND_CHMAP_TRC, PA_CHANNEL_POSITION_TOP_REAR_CENTER }, + /* [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, PA_CHANNEL_POSITION_TOP_FRONT_LEFT_OF_CENTER }, */ + /* [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, PA_CHANNEL_POSITION_TOP_FRONT_RIGHT_OF_CENTER }, */ + /* [SND_CHMAP_TSL] = { SND_CHMAP_TSL, PA_CHANNEL_POSITION_TOP_SIDE_LEFT }, */ + /* [SND_CHMAP_TSR] = { SND_CHMAP_TSR, PA_CHANNEL_POSITION_TOP_SIDE_RIGHT }, */ + /* [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, PA_CHANNEL_POSITION_LEFT_LFE }, */ + /* [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, PA_CHANNEL_POSITION_RIGHT_LFE }, */ + /* [SND_CHMAP_BC] = { SND_CHMAP_BC, PA_CHANNEL_POSITION_BOTTOM_CENTER }, */ + /* [SND_CHMAP_BLC] = { SND_CHMAP_BLC, PA_CHANNEL_POSITION_BOTTOM_LEFT_OF_CENTER }, */ + /* [SND_CHMAP_BRC] = { SND_CHMAP_BRC, PA_CHANNEL_POSITION_BOTTOM_RIGHT_OF_CENTER }, */ +}; + +static void ucm_split_to_channel_map(pa_channel_map *m, const pa_alsa_ucm_split *s) +{ + const int n = sizeof(chmap_info) / sizeof(chmap_info[0]); + int i; + int aux = 0; + + for (i = 0; i < s->channels; ++i) { + int p = s->pos[i]; + + if (p >= 0 && p < n && (int)chmap_info[p].pos == p) + m->map[i] = chmap_info[p].channel; + else + m->map[i] = PA_CHANNEL_POSITION_AUX0 + aux++; + + if (aux >= 32) + break; + } + + m->channels = i; +} + +static int ucm_create_mapping_direction( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + 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_name, 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_device) { /* new mapping */ + 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; + + 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); + + if (is_sink && device->playback_split) { + m->split = pa_xmemdup(device->playback_split, sizeof(*m->split)); + ucm_split_to_channel_map(&m->channel_map, m->split); + } else if (!is_sink && device->capture_split) { + m->split = pa_xmemdup(device->capture_split, sizeof(*m->split)); + ucm_split_to_channel_map(&m->channel_map, m->split); + } + + alsa_mapping_add_ucm_device(m, device); + + return 0; +} + +static int ucm_create_mapping_for_modifier( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + 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, mod_name, is_sink); + + if (!m) + return -1; + + pa_log_info("UCM mapping: %s modifier %s", m->name, mod_name); + + if (!m->ucm_context.ucm_device && !m->ucm_context.ucm_modifier) { /* new mapping */ + 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; + } + + 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_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, device, verb_name, device_name, sink, true); + if (ret == 0 && source) + ret = ucm_create_mapping_direction(ucm, ps, 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, + pa_idxset *mappings, + const char *profile_name, + const char *profile_desc, + unsigned int profile_priority) { + + pa_alsa_profile *p; + pa_alsa_mapping *map; + uint32_t idx; + + pa_assert(ps); + + if (pa_hashmap_get(ps->profiles, profile_name)) { + pa_log("Profile %s already exists", profile_name); + return -1; + } + + p = pa_xnew0(pa_alsa_profile, 1); + p->profile_set = ps; + p->name = pa_xstrdup(profile_name); + p->description = pa_xstrdup(profile_desc); + p->priority = profile_priority; + p->ucm_context.verb = verb; + + 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); + + PA_IDXSET_FOREACH(map, mappings, idx) + ucm_add_mapping(p, map); + + pa_alsa_profile_dump(p); + + return 0; +} + +static int ucm_create_verb_profiles( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_ucm_verb *verb, + const char *verb_name, + const char *verb_desc) { + + pa_idxset *verb_devices, *p_devices, *p_mappings; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + int i = 0; + int n_profiles = 0; + const char *name, *sink, *source; + char *p_name, *p_desc, *tmp; + unsigned int verb_priority, p_priority; + uint32_t idx; + void *state = NULL; + + /* TODO: get profile priority from policy management */ + verb_priority = verb->priority; + + if (verb_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) { + verb_priority = verb_info[i].priority; + break; + } + } + pa_xfree(verb_cmp); + } + + 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, 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, mod, verb_name, name, sink, true); + else if (source) + ucm_create_mapping_for_modifier(ucm, ps, mod, verb_name, name, source, false); + } + + verb_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + PA_LLIST_FOREACH(dev, verb->devices) + pa_idxset_put(verb_devices, dev, NULL); + + while ((p_devices = iterate_maximal_device_subsets(verb_devices, &state))) { + p_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + /* Add the mappings that include our selected devices */ + PA_IDXSET_FOREACH(dev, p_devices, idx) { + if (dev->playback_mapping) + pa_idxset_put(p_mappings, dev->playback_mapping, NULL); + if (dev->capture_mapping) + pa_idxset_put(p_mappings, dev->capture_mapping, NULL); + } + + /* Add mappings only for the modifiers that can work with our + * device selection */ + PA_LLIST_FOREACH(mod, verb->modifiers) + if (pa_idxset_isempty(mod->supported_devices) || pa_idxset_issubset(mod->supported_devices, p_devices)) + if (pa_idxset_isdisjoint(mod->conflicting_devices, p_devices)) { + if (mod->playback_mapping) + pa_idxset_put(p_mappings, mod->playback_mapping, NULL); + if (mod->capture_mapping) + pa_idxset_put(p_mappings, mod->capture_mapping, NULL); + } + + /* If we'll have multiple profiles for this verb, their names + * must be unique. Use a list of chosen devices to disambiguate + * them. If the profile contains all devices of a verb, we'll + * generate only onle profile whose name should be the verb + * name. GUIs usually show the profile description instead of + * the name, add the device names to those as well. */ + tmp = devset_name(p_devices, ", "); + if (pa_idxset_equals(p_devices, verb_devices)) { + p_name = pa_xstrdup(verb_name); + p_desc = pa_xstrdup(verb_desc); + } else { + p_name = pa_sprintf_malloc("%s (%s)", verb_name, tmp); + p_desc = pa_sprintf_malloc("%s (%s)", verb_desc, tmp); + } + + /* Make sure profiles with higher-priority devices are + * prioritized. */ + p_priority = verb_priority + devset_playback_priority(p_devices, false) + devset_capture_priority(p_devices, false); + + if (ucm_create_profile(ucm, ps, verb, p_mappings, p_name, p_desc, p_priority) == 0) { + pa_log_debug("Created profile %s for UCM verb %s", p_name, verb_name); + n_profiles++; + } + + pa_xfree(tmp); + pa_xfree(p_name); + pa_xfree(p_desc); + pa_idxset_free(p_mappings, NULL); + pa_idxset_free(p_devices, NULL); + } + + pa_idxset_free(verb_devices, NULL); + + if (n_profiles == 0) { + pa_log("UCM verb %s created no profiles", verb_name); + return -1; + } + + 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; + 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; + + dev = context->ucm_device; + mdev = pa_sprintf_malloc("%shw:%i", alib_prefix ? alib_prefix : "", pcm_card); + if (mdev == NULL) + return; + 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, bool max_channels) { + 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 (!m->split) { + if (max_channels) { + errno = EINVAL; + return NULL; + } + + if (exact_channels) { + try_map = m->channel_map; + try_ss.channels = try_map.channels; + } else + pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA); + } else { + if (!m->split->leader) { + errno = EINVAL; + return NULL; + } + + exact_channels = false; + try_ss.channels = max_channels ? PA_CHANNELS_MAX : m->split->hw_channels; + pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX); + } + + try_period_size = + pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) / + 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, NULL, NULL, exact_channels); + + if (pcm) { + if (m->split) { + const char *mode_name = mode == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture"; + + if (try_map.channels < m->split->hw_channels) { + pa_logl((max_channels ? PA_LOG_NOTICE : PA_LOG_DEBUG), + "Error in ALSA UCM profile for %s (%s): %sChannels=%d > avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); + + /* Retry with max channel count, in case ALSA rounded down */ + if (!max_channels) { + pa_alsa_close(&pcm); + return mapping_open_pcm(ucm, m, mode, true); + } + + /* Just accept whatever we got... Some of the routings won't get connected + * anywhere */ + m->split->hw_channels = try_map.channels; + m->split->broken = true; + } else if (try_map.channels > m->split->hw_channels) { + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannels=%d < avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); + m->split->hw_channels = try_map.channels; + m->split->broken = true; + } + } else if (!exact_channels) { + m->channel_map = try_map; + } + mapping_init_eld(m, pcm); + } + + return pcm; +} + +static void pa_alsa_init_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction) +{ + pa_proplist *props = pa_proplist_new(); + uint32_t idx; + pa_alsa_mapping *m; + + if (direction == PA_DIRECTION_OUTPUT) + pa_alsa_init_proplist_pcm(NULL, props, leader->output_pcm); + else + pa_alsa_init_proplist_pcm(NULL, props, leader->input_pcm); + + PA_IDXSET_FOREACH(m, mappings, idx) { + if (!m->split) + continue; + if (!pa_streq(m->device_strings[0], leader->device_strings[0])) + continue; + + if (direction == PA_DIRECTION_OUTPUT) + pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props); + else + pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props); + + /* Update HW channel count to match probed one */ + m->split->hw_channels = leader->split->hw_channels; + } + + pa_proplist_free(props); +} + +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; + + if (!m->split) + pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); + else + pa_alsa_init_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT); + + pa_alsa_close(&m->output_pcm); + } + + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (p->supported) + m->supported++; + + if (!m->input_pcm) + continue; + + if (!m->split) + pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); + else + pa_alsa_init_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT); + + 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; + bool has_control; + + dev = context->ucm_device; + if (!dev->jack || !dev->jack->mixer_device_name) + return; + + 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); + return; + } + + 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; + const char *verb_name; + uint32_t idx; + + PA_HASHMAP_FOREACH(p, ps->profiles, state) { + pa_log_info("Probing profile %s", p->name); + + /* change verb */ + verb_name = pa_proplist_gets(p->ucm_context.verb->proplist, PA_ALSA_PROP_UCM_NAME); + pa_log_info("Set ucm verb to %s", verb_name); + + if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) { + pa_log("Profile '%s': failed to set verb %s", p->name, verb_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; + } + + if (m->split && !m->split->leader) + continue; + + m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK, false); + if (!m->output_pcm) { + pa_log_info("Profile '%s' mapping '%s': output PCM open failed", + p->name, m->name); + 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; + } + + if (m->split && !m->split->leader) + continue; + + m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE, false); + if (!m->input_pcm) { + pa_log_info("Profile '%s' mapping '%s': input PCM open failed", + p->name, m->name); + p->supported = false; + break; + } + } + } + + if (!p->supported) { + profile_finalize_probing(p); + pa_log_info("Profile %s not supported", p->name); + 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 profiles 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_verb_profiles(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); + + pa_idxset_free(di->conflicting_devices, NULL); + pa_idxset_free(di->supported_devices, NULL); + + pa_xfree(di->eld_mixer_device_name); + + pa_xfree(di->playback_split); + pa_xfree(di->capture_split); + + 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); + pa_idxset_free(mi->conflicting_devices, NULL); + pa_idxset_free(mi->supported_devices, NULL); + 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; + + dev = context->ucm_device; + if (dev) { + /* clear ucm device pointer to mapping */ + if (context->direction == PA_DIRECTION_OUTPUT) + dev->playback_mapping = NULL; + else + dev->capture_mapping = NULL; + } + + mod = context->ucm_modifier; + if (mod) { + if (context->direction == PA_DIRECTION_OUTPUT) + mod->playback_mapping = NULL; + else + mod->capture_mapping = 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) { + ucm_modifier_enable(ucm, mod); + } + + 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) + ucm_modifier_disable(ucm, mod); + + break; + } + } +} + +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) + pa_device_port_set_available(port->core_port, port->device->available); +} + +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 *device) { + pa_assert(ucm); + pa_assert(core_port); + pa_assert(device); + + port->ucm = ucm; + port->core_port = core_port; + port->eld_device = -1; + + port->device = device; + pa_dynarray_append(device->ucm_ports, port); + + port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, NULL); + + pa_device_port_set_available(port->core_port, port->device->available); +} + +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->paths) + pa_hashmap_free(ucm_port->paths); + + pa_xfree(ucm_port->eld_mixer_device_name); +} + +long pa_alsa_ucm_port_device_status(pa_alsa_ucm_port_data *data) { + return ucm_device_status(data->ucm, data->device); +} + +#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, pa_alsa_profile *new_profile, pa_alsa_profile *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_port( + 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) { + 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) { +} + +long pa_alsa_ucm_port_device_status(pa_alsa_ucm_port_data *data) { + return -1; +} + +#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..8e4c1a7 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-ucm.h @@ -0,0 +1,318 @@ +#ifndef fooalsaucmhfoo +#define fooalsaucmhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2011 Wolfson Microelectronics PLC + Author Margarita Olaya + Copyright 2012 Feng Wei , 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 . +***/ + +#ifdef HAVE_ALSA_UCM +#include +#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_profile_context pa_alsa_ucm_profile_context; +typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data; +typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume; +typedef struct pa_alsa_ucm_split pa_alsa_ucm_split; + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index); +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map); +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, pa_alsa_profile *new_profile, pa_alsa_profile *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_port( + 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); + +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_split { + /* UCM SplitPCM channel remapping */ + bool leader; + int hw_channels; + int channels; + int idx[PA_CHANNELS_MAX]; + enum snd_pcm_chmap_position pos[PA_CHANNELS_MAX]; + bool broken; +}; + +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; + + pa_alsa_ucm_split *playback_split; + pa_alsa_ucm_split *capture_split; +}; + +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; + + pa_idxset *conflicting_devices; + pa_idxset *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; + bool split_enable; + + 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_alsa_ucm_device *ucm_device; + pa_alsa_ucm_modifier *ucm_modifier; +}; + +struct pa_alsa_ucm_profile_context { + pa_alsa_ucm_verb *verb; +}; + +struct pa_alsa_ucm_port_data { + pa_alsa_ucm_config *ucm; + pa_device_port *core_port; + + pa_alsa_ucm_device *device; + + /* verb name -> pa_alsa_path for volume control */ + pa_hashmap *paths; + /* Current path, set when activating verb */ + pa_alsa_path *path; + + /* ELD info */ + char *eld_mixer_device_name; + int eld_device; /* PCM device number */ +}; + +long pa_alsa_ucm_port_device_status(pa_alsa_ucm_port_data *data); + +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..96d6020 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-util.c @@ -0,0 +1,2065 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + +#include "config.h" + +#include +#include + +#include "alsa-util.h" +#include "alsa-mixer.h" + +#include + +#ifdef HAVE_UDEV +#include +#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_sample_format_t **query_supported_formats, + unsigned int **query_supported_rates, + 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, + query_supported_formats, + query_supported_rates, + 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, + query_supported_formats, + query_supported_rates, + 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, + query_supported_formats, + query_supported_rates, + 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_sample_format_t **query_supported_formats, + unsigned int **query_supported_rates, + 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, + query_supported_formats, + query_supported_rates, + 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, + pa_sample_format_t **query_supported_formats, + unsigned int **query_supported_rates, + 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 (query_supported_formats) + *query_supported_formats = pa_alsa_get_supported_formats(pcm_handle, ss->format); + + if (query_supported_rates) + *query_supported_rates = pa_alsa_get_supported_rates(pcm_handle, ss->rate); + + 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, + pa_sample_format_t **query_supported_formats, + unsigned int **query_supported_rates, + 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, + query_supported_formats, + query_supported_rates, + 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,0) void alsa_local_handler(const char *file, int line, const char *function, int err, const char *fmt, va_list arg) { + pa_log_levelv_meta(PA_LOG_INFO, file, line, function, fmt, arg); +} + +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))) */; +typedef void (*snd_lib2_local_handler_t)(const char *file, int line, const char *function, int err, const char *fmt, va_list args) PA_PRINTF_FUNC(5,0) /* __attribute__ ((format (printf, 5, 0))) */; + +extern int snd_lib_error_set_handler(snd_lib2_error_handler_t handler); +extern snd_local_error_handler_t snd_lib_error_set_local(snd_lib2_local_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); + snd_lib_error_set_local(alsa_local_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_lib_error_set_local(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, name[64]; + + 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); + } + + snprintf(name, sizeof(name), "hw:%d", card); + pa_alsa_init_proplist_ctl(p, name); + +#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; + snd_pcm_sync_id_t sync_id; + + pa_assert(p); + pa_assert(pcm_info); + + if ((card = snd_pcm_info_get_card(pcm_info)) >= 0) + pa_alsa_init_proplist_card(c, p, card); + + 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)); + + sync_id = snd_pcm_info_get_sync(pcm_info); + pa_proplist_setf(p, "alsa.sync.id", "%08x:%08x:%08x:%08x", + sync_id.id32[0], sync_id.id32[1], sync_id.id32[2], sync_id.id32[3]); +} + +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); +} + +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); + + if ((t = snd_ctl_card_info_get_id(info)) && *t) + pa_proplist_sets(p, "alsa.id", t); + + snd_ctl_close(ctl); +} + +#if 0 +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); +} +#endif + +static void dump_supported_rates(unsigned int* values) +{ + pa_strbuf *buf; + char *str; + int i; + + buf = pa_strbuf_new(); + + for (i = 0; values[i]; i++) { + pa_strbuf_printf(buf, " %u", values[i]); + } + + str = pa_strbuf_to_string_free(buf); + pa_log_debug("Supported rates:%s", str); + pa_xfree(str); +} + +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, + 352800, 384000, + 705600, 768000 }; + 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; + } + + dump_supported_rates(rates); + 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_pcm[] = { + [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 all_formats[] = { + PA_SAMPLE_U8, + PA_SAMPLE_ALAW, + PA_SAMPLE_ULAW, + PA_SAMPLE_S16LE, + PA_SAMPLE_S16BE, + PA_SAMPLE_FLOAT32LE, + PA_SAMPLE_FLOAT32BE, + PA_SAMPLE_S32LE, + PA_SAMPLE_S32BE, + PA_SAMPLE_S24LE, + PA_SAMPLE_S24BE, + PA_SAMPLE_S24_32LE, + PA_SAMPLE_S24_32BE, + }; + 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, format_trans_to_pcm[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++] = 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_pcm[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; +} + +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, + unsigned int subdevice) { + snd_mixer_elem_t *elem; + + for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) { + snd_hctl_elem_t **_helem, *helem; + if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_PULSEAUDIO) + continue; + _helem = snd_mixer_elem_get_private(elem); + helem = *_helem; + 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; + if (snd_hctl_elem_get_subdevice(helem) != subdevice) + 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, 0); +} + +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, 0); +} + +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 void mixer_melem_free(snd_mixer_elem_t *elem) +{ + snd_hctl_elem_t **_helem; + _helem = snd_mixer_elem_get_private(elem); + pa_xfree(_helem); +} + +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); + snd_hctl_elem_t **_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 + // assertion in alsa-lib since the list is not empty. + _helem = snd_mixer_elem_get_private(melem); + *_helem = NULL; + 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_t *mixer = snd_mixer_class_get_mixer(class); + snd_ctl_elem_iface_t iface = snd_hctl_elem_get_interface(helem); + const char *name = snd_hctl_elem_get_name(helem); + const int index = snd_hctl_elem_get_index(helem); + const int device = snd_hctl_elem_get_device(helem); + const int subdevice = snd_hctl_elem_get_subdevice(helem); + snd_mixer_elem_t *new_melem; + + bool found = true; + + new_melem = pa_alsa_mixer_find(mixer, iface, name, index, device, subdevice); + if (!new_melem) { + _helem = pa_xmalloc(sizeof(snd_hctl_elem_t *)); + *_helem = helem; + /* 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, mixer_melem_free)) < 0) { + pa_log_warn("snd_mixer_elem_new failed: %s", pa_alsa_strerror(err)); + return 0; + } + found = false; + } else { + _helem = snd_mixer_elem_get_private(new_melem); + if (_helem) { + char *s1, *s2; + snd_ctl_elem_id_t *id1, *id2; + snd_ctl_elem_id_alloca(&id1); + snd_ctl_elem_id_alloca(&id2); + snd_hctl_elem_get_id(helem, id1); + snd_hctl_elem_get_id(*_helem, id2); + s1 = snd_ctl_ascii_elem_id_get(id1); + s2 = snd_ctl_ascii_elem_id_get(id2); + pa_log_warn("mixer_class_event - duplicate mixer controls: %s | %s", s1, s2); + free(s2); + free(s1); + return 0; + } + *_helem = helem; + } + + 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 (!found) { + 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, sad_count; + 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); + + /* Fetch Short Audio Descriptors */ + sad_count = (elddata[5] & 0xf0) >> 4; + pa_log_debug("SAD count in ELD info is %u (for device=%d)", sad_count, device); + if (20 + mnl + 3 * sad_count > eldsize) { + pa_log_debug("Invalid SAD count (%u) in ELD info (for device=%d)", sad_count, device); + sad_count = 0; + } + + eld->iec958_codecs = 0; + for (unsigned i = 0; i < sad_count; i++) { + uint8_t *sad = &elddata[20 + mnl + 3 * i]; + + /* https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Audio_Data_Blocks */ + switch ((sad[0] & 0x78) >> 3) { + case 1: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM; + break; + case 2: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_AC3; + break; + case 3: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG; + break; + case 4: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG; + break; + case 5: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG; + break; + case 6: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_MPEG2_AAC; + break; + case 7: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTS; + break; + case 10: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_EAC3; + break; + case 11: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_DTSHD; + break; + case 12: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_TRUEHD; + break; + default: + eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_UNKNOWN; + break; + } + } + return 0; +} diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h new file mode 100644 index 0000000..26c2698 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-util.h @@ -0,0 +1,183 @@ +#ifndef fooalsautilhfoo +#define fooalsautilhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + +#include + +#include + +#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_sample_format_t **query_supported_formats, /* modified at return */ + unsigned int **query_supported_rates, /* 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_sample_format_t **query_supported_formats, /* modified at return */ + unsigned int **query_supported_rates, /* 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 */ + pa_sample_format_t **query_supported_formats, /* modified at return */ + unsigned int **query_supported_rates, /* 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 */ + pa_sample_format_t **query_supported_formats, /* modified at return */ + unsigned int **query_supported_rates, /* 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); +void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name); +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]; + uint64_t iec958_codecs; +}; + +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..84e9f65 --- /dev/null +++ b/spa/plugins/alsa/acp/array.h @@ -0,0 +1,130 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PA_ARRAY_H +#define PA_ARRAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +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..c1126fe --- /dev/null +++ b/spa/plugins/alsa/acp/card.h @@ -0,0 +1,77 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + + +#ifndef PULSE_CARD_H +#define PULSE_CARD_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#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 disable_mixer_path; + bool auto_profile; + bool auto_port; + bool ignore_dB; + uint32_t rate; + uint32_t pro_channels; + + 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 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 . +***/ + +#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..e2f317b --- /dev/null +++ b/spa/plugins/alsa/acp/compat.c @@ -0,0 +1,287 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + +#include +#include + +#include "compat.h" +#include "device-port.h" +#include "alsa-mixer.h" +#include "config.h" + +static const char *port_types[] = { + [PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown", + [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; +} + +static char *try_path(const char *fname, const char *path) +{ + char *result = pa_maybe_prefix_path(fname, path); + + pa_log_trace("Check for file: %s", result); + + if (access(result, R_OK) == 0) + return result; + + pa_xfree(result); + return NULL; +} + +static char *get_xdg_home(const char *key, const char *fallback) +{ + const char *e; + + e = getenv(key); + if (e && *e) { + return strdup(e); + } else { + e = getenv("HOME"); + if (!(e && *e)) + e = getenv("USERPROFILE"); + if (e && *e) + return spa_aprintf("%s/%s", e, fallback); + } + return NULL; +} + +char *get_data_path(const char *data_dir, const char *data_type, const char *fname) +{ + static const char * const subpaths[] = { + "alsa-card-profile/mixer", + "alsa-card-profile", + }; + const char *e; + spa_autofree char *base = NULL; + char *result; + + if (data_dir) + if ((result = try_path(fname, data_dir)) != NULL) + return result; + + e = getenv("ACP_PATHS_DIR"); + if (e && *e && spa_streq(data_type, "paths")) + if ((result = try_path(fname, e)) != NULL) + return result; + + e = getenv("ACP_PROFILES_DIR"); + if (e && *e && spa_streq(data_type, "profile-sets")) + if ((result = try_path(fname, e)) != NULL) + return result; + + base = get_xdg_home("XDG_CONFIG_HOME", ".config"); + if (base) { + SPA_FOR_EACH_ELEMENT_VAR(subpaths, subpath) { + spa_autofree char *path = spa_aprintf("%s/%s/%s", base, *subpath, data_type); + if ((result = try_path(fname, path)) != NULL) + return result; + } + } + + SPA_FOR_EACH_ELEMENT_VAR(subpaths, subpath) { + spa_autofree char *path = spa_aprintf("/etc/%s/%s", *subpath, data_type); + if ((result = try_path(fname, path)) != NULL) + return result; + } + + spa_autofree char *path = spa_aprintf("%s/%s", PA_ALSA_DATA_DIR, data_type); + return pa_maybe_prefix_path(fname, path); +} diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h new file mode 100644 index 0000000..7660e1c --- /dev/null +++ b/spa/plugins/alsa/acp/compat.h @@ -0,0 +1,718 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 . +***/ + + +#ifndef PULSE_COMPAT_H +#define PULSE_COMPAT_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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))) +#define PA_UNUSED __attribute__ ((unused)) +#else +#define PA_LIKELY(x) (x) +#define PA_UNLIKELY(x) (x) +#define PA_PRINTF_FUNC(fmt, arg1) +#define PA_UNUSED +#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*16U) + +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_TRACE = 5, + 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_trace(fmt,...) pa_logl(PA_LOG_TRACE, 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); +} + +typedef struct { + size_t size; + char *ptr; + FILE *f; +} pa_strbuf; + +static inline pa_strbuf *pa_strbuf_new(void) +{ + pa_strbuf *s = pa_xnew0(pa_strbuf,1); + s->f = open_memstream(&s->ptr, &s->size); + return s; +} + +static PA_PRINTF_FUNC(2,3) inline size_t pa_strbuf_printf(pa_strbuf *sb, const char *format, ...) +{ + int ret; + va_list args; + va_start(args, format); + ret = vfprintf(sb->f, format, args); + va_end(args); + return ret > 0 ? ret : 0; +} + +static inline void pa_strbuf_puts(pa_strbuf *sb, const char *t) +{ + fputs(t, sb->f); +} + +static inline bool pa_strbuf_isempty(pa_strbuf *sb) +{ + fflush(sb->f); + return sb->size == 0; +} + +static inline char *pa_strbuf_to_string_free(pa_strbuf *sb) +{ + char *ptr; + fclose(sb->f); + ptr = sb->ptr; + free(sb); + return ptr; +} + +#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; +} + +static PA_PRINTF_FUNC(1,0) inline char *pa_vsprintf_malloc(const char *fmt, va_list args) +{ + char *res; + if (vasprintf(&res, fmt, args) < 0) + res = NULL; + return res; +} + +#define pa_fopen_cloexec(f,m) fopen(f,m"e") + +static inline char *pa_path_get_filename(const char *p) +{ + 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 (c == NULL) + return NULL; + + 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); + + if (l >= (size_t)(INT_MAX / 2)) + return NULL; + l *= 2; + } +#else + return NULL; +#endif +} + +char *get_data_path(const char *data_dir, const char *data_type, const char *fname); + +#include + +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 . +***/ + +#include "config.h" + +#include +#include +#include +#include + +#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 . +***/ + +#include +#include + +#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 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 . +***/ + + +#ifndef PULSE_DEVICE_PORT_H +#define PULSE_DEVICE_PORT_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#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..eb79618 --- /dev/null +++ b/spa/plugins/alsa/acp/dynarray.h @@ -0,0 +1,139 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..02a4950 --- /dev/null +++ b/spa/plugins/alsa/acp/hashmap.h @@ -0,0 +1,199 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..2638133 --- /dev/null +++ b/spa/plugins/alsa/acp/idxset.h @@ -0,0 +1,262 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 == NULL) { + if (ptr == NULL) + return item; + else + continue; + } + if (s->compare_func(item->ptr, ptr) == 0) + 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 pa_idxset_item *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; + } + *idx = PA_IDXSET_INVALID; + return NULL; +} + +static inline pa_idxset_item *pa_idxset_reverse_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; + } + *idx = PA_IDXSET_INVALID; + return NULL; +} + +static inline void *pa_idxset_next(pa_idxset *s, uint32_t *idx) +{ + pa_idxset_item *item; + (*idx)++;; + item = pa_idxset_search(s, idx); + return item ? item->ptr : NULL; +} + +static inline void* pa_idxset_first(pa_idxset *s, uint32_t *idx) +{ + uint32_t i = 0; + pa_idxset_item *item = pa_idxset_search(s, &i); + if (idx) + *idx = i; + return item ? item->ptr : NULL; +} + +static inline void* pa_idxset_last(pa_idxset *s, uint32_t *idx) +{ + uint32_t i = pa_array_get_len(&s->array, pa_idxset_item) - 1; + pa_idxset_item *item = pa_idxset_reverse_search(s, &i); + if (idx) + *idx = i; + return item ? item->ptr : NULL; +} + +static inline void* pa_idxset_steal_last(pa_idxset *s, uint32_t *idx) +{ + uint32_t i = pa_array_get_len(&s->array, pa_idxset_item) - 1; + void *ptr = NULL; + pa_idxset_item *item = pa_idxset_reverse_search(s, &i); + if (idx) + *idx = i; + if (item) { + ptr = item->ptr; + item->ptr = NULL; + pa_array_remove(&s->array, item); + } + 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) { + if (idx) + *idx = PA_IDXSET_INVALID; + return NULL; + } + if (idx) + *idx = item - (pa_idxset_item*)s->array.data; + return item->ptr; +} + +static inline bool pa_idxset_contains(pa_idxset *s, const void *p) +{ + return pa_idxset_get_by_data(s, p, NULL) == p; +} + +static inline bool pa_idxset_isdisjoint(pa_idxset *s, pa_idxset *t) +{ + pa_idxset_item *item; + pa_array_for_each(item, &s->array) { + if (item->ptr && pa_idxset_contains(t, item->ptr)) + return false; + } + return true; +} + +static inline bool pa_idxset_issubset(pa_idxset *s, pa_idxset *t) +{ + pa_idxset_item *item; + pa_array_for_each(item, &s->array) { + if (item->ptr && !pa_idxset_contains(t, item->ptr)) + return false; + } + return true; +} + +static inline bool pa_idxset_issuperset(pa_idxset *s, pa_idxset *t) +{ + return pa_idxset_issubset(t, s); +} + +static inline bool pa_idxset_equals(pa_idxset *s, pa_idxset *t) +{ + return pa_idxset_issubset(s, t) && pa_idxset_issuperset(s, t); +} + +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 . +***/ + +/* 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..a647f52 --- /dev/null +++ b/spa/plugins/alsa/acp/proplist.h @@ -0,0 +1,191 @@ +/* ALSA Card Profile */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PA_PROPLIST_H +#define PA_PROPLIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#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..de786b7 --- /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 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 . +***/ + +#ifndef PA_VOLUME_H +#define PA_VOLUME_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +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 (dB == -INFINITY || 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..5d46fee --- /dev/null +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -0,0 +1,1231 @@ +/* Spa ALSA Device */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 replace_string(const char *str, const char *val, const char *rep, + char *buf, size_t size) +{ + struct spa_strbuf s; + const char *p; + size_t len = strlen(val); + + spa_assert(len > 0); + spa_strbuf_init(&s, buf, size); + + while (1) { + p = strstr(str, val); + if (!p) + break; + + spa_strbuf_append(&s, "%.*s%s", (int)SPA_PTRDIFF(p, str), str, rep); + str = p + len; + } + + spa_strbuf_append(&s, "%s", str); + 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[210], channels[16], ch[12], routes[16]; + char card_index[16], card_name[64], *p; + char positions[SPA_AUDIO_MAX_CHANNELS * 12]; + char codecs[512]; + struct spa_device_object_info info; + struct acp_card *card = this->card; + const char *stream, *card_id; + + 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 + 11) * sizeof(*items)); + n_items = 0; + + snprintf(card_index, sizeof(card_index), "%d", card->index); + card_id = acp_dict_lookup(&card->props, "alsa.id"); + snprintf(card_name, sizeof(card_name), "%s", card_id ? card_id : card_index); + + replace_string(dev->device_strings[0], "%f", card_index, device_name, sizeof(device_name)); + + snprintf(path, sizeof(path), "alsa:acp:%s:%d:%s", card_name, dev->index, 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); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, stream); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ICON_NAME, "audio-card-analog"); + + snprintf(channels, sizeof(channels), "%d", dev->format.channels); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels); + + 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); + + if (dev->n_codecs > 0) { + acp_iec958_codecs_to_json(dev->codecs, dev->n_codecs, codecs, sizeof(codecs)); + items[n_items++] = SPA_DICT_ITEM_INIT("iec958.codecs", codecs); + } + + snprintf(routes, sizeof(routes), "%d", dev->n_ports); + items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes); + + 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; + const char *card_id; + + 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)); + + card_id = acp_dict_lookup(&card->props, "alsa.id"); + + n_items = 0; +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + if (card_id) + snprintf(path, sizeof(path), "alsa:acp:%s", card_id); + else + snprintf(path, sizeof(path), "alsa:acp:%d", card->index); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:acp"); + 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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + card = this->card; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_EnumProfile: + if (result.index >= card->n_profiles) + return 0; + + pr = card->profiles[result.index]; + if (SPA_FLAG_IS_SET(pr->flags, ACP_PROFILE_HIDDEN)) + goto next; + param = build_profile(&b.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]; + if (SPA_FLAG_IS_SET(pr->flags, ACP_PROFILE_HIDDEN)) + goto next; + param = build_profile(&b.b, id, pr, true); + break; + + case SPA_PARAM_EnumRoute: + if (result.index >= card->n_ports) + return 0; + + p = card->ports[result.index]; + if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_HIDDEN)) + goto next; + param = build_route(&b.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_HIDDEN)) + goto next; + 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; + if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_HIDDEN)) + goto next; + param = build_route(&b.b, id, p, dev, card->active_profile_index); + if (param == NULL) + return -errno; + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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 uint32_t find_profile_by_name(struct acp_card *card, const char *name) +{ + uint32_t i; + for (i = 0; i < card->n_profiles; i++) { + if (spa_streq(card->profiles[i]->name, name)) + return i; + } + return SPA_ID_INVALID; +} + +static uint32_t find_route_by_name(struct acp_card *card, const char *name) +{ + uint32_t i; + for (i = 0; i < card->n_ports; i++) { + if (spa_streq(card->ports[i]->name, name)) + return i; + } + return SPA_ID_INVALID; +} + +static bool check_active_profile_port(struct impl *this, uint32_t device, uint32_t port_index) +{ + struct acp_port *p; + uint32_t i; + + if (port_index >= this->card->n_ports) + return false; + p = this->card->ports[port_index]; + + /* Port must be in active profile */ + for (i = 0; i < p->n_profiles; i++) + if (p->profiles[i]->index == this->card->active_profile_index) + break; + if (i == p->n_profiles) + return false; + + /* Port must correspond to the device */ + for (i = 0; i< p->n_devices; i++) + if (p->devices[i]->index == device) + break; + if (i == p->n_devices) + return false; + + return true; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Profile: + { + uint32_t idx = SPA_ID_INVALID; + const char *name = NULL; + 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_OPT_Int(&idx), + SPA_PARAM_PROFILE_name, SPA_POD_OPT_String(&name), + 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; + } + if (idx == SPA_ID_INVALID && name == NULL) { + spa_log_warn(this->log, "profile needs name or index"); + return -EINVAL; + } + if (idx == SPA_ID_INVALID) + idx = find_profile_by_name(this->card, name); + if (idx == SPA_ID_INVALID) { + spa_log_warn(this->log, "unknown profile %s", name); + return -EINVAL; + } + acp_card_set_profile(this->card, idx, save ? ACP_PROFILE_SAVE : 0); + emit_info(this, false); + break; + } + case SPA_PARAM_Route: + { + uint32_t idx = SPA_ID_INVALID, device; + const char *name = NULL; + 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_OPT_Int(&idx), + SPA_PARAM_ROUTE_name, SPA_POD_OPT_String(&name), + 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; + if (idx == SPA_ID_INVALID && name == NULL) + return -EINVAL; + + dev = this->card->devices[device]; + if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HIDDEN)) + return -EINVAL; + + if (idx == SPA_ID_INVALID) + idx = find_route_by_name(this->card, name); + if (idx == SPA_ID_INVALID) + return -EINVAL; + if (!check_active_profile_port(this, device, idx)) + return -EINVAL; + + acp_device_set_port(dev, idx, save ? ACP_PORT_SAVE : 0); + if (props) + 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-device.c b/spa/plugins/alsa/alsa-compress-offload-device.c new file mode 100644 index 0000000..6d435fe --- /dev/null +++ b/spa/plugins/alsa/alsa-compress-offload-device.c @@ -0,0 +1,620 @@ +/* Spa ALSA Compress-Offload device */ +/* SPDX-FileCopyrightText: Copyright @ 2023 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compress-offload-api-util.h" +#include "alsa.h" + +static const char default_device[] = "hw:0"; + +struct props { + char device[64]; + unsigned int card_nr; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, 64); + props->card_nr = 0; +} + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + + uint32_t info_all; + struct spa_device_info device_info; + +#define IDX_EnumProfile 0 +#define IDX_Profile 1 + struct spa_param_info params[2]; + + struct spa_hook_list hooks; + + struct props props; + uint32_t n_nodes; + uint32_t n_capture; + uint32_t n_playback; + + uint32_t profile; +}; + +#define ADD_DICT_ITEM(key, value) do { items[n_items++] = SPA_DICT_ITEM_INIT(key, value); } while (0) + +static void emit_node(struct impl *this, const char *device_node, unsigned int device_nr, + enum spa_compress_offload_direction direction, snd_ctl_card_info_t *cardinfo, + uint32_t id) +{ + struct spa_dict_item items[5]; + uint32_t n_items = 0; + char alsa_path[128], path[180]; + char node_name[200]; + char node_desc[200]; + struct spa_device_object_info info; + const char *stream; + + spa_log_debug(this->log, "emitting node info for device %s (card nr %u device nr %u)", + device_node, this->props.card_nr, device_nr); + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + + if (direction == SPA_COMPRESS_OFFLOAD_DIRECTION_PLAYBACK) { + stream = "playback"; + info.factory_name = SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK; + } else { + stream = "capture"; + /* TODO: This is not yet implemented, because getting Compress-Offload + * hardware that can capture audio is difficult to do. The only hardware + * known is the Wolfson ADSP; the only driver in the kernel that exposes + * Compress-Offload capture devices is the one for that hardware. */ + spa_assert_not_reached(); + } + + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + + snprintf(alsa_path, sizeof(alsa_path), "%s,%u", this->props.device, device_nr); + snprintf(path, sizeof(path), "alsa:compressed:%s:%u:%s", snd_ctl_card_info_get_id(cardinfo), device_nr, stream); + snprintf(node_name, sizeof(node_name), "comprC%uD%u", this->props.card_nr, device_nr); + snprintf(node_desc, sizeof(node_desc), "Compress-Offload sink node (ALSA card %u device %u)", this->props.card_nr, device_nr); + + ADD_DICT_ITEM(SPA_KEY_NODE_NAME, node_name); + ADD_DICT_ITEM(SPA_KEY_NODE_DESCRIPTION, node_desc); + ADD_DICT_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_PATH, alsa_path); + /* NOTE: Set alsa.name, since session managers look for this, or for + * SPA_KEY_API_ALSA_PCM_NAME, or other items. The best fit in this + * case seems to be alsa.name, since SPA_KEY_API_ALSA_PCM_NAME is + * PCM specific, as the name suggests. If none of these items are + * provided, session managers may not work properly. WirePlumber's + * alsa.lua script looks for these for example. + * And, since we have no good way of getting a name, just reuse + * the alsa_path here. */ + ADD_DICT_ITEM("alsa.name", alsa_path); + + info.props = &SPA_DICT_INIT(items, n_items); + + spa_log_debug(this->log, "node information:"); + spa_debug_dict(2, info.props); + + spa_device_emit_object_info(&this->hooks, id, &info); +} + +static int set_profile(struct impl *this, uint32_t id) +{ + int ret = 0; + uint32_t i, n_cap, n_play; + char prefix[32]; + int prefix_length; + struct dirent *entry; + DIR *snd_dir = NULL; + snd_ctl_t *ctl_handle = NULL; + snd_ctl_card_info_t *cardinfo; + + spa_log_debug(this->log, "enumerate Compress-Offload nodes for card %s; profile: %d", + this->props.device, id); + + if ((ret = snd_ctl_open(&ctl_handle, this->props.device, 0)) < 0) { + spa_log_error(this->log, "can't open control for card %s: %s", + this->props.device, snd_strerror(ret)); + goto finish; + } + + this->profile = id; + + snd_ctl_card_info_alloca(&cardinfo); + if ((ret = snd_ctl_card_info(ctl_handle, cardinfo)) < 0) { + spa_log_error(this->log, "error card info: %s", snd_strerror(ret)); + goto finish; + } + + /* Clear any previous node object info. */ + 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; + + /* Profile ID 0 is the "off" profile, that is, the profile where the device + * is "disabled". To implement such a disabled state, simply exit here without + * adding any nodes after we removed any existing one (see above). */ + if (id == 0) + { + spa_log_debug(this->log, "\"Off\" profile selected - exiting without " + "creating any nodes after all previous ones were removed"); + goto finish; + } + + spa_scnprintf(prefix, sizeof(prefix), "comprC%uD", this->props.card_nr); + prefix_length = strlen(prefix); + + /* There is no API to enumerate all Compress-Offload devices, so we have + * to stick to walking through the /dev/snd directory entries and looking + * for device nodes that match the comprCD prefix. */ + snd_dir = opendir("/dev/snd"); + if (snd_dir == NULL) + goto errno_error; + + i = 0; + i = n_cap = n_play = 0; + while ((errno = 0, entry = readdir(snd_dir)) != NULL) { + long long device_nr; + enum spa_compress_offload_direction direction; + + if (!(entry->d_type == DT_CHR && spa_strstartswith(entry->d_name, prefix))) + continue; + + /* Parse the device number from the device filename. We know that the filename + * is always structured like this: comprCD + * We consider "comprCD" to form the "prefix" here. Right after + * that prefix, the device number can be parsed, so skip the prefix. */ + device_nr = strtol(entry->d_name + prefix_length, NULL, 10); + if ((device_nr < 0) || (device_nr > UINT_MAX)) { + spa_log_warn(this->log, "device %s contains unusable device number; " + "skipping", entry->d_name); + continue; + } + + if (get_compress_offload_device_direction(this->props.card_nr, device_nr, + this->log, &direction) < 0) + goto finish; + + switch (direction) { + case SPA_COMPRESS_OFFLOAD_DIRECTION_PLAYBACK: + n_play++; + emit_node(this, entry->d_name, device_nr, direction, cardinfo, i++); + break; + case SPA_COMPRESS_OFFLOAD_DIRECTION_CAPTURE: + /* TODO: Disabled for now. See the TODO in emit_node() for details. */ +#if 0 + n_cap++; + emit_node(this, entry->d_name, device_nr, direction, cardinfo, i++); +#endif + break; + } + } + + this->n_capture = n_cap; + this->n_playback = n_play; + this->n_nodes = i; + + this->device_info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Profile].user++; + +finish: + if (snd_dir != NULL) + closedir(snd_dir); + if (ctl_handle != NULL) + snd_ctl_close(ctl_handle); + return ret; + +errno_error: + ret = -errno; + goto finish; +} + +static int emit_info(struct impl *this, bool full) +{ + int err = 0; + struct spa_dict_item items[20]; + uint32_t n_items = 0; + char path[128]; + char device_name[200]; + char device_desc[200]; + + if (full) + this->device_info.change_mask = this->info_all; + + if (this->device_info.change_mask) { + snd_ctl_card_info_t *info; + snd_ctl_t *ctl_hndl; + + 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); + err = snd_ctl_card_info(ctl_hndl, info); + + spa_log_debug(this->log, "close card %s", this->props.device); + snd_ctl_close(ctl_hndl); + + if (err < 0) { + spa_log_error(this->log, "error hardware info: %s", snd_strerror(err)); + return err; + } + + snprintf(path, sizeof(path), "alsa:compressed:%s", snd_ctl_card_info_get_id(info)); + snprintf(device_name, sizeof(device_name), "comprC%u", this->props.card_nr); + snprintf(device_desc, sizeof(device_desc), "Compress-Offload device (ALSA card %u)", this->props.card_nr); + + ADD_DICT_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_DICT_ITEM(SPA_KEY_DEVICE_API, "alsa:compressed"); + ADD_DICT_ITEM(SPA_KEY_DEVICE_NICK, "alsa:compressed"); + ADD_DICT_ITEM(SPA_KEY_DEVICE_NAME, device_name); + ADD_DICT_ITEM(SPA_KEY_DEVICE_DESCRIPTION, device_desc); + ADD_DICT_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_ID, snd_ctl_card_info_get_id(info)); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_COMPONENTS, snd_ctl_card_info_get_components(info)); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_DRIVER, snd_ctl_card_info_get_driver(info)); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_NAME, snd_ctl_card_info_get_name(info)); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_LONGNAME, snd_ctl_card_info_get_longname(info)); + ADD_DICT_ITEM(SPA_KEY_API_ALSA_CARD_MIXERNAME, snd_ctl_card_info_get_mixername(info)); + + this->device_info.props = &SPA_DICT_INIT(items, n_items); + + if (this->device_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->device_info); + this->device_info.change_mask = 0; + } + + 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; + + 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 uint32_t find_profile_by_name(const char *name) +{ + if (spa_streq(name, "off")) + return 0; + else if (spa_streq(name, "on")) + return 1; + return SPA_ID_INVALID; +} + +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 = SPA_ID_INVALID; + const char *name = NULL; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_OPT_Int(&idx), + SPA_PARAM_PROFILE_name, SPA_POD_OPT_String(&name))) < 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; + } + if (idx == SPA_ID_INVALID && name == NULL) + return -EINVAL; + if (idx == SPA_ID_INVALID) + idx = find_profile_by_name(name); + if (idx == SPA_ID_INVALID) + return -EINVAL; + + set_profile(this, idx); + 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 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; + + 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) { + uint32_t i; + 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(this->props.device, 64, "%s", s); + spa_log_debug(this->log, "using ALSA path \"%s\"", this->props.device); + } else if (spa_streq(k, SPA_KEY_API_ALSA_CARD)) { + long long card_nr = strtol(s, NULL, 10); + if ((card_nr >= 0) && (card_nr <= UINT_MAX)) { + this->props.card_nr = card_nr; + spa_log_debug(this->log, "using ALSA card number %u", this->props.card_nr); + } else + spa_log_warn(this->log, "invalid ALSA card number \"%s\"; using default", s); + } + } + } + + this->device_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->device_info.params = this->params; + this->device_info.n_params = SPA_N_ELEMENTS(this->params); + + 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_compress_offload_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_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..d10d9c0 --- /dev/null +++ b/spa/plugins/alsa/alsa-compress-offload-sink.c @@ -0,0 +1,2024 @@ +/* Spa ALSA Compress-Offload sink */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Asymptotic Inc. */ +/* SPDX-FileCopyrightText: Copyright @ 2023 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alsa.h" +#include "compress-offload-api.h" + + +/* + * This creates a PipeWire sink node which uses the ALSA Compress-Offload API + * for writing compressed data ike 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. This sink node still refers to those devices in + * regular ALSA fashion as "hw:x,y" devices, where x = card number and + * y = device number. For example, "hw:4,7" maps to /dev/snd/comprC4D7. + * + * ## Example configuration + *\code{.unparsed} + * context.objects = [ + * { factory = adapter + * args = { + * factory.name = "api.alsa.compress.offload.sink" + * node.name = "Compress-Offload-Sink" + * node.description = "Audio sink for compressed audio" + * media.class = "Audio/Sink" + * api.alsa.path = "hw:0,3" + * node.param.PortConfig = { + * direction = Input + * mode = passthrough + * } + * } + * } + *] + *\endcode + * + * TODO: + * - DLL for adjusting driver timer intervals to match the device timestamps in on_driver_timeout() + * - Automatic loading using alsa-udev + */ + + +/* FLAC support has been present in kernel headers older than 5.5. + * However, those older versions don't support FLAC decoding params. */ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 5, 0) +#define COMPRESS_OFFLOAD_HAS_FLAC_DEC_PARAMS +#endif + +/* Prior to kernel 5.7, WMA9 Pro/Lossless and WMA10 Lossless + * codec profiles were missing. + * As for ALAC and Monkey's Audio (APE), those are new in 5.7. */ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0) +#define COMPRESS_OFFLOAD_SUPPORTS_WMA9_PRO +#define COMPRESS_OFFLOAD_SUPPORTS_WMA9_LOSSLESS +#define COMPRESS_OFFLOAD_SUPPORTS_WMA10_LOSSLESS +#define COMPRESS_OFFLOAD_SUPPORTS_ALAC +#define COMPRESS_OFFLOAD_SUPPORTS_APE +#endif + + +#define CHECK_PORT(this, d, p) (((d) == SPA_DIRECTION_INPUT) && ((p) == 0)) + +#define MAX_BUFFERS (32) + +#define BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA (1 << 0) + + +/* Information about a buffer that got allocated by the PW graph. */ +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_buffer *buf; + struct spa_list link; +}; + + +/* Node properties. These are accessible through SPA_PARAM_Props. */ +struct props { + /* The'"hw::" device. */ + char device[128]; + /* These are the card and device numbers from the + * from the "hw::" device.*/ + int card_nr; + int device_nr; + bool device_name_set; +}; + + +/* Main sink node structure. */ +struct impl { + /* Basic states */ + + 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; + + struct props props; + + bool have_format; + struct spa_audio_info current_audio_info; + + /* This is set to true when the SPA_NODE_COMMAND_Start is + * received, and set back to false when SPA_NODE_COMMAND_Pause + * or SPA_NODE_COMMAND_Suspend is received. */ + bool started; + + bool freewheel; + + /* SPA buffer states */ + + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; + struct spa_list queued_output_buffers; + size_t offset_within_oldest_output_buffer; + + /* Driver and cycle specific states */ + + int driver_timerfd; + struct spa_source driver_timer_source; + uint64_t next_driver_time; + bool following; + /* Duration and rate of one graph cycle. + * The duration equals the quantum size. */ + uint32_t cycle_duration; + int cycle_rate; + + /* Node specific states */ + + uint64_t node_info_all; + struct spa_node_info node_info; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_IO 2 +#define NODE_EnumPortConfig 3 +#define N_NODE_PARAMS 4 + struct spa_param_info node_params[N_NODE_PARAMS]; + struct spa_io_clock *node_clock_io; + struct spa_io_position *node_position_io; + + /* Port specific states */ + + uint64_t port_info_all; + struct spa_port_info port_info; +#define PORT_EnumFormat 0 +#define PORT_Format 1 +#define PORT_IO 2 +#define PORT_Buffers 3 +#define N_PORT_PARAMS 4 + struct spa_param_info port_params[N_PORT_PARAMS]; + struct spa_io_buffers *port_buffers_io; + + /* Compress-Offload specific states */ + + struct compress_offload_api_context *device_context; + struct snd_codec audio_codec_info; + bool device_started; + uint32_t min_fragment_size; + uint32_t max_fragment_size; + uint32_t min_num_fragments; + uint32_t max_num_fragments; + uint32_t configured_fragment_size; + uint32_t configured_num_fragments; + bool device_is_paused; +}; + + + +/* Compress-Offload device and audio codec functions */ + +static int init_audio_codec_info(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate); +static int device_open(struct impl *this); +static void device_close(struct impl *this); +static int device_start(struct impl *this); +static int device_pause(struct impl *this); +static int device_resume(struct impl *this); +static int device_write(struct impl *this, const void *data, uint32_t size); + +/* Driver timer functions */ + +static int set_driver_timeout(struct impl *this, uint64_t time); +static int configure_driver_timer(struct impl *this); +static int start_driver_timer(struct impl *this); +static void stop_driver_timer(struct impl *this); +static void on_driver_timeout(struct spa_source *source); +static inline void check_position_and_clock_config(struct impl *this); +static void reevaluate_following_state(struct impl *this); +static void reevaluate_freewheel_state(struct impl *this); + +/* Miscellaneous functions */ + +static int parse_device(struct impl *this); +static void reset_props(struct props *props); +static void clear_buffers(struct impl *this); +static inline bool is_following(struct impl *this); +static int do_start(struct impl *this); +static void do_stop(struct impl *this); +static int write_queued_output_buffers(struct impl *this); + +/* Node and port functions */ + +static void emit_node_info(struct impl *this, bool full); +static void emit_port_info(struct impl *this, bool full); +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data); +static int impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data); +static int impl_node_sync(void *object, int seq); +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter); +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param); +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size); +static int impl_node_send_command(void *object, const struct spa_command *command); +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props); +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id); +static int port_enum_formats(struct impl *this, int seq, uint32_t start, uint32_t num, + const struct spa_pod *filter, struct spa_pod_builder *b); +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); +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + const struct spa_pod *format); +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); +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); +static int impl_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size); +static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id); +static int impl_node_process(void *object); + + + +/* Compress-Offload device and audio codec functions */ + +struct known_codec_info { + uint32_t codec_id; + uint32_t media_subtype; + const char *name; +}; + +static struct known_codec_info known_codecs[] = { + { SND_AUDIOCODEC_VORBIS, SPA_MEDIA_SUBTYPE_vorbis, "Ogg Vorbis" }, + { SND_AUDIOCODEC_MP3, SPA_MEDIA_SUBTYPE_mp3, "MP3" }, + { SND_AUDIOCODEC_AAC, SPA_MEDIA_SUBTYPE_aac, "AAC" }, + { SND_AUDIOCODEC_FLAC, SPA_MEDIA_SUBTYPE_flac, "FLAC" }, + { SND_AUDIOCODEC_WMA, SPA_MEDIA_SUBTYPE_wma, "WMA" }, +#ifdef COMPRESS_OFFLOAD_SUPPORTS_ALAC + { SND_AUDIOCODEC_ALAC, SPA_MEDIA_SUBTYPE_alac, "ALAC" }, +#endif +#ifdef COMPRESS_OFFLOAD_SUPPORTS_APE + { SND_AUDIOCODEC_APE, SPA_MEDIA_SUBTYPE_ape, "Monkey's Audio (APE)" }, +#endif + { SND_AUDIOCODEC_REAL, SPA_MEDIA_SUBTYPE_ra, "Real Audio" }, + { SND_AUDIOCODEC_AMRWB, SPA_MEDIA_SUBTYPE_amr, "AMR wideband" }, + { SND_AUDIOCODEC_AMR, SPA_MEDIA_SUBTYPE_amr, "AMR" }, +}; + +static int init_audio_codec_info(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate) +{ + struct snd_codec *codec; + uint32_t channels, rate; + const struct spa_type_info *media_subtype_info; + + media_subtype_info = spa_debug_type_find(spa_type_media_subtype, info->media_subtype); + if (media_subtype_info == NULL) { + spa_log_error(this->log, "%p: media subtype %d is unknown", this, info->media_subtype); + return -ENOTSUP; + } + + memset(&this->audio_codec_info, 0, sizeof(this->audio_codec_info)); + codec = &this->audio_codec_info; + + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_vorbis: + codec->id = SND_AUDIOCODEC_VORBIS; + rate = info->info.vorbis.rate; + channels = info->info.vorbis.channels; + spa_log_info(this->log, "%p: initialized codec info to Vorbis; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; + + case SPA_MEDIA_SUBTYPE_mp3: + codec->id = SND_AUDIOCODEC_MP3; + rate = info->info.mp3.rate; + channels = info->info.mp3.channels; + spa_log_info(this->log, "%p: initialized codec info to MP3; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; + + case SPA_MEDIA_SUBTYPE_aac: + codec->id = SND_AUDIOCODEC_AAC; + rate = info->info.aac.rate; + channels = info->info.aac.channels; + spa_log_info(this->log, "%p: initialized codec info to AAC; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; + + case SPA_MEDIA_SUBTYPE_flac: + codec->id = SND_AUDIOCODEC_FLAC; +#ifdef COMPRESS_OFFLOAD_HAS_FLAC_DEC_PARAMS + /* The min/max block sizes are from the FLAC specification: + * https://xiph.org/flac/format.html#blocking + * + * The smallest valid frame possible is 11, which + * is why min_frame_size is set to this quantity. + * + * FFmpeg's flac.h specifies 8192 as the average frame size. + * tinycompress' fcplay uses 4x that amount as the max frame + * size to have enough headroom to be safe. + * We do the same here. + * + * sample_size is set to 0. According to the FLAC spec, this + * is OK to do if a STREAMINFO block was sent into the device + * (see: https://xiph.org/flac/format.html#frame_header), and + * we deal with full FLAC streams here, not just single frames. */ + 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; + codec->options.flac_d.sample_size = 0; +#endif + rate = info->info.flac.rate; + channels = info->info.flac.channels; + spa_log_info(this->log, "%p: initialized codec info to FLAC; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; + + case SPA_MEDIA_SUBTYPE_wma: { + const char *profile_name; + 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; + profile_name = "WMA9"; + break; + case SPA_AUDIO_WMA_PROFILE_WMA10: + codec->profile = SND_AUDIOPROFILE_WMA10; + profile_name = "WMA10"; + break; +#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA9_PRO + case SPA_AUDIO_WMA_PROFILE_WMA9_PRO: + codec->profile = SND_AUDIOPROFILE_WMA9_PRO; + profile_name = "WMA9 Pro"; + break; +#endif +#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA9_LOSSLESS + case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS; + profile_name = "WMA9 Lossless"; + break; +#endif +#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA10_LOSSLESS + case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS; + profile_name = "WMA10 Lossless"; + break; +#endif + default: + spa_log_error(this->log, "%p: Invalid WMA 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; + spa_log_info(this->log, "%p: initialized codec info to WMA; rate: %" + PRIu32 "; channels: %" PRIu32 "; profile %s", this, + rate, channels, profile_name); + break; + } + +#ifdef COMPRESS_OFFLOAD_SUPPORTS_ALAC + case SPA_MEDIA_SUBTYPE_alac: + codec->id = SND_AUDIOCODEC_ALAC; + rate = info->info.alac.rate; + channels = info->info.alac.channels; + spa_log_info(this->log, "%p: initialized codec info to ALAC; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; +#endif + +#ifdef COMPRESS_OFFLOAD_SUPPORTS_APE + case SPA_MEDIA_SUBTYPE_ape: + codec->id = SND_AUDIOCODEC_APE; + rate = info->info.ape.rate; + channels = info->info.ape.channels; + spa_log_info(this->log, "%p: initialized codec info to APE (Monkey's Audio);" + " rate: %" PRIu32 "; channels: %" PRIu32, this, rate, channels); + break; +#endif + + case SPA_MEDIA_SUBTYPE_ra: + codec->id = SND_AUDIOCODEC_REAL; + rate = info->info.ra.rate; + channels = info->info.ra.channels; + spa_log_info(this->log, "%p: initialized codec info to Real Audio; rate: %" + PRIu32 "; channels: %" PRIu32, this, rate, 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; + spa_log_info(this->log, "%p: initialized codec info to %s; rate: %" + PRIu32 "; channels: %" PRIu32, this, + (codec->id == SND_AUDIOCODEC_AMRWB) ? "AMR wideband" : "AMR", + rate, channels); + break; + + default: + spa_log_error(this->log, "%p: media subtype %s is not supported", this, media_subtype_info->name); + return -ENOTSUP; + } + + codec->ch_in = channels; + codec->ch_out = channels; + codec->sample_rate = rate; + + codec->rate_control = 0; + codec->level = 0; + codec->ch_mode = 0; + codec->format = 0; + + *out_rate = rate; + + return 0; +} + +static int device_open(struct impl *this) +{ + assert(this->device_context == NULL); + + spa_log_info(this->log, "%p: opening Compress-Offload device, card #%d device #%d", + this, this->props.card_nr, this->props.device_nr); + + this->device_context = compress_offload_api_open(this->props.card_nr, this->props.device_nr, this->log); + if (this->device_context == NULL) + return -errno; + + return 0; +} + +static void device_close(struct impl *this) +{ + if (this->device_context == NULL) + return; + + spa_log_info(this->log, "%p: closing Compress-Offload device, card #%d device #%d", + this, this->props.card_nr, this->props.device_nr); + + if (this->device_started) + compress_offload_api_stop(this->device_context); + + compress_offload_api_close(this->device_context); + + this->device_context = NULL; + this->device_started = false; + this->device_is_paused = false; + + this->have_format = false; +} + +static int device_start(struct impl *this) +{ + assert(this->device_context != NULL); + + if (compress_offload_api_start(this->device_context) < 0) + return -errno; + + this->device_started = true; + + return 0; +} + +static int device_pause(struct impl *this) +{ + /* device_pause() can sometimes be called when the device context is already + * gone. In particular, this can happen when the suspend command is received + * after the pause command. */ + if (this->device_context == NULL) + return 0; + + if (this->device_is_paused) + return 0; + + if (compress_offload_api_pause(this->device_context) < 0) + return -errno; + + this->device_is_paused = true; + + return 0; +} + +static int device_resume(struct impl *this) +{ + assert(this->device_context != NULL); + + if (!this->device_is_paused) + return 0; + + if (compress_offload_api_resume(this->device_context) < 0) + return -errno; + + this->device_is_paused = false; + + return 0; +} + +static int device_write(struct impl *this, const void *data, uint32_t size) +{ + int res; + uint32_t num_bytes_to_write; + struct snd_compr_avail available_space; + + /* In here, try to write out as much data as possible, + * in a non-blocking manner, retaining the unwritten + * data for the next write call. */ + + if (SPA_UNLIKELY((res = compress_offload_api_get_available_space( + this->device_context, &available_space)) < 0)) + return res; + + /* We can only write data if there is at least enough space for one + * fragment's worth of data, or if the data we want to write is + * small (smaller than a fragment). The latter can happen when we + * are writing the last few bits of the compressed audio medium. + * When the former happens, we try to write as much data as we + * can, limited by the amount of space available in the device. */ + if ((available_space.avail < this->min_fragment_size) && (available_space.avail < size)) + return 0; + + num_bytes_to_write = SPA_MIN(size, available_space.avail); + res = compress_offload_api_write(this->device_context, data, num_bytes_to_write); + + if (SPA_UNLIKELY(res < 0)) { + if (res == -EBADFD) + spa_log_debug(this->log, "%p: device is paused", this); + else + spa_log_error(this->log, "%p: write error: %s", this, spa_strerror(res)); + return res; + } + + spa_log_trace_fp(this->log, "%p: wrote %d bytes; original request: %" PRIu32 "; adjusted " + "for available space in device: %" PRIu32, this, res, size, num_bytes_to_write); + + if (SPA_UNLIKELY(((uint32_t)res) > num_bytes_to_write)) { + spa_log_error(this->log, "%p: wrote more bytes than what was requested; " + "requested: %" PRId32 " wrote: %d", this, num_bytes_to_write, res); + return -EIO; + } + + return res; +} + + + +/* Driver timer functions */ + +static int set_driver_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; + spa_system_timerfd_settime(this->data_system, this->driver_timerfd, + SPA_FD_TIMER_ABSTIME, &ts, NULL); + + return 0; +} + +static int configure_driver_timer(struct impl *this) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0) { + spa_log_error(this->log, "%p: could not get time from monotonic sysclock: %s", + this, spa_strerror(res)); + return res; + } + this->next_driver_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (this->following) + set_driver_timeout(this, 0); + else + set_driver_timeout(this, this->next_driver_time); + + return 0; +} + +static int start_driver_timer(struct impl *this) +{ + int res; + + spa_log_debug(this->log, "%p: starting driver timer", this); + + this->driver_timer_source.func = on_driver_timeout; + this->driver_timer_source.data = this; + this->driver_timer_source.fd = this->driver_timerfd; + this->driver_timer_source.mask = SPA_IO_IN; + this->driver_timer_source.rmask = 0; + + spa_loop_add_source(this->data_loop, &this->driver_timer_source); + + if (SPA_UNLIKELY((res = configure_driver_timer(this)) < 0)) + return res; + + return 0; +} + +static int do_remove_driver_timer_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_loop_remove_source(this->data_loop, &this->driver_timer_source); + set_driver_timeout(this, 0); + + return 0; +} + +static void stop_driver_timer(struct impl *this) +{ + spa_log_debug(this->log, "%p: stopping driver timer", this); + + /* Perform the actual stop within + * the dataloop to avoid data races. */ + spa_loop_invoke(this->data_loop, do_remove_driver_timer_source, 0, NULL, 0, true, this); +} + +static void on_driver_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + + uint64_t expire, current_time; + int res; + + if (SPA_LIKELY(this->started)) { + if (SPA_UNLIKELY((res = spa_system_timerfd_read(this->data_system, + this->driver_timerfd, &expire)) < 0)) { + if (res != -EAGAIN) + spa_log_warn(this->log, "%p: error reading from timerfd: %s", + this, spa_strerror(res)); + return; + } + } + + if (SPA_LIKELY(this->node_position_io != NULL)) { + this->cycle_duration = this->node_position_io->clock.target_duration; + this->cycle_rate = this->node_position_io->clock.target_rate.denom; + } else { + /* This can happen at the very beginning if node_position_io + * isn't passed to this node in time. */ + this->cycle_duration = 1024; + this->cycle_rate = 48000; + } + + current_time = this->next_driver_time; + + this->next_driver_time += ((uint64_t)(this->cycle_duration)) * 1000000000ULL / this->cycle_rate; + if (this->node_clock_io != NULL) { + this->node_clock_io->nsec = current_time; + this->node_clock_io->rate = this->node_clock_io->target_rate; + this->node_clock_io->position += this->node_clock_io->duration; + this->node_clock_io->duration = this->cycle_duration; + this->node_clock_io->delay = 0; + this->node_clock_io->rate_diff = 1.0; + this->node_clock_io->next_nsec = this->next_driver_time; + spa_log_trace_fp(this->log, "%p: clock IO updated to: nsec %" PRIu64 + " pos %" PRIu64 " dur %" PRIu64 " next-nsec %" PRIu64, this, + this->node_clock_io->nsec, this->node_clock_io->position, + this->node_clock_io->duration, this->node_clock_io->next_nsec); + } + + /* Adapt the graph cycle progression to the needs of the sink. + * If the sink still has data to output, don't advance. */ + if (spa_list_is_empty(&this->queued_output_buffers)) { + struct spa_io_buffers *io = this->port_buffers_io; + + if (SPA_LIKELY(io != NULL)) { + spa_log_trace_fp(this->log, "%p: ran out of buffers to output, " + "need more; IO status: %d", this, io->status); + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); + } else { + /* This should not happen. If it does, then there may be + * an error in when the timer is stopped. When it happens, + * do not schedule a next timeout. */ + spa_log_warn(this->log, "%p: buffers IO was set to NULL before " + "the driver timer was stopped", this); + set_driver_timeout(this, 0); + return; + } + } else { + write_queued_output_buffers(this); + } + + // TODO check for impossible timeouts (only relevant when taking device timestamps into account) + + set_driver_timeout(this, this->next_driver_time); +} + +static inline void check_position_and_clock_config(struct impl *this) +{ + if (SPA_LIKELY(this->node_position_io != NULL)) { + this->cycle_duration = this->node_position_io->clock.duration; + this->cycle_rate = this->node_position_io->clock.rate.denom; + } else { + /* This can happen at the very beginning if node_position_io + * isn't passed to this node in time. */ + this->cycle_duration = 1024; + this->cycle_rate = 48000; + } +} + +static int do_reevaluate_following_state(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + configure_driver_timer(this); + return 0; +} + +static void reevaluate_following_state(struct impl *this) +{ + bool following; + + if (!this->started) + return; + + following = is_following(this); + if (following != this->following) { + spa_log_debug(this->log, "%p: following state changed: %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reevaluate_following_state, 0, NULL, 0, true, this); + } +} + +static void reevaluate_freewheel_state(struct impl *this) +{ + bool freewheel; + + if (!this->started) + return; + + freewheel = (this->node_position_io != NULL) && + SPA_FLAG_IS_SET(this->node_position_io->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL); + + if (this->freewheel != freewheel) { + spa_log_debug(this->log, "%p: freewheel state changed: %d->%d", this, this->freewheel, freewheel); + this->freewheel = freewheel; + if (freewheel) + device_pause(this); + else + device_resume(this); + } +} + + + +/* Miscellaneous functions */ + +static int parse_device(struct impl *this) +{ + char *device; + char *nextptr; +#define NUM_DEVICE_VALUES (2) + long values[NUM_DEVICE_VALUES]; + int value_index; + + device = this->props.device; + + /* Valid devices always match the "hw:," pattern. */ + + if (strncmp(device, "hw:", 3) != 0) { + spa_log_error(this->log, "%p: device \"%s\" does not begin with \"hw:\"", this, device); + return -EINVAL; + } + + nextptr = device + 3; + for (value_index = 0; ; ++value_index) { + const char *value_label; + + switch (value_index) { + case 0: value_label = "card"; break; + case 1: value_label = "device"; break; + default: spa_assert_not_reached(); + } + + errno = 0; + values[value_index] = strtol(nextptr, &nextptr, 10); + if (errno != 0) { + spa_log_error(this->log, "%p: device \"%s\" has invalid %s value", + this, device, value_label); + return -EINVAL; + } + + if (values[value_index] < 0) { + spa_log_error(this->log, "%p: device \"%s\" has negative %s value", + this, device, value_label); + return -EINVAL; + } + + if (values[value_index] > INT_MAX) { + spa_log_error(this->log, "%p: device \"%s\" has %s value larger than %d", + this, device, value_label, INT_MAX); + return -EINVAL; + } + + if (value_index >= (NUM_DEVICE_VALUES - 1)) + break; + + if ((*nextptr) != ',') { + spa_log_error(this->log, "%p: expected ',' separator between numbers in " + "device \"%s\", got '%c'", this, device, *nextptr); + return -EINVAL; + } + + /* Skip the comma between the values. */ + nextptr++; + } + + this->props.card_nr = values[0]; + this->props.device_nr = values[1]; + + return 0; +} + +static void reset_props(struct props *props) +{ + memset(props->device, 0, sizeof(props->device)); + props->card_nr = 0; + props->device_nr = 0; + props->device_name_set = false; +} + +static void clear_buffers(struct impl *this) +{ + if (this->n_buffers > 0) { + spa_log_debug(this->log, "%p: clearing buffers", this); + spa_list_init(&this->queued_output_buffers); + this->n_buffers = 0; + } +} + +static inline bool is_following(struct impl *this) +{ + return (this->node_position_io != NULL) && + (this->node_clock_io != NULL) && + (this->node_position_io->clock.id != this->node_clock_io->id); +} + +static int do_start(struct impl *this) +{ + int res; + + if (this->started) + return 0; + + this->following = is_following(this); + spa_log_debug(this->log, "%p: starting output; starting as follower: %d", + this, this->following); + + if (SPA_UNLIKELY((res = start_driver_timer(this)) < 0)) + return res; + + this->started = true; + + /* Not starting the compress-offload device here right away. + * That's because we first need to give it at least one + * fragment's worth of data. Starting the device prior to + * that results in buffer underflows inside the device. */ + + return 0; +} + +static void do_stop(struct impl *this) +{ + if (!this->started) + return; + + spa_log_debug(this->log, "%p: stopping output", this); + + device_pause(this); + + this->started = false; + + stop_driver_timer(this); +} + +static int write_queued_output_buffers(struct impl *this) +{ + int res; + uint32_t total_num_written_bytes; + bool wrote_data = false; + + check_position_and_clock_config(this); + + /* In here, we write as much data as possible. The device may + * initially not have sufficient space, but it is possible + * that due to ongoing data consumption, it can accommodate + * for more data in a next attempt, hence the "again" label. + * + * If during the write attempts, only a portion of a chunk + * is written, we must keep track of the portion that hasn't + * been consumed yet. offset_within_oldest_output_buffer + * exists for this purpose. In this sink node, each SPA + * buffer has exactly one chunk, so when a chunk is fully + * consumed, the corresponding buffer is removed from the + * queued_output_buffers list, marked as available, and + * returned to the pool through spa_node_call_reuse_buffer(). */ +again: + total_num_written_bytes = 0; + + while (!spa_list_is_empty(&this->queued_output_buffers)) { + struct buffer *b; + struct spa_data *d; + uint32_t chunk_start_offset, chunk_size, pending_data_size; + bool reuse_buffer = false; + + b = spa_list_first(&this->queued_output_buffers, struct buffer, link); + d = b->buf->datas; + assert(b->buf->n_datas >= 1); + + chunk_start_offset = d[0].chunk->offset + this->offset_within_oldest_output_buffer; + chunk_size = d[0].chunk->size; + + /* An empty chunk signals that the source is skipping this cycle. This + * is normal and necessary in cases when the compressed data frames are + * longer than the quantum size. The source then has to keep track of + * the excess lengths, and if these sum up to the length of a quantum, + * it sends a buffer with an empty chunk to compensate. If this is not + * done, there will eventually be an overflow, this sink will miss + * cycles, and audible errors will occur. */ + if (chunk_size != 0) { + int num_written_bytes; + + pending_data_size = chunk_size - this->offset_within_oldest_output_buffer; + + chunk_start_offset = SPA_MIN(chunk_start_offset, d[0].maxsize); + pending_data_size = SPA_MIN(pending_data_size, d[0].maxsize - chunk_start_offset); + + num_written_bytes = device_write(this, SPA_PTROFF(d[0].data, chunk_start_offset, void), pending_data_size); + if (SPA_UNLIKELY(num_written_bytes < 0)) + return num_written_bytes; + if (num_written_bytes == 0) + break; + + this->offset_within_oldest_output_buffer += num_written_bytes; + + total_num_written_bytes += num_written_bytes; + wrote_data = wrote_data || (num_written_bytes > 0); + + if (this->offset_within_oldest_output_buffer >= chunk_size) { + spa_log_trace_fp(this->log, "%p: buffer with ID %u was fully written; reusing this buffer", this, b->id); + reuse_buffer = true; + this->offset_within_oldest_output_buffer = 0; + } + } else { + spa_log_trace_fp(this->log, "%p: buffer with ID %u has empty chunk; reusing this buffer", this, b->id); + reuse_buffer = true; + } + + if (reuse_buffer) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA); + this->port_buffers_io->buffer_id = b->id; + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } + } + + if (!spa_list_is_empty(&this->queued_output_buffers) && (total_num_written_bytes > 0)) + goto again; + + /* We start the device only after having written data to avoid + * underruns due to an under-populated device ringbuffer. */ + if (wrote_data && !this->device_started) { + spa_log_debug(this->log, "%p: starting device", this); + if ((res = device_start(this)) < 0) { + spa_log_error(this->log, "%p: starting device failed: %s", this, spa_strerror(res)); + return res; + } + this->device_started = true; + } + + return 0; +} + + +/* Node and port functions */ + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "alsa" }, + { SPA_KEY_MEDIA_CLASS, "Audio/Sink" }, + { SPA_KEY_NODE_DRIVER, "true" }, + { SPA_KEY_NODE_PAUSE_ON_IDLE, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->node_info.change_mask : 0; + + if (full) + this->node_info.change_mask = this->node_info_all; + if (this->node_info.change_mask) { + this->node_info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->node_info); + this->node_info.change_mask = old; + } +} + +static void emit_port_info(struct impl *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) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &this->port_info); + this->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, 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_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA Compress-Offload device"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device))); + 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_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; + + case SPA_PARAM_EnumPortConfig: + { + switch (result.index) { + case 0: + /* Force ports to be configured to run in passthrough mode. + * This is essential when dealing with compressed data. */ + 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_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_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)) + ); + + spa_log_debug(this->log, "%p: setting device name to \"%s\"", this, p->device); + + p->device_name_set = true; + + if ((res = parse_device(this)) < 0) { + p->device_name_set = false; + return res; + } + + emit_node_info(this, false); + + break; + } + + default: + res = -ENOENT; + break; + } + + return res; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + spa_log_debug(this->log, "%p: got clock IO", this); + this->node_clock_io = data; + break; + case SPA_IO_Position: + spa_log_debug(this->log, "%p: got position IO", this); + this->node_position_io = data; + break; + default: + return -ENOENT; + } + + reevaluate_following_state(this); + reevaluate_freewheel_state(this); + + return 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); + + spa_log_debug(this->log, "%p: got new command: %s", this, + spa_debug_type_find_name(spa_type_node_command_id, SPA_NODE_COMMAND_ID(command))); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if (SPA_UNLIKELY((res = device_open(this)) < 0)) + return res; + break; + + case SPA_NODE_COMMAND_ParamEnd: + device_close(this); + break; + + case SPA_NODE_COMMAND_Start: + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + if (SPA_UNLIKELY((res = do_start(this)) < 0)) + return res; + + break; + + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + do_stop(this); + 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(struct impl *this, int seq, uint32_t start, uint32_t num, + const struct spa_pod *filter, struct spa_pod_builder *b) +{ + bool device_started, device_opened; + struct spa_result_node_params result; + struct spa_pod *fmt; + const struct known_codec_info *codec_info; + uint32_t count = 0; + int res; + bool codec_supported; + struct spa_audio_info info; + + device_opened = (this->device_context != NULL); + device_started = this->device_started; + + spa_log_debug(this->log, "%p: about to enumerate supported codecs: " + "device opened: %d have configured format: %d device started: %d", + this, device_opened, this->have_format, device_started); + + if (!this->started && this->have_format) { + spa_log_debug(this->log, "%p: closing device to reset configured format", this); + device_close(this); + device_opened = false; + } + + if (!device_opened) { + if ((res = device_open(this)) < 0) + return res; + } + + spa_zero(result); + result.id = SPA_PARAM_EnumFormat; + result.next = start; + +next: + result.index = result.next++; + + if (result.index >= SPA_N_ELEMENTS(known_codecs)) + goto enum_end; + + codec_info = &(known_codecs[result.index]); + + codec_supported = compress_offload_api_supports_codec(this->device_context, codec_info->codec_id); + + spa_log_debug(this->log, "%p: codec %s supported: %s", this, + codec_info->name, codec_supported ? "yes" : "no"); + + if (!codec_supported) + goto next; + + spa_zero(info); + info.media_type = SPA_MEDIA_TYPE_audio; + info.media_subtype = codec_info->media_subtype; + + if ((fmt = spa_format_audio_build(b, SPA_PARAM_EnumFormat, &info)) == NULL) { + res = -errno; + spa_log_error(this->log, "%p: error while building enumerated audio info: %s", + this, spa_strerror(res)); + return res; + } + + if (spa_pod_filter(b, &result.param, fmt, 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; + + if (!device_opened) + device_close(this); + + spa_log_debug(this->log, "%p: done enumerating supported codecs", this); + + return res; +} + +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 impl *this = object; + struct spa_pod *param = NULL; + 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); + + 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 port_enum_formats(this, seq, start, num, filter, &b); + + case SPA_PARAM_Format: + if (!this->have_format) { + spa_log_debug(this->log, "%p: attempted to enumerate current " + "format, but no current audio info set", this); + return -EIO; + } + + if (result.index > 0) + return 0; + + spa_log_debug(this->log, "%p: current audio info is set; " + "enumerating currently set format", this); + + param = spa_format_audio_build(&b, id, &this->current_audio_info); + 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_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(1, 1, MAX_BUFFERS), + /* blocks is set to 1 since we don't have planar data */ + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->configured_fragment_size * this->configured_num_fragments, + this->configured_fragment_size * this->configured_num_fragments, + this->max_fragment_size * this->max_num_fragments), + /* "stride" has no meaning when dealing with compressed data */ + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(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(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + int res; + + if (format == NULL) { + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "%p: clearing format and closing device", this); + device_close(this); + clear_buffers(this); + } else { + struct spa_audio_info info = { 0 }; + uint32_t rate; + const struct snd_compr_caps *compress_offload_caps; + + spa_log_debug(this->log, "%p: about to set format", this); + + if ((res = spa_format_audio_parse(format, &info)) < 0) { + spa_log_error(this->log, "%p: error while parsing audio format: %s", + this, spa_strerror(res)); + return res; + } + + if (this->device_context != NULL) { + spa_log_debug(this->log, "%p: need to close device to be able to reopen it with new format", this); + device_close(this); + } + + if ((res = init_audio_codec_info(this, &info, &rate)) < 0) + return res; + + if ((res = device_open(this)) < 0) + return res; + + if (!compress_offload_api_supports_codec(this->device_context, this->audio_codec_info.id)) { + spa_log_error(this->log, "%p: codec is not supported by the device", this); + device_close(this); + return -ENOTSUP; + } + + if ((res = compress_offload_api_set_params(this->device_context, &(this->audio_codec_info), 0, 0)) < 0) + return res; + + compress_offload_caps = compress_offload_api_get_caps(this->device_context); + + this->min_fragment_size = compress_offload_caps->min_fragment_size; + this->max_fragment_size = compress_offload_caps->max_fragment_size; + this->min_num_fragments = compress_offload_caps->min_fragments; + this->max_num_fragments = compress_offload_caps->max_fragments; + + spa_log_debug( + this->log, + "%p: min/max fragment size: %" PRIu32 "/%" PRIu32 " min/max num fragments: %" PRIu32 "/%" PRIu32, + this, + this->min_fragment_size, this->max_fragment_size, + this->min_num_fragments, this->max_num_fragments + ); + + compress_offload_api_get_fragment_config(this->device_context, + &(this->configured_fragment_size), + &(this->configured_num_fragments)); + + spa_log_debug( + this->log, "%p: configured fragment size: %" PRIu32 " configured num fragments: %" PRIu32, + this, + this->configured_fragment_size, this->configured_num_fragments + ); + + this->current_audio_info = info; + this->have_format = true; + this->port_info.rate = SPA_FRACTION(1, rate); + } + + this->node_info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->node_info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_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); + } 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 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 impl *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; + default: + res = -ENOENT; + break; + } + 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 impl *this = object; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + if (this->n_buffers > 0) { + spa_log_debug(this->log, "%p: %u buffers currently already in use; stopping device " + "to remove them before using new ones", this, this->n_buffers); + do_stop(this); + clear_buffers(this); + } + + spa_log_debug(this->log, "%p: using a pool with %d buffer(s)", this, n_buffers); + + 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->id = i; + b->flags = BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA; + b->buf = buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + + spa_log_debug(this->log, "%p: got buffer with ID %d bufptr %p data %p", this, i, b->buf, d[0].data); + } + + this->n_buffers = n_buffers; + + 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 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_IO_Buffers: + spa_log_debug(this->log, "%p: got buffers IO with data %p", this, data); + this->port_buffers_io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_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 spa_io_buffers *io; + struct buffer *b; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + io = this->port_buffers_io; + spa_return_val_if_fail(io != NULL, -EIO); + + /* Sinks aren't supposed to actually consume anything + * when the graph runs in freewheel mode. */ + if (this->node_position_io && this->node_position_io->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + + /* Add the incoming data if there is some. We place the data in + * a queue instead of just consuming it directly. This allows for + * adjusting driver cycles to the needs of the sink - if the sink + * already has data queued, it does not yet need to schedule a next + * cycle. See on_driver_timeout() for details. This is only relevnt + * if the sink is running as the graph's driver. */ + if ((io->status == SPA_STATUS_HAVE_DATA) && (io->buffer_id < this->n_buffers)) { + b = &this->buffers[io->buffer_id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA)) { + spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + + if (this->device_is_paused) { + spa_log_debug(this->log, "%p: resuming paused device", this); + if ((res = device_resume(this)) < 0) { + io->status = res; + return SPA_STATUS_STOPPED; + } + } + + spa_log_trace_fp(this->log, "%p: queuing buffer %u", this, io->buffer_id); + spa_list_append(&this->queued_output_buffers, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA); + /* This is essential to be able to hold back this buffer + * (which is because we queued it in a custom list for late + * consumption). By setting buffer_id to SPA_ID_INVALID, + * we essentially inform the graph that it must not attempt + * to return this buffer to the buffer pool. */ + io->buffer_id = SPA_ID_INVALID; + + if (SPA_UNLIKELY((res = write_queued_output_buffers(this)) < 0)) { + io->status = res; + return SPA_STATUS_STOPPED; + } + + io->status = SPA_STATUS_OK; + } + + return SPA_STATUS_HAVE_DATA; +} + + + +/* SPA node information and init / clear procedures */ + +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_port_enum_params, + .port_set_param = impl_port_set_param, + .port_use_buffers = impl_port_use_buffers, + .port_set_io = impl_port_set_io, + .port_reuse_buffer = impl_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; + + device_close(this); + + if (this->driver_timerfd > 0) { + spa_system_close(this->data_system, this->driver_timerfd); + this->driver_timerfd = -1; + } + + spa_log_info(this->log, "%p: created Compress-Offload sink", 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; + uint32_t i; + 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); + /* A logger must always exist, otherwise something is very wrong. */ + assert(this->log != NULL); + alsa_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, "%p: could not find a loop", this); + res = -EINVAL; + goto error; + } + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + if (this->data_system == NULL) { + spa_log_error(this->log, "%p: could not find a data system", this); + res = -EINVAL; + goto error; + } + + 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->have_format = false; + + this->started = false; + + this->freewheel = false; + + this->n_buffers = 0; + spa_list_init(&this->queued_output_buffers); + this->offset_within_oldest_output_buffer = 0; + + res = this->driver_timerfd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + if (SPA_UNLIKELY(res < 0)) { + spa_log_error(this->log, "%p: could not create driver timerfd: %s", this, spa_strerror(res)); + goto error; + } + + this->next_driver_time = 0; + this->following = false; + + this->node_info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->node_info = SPA_NODE_INFO_INIT(); + this->node_info.max_input_ports = 1; + this->node_info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->node_params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->node_params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->node_params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->node_params[NODE_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->node_info.params = this->node_params; + this->node_info.n_params = N_NODE_PARAMS; + this->node_clock_io = NULL; + this->node_position_io = NULL; + + 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_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->port_info.params = this->port_params; + this->port_info.n_params = N_PORT_PARAMS; + this->port_buffers_io = NULL; + + this->device_context = NULL; + this->device_started = false; + memset(&this->audio_codec_info, 0, sizeof(this->audio_codec_info)); + this->device_is_paused = false; + + spa_log_info(this->log, "%p: initialized Compress-Offload sink", this); + + 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(this->props.device, sizeof(this->props.device), "%s", s); + if ((res = parse_device(this)) < 0) + return res; + } + } + +finish: + return res; + +error: + impl_clear((struct spa_handle *)this); + goto finish; +} + +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; +} + + + +/* Factory info */ + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity , Carlos Rafael Giani " }, + { 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"=]" }, +}; + +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..5378fa3 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-device.c @@ -0,0 +1,611 @@ +/* Spa ALSA Device */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + + uint32_t info_all; + struct spa_device_info device_info; + +#define IDX_EnumProfile 0 +#define IDX_Profile 1 + struct spa_param_info params[2]; + + 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; + + 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, "enumerate PCM nodes for card %s; profile: %d", + this->props.device, id); + + 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, "done enumerating PCM nodes for card %s", this->props.device); + snd_ctl_close(ctl_hndl); + + this->device_info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Profile].user++; + + 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; + char path[128]; + + if (full) + this->device_info.change_mask = this->info_all; + + if (this->device_info.change_mask) { + snd_ctl_card_info_t *info; + snd_ctl_t *ctl_hndl; + + 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); + err = snd_ctl_card_info(ctl_hndl, info); + + spa_log_debug(this->log, "close card %s", this->props.device); + snd_ctl_close(ctl_hndl); + + if (err < 0) { + spa_log_error(this->log, "error hardware info: %s", snd_strerror(err)); + return err; + } + +#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)); + this->device_info.props = &SPA_DICT_INIT(items, n_items); +#undef ADD_ITEM + + if (this->device_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->device_info); + this->device_info.change_mask = 0; + } + + 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; + + 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 uint32_t find_profile_by_name(const char *name) +{ + if (spa_streq(name, "off")) + return 0; + else if (spa_streq(name, "on")) + return 1; + return SPA_ID_INVALID; +} + +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 = SPA_ID_INVALID; + const char *name = NULL; + + if (param == NULL) { + idx = 1; + } else if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_OPT_Int(&idx), + SPA_PARAM_PROFILE_name, SPA_POD_OPT_String(&name))) < 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; + } + if (idx == SPA_ID_INVALID && name == NULL) { + spa_log_warn(this->log, "profile needs name or index"); + return -EINVAL; + } + if (idx == SPA_ID_INVALID) + idx = find_profile_by_name(name); + if (idx == SPA_ID_INVALID) { + spa_log_warn(this->log, "unknown profile %s", name); + return -EINVAL; + } + + set_profile(this, idx); + 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 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); + + this->device_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->device_info.params = this->params; + this->device_info.n_params = SPA_N_ELEMENTS(this->params); + + 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_pcm_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..6c340cc --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-sink.c @@ -0,0 +1,990 @@ +/* Spa ALSA Sink */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Sink"); +} + +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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b.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.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.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.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.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.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.b, &f, + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b.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.b, SPA_PROP_iec958Codecs, 0); + spa_pod_builder_array(&b.b, sizeof(uint32_t), SPA_TYPE_Id, + n_codecs, codecs); + } + spa_alsa_add_prop_params(this, &b.b); + param = spa_pod_builder_pop(&b.b, &f); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b.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.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.b, id, &this->process_latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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: + if (size > 0 && size < sizeof(struct spa_io_position)) + return -EINVAL; + 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(¶ms)); + + 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); + } + spa_alsa_emit_node_info(this, false); + spa_alsa_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); + + spa_alsa_emit_node_info(this, false); + spa_alsa_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; + + this->want_started = true; + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + this->want_started = false; + 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); + + spa_alsa_emit_node_info(this, true); + spa_alsa_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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + 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.b, id, + &this->current_format.info.raw); + break; + case SPA_MEDIA_SUBTYPE_iec958: + param = spa_format_audio_iec958_build(&b.b, id, + &this->current_format.info.iec958); + break; + case SPA_MEDIA_SUBTYPE_dsd: + param = spa_format_audio_dsd_build(&b.b, id, + &this->current_format.info.dsd); + break; + default: + return -EIO; + } + break; + + case SPA_PARAM_Buffers: + { + uint32_t min_buffers; + + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + min_buffers = (this->quantum_limit * 4 * this->frame_scale) > this->buffer_frames ? 2 : 1; + + param = spa_pod_builder_add_object(&b.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(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.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.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.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.b, id, &latency); + break; + } + default: + return 0; + } + break; + case SPA_PARAM_Tag: + switch (result.index) { + case 0: case 1: + if ((param = this->tag[result.index]) == NULL) + goto next; + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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; + spa_alsa_emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + 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); + } + spa_alsa_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 = 0; + + 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: + { + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(other); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (info.direction != other) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + spa_alsa_emit_port_info(this, false); + break; + } + case SPA_PARAM_Tag: + { + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + if (param != NULL) { + struct spa_tag_info info; + void *state = NULL; + if (spa_tag_parse(param, &info, &state) < 0 || + info.direction != other) + return -EINVAL; + } + if (spa_tag_compare(param, this->tag[other]) != 0) { + free(this->tag[other]); + this->tag[other] = param ? spa_pod_copy(param) : NULL; + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Tag].user++; + spa_alsa_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; + 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; + if (this->rate_match) + spa_alsa_update_rate_match(this); + 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; + } + if (!spa_list_is_empty(&this->ready)) { + 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); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main loop is needed"); + return -EINVAL; + } + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + 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_params[PORT_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, 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 " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the alsa API" }, + { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_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..a86e8aa --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-source.c @@ -0,0 +1,933 @@ +/* Spa ALSA Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + spa_scnprintf(props->media_class, sizeof(props->media_class), "%s", "Audio/Source"); +} + +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]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + p = &this->props; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_PropInfo: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b.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.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.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.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.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.b, &f, + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b.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.b); + param = spa_pod_builder_pop(&b.b, &f); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b.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.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.b, id, &this->process_latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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: + if (size > 0 && size < sizeof(struct spa_io_position)) + return -EINVAL; + 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(¶ms)); + + 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); + } + + spa_alsa_emit_node_info(this, false); + spa_alsa_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); + + spa_alsa_emit_node_info(this, false); + spa_alsa_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; + + this->want_started = true; + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + this->want_started = false; + 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); + + spa_alsa_emit_node_info(this, true); + spa_alsa_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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + 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.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.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(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.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.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.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.b, id, &latency); + break; + } + default: + return 0; + } + break; + case SPA_PARAM_Tag: + switch (result.index) { + case 0: case 1: + if ((param = this->tag[result.index]) == NULL) + goto next; + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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; + spa_alsa_emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + 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); + } + spa_alsa_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 = 0; + + 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: + { + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(other); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (info.direction != other) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + spa_alsa_emit_port_info(this, false); + break; + } + case SPA_PARAM_Tag: + { + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + if (param != NULL) { + struct spa_tag_info info; + void *state = NULL; + if (spa_tag_parse(param, &info, &state) < 0 || + info.direction != other) + return -EINVAL; + } + if (spa_tag_compare(param, this->tag[other]) != 0) { + free(this->tag[other]); + this->tag[other] = param ? spa_pod_copy(param) : NULL; + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Tag].user++; + spa_alsa_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; + if (this->rate_match) + spa_alsa_update_rate_match(this); + 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); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main loop is needed"); + return -EINVAL; + } + if (this->data_loop == NULL) { + spa_log_error(this->log, "%p: a data loop is needed", this); + return -EINVAL; + } + 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_params[PORT_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, 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 " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Record audio with the alsa API" }, + { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=]" }, +}; + +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..36834ea --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm.c @@ -0,0 +1,3902 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "alsa-pcm.h" + +static struct spa_list cards = SPA_LIST_INIT(&cards); +static struct spa_list states = SPA_LIST_INIT(&states); + +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, bool ucm_split) +{ + struct card *c; + char card_name[128]; + const char *alibpref = NULL; + int err; + + if (index == SPA_ID_INVALID) + return NULL; + + if ((c = find_card(index)) != NULL) + return c; + + c = calloc(1, sizeof(*c)); + c->ref = 1; + c->index = index; + + if (ucm) { + const char *split_prefix = ucm_split ? "<<>>" : ""; + + snprintf(card_name, sizeof(card_name), "%shw:%i", split_prefix, index); + err = snd_use_case_mgr_open(&c->ucm, card_name); + if (err < 0) { + char *name; + err = snd_card_get_name(index, &name); + if (err < 0) + goto error; + + snprintf(card_name, sizeof(card_name), "%s%s", split_prefix, 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) +{ + if (!c) + return; + + 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); +} + +#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; } + +static int write_bind_ctl_param(struct state *state, const char *name, const char *param) { + int err; + unsigned int count, idx; + char _name[1024]; + + for (unsigned int i = 0; i < state->num_bind_ctls; i++) { + snd_ctl_elem_info_t *info = state->bound_ctls[i].info; + bool changed = false; + int type; + + if(!state->bound_ctls[i].value || !info) + continue; + + snprintf(_name, sizeof(_name), "api.alsa.bind-ctl.%s", + snd_ctl_elem_info_get_name(info)); + + if (!spa_streq(name, _name)) + continue; + + type = snd_ctl_elem_info_get_type(info); + count = snd_ctl_elem_info_get_count(info); + + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: { + bool b = spa_atob(param); + + for (idx = 0; idx < count; idx++) + snd_ctl_elem_value_set_boolean(state->bound_ctls[i].value, idx, b); + changed = true; + } + break; + + case SND_CTL_ELEM_TYPE_INTEGER: { + long l = (long) atoi(param); + + for (idx = 0; idx < count; idx++) + snd_ctl_elem_value_set_integer(state->bound_ctls[i].value, idx, l); + changed = true; + } + break; + + default: + spa_log_warn(state->log, "%s ctl '%s' not supported", + snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)), + snd_ctl_elem_info_get_name(info)); + break; + } + + if(changed) + CHECK(snd_ctl_elem_write(state->ctl, state->bound_ctls[i].value), "snd_ctl_elem_write"); + return 0; + } + + return 0; +} + +static int alsa_set_param(struct state *state, const char *k, const char *s) +{ + int fmt_change = 0; + 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_type_audio_format_from_short_name(s); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + spa_alsa_parse_position(&state->default_pos, s, strlen(s)); + 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.disable-tsched")) { + state->disable_tsched = 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, "api.alsa.htimestamp")) { + state->htimestamp = spa_atob(s); + } else if (spa_streq(k, "api.alsa.htimestamp.max-errors")) { + state->htimestamp_max_errors = atoi(s); + } else if (spa_streq(k, "api.alsa.auto-link")) { + state->auto_link = 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 if (spa_strstartswith(k, "api.alsa.bind-ctl.")) { + write_bind_ctl_param(state, k, s); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_MEDIA_CLASS)) { + spa_scnprintf(state->props.media_class, sizeof(state->props.media_class), "%s", s); + } else if (spa_streq(k, "api.alsa.split.parent")) { + state->is_split_parent = true; + } else + return 0; + + 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; +} + +static struct spa_pod *enum_bind_ctl_propinfo(struct state *state, uint32_t idx, struct spa_pod_builder *b) +{ + char param_name[1024]; + char param_desc[1024]; + snd_ctl_elem_info_t *info = state->bound_ctls[idx].info; + + if (!info) { + // This will end iteration early, so print a warning + spa_log_warn(state->log, "Don't have prop info for bind ctl, bailing"); + return NULL; + } + + snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s", + snd_ctl_elem_info_get_name(info)); + snprintf(param_desc, sizeof(param_desc), "Value of ALSA control '%s'", + snd_ctl_elem_info_get_name(info)); + + // We don't have meaningful default values + switch (snd_ctl_elem_info_get_type(info)) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + return spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(param_name), + SPA_PROP_INFO_description, SPA_POD_String(param_desc), + SPA_PROP_INFO_type, SPA_POD_Bool(false), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + + case SND_CTL_ELEM_TYPE_INTEGER: + return spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(param_name), + SPA_PROP_INFO_description, SPA_POD_String(param_desc), + SPA_PROP_INFO_type, SPA_POD_Int(0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + + case SND_CTL_ELEM_TYPE_INTEGER64: + return spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(param_name), + SPA_PROP_INFO_description, SPA_POD_String(param_desc), + SPA_PROP_INFO_type, SPA_POD_Long(0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + + case SND_CTL_ELEM_TYPE_ENUMERATED: + return spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(param_name), + SPA_PROP_INFO_description, SPA_POD_String(param_desc), + SPA_PROP_INFO_type, SPA_POD_Int(0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + + default: + // FIXME: we can probably support bytes but the length seems unknown in the API + spa_log_warn(state->log, "%s ctl '%s' not supported", + snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)), + snd_ctl_elem_info_get_name(info)); + return NULL; + } +} + +struct spa_pod *spa_alsa_enum_propinfo(struct state *state, + uint32_t idx, struct spa_pod_builder *b) +{ + 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.disable-tsched"), + SPA_PROP_INFO_description, SPA_POD_String("Disable timer based scheduling"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_tsched), + 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.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 13: + 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 14: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.htimestamp"), + SPA_PROP_INFO_description, SPA_POD_String("Use hires timestamps"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->htimestamp), + 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("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 16: + 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 17: + 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; + case 18: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.htimestamp.max-errors"), + SPA_PROP_INFO_description, SPA_POD_String("Max errors before disabling htimestamp"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->htimestamp_max_errors, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + // While adding params here, update the math in default too + default: + idx -= 18; + if (idx <= state->num_bind_ctls) + param = enum_bind_ctl_propinfo(state, idx - 1, b); + else + return NULL; + } + return param; +} + +static void add_bind_ctl_param(struct state *state, const snd_ctl_elem_value_t *elem, const snd_ctl_elem_info_t *info, + struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; + char param_name[1024]; + unsigned int count = snd_ctl_elem_info_get_count(info); + int type = snd_ctl_elem_info_get_type(info); + bool is_array = count > 1 && type != SND_CTL_ELEM_TYPE_BYTES; + + snprintf(param_name, sizeof(param_name), "api.alsa.bind-ctl.%s", + snd_ctl_elem_info_get_name(info)); + spa_pod_builder_string(b, param_name); + + if (is_array) + spa_pod_builder_push_array(b, &f[0]); + + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + for (unsigned int i = 0; i < count; i++) + spa_pod_builder_bool(b, snd_ctl_elem_value_get_boolean(elem, i)); + break; + + case SND_CTL_ELEM_TYPE_INTEGER: + for (unsigned int i = 0; i < count; i++) + spa_pod_builder_int(b, snd_ctl_elem_value_get_integer(elem, i)); + break; + + case SND_CTL_ELEM_TYPE_INTEGER64: + for (unsigned int i = 0; i < count; i++) + spa_pod_builder_long(b, snd_ctl_elem_value_get_integer64(elem, i)); + break; + + case SND_CTL_ELEM_TYPE_ENUMERATED: + for (unsigned int i = 0; i < count; i++) + spa_pod_builder_int(b, snd_ctl_elem_value_get_enumerated(elem, i)); + break; + + case SND_CTL_ELEM_TYPE_BYTES: + spa_pod_builder_bytes(b, snd_ctl_elem_value_get_bytes(elem), count); + break; + + default: + spa_log_warn(state->log, "%s ctl '%s' not supported", + snd_ctl_elem_type_name(snd_ctl_elem_info_get_type(info)), + snd_ctl_elem_info_get_name(info)); + break; + } + + if (is_array) + spa_pod_builder_pop(b, &f[0]); +} + +static void add_bind_ctl_params(struct state *state, struct spa_pod_builder *b) +{ + int err; + + for (unsigned int i = 0; i < state->num_bind_ctls; i++) { + + if(!state->bound_ctls[i].value || !state->bound_ctls[i].info) + continue; + + err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value); + if (err < 0) { + spa_log_warn(state->log, "Could not read elem value for '%s': %s", + state->bound_ctls[i].name, snd_strerror(err)); + } + + add_bind_ctl_param(state, state->bound_ctls[i].value, state->bound_ctls[i].info, b); + } +} + +int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; + 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.disable-tsched"); + spa_pod_builder_bool(b, state->disable_tsched); + + 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, "api.alsa.htimestamp"); + spa_pod_builder_bool(b, state->htimestamp); + + spa_pod_builder_string(b, "api.alsa.htimestamp.max-errors"); + spa_pod_builder_int(b, state->htimestamp_max_errors); + + 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); + + add_bind_ctl_params(state, b); + + 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; +} + + +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, +}; + +static void silence_error_handler(const char *file, int line, + const char *function, int err, const char *fmt, ...) +{ +} + +static void fill_device_name(struct state *state, const char *params, char device_name[], size_t len) +{ + spa_scnprintf(device_name, len, "%s%s%s", + state->card && state->card->ucm_prefix ? state->card->ucm_prefix : "", + state->props.device, params ? params : ""); +} + +static void bind_ctl_event(struct spa_source *source) +{ + struct state *state = source->data; + snd_ctl_event_t *ev; + snd_ctl_elem_id_t *id, *bound_id; + snd_ctl_elem_value_t *old_value; + unsigned short revents; + int err; + + // Do the same demangling of revents we do for PCM pollfds + for (int i = 0; i < state->ctl_n_fds; i++) { + state->ctl_pfds[i].revents = state->ctl_sources[i].rmask; + state->ctl_sources[i].rmask = 0; + } + + err = snd_ctl_poll_descriptors_revents(state->ctl, state->ctl_pfds, state->ctl_n_fds, &revents); + if (SPA_UNLIKELY(err < 0)) { + spa_log_warn(state->log, "Could not read ctl revents: %s", snd_strerror(err)); + return; + } + + if (!revents) { + spa_log_trace(state->log, "Got a bind ctl wakeup but no actual event"); + return; + } + + snd_ctl_event_alloca(&ev); + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_id_alloca(&bound_id); + snd_ctl_elem_value_alloca(&old_value); + + while ((err = snd_ctl_read(state->ctl, ev) > 0)) { + bool changed = false; + + if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM) + continue; + + snd_ctl_event_elem_get_id(ev, id); + + for (unsigned int i = 0; i < state->num_bind_ctls; i++) { + int err; + + if(!state->bound_ctls[i].value || !state->bound_ctls[i].info) + continue; + + // Check if we have the right element + snd_ctl_elem_value_get_id(state->bound_ctls[i].value, bound_id); + if (snd_ctl_elem_id_compare_set(id, bound_id) || + snd_ctl_elem_id_compare_numid(id, bound_id)) { + continue; + } + + snd_ctl_elem_value_copy(old_value, state->bound_ctls[i].value); + + err = snd_ctl_elem_read(state->ctl, state->bound_ctls[i].value); + if (err < 0) { + spa_log_warn(state->log, "Could not read ctl '%s': %s", + state->bound_ctls[i].name, snd_strerror(err)); + continue; + } + + if (snd_ctl_elem_value_compare(old_value, state->bound_ctls[i].value) != 0) { + // We don't need to check all the ctls, if one changed, + // we'll emit a notification and they'll be read when + // the props are read + spa_log_debug(state->log, "bound ctl '%s' has changed", state->bound_ctls[i].name); + changed = true; + break; + } + } + + if (changed) { + state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + state->params[NODE_Props].user++; + spa_alsa_emit_node_info(state, false); + } + } + + if (err < 0 && err != -EAGAIN) + spa_log_warn(state->log, "Could not read ctl: %s", snd_strerror(err)); +} + +static void fetch_bind_ctls(struct state *state) +{ + snd_ctl_elem_list_t* element_list; + unsigned int elem_count = 0; + int err; + + if (!state->num_bind_ctls) + return; + + snd_ctl_elem_list_alloca(&element_list); + + /* Get number of elements */ + err = snd_ctl_elem_list(state->ctl, element_list); + if (SPA_UNLIKELY(err < 0)) { + spa_log_warn(state->log, "Couldn't get elem list count. Error: %s", + snd_strerror(err)); + return; + } + + elem_count = snd_ctl_elem_list_get_count(element_list); + err = snd_ctl_elem_list_alloc_space(element_list, elem_count); + if (SPA_UNLIKELY(err < 0)) { + spa_log_error(state->log, "Couldn't allocate elem_list space. Error: %s", + snd_strerror(err)); + return; + } + + /* Get identifiers */ + err = snd_ctl_elem_list(state->ctl, element_list); + if (SPA_UNLIKELY(err < 0)) { + spa_log_warn(state->log, "Couldn't get elem list. Error: %s", + snd_strerror(err)); + goto cleanup; + } + + + for (unsigned int i = 0; i < state->num_bind_ctls; i++) { + unsigned int numid = 0; + + for (unsigned int j = 0; j < elem_count; j++) { + const char* element_name = snd_ctl_elem_list_get_name(element_list, j); + + if (!strcmp(element_name, state->bound_ctls[i].name)) { + numid = snd_ctl_elem_list_get_numid(element_list, j); + break; + } + } + + /* zero = invalid numid */ + if (SPA_UNLIKELY(!numid)) { + spa_log_warn(state->log, "Didn't find ctl: '%s', count: %u", + state->bound_ctls[i].name, elem_count); + continue; + } + + snd_ctl_elem_info_malloc(&state->bound_ctls[i].info); + snd_ctl_elem_info_set_numid(state->bound_ctls[i].info, numid); + + err = snd_ctl_elem_info(state->ctl, state->bound_ctls[i].info); + if (SPA_UNLIKELY(err < 0)) { + spa_log_warn(state->log, "Could not read elem info for '%s': %s", + state->bound_ctls[i].name, snd_strerror(err)); + + snd_ctl_elem_info_free(state->bound_ctls[i].info); + state->bound_ctls[i].info = NULL; + continue; + } + + snd_ctl_elem_value_malloc(&state->bound_ctls[i].value); + snd_ctl_elem_value_set_numid(state->bound_ctls[i].value, numid); + + spa_log_debug(state->log, "Binding ctl for '%s'", + snd_ctl_elem_info_get_name(state->bound_ctls[i].info)); + } + +cleanup: + snd_ctl_elem_list_free_space(element_list); +} + +int open_card_ctl(struct state *state) +{ + int err; + char card_name[256]; + + snprintf(card_name, sizeof(card_name), "hw:%d", state->card_index); + spa_log_debug(state->log, "Trying to open ctl device '%s'", card_name); + + err = snd_ctl_open(&state->ctl, card_name, SND_CTL_NONBLOCK); + if (err < 0) { + spa_log_info(state->log, "%s could not find ctl card: %s", + card_name, snd_strerror(err)); + return err; + } + + return 0; +} + +static void bind_ctls_for_params(struct state *state) +{ + int err; + + if (state->num_bind_ctls == 0) + return; + + if (!state->ctl) { + err = open_card_ctl(state); + if (err < 0) + return; + } + + state->ctl_n_fds = snd_ctl_poll_descriptors_count(state->ctl); + if (state->ctl_n_fds > (int)SPA_N_ELEMENTS(state->ctl_sources)) { + spa_log_warn(state->log, "Too many poll descriptors (%d), listening to a subset", state->ctl_n_fds); + state->ctl_n_fds = SPA_N_ELEMENTS(state->ctl_sources); + } + + if ((err = snd_ctl_poll_descriptors(state->ctl, state->ctl_pfds, state->ctl_n_fds)) < 0) { + spa_log_warn(state->log, "Could not get poll descriptors: %s", snd_strerror(err)); + return; + } + + snd_ctl_subscribe_events(state->ctl, 1); + + for (int i = 0; i < state->ctl_n_fds; i++) { + state->ctl_sources[i].func = bind_ctl_event; + state->ctl_sources[i].data = state; + state->ctl_sources[i].fd = state->ctl_pfds[i].fd; + state->ctl_sources[i].mask = SPA_IO_IN; + state->ctl_sources[i].rmask = 0; + spa_loop_add_source(state->main_loop, &state->ctl_sources[i]); + } + + fetch_bind_ctls(state); +} + +int spa_alsa_init(struct state *state, const struct spa_dict *info) +{ + uint32_t i; + int err; + const char *str; + + spa_list_init(&state->followers); + spa_list_init(&state->rt.followers); + + snd_config_update_free_global(); + + if ((str = spa_dict_lookup(info, "device.profile.pro")) != NULL) + state->is_pro = spa_atob(str); + + state->multi_rate = true; + state->htimestamp = false; + state->htimestamp_max_errors = MAX_HTIMESTAMP_ERROR; + state->card_index = SPA_ID_INVALID; + + 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); + if (state->open_ucm) + state->props.use_chmap = true; + } else if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &state->quantum_limit, 0); + } else if (spa_streq(k, SPA_KEY_API_ALSA_BIND_CTLS)) { + struct spa_json it[1]; + char v[256]; + unsigned int i = 0; + + /* Read a list of ALSA control names to bind as params */ + if (spa_json_begin_array_relax(&it[0], s, strlen(s)) <= 0) + continue; + + while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 && + i < SPA_N_ELEMENTS(state->bound_ctls)) { + snprintf(state->bound_ctls[i].name, + sizeof(state->bound_ctls[i].name), "%s", v); + i++; + } + state->num_bind_ctls = i; + + /* We'll do the actual binding after checking the card exists */ + } else { + alsa_set_param(state, k, s); + } + } + + if (state->card_index == SPA_ID_INVALID) { + /* If we don't have a card index, see if we have a *: string */ + sscanf(state->props.device, "%*[^:]:%u", &state->card_index); + if (state->card_index == SPA_ID_INVALID) { + spa_log_info(state->log, "Could not determine card index. %s and/or clock.name " + "may need to be configured manually", SPA_KEY_API_ALSA_PCM_CARD); + } + } + + if (state->clock_name[0] == '\0' && state->card_index != SPA_ID_INVALID) + snprintf(state->clock_name, sizeof(state->clock_name), + "api.alsa.%s-%u", + state->stream == SND_PCM_STREAM_PLAYBACK ? "p" : "c", + 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, state->is_split_parent); + + state->log_file = fopencookie(state, "w", io_funcs); + if (state->log_file == NULL) { + spa_log_error(state->log, "can't create log file"); + return -errno; + } + CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed"); + + spa_list_append(&states, &state->link); + + state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + state->rate_limit.burst = 1; + + bind_ctls_for_params(state); + + return 0; +} + +int spa_alsa_clear(struct state *state) +{ + int err; + struct state *follower; + + spa_list_remove(&state->link); + release_card(state->card); + + if (state->driver != NULL) { + spa_list_remove(&state->driver_link); + state->driver = NULL; + } + if (state->rt.driver != NULL) { + spa_list_remove(&state->rt.driver_link); + state->rt.driver = NULL; + } + spa_list_consume(follower, &state->followers, driver_link) { + spa_list_remove(&follower->driver_link); + follower->driver = NULL; + } + spa_list_consume(follower, &state->rt.followers, rt.driver_link) { + spa_list_remove(&follower->rt.driver_link); + follower->rt.driver = NULL; + } + + 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); + + free(state->tag[0]); + free(state->tag[1]); + + if (state->ctl) { + for (int i = 0; i < state->ctl_n_fds; i++) { + spa_loop_remove_source(state->main_loop, &state->ctl_sources[i]); + } + + snd_ctl_close(state->ctl); + state->ctl = NULL; + + for (unsigned int i = 0; i < state->num_bind_ctls; i++) { + if (state->bound_ctls[i].info) { + snd_ctl_elem_info_free(state->bound_ctls[i].info); + state->bound_ctls[i].info = NULL; + } + if (state->bound_ctls[i].value) { + snd_ctl_elem_value_free(state->bound_ctls[i].value); + state->bound_ctls[i].value = NULL; + } + } + } + + return err; +} + +static int probe_pitch_ctl(struct state *state) +{ + snd_ctl_elem_id_t *id; + /* TODO: Add configuration params for the control name and units */ + const char *elem_name = + state->stream == SND_PCM_STREAM_CAPTURE ? + "Capture Pitch 1000000" : + "Playback Pitch 1000000"; + bool opened = false; + int err; + + snd_lib_error_set_handler(silence_error_handler); + + if (!state->ctl) { + err = open_card_ctl(state); + if (err < 0) + goto error; + + opened = true; + } + + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_id_set_name(id, elem_name); + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM); + + snd_ctl_elem_value_malloc(&state->pitch_elem); + snd_ctl_elem_value_set_id(state->pitch_elem, id); + + err = snd_ctl_elem_read(state->ctl, state->pitch_elem); + if (err < 0) { + spa_log_debug(state->log, "%s: did not find ctl: %s", + elem_name, snd_strerror(err)); + + snd_ctl_elem_value_free(state->pitch_elem); + state->pitch_elem = NULL; + + if (opened) { + snd_ctl_close(state->ctl); + state->ctl = NULL; + } + + goto error; + } + + snd_ctl_elem_value_set_integer(state->pitch_elem, 0, 1000000); + CHECK(snd_ctl_elem_write(state->ctl, state->pitch_elem), "snd_ctl_elem_write"); + state->last_rate = 1.0; + + spa_log_info(state->log, "found ctl %s", elem_name); + err = 0; +error: + snd_lib_error_set_handler(NULL); + return err; +} + +static int do_link(struct state *driver, struct state *state) +{ + int res; + snd_pcm_status_t *status; + + snd_pcm_status_alloca(&status); + snd_pcm_status(driver->hndl, status); + snd_pcm_status_dump(status, state->output); + snd_pcm_status(state->hndl, status); + snd_pcm_status_dump(status, state->output); + fflush(state->log_file); + + res = snd_pcm_link(driver->hndl, state->hndl); + if (res >= 0 || res == -EALREADY) + state->linked = true; + + spa_log_info(state->log, "%p: linked to driver %p: %u (%s)", + state, driver, state->linked, snd_strerror(res)); + return 0; +} + +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; + + fill_device_name(state, params, device_name, sizeof(device_name)); + spa_scnprintf(state->name, sizeof(state->name), "%s%s", + props->device, state->stream == SND_PCM_STREAM_CAPTURE ? "c" : "p"); + + 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 (!state->disable_tsched) { + 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; + } else { + /* ALSA pollfds may only be ready after setting swparams, so + * these are initialised in spa_alsa_start() */ + } + + state->opened = true; + state->sample_count = 0; + state->sample_time = 0; + + probe_pitch_ctl(state); + + return 0; + +error_exit_close: + spa_log_info(state->log, "%p: Device '%s' closing: %s", state, state->name, + spa_strerror(err)); + snd_pcm_close(state->hndl); + return err; +} + +static void try_unlink(struct state *state) +{ + struct state *follower; + + if (state->driver != NULL && state->linked) { + snd_pcm_unlink(state->hndl); + spa_log_info(state->log, "%p: unlinked from driver %p", + state, state->driver); + state->linked = false; + } + spa_list_for_each(follower, &state->followers, driver_link) { + if (follower->opened && follower->linked) { + snd_pcm_unlink(follower->hndl); + spa_log_info(state->log, "%p: follower unlinked from driver %p", + follower, state); + follower->linked = false; + } + } +} + +int spa_alsa_close(struct state *state) +{ + int err = 0; + + if (!state->opened) + return 0; + + try_unlink(state); + + spa_alsa_pause(state); + + spa_log_info(state->log, "%p: Device '%s' closing", state, state->name); + if ((err = snd_pcm_close(state->hndl)) < 0) + spa_log_warn(state->log, "%s: close failed: %s", state->name, + snd_strerror(err)); + + if (!state->disable_tsched) + spa_system_close(state->data_system, state->timerfd); + else + state->n_fds = 0; + + if (state->have_format && state->card) + state->card->format_ref--; + + state->have_format = false; + state->opened = false; + state->linked = false; + + if (state->pitch_elem) { + snd_ctl_elem_value_free(state->pitch_elem); + state->pitch_elem = NULL; + + // Close it unless we've got some bind_ctls we're listening to + if (state->ctl_n_fds == 0) { + snd_ctl_close(state->ctl); + state->ctl = NULL; + } + } + + return err; +} + +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 && 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.target_rate.denom : DEFAULT_RATE; + + rate = SPA_CLAMP(rate, min, max); + + spa_log_debug(state->log, "rate:%u multi:%d card:%u def:%d", + rate, state->multi_rate, state->card ? state->card->rate : 0, state->default_rate); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + 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; + } + + spa_log_debug(state->log, "%p: using chmap", state); + + 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; + spa_log_debug(state->log, "%p: using provided default", state); + } else if (min <= 8) { + map = &default_map[min]; + spa_log_debug(state->log, "%p: using default %d channel map", state, 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(¶ms); + 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->name, 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->name, 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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + 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->name, 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->name, buf); + return -ENOTSUP; + } + + 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(¶ms); + 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(¶ms); + 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; +} + +/* 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); +} + +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; +} + +static void recalc_headroom(struct state *state) +{ + uint32_t latency; + uint32_t rate = 0; + + if (state->position != NULL) + rate = state->position->clock.target_rate.denom; + + state->headroom = state->default_headroom; + if (!state->disable_tsched || state->resample) { + /* When using timers, we might miss the pointer update for batch + * devices so add some extra headroom. With IRQ, we know the pointers + * are updated when we wake up and we don't need the headroom. */ + if (state->is_batch) + state->headroom += state->period_frames; + /* Add 32 extra samples of headroom to handle jitter in capture. + * For IRQ, we don't need this because when we wake up, we have + * exactly enough samples to read or write. */ + if (state->stream == SND_PCM_STREAM_CAPTURE) + state->headroom = SPA_MAX(state->headroom, 32u); + } + if (SPA_LIKELY(state->buffer_frames >= state->threshold)) + state->headroom = SPA_MIN(state->headroom, state->buffer_frames - state->threshold); + else + state->headroom = 0; + + latency = SPA_MAX(state->min_delay, SPA_MIN(state->max_delay, state->headroom)); + if (rate != 0 && state->rate != 0) + latency = SPA_SCALE32_UP(latency, rate, state->rate); + + state->latency[state->port_direction].min_rate = + state->latency[state->port_direction].max_rate = latency; +} + +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; + char spdif_params[128] = ""; + uint32_t default_period; + + spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened, + state->have_format, state->started); + + state->use_mmap = !state->disable_mmap; + state->force_rate = false; + + 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_type_audio_iec958_codec_to_short_name(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); + state->force_rate = true; + 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->name); + 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(¶ms); + /* 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->name, 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->name, 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 && + 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->name, 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->name, 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; + + /* make sure we update threshold in check_position_config() because they depend + * on the samplerate. */ + state->driver_duration = 0; + state->driver_rate.denom = 0; + + state->have_format = true; + if (state->card && state->card->format_ref++ == 0) + state->card->rate = rrate; + + dir = 0; + period_size = state->default_period_size; + state->is_batch = snd_pcm_hw_params_is_batch(params) && !state->disable_batch; + + default_period = SPA_SCALE32_UP(DEFAULT_PERIOD, state->rate, DEFAULT_RATE); + default_period = flp2(2 * default_period - 1); + + /* no period size specified. If we are batch or not using timers, + * use the graph duration as the period */ + if (period_size == 0 && (state->is_batch || state->disable_tsched)) + period_size = state->position ? state->position->clock.target_duration : default_period; + if (period_size == 0) + period_size = default_period; + + if (!state->disable_tsched || state->resample) { + if (state->is_batch) { + /* 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; + } else { + /* disable ALSA wakeups */ + 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->name); + 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->name); + return -EIO; + } + + state->max_delay = state->buffer_frames / 2; + if (spa_strstartswith(state->props.device, "a52") || + spa_strstartswith(state->props.device, "dca") || + (spa_strstartswith(state->props.device, "plug:") && + strstr(state->props.device, "a52:"))) + state->min_delay = SPA_MIN(2048u, state->buffer_frames); + else + state->min_delay = 0; + + state->start_delay = state->default_start_delay; + + recalc_headroom(state); + + spa_log_info(state->log, "%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 batch:%u tsched:%u", + state->name, 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, + state->is_batch, !state->disable_tsched); + + /* write the parameters to device */ + CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params"); + + return match ? 0 : 1; +} + +int spa_alsa_update_rate_match(struct state *state) +{ + uint64_t pitch, last_pitch; + int err; + + if (!state->pitch_elem) + return -ENOENT; + + /* The rate/pitch defines the rate of input to output (if there were a + * resampler, it's the ratio of input samples to output samples). This + * means that to adjust the playback rate, we need to apply the inverse + * of the given rate. */ + if (state->stream == SND_PCM_STREAM_CAPTURE) { + pitch = (uint64_t)(1000000 * state->rate_match->rate); + last_pitch = (uint64_t)(1000000 * state->last_rate); + } else { + pitch = (uint64_t)(1000000 / state->rate_match->rate); + last_pitch = (uint64_t)(1000000 / state->last_rate); + } + + /* The pitch adjustment is limited to 1 ppm according to the spec, but + * let's avoid very granular changes so that we don't spam the host + * (and ourselves, if bind-ctls are enabled). */ + if (SPA_ABS((int)pitch - (int)last_pitch) < 10) + return 0; + + snd_ctl_elem_value_set_integer(state->pitch_elem, 0, pitch); + CHECK(snd_ctl_elem_write(state->ctl, state->pitch_elem), "snd_ctl_elem_write"); + + spa_log_trace_fp(state->log, "%s %u set rate to %g", + state->name, state->stream, state->rate_match->rate); + + state->last_rate = state->rate_match->rate; + + return 0; +} + +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(¶ms); + + /* 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"); + + if (state->disable_tsched) { + snd_pcm_uframes_t avail_min = 0; + + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + /* wake up when buffer has target frames or less data (will underrun soon) */ + if (state->buffer_frames >= (state->threshold + state->headroom)) + avail_min = state->buffer_frames - (state->threshold + state->headroom); + } else { + /* wake up when there's target frames or more (enough for us to read and push a buffer) */ + avail_min = SPA_MIN(state->threshold + state->headroom, state->buffer_frames); + } + + CHECK(snd_pcm_sw_params_set_avail_min(hndl, params, avail_min), "set_avail_min"); + } + + /* 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; +} + +static 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->name, 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->name, 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 void reset_buffers(struct state *this) +{ + uint32_t i; + + spa_list_init(&this->free); + spa_list_init(&this->ready); + this->ready_offset = 0; + + for (i = 0; i < this->n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + 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 do_prepare(struct state *state) +{ + int err; + + state->last_threshold = state->threshold; + + spa_log_debug(state->log, "%p: start threshold:%d duration:%d rate:%d follower:%d match:%d resample:%d", + state, state->threshold, state->driver_duration, state->driver_rate.denom, + state->following, state->matching, state->resample); + + CHECK(set_swparams(state), "swparams"); + + if ((!state->linked) && (err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) { + spa_log_error(state->log, "%s: snd_pcm_prepare error: %s", + state->name, snd_strerror(err)); + return err; + } + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + snd_pcm_uframes_t silence = state->start_delay + state->threshold + state->headroom; + if (state->disable_tsched) + silence += state->threshold; + spa_alsa_silence(state, silence); + } + + reset_buffers(state); + state->alsa_sync = true; + state->alsa_sync_warning = false; + state->alsa_started = false; + + return 0; +} + +static inline int do_drop(struct state *state) +{ + int res; + spa_log_debug(state->log, "%p: snd_pcm_drop linked:%u", state, state->linked); + if (!state->linked && (res = snd_pcm_drop(state->hndl)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_drop: %s", + state->name, snd_strerror(res)); + return res; + } + return 0; +} + +static inline int do_start(struct state *state) +{ + int res; + if (SPA_UNLIKELY(!state->alsa_started)) { + spa_log_debug(state->log, "%p: snd_pcm_start linked:%u", state, state->linked); + if (!state->linked && (res = snd_pcm_start(state->hndl)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_start: %s", + state->name, snd_strerror(res)); + return res; + } + state->alsa_started = true; + } + return 0; +} + +static inline int check_position_config(struct state *state, bool starting); + +static int alsa_recover(struct state *state) +{ + int res, st, retry = 0; + snd_pcm_status_t *status; + struct state *driver, *follower; + + 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->name, 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; + missing += state->start_delay + state->threshold + state->headroom; + + spa_log_trace(state->log, "%p: xrun of %"PRIu64" usec %"PRIu64, + state, delay, missing); + + if (state->clock) { + state->clock->xrun += SPA_SCALE32_UP(missing, + state->clock->rate.denom, state->rate); + } + spa_node_call_xrun(&state->callbacks, + SPA_TIMEVAL_TO_USEC(&trigger), delay, NULL); + break; + } + case SND_PCM_STATE_SUSPENDED: + spa_log_info(state->log, "%s: recover from state %s", + state->name, snd_pcm_state_name(st)); + while (retry++ < 5 && (res = snd_pcm_resume(state->hndl)) == -EAGAIN) + /* wait until suspend flag is released */ + poll(NULL, 0, 1000); + if (res >= 0) + return res; + /* try to drop and prepare below */ + break; + default: + spa_log_error(state->log, "%s: recover from error state %s", + state->name, snd_pcm_state_name(st)); + break; + } + +recover: + if (state->driver && state->linked) + driver = state->driver; + else + driver = state; + + do_drop(driver); + spa_list_for_each(follower, &driver->rt.followers, rt.driver_link) { + if (follower != driver && follower->linked) { + do_drop(follower); + check_position_config(follower, false); + } + } + do_prepare(driver); + spa_list_for_each(follower, &driver->rt.followers, rt.driver_link) { + if (follower != driver && follower->linked) + do_prepare(follower); + } + do_start(driver); + spa_list_for_each(follower, &driver->rt.followers, rt.driver_link) { + if (follower != driver && follower->linked) + do_start(follower); + } + return 0; +} + +static inline snd_pcm_sframes_t alsa_avail(struct state *state) +{ + snd_pcm_sframes_t avail; + if (!state->matching && state->disable_tsched && !state->resample) + avail = snd_pcm_avail_update(state->hndl); + else + avail = snd_pcm_avail(state->hndl); + return avail; +} + +static int get_avail(struct state *state, uint64_t current_time, snd_pcm_uframes_t *delay) +{ + int res, suppressed; + snd_pcm_sframes_t avail; + + if (SPA_UNLIKELY((avail = alsa_avail(state)) < 0)) { + if ((res = alsa_recover(state)) < 0) + return res; + if ((avail = alsa_avail(state)) < 0) { + if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d suppressed) snd_pcm_avail after recover: %s", + state->name, suppressed, snd_strerror(avail)); + } + avail = state->threshold * 2; + } + } + *delay = avail; + + if (state->htimestamp) { + snd_pcm_uframes_t havail; + snd_htimestamp_t tstamp; + uint64_t then; + + if ((res = snd_pcm_htimestamp(state->hndl, &havail, &tstamp)) < 0) { + if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d suppressed) snd_pcm_htimestamp error: %s", + state->name, suppressed, snd_strerror(res)); + } + return avail; + } + avail = havail; + *delay = havail; + if ((then = SPA_TIMESPEC_TO_NSEC(&tstamp)) != 0) { + int64_t diff; + + if (then < current_time) + diff = ((int64_t)(current_time - then)) * state->rate / SPA_NSEC_PER_SEC; + else + diff = -((int64_t)(then - current_time)) * state->rate / SPA_NSEC_PER_SEC; + + spa_log_trace_fp(state->log, "%"PRIu64" %"PRIu64" %"PRIi64, current_time, then, diff); + + if (SPA_ABS(diff) < state->threshold * 3) { + *delay += SPA_CLAMP(diff, -((int64_t)state->threshold), (int64_t)state->threshold); + state->htimestamp_error = 0; + } else if (state->htimestamp_max_errors) { + if (++state->htimestamp_error > state->htimestamp_max_errors) { + spa_log_error(state->log, "%s: wrong htimestamps from driver, disabling", + state->name); + state->htimestamp_error = 0; + state->htimestamp = false; + } + else if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d suppressed) impossible htimestamp diff:%"PRIi64, + state->name, suppressed, diff); + } + } + } + } + return avail; +} + +static int get_status(struct state *state, uint64_t current_time, snd_pcm_uframes_t *avail, + snd_pcm_uframes_t *delay, snd_pcm_uframes_t *target) +{ + int res; + snd_pcm_uframes_t a, d; + + if ((res = get_avail(state, current_time, &d)) < 0) + return res; + + a = SPA_MIN(res, (int)state->buffer_frames); + + 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) { + *avail = state->buffer_frames - a; + *delay = state->buffer_frames - SPA_MIN(d, state->buffer_frames); + *target = state->threshold + state->headroom; + } else { + *avail = a; + *delay = d; + *target = SPA_MAX(state->threshold, state->read_size) + state->headroom; + } + *target = SPA_CLAMP(*target, state->min_delay, state->max_delay); + 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, avg; + int32_t diff; + + if (state->disable_tsched && !follower) { + err = (int64_t)(current_time - state->next_time); + err = err / 1e9 * state->rate; + } else { + 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_resync) { + state->alsa_sync = true; + if (err > state->max_error) + err = state->max_error; + } else if (err < -state->max_resync) { + state->alsa_sync = true; + if (err < -state->max_error) + err = -state->max_error; + } + + if (!follower || state->matching) { + corr = spa_dll_update(&state->dll, err); + + avg = (state->err_avg * state->err_wdw + (err - state->err_avg)) / (state->err_wdw + 1.0); + state->err_var = (state->err_var * state->err_wdw + + (err - state->err_avg) * (err - avg)) / (state->err_wdw + 1.0); + state->err_avg = avg; + } else { + corr = 1.0; + } + + if (diff < 0) + state->next_time += (uint64_t)(diff / corr * 1e9 / state->rate); + + if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) { + double bw; + + state->base_time = state->next_time; + + bw = (fabs(state->err_avg) + sqrt(fabs(state->err_var)))/1000.0; + + spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f " + "bw:%f thr:%u del:%ld target:%ld err:%f max_err:%f max_resync: %f var:%f:%f:%f", + state->name, follower, state->matching, + corr, state->dll.bw, state->threshold, delay, target, + err, state->max_error, state->max_resync, state->err_avg, state->err_var, bw); + + spa_dll_set_bw(&state->dll, + SPA_CLAMPD(bw, 0.001, SPA_DLL_BW_MAX), + state->threshold, state->rate); + } + + if (state->rate_match) { + if (state->stream == SND_PCM_STREAM_PLAYBACK) + state->rate_match->rate = corr; + else + state->rate_match->rate = 1.0/corr; + + if (state->pitch_elem && state->matching) + spa_alsa_update_rate_match(state); + else + SPA_FLAG_UPDATE(state->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, state->matching); + } + + state->next_time += (uint64_t)(state->threshold / corr * 1e9 / state->rate); + + if (SPA_LIKELY(state->clock)) { + state->clock->nsec = current_time; + state->clock->rate = state->driver_rate; + state->clock->position += state->clock->duration; + state->clock->duration = state->driver_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 %ld %f %f %u", + state, follower, current_time, corr, delay, target, err, state->threshold * corr, + state->threshold); + + return 0; +} + +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 = !state->pitch_elem && + (((uint32_t)state->rate != state->driver_rate.denom) || state->matching); + recalc_headroom(state); + + spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d", + state->position->clock.name, state->driver_rate.denom, + state->clock_name, state->rate, + state->matching, state->resample); + return 0; +} + +static void update_sources(struct state *state, bool active) +{ + if (state->disable_tsched && state->rt.sources_added) { + for (int i = 0; i < state->n_fds; i++) { + state->source[i].mask = active ? state->pfds[i].events : 0; + spa_loop_update_source(state->data_loop, &state->source[i]); + } + } +} + +static inline int check_position_config(struct state *state, bool starting) +{ + uint64_t target_duration; + struct spa_fraction target_rate; + struct spa_io_position *pos; + + if (SPA_UNLIKELY((pos = state->position) == NULL)) + return 0; + + if (state->disable_tsched && (starting || state->started) && !state->following) { + target_duration = state->period_frames; + target_rate = SPA_FRACTION(1, state->rate); + pos->clock.target_duration = target_duration; + pos->clock.target_rate = target_rate; + } else { + target_duration = pos->clock.target_duration; + if (state->force_rate && !state->following) { + target_rate = SPA_FRACTION(1, state->rate); + pos->clock.target_rate = target_rate; + } else { + target_rate = pos->clock.target_rate; + } + } + if (target_duration == 0 || target_rate.denom == 0) + return -EIO; + + if (SPA_UNLIKELY((state->driver_duration != target_duration) || + (state->driver_rate.denom != target_rate.denom))) { + spa_log_info(state->log, "%p: follower:%d duration:%u->%"PRIu64" rate:%d->%d", + state, state->following, state->driver_duration, target_duration, + state->driver_rate.denom, target_rate.denom); + + state->driver_duration = target_duration; + state->driver_rate = target_rate; + state->threshold = SPA_SCALE32_UP(state->driver_duration, state->rate, state->driver_rate.denom); + state->max_error = SPA_MAX(256.0f, (state->threshold + state->headroom) / 2.0f); + state->max_resync = SPA_MIN(state->threshold + state->headroom, state->max_error); + state->err_wdw = (double)state->driver_rate.denom/state->driver_duration; + state->resample = !state->pitch_elem && + (((uint32_t)state->rate != state->driver_rate.denom) || state->matching); + state->alsa_sync = true; + } + return 0; +} + +static int alsa_write_sync(struct state *state, uint64_t current_time) +{ + int res, suppressed; + snd_pcm_uframes_t avail, delay, target; + bool following = state->following; + + if (SPA_UNLIKELY((res = check_position_config(state, false)) < 0)) + return res; + + if (SPA_UNLIKELY((res = get_status(state, current_time, &avail, &delay, &target)) < 0)) { + spa_log_error(state->log, "get_status error: %s", spa_strerror(res)); + state->next_time += (uint64_t)(state->threshold * 1e9 / state->rate); + return res; + } + + if (SPA_UNLIKELY(!following && state->alsa_started && delay > target + state->max_error)) { + spa_log_trace(state->log, "%p: early wakeup %ld %lu %lu", state, + avail, 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, following)) < 0)) + return res; + + if (following && state->alsa_started && !state->linked) { + 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 ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) + lev = SPA_LOG_LEVEL_DEBUG; + + spa_log_lev(state->log, lev, "%s: follower avail:%lu delay:%ld " + "target:%ld thr:%u, resync (%d suppressed)", + state->name, avail, delay, + target, state->threshold, suppressed); + + if (avail > target) + snd_pcm_rewind(state->hndl, avail - target); + else if (avail < target) + spa_alsa_silence(state, target - avail); + avail = target; + spa_dll_init(&state->dll); + state->alsa_sync = false; + } else + state->alsa_sync_warning = true; + } + return 0; +} + +static int alsa_write_frames(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; + snd_pcm_sframes_t commitres; + int res = 0; + size_t frame_size = state->frame_size; + + total_written = 0; +again: + frames = state->buffer_frames; + 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->name, snd_strerror(res)); + alsa_recover(state); + return res; + } + spa_log_trace_fp(state->log, "%p: begin offset:%ld avail:%ld threshold:%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(channel_area_addr(&my_areas[i], off), + 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 offset:%ld written:%ld sample_count:%"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)) { + if (commitres == -EPIPE || commitres == -ESTRPIPE) { + spa_log_warn(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->name, snd_strerror(commitres)); + } else { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->name, snd_strerror(commitres)); + return res; + } + } + if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) { + spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld", + state->name, 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); + + update_sources(state, true); + + return 0; +} + +int spa_alsa_write(struct state *state) +{ + if (state->following && state->rt.driver == NULL) { + uint64_t current_time = state->position->clock.nsec; + alsa_write_sync(state, current_time); + } + return alsa_write_frames(state); +} + +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->name); + 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, + channel_area_addr(&my_areas[i], offset), + l0); + if (SPA_UNLIKELY(l1 > 0)) + spa_memcpy(SPA_PTROFF(d[i].data, l0, void), + channel_area_addr(&my_areas[i], 0), + 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; +} + +static int alsa_read_sync(struct state *state, uint64_t current_time) +{ + int res, suppressed; + snd_pcm_uframes_t avail, delay, target, max_read; + bool following = state->following; + + if (SPA_UNLIKELY(!state->alsa_started)) + return 0; + + if (SPA_UNLIKELY((res = check_position_config(state, false)) < 0)) + return res; + + if (SPA_UNLIKELY((res = get_status(state, current_time, &avail, &delay, &target)) < 0)) { + spa_log_error(state->log, "get_status error: %s", spa_strerror(res)); + state->next_time += (uint64_t)(state->threshold * 1e9 / state->rate); + return res; + } + + if (SPA_UNLIKELY(!following && avail < state->read_size)) { + spa_log_trace(state->log, "%p: early wakeup %ld %ld %ld %d", state, + delay, avail, target, state->read_size); + state->next_time = current_time + (state->read_size - avail) * SPA_NSEC_PER_SEC / + state->rate; + return -EAGAIN; + } + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, following)) < 0)) + return res; + + max_read = state->buffer_frames; + if (following && !state->linked) { + 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 ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) + lev = SPA_LOG_LEVEL_DEBUG; + + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u " + "resample:%d, resync (%d suppressed)", state->name, delay, + target, state->threshold, state->resample, suppressed); + + if (avail < target) + max_read = target - avail; + else if (avail > target) { + snd_pcm_forward(state->hndl, avail - target); + avail = target; + } + state->alsa_sync = false; + spa_dll_init(&state->dll); + } else + state->alsa_sync_warning = true; + + if (avail < state->read_size) + max_read = 0; + } + state->max_read = SPA_MIN(max_read, state->read_size); + return 0; +} + +static int alsa_read_frames(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + snd_pcm_uframes_t total_read = 0, avail; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t read, frames, offset; + snd_pcm_sframes_t commitres; + int res = 0; + + frames = state->max_read; + + if (state->use_mmap) { + avail = state->buffer_frames; + if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &avail)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->name, snd_strerror(res)); + alsa_recover(state); + return res; + } + spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld avail:%ld thres:%d", state, + offset, frames, avail, 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) { + enum spa_log_level lev; + + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_ERROR; + else + lev = SPA_LOG_LEVEL_INFO; + + spa_log_lev(state->log, lev, "%s: snd_pcm_mmap_commit error %lu %lu %lu: %s", + state->name, frames, avail, 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->name, commitres, read); + } + } + + state->sample_count += total_read; + + return 0; +} + +int spa_alsa_read(struct state *state) +{ + if (state->following && state->rt.driver == NULL) { + uint64_t current_time = state->position->clock.nsec; + alsa_read_sync(state, current_time); + } + return alsa_read_frames(state); +} + +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_UNLIKELY(spa_list_is_empty(&state->free))) { + spa_log_warn(state->log, "%s: no more buffers", state->name); + 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 playback_ready(struct state *state) +{ + struct spa_io_buffers *io = state->io; + + spa_log_trace_fp(state->log, "%p: %d", state, io->status); + + update_sources(state, false); + + io->status = SPA_STATUS_NEED_DATA; + return spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA); +} + +static int capture_ready(struct state *state) +{ + struct spa_io_buffers *io; + bool have_data; + + have_data = !spa_list_is_empty(&state->ready); + + io = state->io; + if (io != NULL && + (io->status != SPA_STATUS_HAVE_DATA || state->rate_match != NULL)) { + struct buffer *b; + + if (SPA_LIKELY(io->buffer_id < state->n_buffers)) + spa_alsa_recycle_buffer(state, io->buffer_id); + + if (SPA_LIKELY(have_data)) { + 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; + } else { + io->buffer_id = SPA_ID_INVALID; + } + spa_log_trace_fp(state->log, "%p: output buffer:%d", state, io->buffer_id); + } + if (have_data) + spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA); + return 0; +} + +static uint64_t get_time_ns(struct state *state) +{ + struct timespec now; + if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0) + return 0; + return SPA_TIMESPEC_TO_NSEC(&now); +} + +static inline int alsa_do_wakeup_work(struct state *state, uint64_t current_time) +{ + struct state *follower; + int res; + + /* first do all the sync */ + if (state->stream == SND_PCM_STREAM_CAPTURE) + res = alsa_read_sync(state, current_time); + else + res = alsa_write_sync(state, current_time); + /* we can get -EAGAIN when we need to wait some more */ + if (SPA_UNLIKELY(res == -EAGAIN)) + return res; + + spa_list_for_each(follower, &state->rt.followers, rt.driver_link) { + if (follower == state) + continue; + if (follower->stream == SND_PCM_STREAM_CAPTURE) + alsa_read_sync(follower, current_time); + else + alsa_write_sync(follower, current_time); + } + + /* then read this source, the sinks will be written to when the + * graph completes. We can't read other follower sources yet because + * the resampler first needs to run. */ + if (state->stream == SND_PCM_STREAM_CAPTURE) + alsa_read_frames(state); + + /* and then trigger the graph */ + if (state->stream == SND_PCM_STREAM_PLAYBACK) + playback_ready(state); + else + capture_ready(state); + + return 0; +} + +static void alsa_irq_wakeup_event(struct spa_source *source) +{ + struct state *state = source->data; + uint64_t current_time; + int res, err; + unsigned short revents; + snd_pcm_uframes_t havail; + snd_htimestamp_t tstamp; + + // First, take a snapshot of the wakeup time + current_time = get_time_ns(state); + // If the hi-res timestamps are working, we will get a timestamp that + // is earlier then current_time + if ((res = snd_pcm_htimestamp(state->hndl, &havail, &tstamp)) == 0) { + uint64_t htime = SPA_TIMESPEC_TO_NSEC(&tstamp); + if (htime < current_time) { + current_time = htime; + } + } + + for (int i = 0; i < state->n_fds; i++) { + state->pfds[i].revents = state->source[i].rmask; + /* Reset so that we only handle all our sources' events once */ + state->source[i].rmask = 0; + } + + /* ALSA poll fds need to be "demangled" to know whether it's a real wakeup */ + if (SPA_UNLIKELY(err = snd_pcm_poll_descriptors_revents(state->hndl, + state->pfds, state->n_fds, &revents))) { + spa_log_error(state->log, "Could not look up revents: %s", + snd_strerror(err)); + return; + } + + if (!revents) { + spa_log_trace_fp(state->log, "Woken up with no work to do"); + return; + } + if (revents & POLLERR) { + spa_log_trace_fp(state->log, "poll error"); + if ((res = alsa_recover(state)) < 0) + return; + } + alsa_do_wakeup_work(state, current_time); +} + +static void alsa_timer_wakeup_event(struct spa_source *source) +{ + struct state *state = source->data; + uint64_t expire, current_time; + int res, suppressed; + + 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; + } + } + current_time = state->next_time; + + alsa_do_wakeup_work(state, current_time); + + if (state->next_time > current_time + SPA_NSEC_PER_SEC || + current_time > state->next_time + SPA_NSEC_PER_SEC) { + if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_error(state->log, "%s: impossible timeout %" + PRIu64" %"PRIu64" %"PRIi64" %d %"PRIi64" (%d suppressed)", + state->name, current_time, state->next_time, + state->next_time - current_time, state->threshold, + state->sample_count, suppressed); + } + state->next_time = (uint64_t)(current_time + state->threshold * 1e9 / state->rate); + } + set_timeout(state, state->next_time); +} + +static void remove_sources(struct state *state) +{ + int i; + if (state->rt.sources_added) { + for (i = 0; i < state->n_fds; i++) + spa_loop_remove_source(state->data_loop, &state->source[i]); + state->rt.sources_added = false; + } +} + +static void add_sources(struct state *state) +{ + int i; + if (!state->rt.sources_added) { + for (i = 0; i < state->n_fds; i++) + spa_loop_add_source(state->data_loop, &state->source[i]); + state->rt.sources_added = true; + } +} + +static int do_state_sync(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct state *state = user_data; + struct rt_state *rt = &state->rt; + + if (state->started) { + state->next_time = get_time_ns(state); + + if (rt->driver != state->driver) { + spa_dll_init(&state->dll); + + if (rt->driver != NULL) + spa_list_remove(&rt->driver_link); + if (state->driver != NULL) + spa_list_append(&state->driver->rt.followers, &rt->driver_link); + rt->driver = state->driver; + spa_log_debug(state->log, "state:%p -> driver:%p", state, state->driver); + + if(state->linked && state->matching) + try_unlink(state); + } + if (state->following) { + remove_sources(state); + set_timeout(state, 0); + } else { + add_sources(state); + if (!state->disable_tsched) + set_timeout(state, state->next_time); + } + } else { + if (rt->driver) { + spa_list_remove(&rt->driver_link); + rt->driver = NULL; + } + if (!state->disable_tsched) + set_timeout(state, 0); + remove_sources(state); + } + return 0; +} + +int spa_alsa_prepare(struct state *state) +{ + struct state *follower; + int err; + + if (!state->opened) + return -EIO; + + spa_alsa_pause(state); + + if (state->prepared) + return 0; + + if (check_position_config(state, true) < 0) { + spa_log_error(state->log, "%s: invalid position config", state->name); + return -EIO; + } + if ((err = do_prepare(state)) < 0) + return err; + + spa_list_for_each(follower, &state->followers, driver_link) { + if (follower != state && !follower->matching) { + if (spa_alsa_prepare(follower) < 0) + continue; + if (!follower->linked && state->auto_link) + do_link(state, follower); + } + } + + state->prepared = true; + + return 0; +} + +int spa_alsa_start(struct state *state) +{ + struct state *follower; + int err; + + if (state->started) + return 0; + else if (!state->opened) + return -EIO; + + spa_alsa_prepare(state); + + if (!state->disable_tsched) { + /* Timer-based scheduling */ + state->source[0].func = alsa_timer_wakeup_event; + state->source[0].data = state; + state->source[0].fd = state->timerfd; + state->source[0].mask = SPA_IO_IN; + state->source[0].rmask = 0; + state->n_fds = 1; + } else { + /* ALSA period-based scheduling */ + err = snd_pcm_poll_descriptors_count(state->hndl); + if (err < 0) { + spa_log_error(state->log, "Could not get poll descriptor count: %s", + snd_strerror(err)); + return err; + } + if (err > MAX_POLL) { + spa_log_error(state->log, "Unsupported poll descriptor count: %d", err); + return -EIO; + } + state->n_fds = err; + + if ((err = snd_pcm_poll_descriptors(state->hndl, state->pfds, state->n_fds)) < 0) { + spa_log_error(state->log, "Could not get poll descriptors: %s", + snd_strerror(err)); + return err; + } + + /* We only add the source to the data loop if we're driving. + * This is done in add_sources() */ + for (int i = 0; i < state->n_fds; i++) { + state->source[i].func = alsa_irq_wakeup_event; + state->source[i].data = state; + state->source[i].fd = state->pfds[i].fd; + state->source[i].mask = state->pfds[i].events; + state->source[i].rmask = 0; + } + } + + spa_list_for_each(follower, &state->followers, driver_link) + if (follower != state) + spa_alsa_start(follower); + + /* start capture now. We should have some data when the timer or IRQ + * goes off later */ + if (state->stream == SND_PCM_STREAM_CAPTURE) { + if ((err = do_start(state)) < 0) + return err; + } + + /* playback will start after first write. Without tsched, we start + * right away so that the fds become active in poll right away. */ + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + if (state->disable_tsched || state->start_delay > 0) + if ((err = do_start(state)) < 0) + return err; + } + + state->started = true; + spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + + return 0; +} + +static struct state *find_state(uint32_t id) +{ + struct state *state; + spa_list_for_each(state, &states, link) { + if (state->clock != NULL && state->clock->id == id) + return state; + } + return NULL; +} + +int spa_alsa_reassign_follower(struct state *state) +{ + bool following, freewheel; + struct spa_io_position *pos = state->position; + struct spa_io_clock *clock = state->clock; + struct state *driver; + + if (clock != NULL) + spa_scnprintf(clock->name, sizeof(clock->name), "%s", state->clock_name); + + following = pos && clock && pos->clock.id != clock->id; + + driver = pos != NULL ? find_state(pos->clock.id) : NULL; + + if (driver != state->driver) { + spa_log_debug(state->log, "%p: reassign driver %p->%p", state, state->driver, driver); + if (state->driver != NULL) + spa_list_remove(&state->driver_link); + if (driver != NULL) { + spa_list_append(&driver->followers, &state->driver_link); + } + state->driver = driver; + } + if (following != state->following) { + spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following); + state->following = following; + } + setup_matching(state); + if (state->started) + spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + else if (state->want_started) + spa_alsa_start(state); + + freewheel = pos != NULL && SPA_FLAG_IS_SET(pos->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 (state->started) { + if (freewheel) + snd_pcm_pause(state->hndl, 1); + else + snd_pcm_pause(state->hndl, 0); + } + } + state->alsa_sync_warning = false; + return 0; +} + +int spa_alsa_pause(struct state *state) +{ + struct state *follower; + + if (!state->started) + return 0; + + spa_log_debug(state->log, "%p: pause", state); + + state->started = false; + spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + + spa_list_for_each(follower, &state->followers, driver_link) + spa_alsa_pause(follower); + + do_drop(state); + + state->prepared = false; + + return 0; +} + +void spa_alsa_emit_node_info(struct state *state, bool full) +{ + uint64_t old = full ? state->info.change_mask : 0; + + if (full) + state->info.change_mask = state->info_all; + if (state->info.change_mask) { + struct spa_dict_item items[7]; + uint32_t i, n_items = 0; + char latency[64] = "", period[64] = "", nperiods[64] = "", headroom[64] = ""; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, state->props.media_class); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + + if (state->have_format) + snprintf(latency, sizeof(latency), "%lu/%d", + state->buffer_frames / (2 * state->frame_scale), state->rate); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency[0] ? latency : NULL); + + if (state->have_format) + snprintf(period, sizeof(period), "%lu", state->period_frames); + else if (state->default_period_size) + snprintf(period, sizeof(period), "%u", state->default_period_size); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period[0] ? period : NULL); + + if (state->have_format) + snprintf(nperiods, sizeof(nperiods), "%lu", + state->period_frames != 0 ? state->buffer_frames / state->period_frames : 0); + else if (state->default_period_num) + snprintf(nperiods, sizeof(nperiods), "%u", state->default_period_size); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods[0] ? nperiods : NULL); + + if (state->have_format) + snprintf(headroom, sizeof(headroom), "%u", state->headroom); + else if (state->default_headroom) + snprintf(headroom, sizeof(headroom), "%u", state->default_headroom); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom[0] ? headroom : NULL); + + state->info.props = &SPA_DICT_INIT(items, n_items); + + if (state->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < state->info.n_params; i++) { + if (state->params[i].user > 0) { + state->params[i].flags ^= SPA_PARAM_INFO_SERIAL; + state->params[i].user = 0; + } + } + } + spa_node_emit_info(&state->hooks, &state->info); + + state->info.change_mask = old; + } +} + +void spa_alsa_emit_port_info(struct state *state, bool full) +{ + uint64_t old = full ? state->port_info.change_mask : 0; + + if (full) + state->port_info.change_mask = state->port_info_all; + if (state->port_info.change_mask) { + uint32_t i; + static const struct spa_dict_item info_items[] = { + { SPA_KEY_PORT_GROUP, "stream.0" }, + }; + state->port_info.props = &SPA_DICT_INIT_ARRAY(info_items); + if (state->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < state->port_info.n_params; i++) { + if (state->port_params[i].user > 0) { + state->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL; + state->port_params[i].user = 0; + } + } + } + spa_node_emit_port_info(&state->hooks, + state->stream == SND_PCM_STREAM_PLAYBACK ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT, + 0, &state->port_info); + state->port_info.change_mask = old; + } +} diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h new file mode 100644 index 0000000..bd77abc --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm.h @@ -0,0 +1,365 @@ +/* Spa ALSA Sink */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ALSA_UTILS_H +#define SPA_ALSA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alsa.h" + + +#define MAX_RATES 16 + +#define DEFAULT_PERIOD 1024u +#define DEFAULT_RATE 48000u +#define DEFAULT_CHANNELS 2u +/* CHMAP defaults to true when using UCM */ +#define DEFAULT_USE_CHMAP false + +#define MAX_HTIMESTAMP_ERROR 64 + +struct props { + char device[64]; + char device_name[128]; + char card_name[128]; + char media_class[128]; + bool use_chmap; +}; + +#define MAX_BUFFERS 32 +#define MAX_POLL 16 + +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 rt_state { + struct spa_list followers; + struct state *driver; + struct spa_list driver_link; + + unsigned int sources_added:1; + unsigned int following:1; +}; + +struct bound_ctl { + char name[256]; + snd_ctl_elem_info_t *info; + snd_ctl_elem_value_t *value; +}; + +struct state { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_system *data_system; + struct spa_loop *data_loop; + struct spa_loop *main_loop; + + FILE *log_file; + struct spa_ratelimit rate_limit; + + uint32_t card_index; + struct card *card; + snd_pcm_stream_t stream; + snd_output_t *output; + char name[64]; + + 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; + + unsigned int opened:1; + unsigned int prepared:1; + unsigned int started:1; + unsigned int want_started:1; + 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:1; + unsigned int disable_batch:1; + unsigned int disable_tsched:1; + unsigned int is_split_parent:1; + char clock_name[64]; + uint32_t quantum_limit; + + 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 delay; + uint32_t read_size; + uint32_t max_read; + + 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 PORT_Tag 6 +#define N_PORT_PARAMS 7 + 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; + + /* Either a single source for tsched, or a set of pollfds from ALSA */ + struct spa_source source[MAX_POLL]; + int timerfd; + struct pollfd pfds[MAX_POLL]; + int n_fds; + uint32_t threshold; + uint32_t last_threshold; + uint32_t headroom; + uint32_t start_delay; + uint32_t min_delay; + uint32_t max_delay; + uint32_t htimestamp_error; + uint32_t htimestamp_max_errors; + + struct spa_fraction driver_rate; + uint32_t driver_duration; + + unsigned int alsa_started:1; + unsigned int alsa_sync:1; + unsigned int alsa_sync_warning: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; + unsigned int htimestamp:1; + unsigned int is_pro:1; + unsigned int sources_added:1; + unsigned int auto_link:1; + unsigned int linked:1; + unsigned int is_batch:1; + unsigned int force_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; + double max_resync; + double err_avg, err_var, err_wdw; + + struct spa_latency_info latency[2]; + struct spa_process_latency_info process_latency; + + struct spa_pod *tag[2]; + + /* for rate match and bind ctls */ + snd_ctl_t *ctl; + + /* Rate match via an ALSA ctl */ + snd_ctl_elem_value_t *pitch_elem; + double last_rate; + + /* ALSA ctls exposed as params */ + unsigned int num_bind_ctls; + struct bound_ctl bound_ctls[16]; + struct pollfd ctl_pfds[MAX_POLL]; + struct spa_source ctl_sources[MAX_POLL]; + int ctl_n_fds; + + struct spa_list link; + + struct spa_list followers; + struct state *driver; + struct spa_list driver_link; + + struct rt_state rt; +}; + +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_update_rate_match(struct state *state); + +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_prepare(struct state *state); +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); + +void spa_alsa_emit_node_info(struct state *state, bool full); +void spa_alsa_emit_port_info(struct state *state, bool full); + +static inline void spa_alsa_parse_position(struct channel_map *map, const char *val, size_t len) +{ + spa_audio_parse_position(val, len, map->pos, &map->channels); +} + +static inline uint32_t spa_alsa_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len) +{ + return spa_json_str_array_uint32(val, len, rates, max); +} + +static inline uint32_t spa_alsa_iec958_codec_from_name(const char *name) +{ + return spa_type_audio_iec958_codec_from_short_name(name); +} + +static inline void spa_alsa_parse_iec958_codecs(uint64_t *codecs, const char *val, size_t len) +{ + struct spa_json it[1]; + char v[256]; + + if (spa_json_begin_array_relax(&it[0], val, len) <= 0) + return; + + *codecs = 0; + while (spa_json_get_string(&it[0], 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; +} + +/* This function is also as snd_pcm_channel_area_addr() since 1.2.6 which is not yet + * in ubuntu and I can't figure out how to do the ALSA version check. */ +static inline void *channel_area_addr(const snd_pcm_channel_area_t *area, snd_pcm_uframes_t offset) +{ + return (char *)area->addr + (area->first + area->step * offset) / 8; +} + +#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..7ec3932 --- /dev/null +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -0,0 +1,1011 @@ +/* Spa ALSA Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alsa-seq.h" + +#define DEFAULT_DEVICE "default" +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" +#define DEFAULT_DISABLE_LONGNAME true + +static void reset_props(struct props *props) +{ + strncpy(props->device, DEFAULT_DEVICE, sizeof(props->device)); + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->disable_longname = DEFAULT_DISABLE_LONGNAME; +} + +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" }, + { "priority.driver", "1" }, +}; + +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[6]; + uint32_t n_items = 0; + int card_id; + snd_seq_port_info_t *info; + snd_seq_client_info_t *client_info; + const char *client_name, *port_name, *dir, *pn; + char prefix[32] = ""; + char card[8]; + char name[256]; + char path[128]; + char alias[128]; + char stream[32]; + + 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); + + card_id = snd_seq_client_info_get_card(client_info); + client_name = snd_seq_client_info_get_name(client_info); + port_name = snd_seq_port_info_get_name(info); + dir = port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback"; + + if (!this->props.disable_longname) + snprintf(prefix, sizeof(prefix), "[%d:%d] ", + port->addr.client, port->addr.port); + + pn = port_name; + if (spa_strstartswith(pn, client_name)) + pn += strlen(client_name); + + snprintf(name, sizeof(name), "%s%s%s (%s)", prefix, + client_name, pn, dir); + clean_name(name); + + snprintf(stream, sizeof(stream), "client_%d", port->addr.client); + clean_name(stream); + + snprintf(path, sizeof(path), "alsa:seq:%s:%s:%s_%d", + this->props.device, stream, dir, port->addr.port); + clean_name(path); + + snprintf(alias, sizeof(alias), "%s:%s", client_name, port_name); + clean_name(alias); + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, stream); + if (card_id != -1) { + snprintf(card, sizeof(card), "%d", card_id); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card); + } + port->info.props = &SPA_DICT_INIT(items, n_items); + + 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), + SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(1u<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), + SPA_FORMAT_CONTROL_types, SPA_POD_Int(1u<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->quantum_limit, 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 }; + uint32_t types; + + 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; + + if ((err = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_CONTROL_types, SPA_POD_Int(&types))) < 0) + return err; + if (types != 1u << SPA_CONTROL_UMP) + return -EINVAL; + + port->current_format = info; + port->have_format = true; + } + + 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 = 0; + + 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 (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; + + 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); + + this->quantum_limit = 8192; + this->min_pool_size = 500; + this->max_pool_size = 2000; + this->ump = 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)) { + 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, "clock.quantum-limit")) { + spa_atou32(s, &this->quantum_limit, 0); + } else if (spa_streq(k, SPA_KEY_API_ALSA_DISABLE_LONGNAME)) { + this->props.disable_longname = spa_atob(s); + } else if (spa_streq(k, "api.alsa.seq.min-pool")) { + spa_atou32(s, &this->min_pool_size, 0); + } else if (spa_streq(k, "api.alsa.seq.max-pool")) { + spa_atou32(s, &this->max_pool_size, 0); + } else if (spa_streq(k, "api.alsa.seq.ump")) { + this->ump = 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 " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Bridge midi ports with the alsa sequencer API" }, + { SPA_KEY_FACTORY_USAGE, + "["SPA_KEY_API_ALSA_PATH"=] " + "[ clock.name=] " + "[ clock.quantum-limit=] " + "["SPA_KEY_API_ALSA_DISABLE_LONGNAME"=] " + "[ api.alsa.seq.min-pool=] " + "[ api.alsa.seq.max-pool=]" + "[ api.alsa.seq.ump = ]" + }, +}; + +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..eb12bde --- /dev/null +++ b/spa/plugins/alsa/alsa-seq.c @@ -0,0 +1,1212 @@ +/* Spa ALSA Sequencer */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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, bool probe_ump) +{ + 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; + + if (!state->ump) { + spa_log_info(state->log, "%p: ALSA UMP MIDI disabled", state); + return 0; + } + +#ifdef HAVE_ALSA_UMP + res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0); + if (!res) { + snd_seq_client_info_t *info = NULL; + + /* Double check client version */ + res = snd_seq_client_info_malloc(&info); + if (!res) + res = snd_seq_get_client_info(conn->hndl, info); + if (!res) { + res = snd_seq_client_info_get_midi_version(info); + if (res == SND_SEQ_CLIENT_UMP_MIDI_2_0) + res = 0; + else + res = -EIO; + } + if (info) + snd_seq_client_info_free(info); + } +#else + res = -EOPNOTSUPP; +#endif + + if (res < 0) { + spa_log_lev(state->log, (probe_ump ? SPA_LOG_LEVEL_INFO : SPA_LOG_LEVEL_ERROR), + "%p: ALSA failed to enable UMP MIDI: %s", state, snd_strerror(res)); + if (!probe_ump) { + snd_seq_close(conn->hndl); + return res; /* either all are UMP or none are UMP */ + } + + state->ump = false; + } else { + spa_log_debug(state->log, "%p: ALSA UMP MIDI enabled", state); + state->ump = true; + } + + 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); +} + +#ifdef HAVE_ALSA_UMP +static void debug_ump_event(struct seq_state *state, snd_seq_ump_event_t *ev) +{ + if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) + return; + + 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); +} +#endif + +static void alsa_seq_on_sys(struct spa_source *source) +{ + struct seq_state *state = source->data; + const bool ump = state->ump; + int res; + + while (1) { + const snd_seq_addr_t *addr; + snd_seq_event_type_t type; + + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev; + + res = snd_seq_ump_event_input(state->sys.hndl, &ev); + if (res <= 0) + break; + + debug_ump_event(state, ev); + + addr = &ev->data.addr; + type = ev->type; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev; + + res = snd_seq_event_input(state->sys.hndl, &ev); + if (res <= 0) + break; + + debug_event(state, ev); + + addr = &ev->data.addr; + type = ev->type; + } + + if (addr->client == state->event.addr.client) + continue; + + switch (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", + type, addr->client, addr->port); + break; + + } + } +} + +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; + snd_seq_client_pool_t *pool; + struct seq_conn reserve[16]; + size_t pool_size; + + 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, "open %d", i); + if ((res = seq_open(state, &reserve[i], false, (i == 0))) < 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)); + } + + /* Increase client pool sizes. This determines the max sysex message that + * can be received. */ + snd_seq_client_pool_alloca(&pool); + if ((res = snd_seq_get_client_pool(state->event.hndl, pool)) < 0) { + spa_log_warn(state->log, "failed to get pool: %s", snd_strerror(res)); + } else { + /* make sure we at least use the default size */ + pool_size = snd_seq_client_pool_get_output_pool(pool); + pool_size = SPA_MAX(pool_size, snd_seq_client_pool_get_input_pool(pool)); + + /* The pool size is in cells, which are about 24 bytes long. Try to + * make sure we can fit sysex of at least twice the quantum limit. */ + pool_size = SPA_MAX(pool_size, state->quantum_limit * 2 / 24); + /* The kernel ignores values larger than 2000 (by default) so clamp + * this here. It's configurable in case the kernel was modified. */ + pool_size = SPA_CLAMP(pool_size, state->min_pool_size, state->max_pool_size); + + snd_seq_client_pool_set_input_pool(pool, pool_size); + snd_seq_client_pool_set_output_pool(pool, pool_size); + + if ((res = snd_seq_set_client_pool(state->event.hndl, pool)) < 0) { + spa_log_warn(state->log, "failed to set pool: %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) +{ + struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; + const bool ump = state->ump; + uint32_t i; + uint32_t *data; + uint8_t midi1_data[MAX_EVENT_SIZE]; + uint32_t ump_data[MAX_EVENT_SIZE]; + long size; + int res = -1; + + /* copy all new midi events into their port buffers */ + while (1) { + const snd_seq_addr_t *addr; + struct seq_port *port; + uint64_t ev_time, diff; + uint32_t offset; + void *event; + uint8_t *midi1_ptr; + size_t midi1_size = 0; + uint64_t ump_state = 0; + snd_seq_event_type_t SPA_UNUSED type; + + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev; + + res = snd_seq_ump_event_input(state->event.hndl, &ev); + if (res <= 0) + break; + + debug_ump_event(state, ev); + + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev; + + res = snd_seq_event_input(state->event.hndl, &ev); + if (res <= 0) + break; + + debug_event(state, ev); + + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; + } + + 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; + } + + /* queue_time is the estimated current time of the queue as calculated by + * the DLL. Calculate the age of the event. */ + 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; + + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev = event; + + data = (uint32_t*)&ev->ump[0]; + size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev = event; + + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } + + midi1_ptr = midi1_data; + midi1_size = size; + } + + do { + if (!ump) { + data = ump_data; + size = spa_ump_from_midi(&midi1_ptr, &midi1_size, + ump_data, sizeof(ump_data), 0, &ump_state); + if (size <= 0) + break; + } + + spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", + type, ev_time, offset, size, addr->client, addr->port); + + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&port->builder, data, size); + + /* make sure we can fit at least one control event of max size otherwise + * we keep the event in the queue and try to copy it in the next cycle */ + if (port->builder.state.offset + + sizeof(struct spa_pod_control) + + MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) + goto done; + + } while (!ump); + } + +done: + if (res < 0 && res != -EAGAIN) + spa_log_warn(state->log, "event read failed: %s", snd_strerror(res)); + + /* 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; + + if (port->builder.state.offset > port->buffer->buf->datas[0].maxsize) { + spa_log_warn(state->log, "control overflow: %d > %d", + port->builder.state.offset, + port->buffer->buf->datas[0].maxsize); + } + + /* 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]; + const bool ump = state->ump; + 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; + 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) { + size_t body_size; + uint8_t *body; + + if (c->type != SPA_CONTROL_UMP) + continue; + + body = SPA_POD_BODY(&c->value); + body_size = SPA_POD_BODY_SIZE(&c->value); + + 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; + + spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%zd port:%d.%d", + out_time, c->offset, body_size, port->addr.client, port->addr.port); + + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t ev; + + snd_seq_ump_ev_clear(&ev); + snd_seq_ev_set_ump_data(&ev, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t ev; + uint8_t data[MAX_EVENT_SIZE]; + int size; + + if ((size = spa_ump_to_midi((uint32_t *)body, body_size, data, sizeof(data))) <= 0) + continue; + + snd_seq_ev_clear(&ev); + + snd_midi_event_reset_encode(stream->codec); + if ((size = snd_midi_event_encode(stream->codec, data, size, &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); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + 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 (SPA_LIKELY(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 q1, q2; + + /* 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->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; + state->queue_next = queue_real; + } + + /* track our estimated elapsed time against the real elapsed queue time */ + q1 = NSEC_TO_CLOCK(&state->rate, state->queue_next); + q2 = NSEC_TO_CLOCK(&state->rate, queue_real); + err = ((int64_t)q1 - (int64_t) q2); + + if (fabs(err) > state->threshold) + spa_dll_init(&state->dll); + + err = SPA_CLAMP(err, -64, 64); + corr = spa_dll_update(&state->dll, err); + + /* this is our current estimated queue time and rate */ + state->queue_time = state->queue_next; + state->queue_corr = corr; + + /* make a new estimated queue time with the current quantum, if we are following, + * use the rate correction, else we will use the rate correction only for the new + * timeout. */ + if (state->following) + state->queue_next += (uint64_t)(state->threshold * corr * 1e9 / state->rate.denom); + else + state->queue_next += (uint64_t)(state->threshold * 1e9 / state->rate.denom); + + 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 += (uint64_t)(state->threshold / corr * 1e9 / state->rate.denom); + + if (SPA_LIKELY(state->clock)) { + state->clock->nsec = nsec; + state->clock->rate = state->rate; + state->clock->position += state->clock->duration; + state->clock->duration = state->duration; + state->clock->delay = (int64_t)(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); + + if (SPA_LIKELY(state->position)) { + struct spa_io_clock *clock = &state->position->clock; + state->rate = clock->target_rate; + if (state->rate.num == 0 || state->rate.denom == 0) + state->rate = SPA_FRACTION(1, 48000); + state->duration = clock->target_duration; + } else { + state->rate = SPA_FRACTION(1, 48000); + state->duration = 1024; + } + state->threshold = state->duration; + + 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->queue_time = 0; + state->queue_corr = 1.0; + spa_dll_init(&state->dll); + 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); + + res = set_timers(state); + + return res; +} + +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; + int res; + + if ((res = set_timers(state)) < 0) + spa_log_error(state->log, "can't set timers: %s", spa_strerror(res)); + 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..274311c --- /dev/null +++ b/spa/plugins/alsa/alsa-seq.h @@ -0,0 +1,191 @@ +/* Spa ALSA Sequencer */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ALSA_SEQ_H +#define SPA_ALSA_SEQ_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "config.h" + +#include +#ifdef HAVE_ALSA_UMP +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "alsa.h" + + +struct props { + char device[64]; + char clock_name[64]; + bool disable_longname; +}; + +#define MAX_EVENT_SIZE 256 +#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; + + uint32_t quantum_limit; + uint32_t min_pool_size; + uint32_t max_pool_size; + + 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; + uint64_t queue_next; + double queue_corr; + + unsigned int opened:1; + unsigned int started:1; + unsigned int following:1; + unsigned int ump: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..9420401 --- /dev/null +++ b/spa/plugins/alsa/alsa-udev.c @@ -0,0 +1,1166 @@ +/* Spa ALSA udev */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alsa.h" + +#define MAX_CARDS 64 + +enum action { + ACTION_CHANGE, + ACTION_REMOVE, +}; + +/* Used for unavailable devices in the card structure. */ +#define ID_DEVICE_NOT_SUPPORTED 0 + +/* This represents an ALSA card. + * One card can have up to 1 PCM and 1 Compress-Offload device. */ +struct card { + unsigned int card_nr; + struct udev_device *udev_device; + unsigned int unavailable:1; + unsigned int accessible:1; + unsigned int ignored:1; + unsigned int emitted:1; + + /* Local SPA object IDs. (Global IDs are produced by PipeWire + * out of this using its registry.) Compress-Offload or PCM + * is not available, the corresponding ID is set to + * ID_DEVICE_NOT_SUPPORTED (= 0). + * PCM device IDs are (card nr + 1) * 2, and Compress-Offload + * device IDs are (card nr + 1) * 2 + 1. Assigning IDs like this + * makes it easy to deal with removed devices. (card nr + 1) + * is used because 0 is a valid ALSA card number. */ + uint32_t pcm_device_id; + uint32_t compress_offload_device_id; +}; + +static uint32_t calc_pcm_device_id(struct card *card) +{ + return (card->card_nr + 1) * 2 + 0; +} + +static uint32_t calc_compress_offload_device_id(struct card *card) +{ + return (card->card_nr + 1) * 2 + 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 card cards[MAX_CARDS]; + unsigned int n_cards; + + struct spa_source source; + struct spa_source notify; + unsigned int use_acp:1; + unsigned int expose_busy: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 card *add_card(struct impl *this, unsigned int card_nr, struct udev_device *udev_device) +{ + struct card *card; + + if (this->n_cards >= MAX_CARDS) + return NULL; + + card = &this->cards[this->n_cards++]; + spa_zero(*card); + card->card_nr = card_nr; + udev_device_ref(udev_device); + card->udev_device = udev_device; + + return card; +} + +static struct card *find_card(struct impl *this, unsigned int card_nr) +{ + unsigned int i; + for (i = 0; i < this->n_cards; i++) { + if (this->cards[i].card_nr == card_nr) + return &this->cards[i]; + } + return NULL; +} + +static void remove_card(struct impl *this, struct card *card) +{ + udev_device_unref(card->udev_device); + *card = this->cards[--this->n_cards]; +} + +static void clear_cards(struct impl *this) +{ + unsigned int i; + for (i = 0; i < this->n_cards; i++) + udev_device_unref(this->cards[i].udev_device); + this->n_cards = 0; +} + +static unsigned int get_card_nr(struct impl *this, struct udev_device *udev_device) +{ + const char *e, *str; + + if (udev_device_get_property_value(udev_device, "ACP_IGNORE")) + return SPA_ID_INVALID; + + if ((str = udev_device_get_property_value(udev_device, "SOUND_CLASS")) && spa_streq(str, "modem")) + return SPA_ID_INVALID; + + if (udev_device_get_property_value(udev_device, "SOUND_INITIALIZED") == NULL) + return SPA_ID_INVALID; + + if ((str = udev_device_get_property_value(udev_device, "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) +{ + 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); + + spa_autoptr(FILE) f = fopen(path, "re"); + if (f == NULL) + return -errno; + sz = fread(buf, 1, sizeof(buf) - 1, f); + buf[sz] = '\0'; + return spa_strstartswith(buf, "modem") ? -ENXIO : 0; +} + +static int get_num_pcm_devices(unsigned int card_nr) +{ + char prefix[32]; + 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_nr); + + spa_autoptr(DIR) snd = opendir("/dev/snd"); + if (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; + } + } + + return errno != 0 ? -errno : num_dev; +} + +static int get_num_compress_offload_devices(unsigned int card_nr) +{ + char prefix[32]; + struct dirent *entry; + int num_dev = 0; + + /* Check if card has Compress-Offload devices, without opening them */ + + spa_scnprintf(prefix, sizeof(prefix), "comprC%uD", card_nr); + + spa_autoptr(DIR) snd = opendir("/dev/snd"); + if (snd == NULL) + return -errno; + + while ((errno = 0, entry = readdir(snd)) != NULL) { + if (!(entry->d_type == DT_CHR && + spa_strstartswith(entry->d_name, prefix))) + continue; + + ++num_dev; + } + + return errno != 0 ? -errno : num_dev; +} + +static int check_udev_environment(struct udev *udev, const char *devname) +{ + char path[PATH_MAX]; + struct udev_device *dev; + int ret = 0; + + /* Check for ACP_IGNORE on a specific PCM device (not the whole card) */ + spa_scnprintf(path, sizeof(path), "/sys/class/sound/%s", devname); + + dev = udev_device_new_from_syspath(udev, path); + if (dev == NULL) + return 0; + + if (udev_device_get_property_value(dev, "ACP_IGNORE")) + ret = -ENXIO; + + udev_device_unref(dev); + + return ret; +} + +static int check_pcm_device_availability(struct impl *this, struct card *card, + int *num_pcm_devices) +{ + char path[PATH_MAX]; + char buf[16]; + size_t sz; + struct dirent *entry, *entry_pcm; + int res; + + res = get_num_pcm_devices(card->card_nr); + if (res < 0) { + spa_log_error(this->log, "Error finding PCM devices for ALSA card %u: %s", + card->card_nr, spa_strerror(res)); + return res; + } + *num_pcm_devices = res; + + spa_log_debug(this->log, "card %u has %d PCM device(s)", + card->card_nr, *num_pcm_devices); + + /* + * 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; + if (this->expose_busy) + return res; + + spa_scnprintf(path, sizeof(path), "/proc/asound/card%u", card->card_nr); + + spa_autoptr(DIR) card_dir = opendir(path); + if (card_dir == NULL) + goto done; + + while ((errno = 0, entry = readdir(card_dir)) != NULL) { + if (!(entry->d_type == DT_DIR && + spa_strstartswith(entry->d_name, "pcm"))) + continue; + + spa_scnprintf(path, sizeof(path), "pcmC%uD%s", + card->card_nr, entry->d_name+3); + if (check_device_pcm_class(path) < 0) + continue; + /* Check udev environment */ + if (check_udev_environment(this->udev, path) < 0) + continue; + + /* Check busy status */ + spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s", + card->card_nr, entry->d_name); + + spa_autoptr(DIR) pcm = opendir(path); + if (pcm == 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", + card->card_nr, entry->d_name, entry_pcm->d_name); + + spa_autoptr(FILE) f = fopen(path, "re"); + if (f == NULL) + goto done; + sz = fread(buf, 1, 6, f); + buf[sz] = '\0'; + + if (!spa_strstartswith(buf, "closed")) { + spa_log_debug(this->log, "card %u pcm device %s busy", + card->card_nr, entry->d_name); + res = -EBUSY; + goto done; + } + spa_log_debug(this->log, "card %u pcm device %s free", + card->card_nr, entry->d_name); + } + if (errno != 0) + goto done; + } + if (errno != 0) + goto done; + +done: + if (errno != 0) { + spa_log_info(this->log, "card %u: failed to find busy status (%s)", + card->card_nr, spa_strerror(-errno)); + } + + return res; +} + +static int check_compress_offload_device_availability(struct impl *this, struct card *card, + int *num_compress_offload_devices) +{ + int res; + + res = get_num_compress_offload_devices(card->card_nr); + if (res < 0) { + spa_log_error(this->log, "Error finding Compress-Offload devices for ALSA card %u: %s", + card->card_nr, spa_strerror(res)); + return res; + } + *num_compress_offload_devices = res; + + spa_log_debug(this->log, "card %u has %d Compress-Offload device(s)", + card->card_nr, *num_compress_offload_devices); + + return 0; +} + +static int emit_added_object_info(struct impl *this, struct card *card) +{ + char path[32]; + int res, num_pcm_devices, num_compress_offload_devices; + const char *str; + struct udev_device *udev_device = card->udev_device; + + /* + * inotify close events under /dev/snd must not be emitted, except after setting + * card->emitted to true. alsalib functions can be used after that. + */ + + snprintf(path, sizeof(path), "hw:%u", card->card_nr); + + if ((res = check_pcm_device_availability(this, card, &num_pcm_devices)) < 0) + return res; + if ((res = check_compress_offload_device_availability(this, card, &num_compress_offload_devices)) < 0) + return res; + + if ((num_pcm_devices == 0) && (num_compress_offload_devices == 0)) { + spa_log_debug(this->log, "no PCM and no Compress-Offload devices for %s", path); + card->ignored = true; + return -ENODEV; + } + + card->emitted = true; + + if (num_pcm_devices > 0) { + struct spa_device_object_info info; + char *cn = NULL, *cln = NULL; + struct spa_dict_item items[25]; + unsigned int n_items = 0; + + card->pcm_device_id = calc_pcm_device_id(card); + + spa_log_debug(this->log, "emitting ACP/PCM device interface for card %s; " + "using local alsa-udev object ID %" PRIu32, path, card->pcm_device_id); + + 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(card->card_nr, &cn) >= 0) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_NAME, cn); + if (snd_card_get_longname(card->card_nr, &cln) >= 0) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_LONGNAME, cln); + + if ((str = udev_device_get_property_value(udev_device, "ACP_NAME")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, str); + + if ((str = udev_device_get_property_value(udev_device, "ACP_PROFILE_SET")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PROFILE_SET, str); + + if ((str = udev_device_get_property_value(udev_device, "SOUND_CLASS")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CLASS, str); + + if ((str = udev_device_get_property_value(udev_device, "USEC_INITIALIZED")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str); + + str = udev_device_get_property_value(udev_device, "ID_PATH"); + if (!(str && *str)) + str = udev_device_get_syspath(udev_device); + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str); + } + if ((str = udev_device_get_devpath(udev_device)) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str); + } + if ((str = udev_device_get_property_value(udev_device, "ID_ID")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str); + } + if ((str = udev_device_get_property_value(udev_device, "ID_BUS")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str); + } + if ((str = udev_device_get_property_value(udev_device, "SUBSYSTEM")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str); + } + if ((str = udev_device_get_property_value(udev_device, "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(udev_device, "ID_VENDOR_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(udev_device, "ID_VENDOR_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(udev_device, "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(udev_device, "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(udev_device, "ID_MODEL_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(udev_device, "ID_MODEL_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(udev_device, "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(udev_device, "ID_SERIAL")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str); + } + if ((str = udev_device_get_property_value(udev_device, "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_log_debug(this->log, "interface information:"); + spa_debug_log_dict(this->log, SPA_LOG_LEVEL_DEBUG, 2, info.props); + + spa_device_emit_object_info(&this->hooks, card->pcm_device_id, &info); + free(cn); + free(cln); + } else { + card->pcm_device_id = ID_DEVICE_NOT_SUPPORTED; + } + + if (num_compress_offload_devices > 0) { + struct spa_device_object_info info; + struct spa_dict_item items[11]; + unsigned int n_items = 0; + char device_name[200]; + char device_desc[200]; + + card->compress_offload_device_id = calc_compress_offload_device_id(card); + + spa_log_debug(this->log, "emitting Compress-Offload device interface for card %s; " + "using local alsa-udev object ID %" PRIu32, path, card->compress_offload_device_id); + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + snprintf(device_name, sizeof(device_name), "comprC%u", card->card_nr); + snprintf(device_desc, sizeof(device_desc), "Compress-Offload device (ALSA card %u)", card->card_nr); + + 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:compressed"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, device_name); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, device_desc); + 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 ((str = udev_device_get_property_value(udev_device, "USEC_INITIALIZED")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str); + + str = udev_device_get_property_value(udev_device, "ID_PATH"); + if (!(str && *str)) + str = udev_device_get_syspath(udev_device); + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str); + } + if ((str = udev_device_get_devpath(udev_device)) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str); + } + if ((str = udev_device_get_property_value(udev_device, "SUBSYSTEM")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str); + } + + info.props = &SPA_DICT_INIT(items, n_items); + + spa_log_debug(this->log, "interface information:"); + spa_debug_log_dict(this->log, SPA_LOG_LEVEL_DEBUG, 2, info.props); + + spa_device_emit_object_info(&this->hooks, card->compress_offload_device_id, &info); + } else { + card->compress_offload_device_id = ID_DEVICE_NOT_SUPPORTED; + } + + return 1; +} + +static bool check_access(struct impl *this, struct card *card) +{ + char path[128], pcm_prefix[32], compr_prefix[32];; + spa_autoptr(DIR) snd = NULL; + struct dirent *entry; + bool accessible = false; + + snprintf(path, sizeof(path), "/dev/snd/controlC%u", card->card_nr); + 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(pcm_prefix, sizeof(pcm_prefix), "pcmC%uD", card->card_nr); + spa_scnprintf(compr_prefix, sizeof(compr_prefix), "comprC%uD", card->card_nr); + while ((entry = readdir(snd)) != NULL) { + if (!(entry->d_type == DT_CHR && + (spa_strstartswith(entry->d_name, pcm_prefix) || + spa_strstartswith(entry->d_name, compr_prefix)))) + continue; + + snprintf(path, sizeof(path), "/dev/snd/%.32s", entry->d_name); + if (access(path, R_OK|W_OK) < 0) { + accessible = false; + break; + } + } + } + + if (accessible != card->accessible) + spa_log_debug(this->log, "%s accessible:%u", path, accessible); + card->accessible = accessible; + + return card->accessible; +} + +static void process_card(struct impl *this, enum action action, struct card *card) +{ + if (card->ignored) + return; + + switch (action) { + case ACTION_CHANGE: { + check_access(this, card); + if (card->accessible && !card->emitted) { + int res = emit_added_object_info(this, card); + if (res < 0) { + if (card->ignored) + spa_log_info(this->log, "ALSA card %u unavailable (%s): it is ignored", + card->card_nr, spa_strerror(res)); + else if (!card->unavailable) + spa_log_info(this->log, "ALSA card %u unavailable (%s): wait for it", + card->card_nr, spa_strerror(res)); + else + spa_log_debug(this->log, "ALSA card %u still unavailable (%s)", + card->card_nr, spa_strerror(res)); + card->unavailable = true; + } else { + if (card->unavailable) + spa_log_info(this->log, "ALSA card %u now available", + card->card_nr); + card->unavailable = false; + } + } else if (!card->accessible && card->emitted) { + card->emitted = false; + + if (card->pcm_device_id != ID_DEVICE_NOT_SUPPORTED) + spa_device_emit_object_info(&this->hooks, card->pcm_device_id, NULL); + if (card->compress_offload_device_id != ID_DEVICE_NOT_SUPPORTED) + spa_device_emit_object_info(&this->hooks, card->compress_offload_device_id, NULL); + } + break; + } + case ACTION_REMOVE: { + uint32_t pcm_device_id = card->pcm_device_id; + uint32_t compress_offload_device_id = card->compress_offload_device_id; + bool emitted = card->emitted; + + remove_card(this, card); + + if (emitted) { + if (pcm_device_id != ID_DEVICE_NOT_SUPPORTED) + spa_device_emit_object_info(&this->hooks, pcm_device_id, NULL); + if (compress_offload_device_id != ID_DEVICE_NOT_SUPPORTED) + spa_device_emit_object_info(&this->hooks, compress_offload_device_id, NULL); + } + break; + } + } +} + +static void process_udev_device(struct impl *this, enum action action, struct udev_device *udev_device) +{ + unsigned int card_nr; + struct card *card; + + if ((card_nr = get_card_nr(this, udev_device)) == SPA_ID_INVALID) + return; + + card = find_card(this, card_nr); + if (action == ACTION_CHANGE && !card) + card = add_card(this, card_nr, udev_device); + + if (!card) + return; + + process_card(this, action, card); +} + +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) + 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 card_nr; + struct card *card; + + 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"); + + /* card becomes accessible or not busy */ + if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) { + if (sscanf(event->name, "controlC%u", &card_nr) != 1 && + sscanf(event->name, "pcmC%uD", &card_nr) != 1) + continue; + if ((card = find_card(this, card_nr)) == NULL) + continue; + + process_card(this, ACTION_CHANGE, card); + } + /* /dev/snd/ might have been removed */ + if ((event->mask & (IN_IGNORED | 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_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 *udev_device; + const char *action; + + udev_device = udev_monitor_receive_device(this->umonitor); + if (udev_device == NULL) + return; + + if ((action = udev_device_get_action(udev_device)) == NULL) + action = "change"; + + spa_log_debug(this->log, "action %s", action); + + start_inotify(this); + + if (spa_streq(action, "add") || spa_streq(action, "change")) { + process_udev_device(this, ACTION_CHANGE, udev_device); + } else if (spa_streq(action, "remove")) { + process_udev_device(this, ACTION_REMOVE, udev_device); + } + udev_device_unref(udev_device); +} + +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_cards (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_cards(struct impl *this) +{ + struct udev_enumerate *enumerate; + struct udev_list_entry *udev_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 (udev_devices = udev_enumerate_get_list_entry(enumerate); udev_devices; + udev_devices = udev_list_entry_get_next(udev_devices)) { + struct udev_device *udev_device; + + udev_device = udev_device_new_from_syspath(this->udev, + udev_list_entry_get_name(udev_devices)); + if (udev_device == NULL) + continue; + + process_udev_device(this, ACTION_CHANGE, udev_device); + + udev_device_unref(udev_device); + } + 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_cards(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); + else if ((str = spa_dict_lookup(info, "alsa.udev.expose-busy")) != NULL) + this->expose_busy = 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..1bc8360 --- /dev/null +++ b/spa/plugins/alsa/alsa.c @@ -0,0 +1,72 @@ +/* Spa ALSA support */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include +#include + +#include "alsa.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_pcm_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; +extern const struct spa_handle_factory spa_alsa_compress_offload_device_factory; +#endif + +SPA_LOG_TOPIC_DEFINE(alsa_log_topic, "spa.alsa"); + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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: +#ifdef HAVE_LIBUDEV + *factory = &spa_alsa_udev_factory; + break; +#else + (*index)++; + SPA_FALLTHROUGH; +#endif + case 3: + *factory = &spa_alsa_pcm_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; + case 7: + *factory = &spa_alsa_compress_offload_device_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..611630a --- /dev/null +++ b/spa/plugins/alsa/alsa.h @@ -0,0 +1,19 @@ +/* Spa ALSA Source */ +/* SPDX-FileCopyrightText: Copyright © 2021 Red Hat, Inc. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_ALSA_H +#define SPA_ALSA_H + +#include + +#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/compress-offload-api-util.c b/spa/plugins/alsa/compress-offload-api-util.c new file mode 100644 index 0000000..5bc1ee6 --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api-util.c @@ -0,0 +1,36 @@ +#include +#include +#include "compress-offload-api.h" +#include "compress-offload-api-util.h" + +int get_compress_offload_device_direction(int card_nr, int device_nr, + struct spa_log *log, + enum spa_compress_offload_direction *direction) +{ + int ret = 0; + struct compress_offload_api_context *device_context; + const struct snd_compr_caps *compr_caps; + + device_context = compress_offload_api_open(card_nr, device_nr, log); + if (device_context == NULL) + return -errno; + + compr_caps = compress_offload_api_get_caps(device_context); + + switch (compr_caps->direction) { + case SND_COMPRESS_PLAYBACK: + *direction = SPA_COMPRESS_OFFLOAD_DIRECTION_PLAYBACK; + break; + case SND_COMPRESS_CAPTURE: + *direction = SPA_COMPRESS_OFFLOAD_DIRECTION_CAPTURE; + break; + default: + spa_log_error(log, "card nr %d device nr %d: unknown direction %#" PRIx32, + card_nr, device_nr, (uint32_t)(compr_caps->direction)); + ret = -EINVAL; + } + + compress_offload_api_close(device_context); + + return ret; +} diff --git a/spa/plugins/alsa/compress-offload-api-util.h b/spa/plugins/alsa/compress-offload-api-util.h new file mode 100644 index 0000000..43cda24 --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api-util.h @@ -0,0 +1,30 @@ +#ifndef SPA_ALSA_COMPRESS_OFFLOAD_DEVICE_UTIL_H +#define SPA_ALSA_COMPRESS_OFFLOAD_DEVICE_UTIL_H + +#include + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define COMPR_API_PRIVATE __attribute__((visibility("hidden"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) +#define COMPR_API_PRIVATE __attribute__((visibility("hidden"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550) +#define COMPR_API_PRIVATE __hidden +#else +#define COMPR_API_PRIVATE +#endif + +enum spa_compress_offload_direction { + SPA_COMPRESS_OFFLOAD_DIRECTION_PLAYBACK, + SPA_COMPRESS_OFFLOAD_DIRECTION_CAPTURE +}; + +/* This exists for situations where both the direction of the compress-offload + * device and the functions from asoundlib.h are needed. It is not possible to + * include asoundlib.h and the compress-offload headers in the same C file, + * since these headers contain conflicting declarations. Provide this direction + * check function to keep the compress-offload headers encapsulated. */ +COMPR_API_PRIVATE int get_compress_offload_device_direction(int card_nr, int device_nr, + struct spa_log *log, + enum spa_compress_offload_direction *direction); + +#endif /* SPA_ALSA_COMPRESS_OFFLOAD_DEVICE_UTIL_H */ diff --git a/spa/plugins/alsa/compress-offload-api.c b/spa/plugins/alsa/compress-offload-api.c new file mode 100644 index 0000000..3f140c7 --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compress-offload-api.h" + + +struct compress_offload_api_context { + int fd; + struct snd_compr_caps caps; + struct spa_log *log; + bool was_configured; + uint32_t fragment_size; + uint32_t num_fragments; +}; + + +struct compress_offload_api_context* compress_offload_api_open(int card_nr, int device_nr, struct spa_log *log) +{ + struct compress_offload_api_context *context; + char fn[256]; + + assert(card_nr >= 0); + assert(device_nr >= 0); + assert(log != NULL); + + context = calloc(1, sizeof(struct compress_offload_api_context)); + if (context == NULL) { + errno = ENOMEM; + return NULL; + } + + context->log = log; + + snprintf(fn, sizeof(fn), "/dev/snd/comprC%uD%u", card_nr, device_nr); + + context->fd = open(fn, O_WRONLY); + if (context->fd < 0) { + spa_log_error(context->log, "could not open device \"%s\": %s (%d)", fn, strerror(errno), errno); + goto error; + } + + if (ioctl(context->fd, SNDRV_COMPRESS_GET_CAPS, &(context->caps)) != 0) { + spa_log_error(context->log, "could not get device caps: %s (%d)", strerror(errno), errno); + goto error; + } + + return context; + +error: + compress_offload_api_close(context); + if (errno == 0) + errno = EIO; + return NULL; +} + + +void compress_offload_api_close(struct compress_offload_api_context *context) +{ + if (context == NULL) + return; + + if (context->fd > 0) + close(context->fd); + + free(context); +} + + +int compress_offload_api_get_fd(struct compress_offload_api_context *context) +{ + assert(context != NULL); + return context->fd; +} + + +int compress_offload_api_set_params(struct compress_offload_api_context *context, struct snd_codec *codec, + uint32_t fragment_size, uint32_t num_fragments) +{ + struct snd_compr_params params; + + assert(context != NULL); + assert(codec != NULL); + assert( + (fragment_size == 0) || + ((fragment_size >= context->caps.min_fragment_size) && (fragment_size <= context->caps.max_fragment_size)) + ); + assert( + (num_fragments == 0) || + ((num_fragments >= context->caps.min_fragments) && (fragment_size <= context->caps.max_fragments)) + ); + + context->fragment_size = (fragment_size != 0) ? fragment_size : context->caps.min_fragment_size; + context->num_fragments = (num_fragments != 0) ? num_fragments : context->caps.max_fragments; + + memset(¶ms, 0, sizeof(params)); + params.buffer.fragment_size = context->fragment_size; + params.buffer.fragments = context->num_fragments; + memcpy(&(params.codec), codec, sizeof(struct snd_codec)); + + if (ioctl(context->fd, SNDRV_COMPRESS_SET_PARAMS, ¶ms) != 0) { + spa_log_error(context->log, "could not set params: %s (%d)", strerror(errno), errno); + return -errno; + } + + context->was_configured = true; + + return 0; +} + + +void compress_offload_api_get_fragment_config(struct compress_offload_api_context *context, + uint32_t *fragment_size, uint32_t *num_fragments) +{ + assert(context != NULL); + assert(fragment_size != NULL); + assert(num_fragments != NULL); + + *fragment_size = context->fragment_size; + *num_fragments = context->num_fragments; +} + + +const struct snd_compr_caps * compress_offload_api_get_caps(struct compress_offload_api_context *context) +{ + assert(context != NULL); + return &(context->caps); +} + + +int compress_offload_api_get_codec_caps(struct compress_offload_api_context *context, + uint32_t codec_id, struct snd_compr_codec_caps *codec_caps) +{ + assert(context != NULL); + assert(codec_id < SND_AUDIOCODEC_MAX); + assert(codec_caps != NULL); + + memset(codec_caps, 0, sizeof(struct snd_compr_codec_caps)); + codec_caps->codec = codec_id; + + if (ioctl(context->fd, SNDRV_COMPRESS_GET_CODEC_CAPS, codec_caps) != 0) { + spa_log_error(context->log, "could not get caps for codec with ID %#08x: %s (%d)", + codec_id, strerror(errno), errno); + return -errno; + } + + return 0; +} + + +bool compress_offload_api_supports_codec(struct compress_offload_api_context *context, uint32_t codec_id) +{ + uint32_t codec_index; + + assert(context != NULL); + assert(codec_id < SND_AUDIOCODEC_MAX); + + for (codec_index = 0; codec_index < context->caps.num_codecs; ++codec_index) { + if (context->caps.codecs[codec_index] == codec_id) + return true; + } + + return false; +} + + +#define RUN_SIMPLE_COMMAND(CONTEXT, CMD, CMD_NAME) \ +{ \ + assert((CONTEXT) != NULL); \ + assert((CMD_NAME) != NULL); \ +\ + if (ioctl((CONTEXT)->fd, (CMD)) < 0) { \ + spa_log_error((CONTEXT)->log, "could not %s device: %s (%d)", (CMD_NAME), strerror(errno), errno); \ + return -errno; \ + } \ +\ + return 0; \ +} + + +int compress_offload_api_start(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_START, "start"); +} + + +int compress_offload_api_stop(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_STOP, "stop"); +} + + +int compress_offload_api_pause(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_PAUSE, "pause"); +} + + +int compress_offload_api_resume(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_RESUME, "resume"); +} + + +int compress_offload_api_drain(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_DRAIN, "drain"); +} + + +int compress_offload_api_get_timestamp(struct compress_offload_api_context *context, + struct snd_compr_tstamp *timestamp) +{ + assert(context != NULL); + assert(timestamp != NULL); + + if (ioctl(context->fd, SNDRV_COMPRESS_TSTAMP, timestamp) < 0) { + spa_log_error(context->log, "could not get timestamp device: %s (%d)", + strerror(errno), errno); + return -errno; + } + + return 0; +} + + +int compress_offload_api_get_available_space(struct compress_offload_api_context *context, + struct snd_compr_avail *available_space) +{ + assert(context != NULL); + assert(available_space != NULL); + + if (ioctl(context->fd, SNDRV_COMPRESS_AVAIL, available_space) < 0) { + spa_log_error(context->log, "could not get available space from device: %s (%d)", + strerror(errno), errno); + return -errno; + } + + return 0; +} + + +int compress_offload_api_write(struct compress_offload_api_context *context, const void *data, size_t size) +{ + int num_bytes_written; + + assert(context != NULL); + assert(data != NULL); + + num_bytes_written = write(context->fd, data, size); + if (num_bytes_written < 0) { + switch (errno) { + case EBADFD: + /* EBADFD indicates that the device is paused and thus is not an error. */ + break; + default: + spa_log_error(context->log, "could not write %zu byte(s): %s (%d)", + size, strerror(errno), errno); + break; + } + + return -errno; + } + + return num_bytes_written; +} diff --git a/spa/plugins/alsa/compress-offload-api.h b/spa/plugins/alsa/compress-offload-api.h new file mode 100644 index 0000000..e0339bd --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api.h @@ -0,0 +1,66 @@ +#ifndef COMPRESS_OFFLOAD_API_H +#define COMPRESS_OFFLOAD_API_H + +#include +#include +#include +#include +#include +#include "compress-offload-api-util.h" + + +struct compress_offload_api_context; + + +/* This is a simple encapsulation of the ALSA Compress-Offload API + * and its ioctl calls. It is intentionally not using any PipeWire + * or SPA headers to allow for porting it or extracting it as its + * own library in the future if needed. It functions as an alternative + * to tinycompress, and was written, because tinycompress lacks + * critical functionality (it does not expose important device caps) + * and adds little value in this particular use case. + * + * Encapsulating the ioctls behind this API also allows for using + * different backends. This might be interesting in the future for + * testing purposes; for example, an alternative backend could exist + * that emulates a compress-offload device by decoding with FFmpeg. + * This would be useful for debugging compressed audio related issues + * in PipeWire on the PC - an important advantage, since getting to + * actual compress-offload hardware can sometimes be difficult. */ + + +COMPR_API_PRIVATE struct compress_offload_api_context* compress_offload_api_open(int card_nr, int device_nr, + struct spa_log *log); +COMPR_API_PRIVATE void compress_offload_api_close(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_get_fd(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_set_params(struct compress_offload_api_context *context, + struct snd_codec *codec, uint32_t fragment_size, + uint32_t num_fragments); +COMPR_API_PRIVATE void compress_offload_api_get_fragment_config(struct compress_offload_api_context *context, + uint32_t *fragment_size, uint32_t *num_fragments); + +COMPR_API_PRIVATE const struct snd_compr_caps * compress_offload_api_get_caps(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_get_codec_caps(struct compress_offload_api_context *context, + uint32_t codec_id, struct snd_compr_codec_caps *codec_caps); +COMPR_API_PRIVATE bool compress_offload_api_supports_codec(struct compress_offload_api_context *context, uint32_t codec_id); + +COMPR_API_PRIVATE int compress_offload_api_start(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_stop(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_pause(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_resume(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_drain(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_get_timestamp(struct compress_offload_api_context *context, + struct snd_compr_tstamp *timestamp); +COMPR_API_PRIVATE int compress_offload_api_get_available_space(struct compress_offload_api_context *context, + struct snd_compr_avail *available_space); + +COMPR_API_PRIVATE int compress_offload_api_write(struct compress_offload_api_context *context, + const void *data, size_t size); + + +#endif /* COMPRESS_OFFLOAD_API_H */ diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build new file mode 100644 index 0000000..4282e0b --- /dev/null +++ b/spa/plugins/alsa/meson.build @@ -0,0 +1,66 @@ +subdir('acp') +subdir('mixer') + +spa_alsa_dependencies = [ spa_dep, alsa_dep, mathlib, epoll_shim_dep, libinotify_dep ] + +spa_alsa_sources = ['alsa.c', + 'alsa.h', + '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 libudev_dep.found() +spa_alsa_sources += [ 'alsa-udev.c' ] +spa_alsa_dependencies += [ libudev_dep ] +endif + +if compress_offload_option.allowed() + spa_alsa_sources += ['alsa-compress-offload-sink.c', + 'alsa-compress-offload-device.c', + 'compress-offload-api-util.c', + 'compress-offload-api.c'] +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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +[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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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. +; = # 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 | # 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 +; # (this only makes sense in path configurations where +; # the exact hardware and driver are known beforehand). +; volume-limit = # Limit the maximum volume by disabling the volume steps above . +; 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/audigy-analog-output-mirror.conf b/spa/plugins/alsa/mixer/paths/audigy-analog-output-mirror.conf new file mode 100644 index 0000000..ff6e86a --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/audigy-analog-output-mirror.conf @@ -0,0 +1,56 @@ +; Mixer path for the Sound Blaster Audigy series, which uses the EMU10K2 DSP. +; We target 'Wave' and other non-'PCM' controls as a special case for when +; the device's stereo-to-all-speakers mirroring mode is in use. (For example, +; the Analog Stereo Output profile.) +; https://docs.kernel.org/sound/cards/audigy-mixer.html +; +; See analog-output.conf.common for an explanation on the directives + +[General] +priority = 99 +description-key = analog-output + +[Element Master] +switch = mute +volume = merge +override-map.1 = all +override-map.2 = all-left,all-right + +[Element Wave] +volume = merge +override-map.1 = all +override-map.2 = all-left,all-right + +# The following elements also exist in analog-output.conf. We list them here +# instead of including that file, for ideal positioning of the Wave element: +# Placing Wave below the Master element prevents Master from reaching its +# loudest until the user raises the unified volume control to maximum. +# (This should reduce the chance of a surprise speaker blow-out.) +# Placing Wave above the per-channel elements yields even steps at low volume. + +[Element Front] +volume = merge +override-map.1 = all-front +override-map.2 = front-left,front-right + +[Element Surround] +volume = merge +override-map.1 = all-rear +override-map.2 = rear-left,rear-right + +[Element Side] +volume = merge +override-map.1 = all-side +override-map.2 = side-left,side-right + +[Element Center] +volume = merge +override-map.1 = all-center +override-map.2 = all-center,all-center + +[Element LFE] +volume = merge +override-map.1 = lfe +override-map.2 = lfe,lfe + +.include analog-output.conf.common diff --git a/spa/plugins/alsa/mixer/paths/audigy-analog-output.conf b/spa/plugins/alsa/mixer/paths/audigy-analog-output.conf new file mode 100644 index 0000000..3736132 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/audigy-analog-output.conf @@ -0,0 +1,44 @@ +; Mixer path for the Sound Blaster Audigy series, which uses the EMU10K2 DSP. +; We target 'PCM Front' and similarly named controls instead of 'Front' et al. +; because the latter affect volume only in the device's stereo-to-all-speakers +; mirroring mode, which is not used by most profiles. +; https://docs.kernel.org/sound/cards/audigy-mixer.html +; +; See analog-output.conf.common for an explanation on the directives + +[General] +priority = 99 +description-key = analog-output + +[Element Master] +switch = mute +volume = merge +override-map.1 = all +override-map.2 = all-left,all-right + +[Element PCM Front] +volume = merge +override-map.1 = all-front +override-map.2 = front-left,front-right + +[Element PCM Surround] +volume = merge +override-map.1 = all-rear +override-map.2 = rear-left,rear-right + +[Element PCM Side] +volume = merge +override-map.1 = all-side +override-map.2 = side-left,side-right + +[Element PCM Center] +volume = merge +override-map.1 = all-center +override-map.2 = all-center,all-center + +[Element PCM LFE] +volume = merge +override-map.1 = lfe +override-map.2 = lfe,lfe + +.include analog-output.conf.common 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..3f5b7d6 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..dd89bd2 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..c734976 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..85b7cfd --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..89da0ee --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..76f4f1d --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..b93daa9 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..1b6086e --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..6ebf80b --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..ae53e79 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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..35aa991 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf @@ -0,0 +1,16 @@ +[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 + +[Jack HDMI] +append-pcm-to-name = no +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 . + +[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 . + + +[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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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/9999-custom.conf b/spa/plugins/alsa/mixer/profile-sets/9999-custom.conf new file mode 100644 index 0000000..df1838d --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/9999-custom.conf @@ -0,0 +1,22 @@ +# 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 . + +; Put your custom profiles here. + +; 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/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 . + +; 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..2bb00a4 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.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 . + +; 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-headset] +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] +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..18e3ee2 --- /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 . + +; Creative Sound Blaster Audigy product line +; +; These are copies of the mappings we find in default.conf, but with analog +; mixer paths targeting appropriate Audigy driver controls, and the small +; change of making analog-stereo and analog-mono non-fallback mappings. +; The latter 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 = audigy-analog-output-mirror 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 = audigy-analog-output-mirror 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-output = audigy-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 = audigy-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 = audigy-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 = audigy-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 = audigy-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 = audigy-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 . + +# Config for CMEDIA USB2.0 High-Speed True HD Audio 147a:e055 +# Added by Jean-Philippe Guillemin + + +[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..f66a945 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/default.conf @@ -0,0 +1,576 @@ +# 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 . + +; 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 + +.include 9999-custom.conf 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 . + +; 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 . + +; 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 . + +; 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/hdmi-ac3.conf b/spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf new file mode 100644 index 0000000..ea6cc9e --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/hdmi-ac3.conf @@ -0,0 +1,110 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; Profile set with HDMI/AC3 profiles. +; +; You can use udev rules to enable these, for example: +; +; ATTRS{subsystem_vendor}=="0x1849", ATTRS{subsystem_device}=="0xaaf0", ENV{ACP_PROFILE_SET}="hdmi-ac3.conf" + +.include default.conf + +[Mapping hdmi-ac3-surround] +description = Digital Surround 5.1 (HDMI/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,3'"} +paths-output = hdmi-output-0 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra1] +description = Digital Surround 5.1 (HDMI 2/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,7'"} +paths-output = hdmi-output-1 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra2] +description = Digital Surround 5.1 (HDMI 3/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,8'"} +paths-output = hdmi-output-2 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra3] +description = Digital Surround 5.1 (HDMI 4/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,9'"} +paths-output = hdmi-output-3 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra4] +description = Digital Surround 5.1 (HDMI 5/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,10'"} +paths-output = hdmi-output-4 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra5] +description = Digital Surround 5.1 (HDMI 6/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,11'"} +paths-output = hdmi-output-5 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra6] +description = Digital Surround 5.1 (HDMI 7/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,12'"} +paths-output = hdmi-output-6 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra7] +description = Digital Surround 5.1 (HDMI 8/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,13'"} +paths-output = hdmi-output-7 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra8] +description = Digital Surround 5.1 (HDMI 9/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,14'"} +paths-output = hdmi-output-8 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra9] +description = Digital Surround 5.1 (HDMI 10/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,15'"} +paths-output = hdmi-output-9 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output + +[Mapping hdmi-ac3-surround-extra10] +description = Digital Surround 5.1 (HDMI 11/AC3) +device-strings = plug:{SLAVE="a52:%f,'hw:%f,16'"} +paths-output = hdmi-output-10 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 1 +direction = output diff --git a/spa/plugins/alsa/mixer/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 . + +; 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 . + +; 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 . + +; Audio profile for the Microsoft Kinect Sensor device in UAC mode. +; +; Copyright (C) 2011 Antonio Ospite +; +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 . + +; 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 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 . + +; 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..f49849e --- /dev/null +++ b/spa/plugins/alsa/test-hw-params.c @@ -0,0 +1,153 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include + +#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..6fa39ab --- /dev/null +++ b/spa/plugins/alsa/test-timer.c @@ -0,0 +1,290 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#define DEFAULT_DEVICE "hw:0" + +#define M_PI_M2f (float)(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_M2f * 440.0f / state->rate; \ + if (state->accumulator >= M_PI_M2f) \ + state->accumulator -= M_PI_M2f; \ + v = (type)(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 += (uint64_t)(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..5c0eb4d --- /dev/null +++ b/spa/plugins/audioconvert/audioadapter.c @@ -0,0 +1,2208 @@ +/* SPA */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audioadapter"); + +#define DEFAULT_ALIGN 16 + +#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1) +#define MAX_RETRY 64 + +/** \cond */ + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + struct spa_plugin_loader *ploader; + + uint32_t max_align; + enum spa_direction direction; + + struct spa_node *target; + + struct spa_node *follower; + struct spa_hook follower_listener; + uint64_t follower_flags; + struct spa_audio_info follower_current_format; + struct spa_audio_info default_format; + int in_set_param; + + struct spa_handle *hnd_convert; + bool unload_handle; + struct spa_node *convert; + struct spa_hook convert_listener; + uint64_t convert_port_flags; + char *convertname; + + 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 IDX_Tag 8 +#define N_NODE_PARAMS 9 + struct spa_param_info params[N_NODE_PARAMS]; + uint32_t convert_params_flags[N_NODE_PARAMS]; + uint32_t follower_params_flags[N_NODE_PARAMS]; + uint64_t follower_port_flags; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + unsigned int add_listener:1; + unsigned int have_format:1; + unsigned int recheck_format:1; + unsigned int started:1; + unsigned int ready:1; + unsigned int async:1; + enum spa_param_port_config_mode mode; + unsigned int follower_removing:1; + unsigned int in_recalc; + + unsigned int warned:1; + unsigned int driver:1; + + int in_enum_sync; +}; + +/** \endcond */ + +static int node_enum_params_sync(struct impl *impl, struct spa_node *node, + uint32_t id, uint32_t *index, const struct spa_pod *filter, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + int res; + impl->in_enum_sync++; + res = spa_node_enum_params_sync(node, id, index, filter, param, builder); + impl->in_enum_sync--; + return res; +} + +static int node_port_enum_params_sync(struct impl *impl, struct spa_node *node, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t *index, const struct spa_pod *filter, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + int res; + impl->in_enum_sync++; + res = spa_node_port_enum_params_sync(node, direction, port_id, id, index, + filter, param, builder); + impl->in_enum_sync--; + return res; +} + +static int follower_enum_params(struct impl *this, + uint32_t id, + uint32_t idx, + struct spa_result_node_params *result, + const struct spa_pod *filter, + struct spa_pod_builder *builder) +{ + int res; + if (result->next < 0x100000 && + this->follower != this->target) { + if ((res = node_enum_params_sync(this, this->target, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + result->next = 0x100000; + } + if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + result->next &= 0xfffff; + if ((res = node_enum_params_sync(this, 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; + + if (this->convert == NULL) + return 0; + + f1 = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction)); + + 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]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + case SPA_PARAM_PortConfig: + if (this->mode == SPA_PARAM_PORT_CONFIG_MODE_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++; + res = 1; + 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: + case SPA_PARAM_Tag: + res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + id, &result.next, filter, &result.param, &b.b); + break; + default: + return -ENOENT; + } + if (res != 1) + return res; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + count++; + + if (count != num) + goto next; + + return 0; +} + +static int link_io(struct impl *this) +{ + int res; + struct spa_io_rate_match *rate_match; + size_t rate_match_size; + + spa_log_debug(this->log, "%p: controls", this); + + spa_zero(this->io_rate_match); + this->io_rate_match.rate = 1.0; + + if (this->follower == this->target) { + rate_match = NULL; + rate_match_size = 0; + } else { + rate_match = &this->io_rate_match; + rate_match_size = sizeof(this->io_rate_match); + } + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_RateMatch, + rate_match, rate_match_size)) < 0) { + spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this, + res, spa_strerror(res)); + } + else if (this->follower != this->target) { + if ((res = spa_node_port_set_io(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_RateMatch, + rate_match, rate_match_size)) < 0) { + spa_log_warn(this->log, "%p: set RateMatch on target failed %d %s", this, + res, spa_strerror(res)); + } + } + return 0; +} + +static int activate_io(struct impl *this, bool active) +{ + int res; + struct spa_io_buffers *data = active ? &this->io_buffers : NULL; + uint32_t size = active ? sizeof(this->io_buffers) : 0; + + if (this->follower == this->target) + return 0; + + if (active) + this->io_buffers = SPA_IO_BUFFERS_INIT; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_Buffers, data, size)) < 0) { + spa_log_warn(this->log, "%p: set Buffers on follower failed %d %s", this, + res, spa_strerror(res)); + return res; + } + else if ((res = spa_node_port_set_io(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_Buffers, data, size)) < 0) { + spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this, + 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) { + struct spa_dict_item *items; + uint32_t n_items = 0; + + if (this->info.props) + n_items = this->info.props->n_items; + items = alloca((n_items + 2) * sizeof(struct spa_dict_item)); + for (i = 0; i < n_items; i++) + items[i] = this->info.props->items[i]; + items[n_items++] = SPA_DICT_ITEM_INIT("adapter.auto-port-config", NULL); + items[n_items++] = SPA_DICT_ITEM_INIT("audio.adapt.follower", NULL); + this->info.props = &SPA_DICT_INIT(items, n_items); + + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + 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; + spa_zero(this->info.props); + } +} + +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_ERROR, 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 = node_port_enum_params_sync(this, node, + direction, port_id, + id, &state, + NULL, ¶m, &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_ERROR, 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; + uint64_t follower_flags, conv_flags; + + spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); + + if (this->follower == this->target) + return 0; + + if (this->n_buffers > 0) + return 0; + + state = 0; + param = NULL; + if ((res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &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 = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &b)) != 1) { + debug_params(this, this->target, + 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_port_flags; + conv_flags = this->convert_port_flags; + + follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + + 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; + + if (this->async) + buffers = SPA_MAX(2u, buffers); + + 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->target, + 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; + + activate_io(this, true); + + return 0; +} + +static void clear_buffers(struct impl *this) +{ + free(this->buffers); + this->buffers = NULL; + this->n_buffers = 0; +} + +static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format) +{ + uint8_t buffer[4096]; + int res; + + spa_log_debug(this->log, "%p: configure format:", this); + + if (format == NULL) { + if (!this->have_format) + return 0; + activate_io(this, false); + } + else { + spa_debug_log_format(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + } + + if ((res = spa_node_port_set_param(this->follower, + this->direction, 0, + 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 = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_Format, &state, + NULL, &fmt, &b)) != 1) + return -EIO; + + format = fmt; + } + + if (this->target != this->follower) { + if ((res = spa_node_port_set_param(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + } + + this->have_format = format != NULL; + clear_buffers(this); + + if (format != NULL) + 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_audioconvert_factory; + +static const struct spa_node_events follower_node_events; + +static int recalc_latency(struct impl *this, struct spa_node *src, enum spa_direction direction, + uint32_t port_id, struct spa_node *dst) +{ + 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: %d:%d", this, direction, port_id); + + if (this->target == this->follower) + return 0; + + while (true) { + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = node_port_enum_params_sync(this, src, + direction, port_id, SPA_PARAM_Latency, + &index, NULL, ¶m, &b)) != 1) { + param = NULL; + break; + } + if ((res = spa_latency_parse(param, &latency)) < 0) + return res; + if (latency.direction == direction) + break; + } + if ((res = spa_node_port_set_param(dst, + SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Latency, 0, param)) < 0) + return res; + + return 0; +} + +static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_direction direction, + uint32_t port_id, struct spa_node *dst) +{ + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + uint8_t buffer[2048]; + struct spa_pod *param; + uint32_t index = 0; + struct spa_tag_info info; + int res; + + spa_log_debug(this->log, "%p: %d:%d", this, direction, port_id); + + if (this->target == this->follower) + return 0; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 2048); + spa_pod_builder_get_state(&b.b, &state); + + while (true) { + void *tag_state = NULL; + spa_pod_builder_reset(&b.b, &state); + if ((res = node_port_enum_params_sync(this, src, + direction, port_id, SPA_PARAM_Tag, + &index, NULL, ¶m, &b.b)) != 1) { + param = NULL; + break; + } + if ((res = spa_tag_parse(param, &info, &tag_state)) < 0) + return res; + if (info.direction == direction) + break; + } + return spa_node_port_set_param(dst, SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Tag, 0, param); +} + + +static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode, + enum spa_direction direction, struct spa_pod *format) +{ + int res = 0; + struct spa_hook l; + bool passthrough = mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough; + bool old_passthrough = this->mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough; + + spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough); + + if (!passthrough && this->convert == NULL) + return -ENOTSUP; + + if (old_passthrough != passthrough) { + if (passthrough) { + /* remove converter split/merge ports */ + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); + } 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; + + this->mode = mode; + + if (old_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, mode); + } + link_io(this); + } + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + SPA_FLAG_CLEAR(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE); + SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, + this->async && this->follower == this->target); + this->params[IDX_Props].user++; + + emit_node_info(this, false); + + spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough); + + 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 (spa_format_audio_parse(param, &info) < 0) + return -EINVAL; + if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + 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_audio_parse(format, &info)) < 0) + return res; + + if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw) + info.info.raw.rate = 0; + else + return -ENOTSUP; + + this->default_format = info; + } + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_none: + return -ENOTSUP; + case SPA_PARAM_PORT_CONFIG_MODE_passthrough: + if ((res = reconfigure_mode(this, mode, dir, format)) < 0) + return res; + break; + case SPA_PARAM_PORT_CONFIG_MODE_convert: + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + if ((res = reconfigure_mode(this, mode, 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; + + res = recalc_latency(this, this->follower, this->direction, 0, this->target); + } + break; + } + + case SPA_PARAM_Props: + { + int in_set_param = ++this->in_set_param; + res = spa_node_set_param(this->follower, id, flags, param); + if (this->target != this->follower && this->in_set_param == in_set_param) + res2 = spa_node_set_param(this->target, 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 fstate, tstate; + struct spa_pod *format, *def; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + int res, fres; + + spa_log_debug(this->log, "%p: have_format:%d recheck:%d", this, this->have_format, + this->recheck_format); + + if (this->target == this->follower) + return 0; + + if (this->have_format && !this->recheck_format) + return 0; + + this->recheck_format = false; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); + + /* first try the ideal converter format, which is likely passthrough */ + tstate = 0; + fres = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, + NULL, &format, &b); + if (fres == 1) { + fstate = 0; + res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, + format, &format, &b); + if (res == 1) + goto found; + } + + /* then try something the follower can accept */ + for (fstate = 0;;) { + format = NULL; + res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, + NULL, &format, &b); + + if (res == -ENOENT) + format = NULL; + else if (res <= 0) + break; + + tstate = 0; + fres = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, + format, &format, &b); + if (fres == 0 && res == 1) + continue; + + res = fres; + break; + } +found: + if (format == NULL) { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_EnumFormat, format, "follower format", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, format, "convert format", res); + res = -ENOTSUP; + goto done; + } + def = spa_format_audio_build(&b, + SPA_PARAM_Format, &this->default_format); + + format = merge_objects(this, &b, SPA_PARAM_Format, + (struct spa_pod_object*)format, + (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; + + 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; + this->ready = true; + this->warned = false; + break; + case SPA_NODE_COMMAND_Suspend: + spa_log_debug(this->log, "%p: suspending", this); + break; + case SPA_NODE_COMMAND_Pause: + spa_log_debug(this->log, "%p: pausing", this); + break; + case SPA_NODE_COMMAND_Flush: + spa_log_debug(this->log, "%p: flushing", this); + this->io_buffers.status = SPA_STATUS_OK; + 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)); + } + + if (res >= 0 && 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)); + } + } + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (res < 0) { + spa_log_debug(this->log, "%p: start failed", this); + this->ready = false; + configure_format(this, 0, NULL); + } else { + this->started = true; + spa_log_debug(this->log, "%p: started", this); + } + break; + case SPA_NODE_COMMAND_Suspend: + configure_format(this, 0, NULL); + this->started = false; + this->warned = false; + this->ready = false; + spa_log_debug(this->log, "%p: suspended", this); + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + this->warned = false; + this->ready = false; + 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 follower_convert_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 (info == NULL) + return; + + spa_log_debug(this->log, "%p: convert port info %s %p %08"PRIx64, this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask); + + this->convert_port_flags = info->flags; + + if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_Latency: + idx = IDX_Latency; + break; + case SPA_PARAM_Tag: + idx = IDX_Tag; + break; + default: + continue; + } + + if (!this->add_listener && + this->convert_params_flags[idx] == info->params[i].flags) + continue; + + this->convert_params_flags[idx] = info->params[i].flags; + + if (this->add_listener) + continue; + + if (idx == IDX_Latency) { + this->in_recalc++; + res = recalc_latency(this, this->target, direction, port_id, this->follower); + this->in_recalc--; + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_Tag) { + this->in_recalc++; + res = recalc_tag(this, this->target, direction, port_id, this->follower); + this->in_recalc--; + spa_log_debug(this->log, "tag: %d (%s)", res, + spa_strerror(res)); + } + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } +} + +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; + struct spa_port_info pi; + + if (direction != this->direction) { + if (port_id == 0) { + /* handle the converter output port into the follower separately */ + follower_convert_port_info(this, direction, port_id, info); + return; + } else + /* the monitor ports are exposed */ + port_id--; + } else if (info) { + pi = *info; + pi.flags = this->follower_port_flags & + (SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL); + info = π + } + + 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 || this->in_enum_sync) + 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_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, + this->async && this->follower == this->target); + + 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 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 (info == NULL) + return; + + if (this->follower_removing) { + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return; + } + + this->follower_port_flags = info->flags; + + spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64" recalc:%u", this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask, + this->in_recalc); + + 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; + case SPA_PARAM_Tag: + idx = IDX_Tag; + 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 && this->in_recalc == 0) { + res = recalc_latency(this, this->follower, direction, port_id, this->target); + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_Tag && this->in_recalc == 0) { + res = recalc_tag(this, this->follower, direction, port_id, this->target); + spa_log_debug(this->log, "tag: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_EnumFormat) { + spa_log_debug(this->log, "new formats"); + /* we will renegotiate when restarting */ + this->recheck_format = true; + } + + 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 || this->in_enum_sync) + 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: + case SPA_NODE_EVENT_RequestProcess: + /* Forward errors and process requests */ + 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 void follower_probe_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + if (info->max_input_ports > 0) + this->direction = SPA_DIRECTION_INPUT; + else + this->direction = SPA_DIRECTION_OUTPUT; +} + +static const struct spa_node_events follower_probe_events = { + SPA_VERSION_NODE_EVENTS, + .info = follower_probe_info, +}; + +static int follower_ready(void *data, int status) +{ + struct impl *this = data; + + spa_log_trace_fp(this->log, "%p: ready %d", this, status); + + if (!this->ready) { + 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 = MAX_RETRY; + while (retry--) { + status = spa_node_process_fast(this->target); + if (status & SPA_STATUS_HAVE_DATA) + break; + + if (status & SPA_STATUS_NEED_DATA) { + status = spa_node_process_fast(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) + res = spa_node_port_reuse_buffer(this->target, port_id, buffer_id); + else + res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id); + + 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->follower != this->target) { + spa_zero(l); + spa_node_add_listener(this->target, &l, &convert_node_events, this); + spa_hook_remove(&l); + } + this->add_listener = false; + + emit_node_info(this, true); + } + 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 +port_enum_formats_for_convert(struct impl *this, int seq, enum spa_direction direction, + uint32_t port_id, uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + int res; + uint32_t count = 0; + struct spa_result_node_params result; + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + if (result.next < 0x100000) { + /* Enumerate follower formats first, until we have enough or we run out */ + if ((res = node_port_enum_params_sync(this, this->follower, direction, port_id, id, + &result.next, filter, &result.param, &b)) != 1) { + if (res == 0 || res == -ENOENT) { + result.next = 0x100000; + goto next; + } else { + spa_log_error(this->log, "could not enum follower format: %s", spa_strerror(res)); + return res; + } + } + } else if (result.next < 0x200000) { + /* Then enumerate converter formats */ + result.next &= 0xfffff; + if ((res = node_port_enum_params_sync(this, this->convert, direction, port_id, id, + &result.next, filter, &result.param, &b)) != 1) { + return res; + } else { + result.next |= 0x100000; + } + } + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count < num) + goto next; + + return 0; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + 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 %u %u", this, seq, id, start, num); + + /* We only need special handling for EnumFormat in convert mode */ + if (id == SPA_PARAM_EnumFormat && this->mode == SPA_PARAM_PORT_CONFIG_MODE_convert) + return port_enum_formats_for_convert(this, seq, direction, port_id, id, + start, num, filter); + else + return spa_node_port_enum_params(this->target, seq, direction, port_id, id, + start, num, filter); +} + +static int +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, " %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + return spa_node_port_set_param(this->target, direction, port_id, id, + flags, param); +} + +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 = MAX_RETRY; + + if (!this->ready) { + if (!this->warned) + spa_log_warn(this->log, "%p: scheduling stopped node", this); + this->warned = true; + 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_fast(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 = spa_node_process_fast(this->target); + /* 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_fast(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 = spa_node_process_fast(this->target); + if (status == 0) + status = SPA_STATUS_NEED_DATA; + else if (status < 0) + break; + + done = (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)); + if (done) + break; + + if (status & SPA_STATUS_NEED_DATA) { + /* the converter needs more data, schedule the + * follower */ + fstatus = spa_node_process_fast(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; + } + } + if (!done) + spa_node_call_xrun(&this->callbacks, 0, 0, NULL); + + } else { + status = spa_node_process_fast(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 load_converter(struct impl *this, const struct spa_dict *info, + const struct spa_support *support, uint32_t n_support) +{ + const char* factory_name = NULL; + struct spa_handle *hnd_convert = NULL; + void *iface_conv = NULL; + bool unload_handle = false; + struct spa_dict_item *items; + struct spa_dict cinfo; + char direction[16]; + uint32_t i; + + items = alloca((info->n_items + 1) * sizeof(struct spa_dict_item)); + cinfo = SPA_DICT(items, 0); + for (i = 0; i < info->n_items; i++) + items[cinfo.n_items++] = info->items[i]; + + snprintf(direction, sizeof(direction), "%s", + SPA_DIRECTION_REVERSE(this->direction) == SPA_DIRECTION_INPUT ? + "input" : "output"); + items[cinfo.n_items++] = SPA_DICT_ITEM("convert.direction", direction); + + factory_name = spa_dict_lookup(&cinfo, "audio.adapt.converter"); + if (factory_name == NULL) + factory_name = SPA_NAME_AUDIO_CONVERT; + + if (spa_streq(factory_name, SPA_NAME_AUDIO_CONVERT)) { + size_t size = spa_handle_factory_get_size(&spa_audioconvert_factory, &cinfo); + + hnd_convert = calloc(1, size); + if (hnd_convert == NULL) + return -errno; + + spa_handle_factory_init(&spa_audioconvert_factory, + hnd_convert, &cinfo, support, n_support); + } else if (this->ploader) { + hnd_convert = spa_plugin_loader_load(this->ploader, factory_name, &cinfo); + if (!hnd_convert) + return -EINVAL; + unload_handle = true; + } else { + return -ENOTSUP; + } + + spa_handle_get_interface(hnd_convert, SPA_TYPE_INTERFACE_Node, &iface_conv); + if (iface_conv == NULL) { + if (unload_handle) + spa_plugin_loader_unload(this->ploader, hnd_convert); + else { + spa_handle_clear(hnd_convert); + free(hnd_convert); + } + return -EINVAL; + } + + this->hnd_convert = hnd_convert; + this->convert = iface_conv; + this->unload_handle = unload_handle; + this->convertname = strdup(factory_name); + + return 0; +} + + +static int do_auto_port_config(struct impl *this, const char *str) +{ + uint32_t state = 0, i; + uint8_t buffer[4096]; + struct spa_pod_builder b; +#define POSITION_PRESERVE 0 +#define POSITION_AUX 1 +#define POSITION_UNKNOWN 2 + int l, res, position = POSITION_PRESERVE; + struct spa_pod *param; + 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[1]; + char key[1024], val[256]; + const char *v; + + if (spa_json_begin_object(&it[0], str, strlen(str)) <= 0) + return -EINVAL; + + while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { + if (spa_json_parse_stringn(v, l, val, sizeof(val)) <= 0) + continue; + + if (spa_streq(key, "mode")) { + mode = spa_debug_type_find_type_short(spa_type_param_port_config_mode, val); + 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, }; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &state, + NULL, ¶m, &b)) != 1) + break; + + if ((res = spa_format_audio_parse(param, &info)) < 0) + continue; + + spa_pod_object_fixate((struct spa_pod_object*)param); + + if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw && + format.media_subtype == SPA_MEDIA_SUBTYPE_raw && + format.info.raw.channels >= info.info.raw.channels) + continue; + + format = info; + have_format = true; + } + if (!have_format) + return -ENOENT; + + if (format.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + if (position == POSITION_AUX) { + for (i = 0; i < format.info.raw.channels; i++) + format.info.raw.position[i] = SPA_AUDIO_CHANNEL_START_Aux + i; + } else if (position == POSITION_UNKNOWN) { + for (i = 0; i < format.info.raw.channels; i++) + format.info.raw.position[i] = SPA_AUDIO_CHANNEL_UNKNOWN; + } + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_build(&b, SPA_PARAM_Format, &format); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction), + 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)); + impl_node_set_param(this, SPA_PARAM_PortConfig, 0, param); + + return 0; +} + +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) { + if (this->unload_handle) + spa_plugin_loader_unload(this->ploader, this->hnd_convert); + else { + spa_handle_clear(this->hnd_convert); + free(this->hnd_convert); + } + free(this->convertname); + } + + clear_buffers(this); + return 0; +} + + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + size_t 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; + const char *str; + int ret; + struct spa_hook probe_listener; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + 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); + + this->ploader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); + + if (info == NULL || + (str = spa_dict_lookup(info, "audio.adapt.follower")) == NULL) + return -EINVAL; + + 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); + + /* just probe the ports to get the direction */ + spa_zero(probe_listener); + spa_node_add_listener(this->follower, &probe_listener, &follower_probe_events, this); + spa_hook_remove(&probe_listener); + + ret = load_converter(this, info, support, n_support); + spa_log_info(this->log, "%p: loaded converter %s, hnd %p, convert %p", this, + this->convertname, this->hnd_convert, this->convert); + if (ret < 0) + return ret; + + if (this->convert == NULL) { + this->target = this->follower; + this->mode = SPA_PARAM_PORT_CONFIG_MODE_passthrough; + } else { + this->target = this->convert; + /* the actual mode is selected below */ + this->mode = SPA_PARAM_PORT_CONFIG_MODE_none; + } + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + 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->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, 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); + if (info && (str = spa_dict_lookup(info, "adapter.auto-port-config")) != NULL) + do_auto_port_config(this, str); + else + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp); + } else { + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL); + } + link_io(this); + + return 0; +} + +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..c1fa022 --- /dev/null +++ b/spa/plugins/audioconvert/audioconvert.c @@ -0,0 +1,4142 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "volume-ops.h" +#include "fmt-ops.h" +#include "channelmix-ops.h" +#include "resample.h" +#include "wavfile.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 MAX_STAGES 64 +#define MAX_GRAPH 9 /* 8 active + 1 replacement slot */ + +#define DEFAULT_MUTE false +#define DEFAULT_VOLUME VOLUME_NORM +#define DEFAULT_MIN_VOLUME 0.0 +#define DEFAULT_MAX_VOLUME 10.0 + +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 volume_ramp_params { + unsigned int volume_ramp_samples; + unsigned int volume_ramp_step_samples; + unsigned int volume_ramp_time; + unsigned int volume_ramp_step_time; + enum spa_audio_volume_ramp_scale scale; +}; + +struct props { + float volume; + float min_volume; + float max_volume; + float prev_volume; + uint32_t n_channels; + uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS]; + struct volumes channel; + struct volumes soft; + struct volumes monitor; + struct volume_ramp_params vrp; + unsigned int have_soft_volume:1; + unsigned int mix_disabled:1; + unsigned int resample_disabled:1; + unsigned int resample_quality; + double rate; + char wav_path[512]; + unsigned int lock_volumes:1; + unsigned int filter_graph_disabled:1; +}; + +static void props_reset(struct props *props) +{ + uint32_t i; + props->volume = DEFAULT_VOLUME; + props->min_volume = DEFAULT_MIN_VOLUME; + props->max_volume = DEFAULT_MAX_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; + spa_zero(props->wav_path); + props->lock_volumes = false; + props->filter_graph_disabled = false; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1<<0) + uint32_t flags; + struct spa_list link; + struct spa_buffer *buf; + void *datas[MAX_DATAS]; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define IDX_Tag 6 +#define N_PORT_PARAMS 7 + struct spa_param_info params[N_PORT_PARAMS]; + char position[16]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_latency_info latency[2]; + unsigned int have_latency:1; + + struct spa_audio_info format; + unsigned int valid:1; + unsigned int have_format:1; + unsigned int is_dsp:1; + unsigned int is_monitor:1; + unsigned int is_control:1; + + uint32_t blocks; + uint32_t stride; + uint32_t maxsize; + + 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_pod *tag; + + uint32_t remap[MAX_PORTS]; + + struct convert conv; + unsigned int need_remap:1; + unsigned int is_passthrough:1; + unsigned int control:1; +}; + +struct stage_context { +#define CTX_DATA_SRC 0 +#define CTX_DATA_DST 1 +#define CTX_DATA_REMAP_DST 2 +#define CTX_DATA_REMAP_SRC 3 +#define CTX_DATA_TMP_0 4 +#define CTX_DATA_TMP_1 5 +#define CTX_DATA_MAX 6 + void **datas[CTX_DATA_MAX]; + uint32_t in_samples; + uint32_t n_samples; + uint32_t n_out; + uint32_t src_idx; + uint32_t dst_idx; + uint32_t final_idx; + struct port *ctrlport; +}; + +struct stage { + struct impl *impl; + bool passthrough; + uint32_t in_idx; + uint32_t out_idx; + void *data; + void (*run) (struct stage *stage, struct stage_context *c); +}; + +struct filter_graph { + struct impl *impl; + int order; + struct spa_handle *handle; + struct spa_filter_graph *graph; + struct spa_hook listener; + uint32_t n_inputs; + uint32_t n_outputs; + bool active; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + struct spa_loop *data_loop; + struct spa_plugin_loader *loader; + + uint32_t n_graph; + uint32_t graph_index[MAX_GRAPH]; + + struct filter_graph filter_graph[MAX_GRAPH]; + int in_filter_props; + int filter_props_count; + + struct stage stages[MAX_STAGES]; + uint32_t n_stages; + + uint32_t cpu_flags; + uint32_t max_align; + uint32_t quantum_limit; + enum spa_direction direction; + + struct spa_ratelimit rate_limit; + + struct props props; + + struct spa_io_position *io_position; + struct spa_io_rate_match *io_rate_match; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumPortConfig 0 +#define IDX_PortConfig 1 +#define IDX_PropInfo 2 +#define IDX_Props 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + + unsigned int monitor:1; + unsigned int monitor_channel_volumes:1; + + struct dir dir[2]; + struct channelmix mix; + struct resample resample; + struct volume volume; + double rate_scale; + struct spa_pod_sequence *vol_ramp_sequence; + uint32_t vol_ramp_offset; + + 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 ramp_volume:1; + unsigned int drained:1; + unsigned int rate_adjust:1; + unsigned int port_ignore_latency:1; + unsigned int monitor_passthrough:1; + unsigned int resample_passthrough:1; + + bool recalc; + + char group_name[128]; + + uint32_t scratch_size; + uint32_t scratch_ports; + float *empty; + float *scratch; + float *tmp[2]; + float *tmp_datas[2][MAX_PORTS]; + + struct wav_file *wav_file; +}; + +#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[5]; + uint32_t n_items = 0; + + if (PORT_IS_DSP(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float 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"); + if (this->port_ignore_latency) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); + } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + } + if (this->group_name[0] != '\0') + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); + port->info.props = &SPA_DICT_INIT(items, n_items); + + if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(port->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id, + 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; + port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + 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->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->n_buffers = 0; + port->have_format = false; + port->is_monitor = is_monitor; + port->is_dsp = is_dsp; + if (port->is_dsp) { + port->format.media_type = SPA_MEDIA_TYPE_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; + } + port->valid = true; + spa_list_init(&port->queue); + + spa_log_debug(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 deinit_port(struct impl *this, enum spa_direction direction, uint32_t port_id) +{ + struct port *port = GET_PORT(this, direction, port_id); + if (port == NULL || !port->valid) + return -ENOENT; + port->valid = false; + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + 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_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, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME)); + 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, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + 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, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + 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, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + 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.min-volume"), + SPA_PROP_INFO_description, SPA_POD_String("Minimum volume level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->min_volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + 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.max-volume"), + SPA_PROP_INFO_description, SPA_POD_String("Maximum volume level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->max_volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + 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.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 13: + 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 14: + 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 15: + 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 16: + 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 17: + 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 18: + 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 19: + 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 20: + 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 21: + 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 22: + 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 23: + 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 24: + 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 25: + 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; + case 26: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"), + SPA_PROP_INFO_description, SPA_POD_String("Path to WAV file"), + SPA_PROP_INFO_type, SPA_POD_String(p->wav_path), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 27: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.lock-volumes"), + SPA_PROP_INFO_description, SPA_POD_String("Disable volume updates"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 28: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Filter graph updates"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 29: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph"), + SPA_PROP_INFO_description, SPA_POD_String("A filter graph to load"), + SPA_PROP_INFO_type, SPA_POD_String(""), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + default: + if (this->filter_graph[0].graph) { + res = spa_filter_graph_enum_prop_info(this->filter_graph[0].graph, + result.index - 30, &b, ¶m); + if (res <= 0) + return res; + } else + 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.min-volume"); + spa_pod_builder_float(&b, this->props.min_volume); + spa_pod_builder_string(&b, "channelmix.max-volume"); + spa_pod_builder_float(&b, this->props.max_volume); + 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_string(&b, "debug.wav-path"); + spa_pod_builder_string(&b, p->wav_path); + spa_pod_builder_string(&b, "channelmix.lock-volumes"); + spa_pod_builder_bool(&b, p->lock_volumes); + spa_pod_builder_string(&b, "audioconvert.filter-graph.disable"); + spa_pod_builder_bool(&b, p->filter_graph_disabled); + spa_pod_builder_string(&b, "audioconvert.filter-graph"); + spa_pod_builder_string(&b, ""); + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + default: + if (result.index > MAX_GRAPH) + return 0; + + if (this->filter_graph[result.index-1].graph == NULL) + goto next; + + res = spa_filter_graph_get_props(this->filter_graph[result.index-1].graph, + &b, ¶m); + if (res < 0) + return res; + if (res == 0) + goto next; + break; + } + 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 void graph_info(void *object, const struct spa_filter_graph_info *info) +{ + struct filter_graph *g = object; + if (!g->active) + return; + g->n_inputs = info->n_inputs; + g->n_outputs = info->n_outputs; +} + +static int apply_props(struct impl *impl, const struct spa_pod *props); + +static void graph_apply_props(void *object, enum spa_direction direction, const struct spa_pod *props) +{ + struct filter_graph *g = object; + struct impl *impl = g->impl; + if (!g->active) + return; + if (apply_props(impl, props) > 0) + emit_node_info(impl, false); +} + +static void graph_props_changed(void *object, enum spa_direction direction) +{ + struct filter_graph *g = object; + struct impl *impl = g->impl; + if (!g->active) + return; + impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + impl->params[IDX_Props].user++; +} + +struct spa_filter_graph_events graph_events = { + SPA_VERSION_FILTER_GRAPH_EVENTS, + .info = graph_info, + .apply_props = graph_apply_props, + .props_changed = graph_props_changed, +}; + +static int setup_filter_graph(struct impl *this, struct spa_filter_graph *graph) +{ + int res; + char rate_str[64]; + struct dir *dir; + + if (graph == NULL) + return 0; + + dir = &this->dir[SPA_DIRECTION_REVERSE(this->direction)]; + snprintf(rate_str, sizeof(rate_str), "%d", dir->format.info.raw.rate); + + spa_filter_graph_deactivate(graph); + res = spa_filter_graph_activate(graph, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate_str))); + return res; +} + +static int do_sync_filter_graph(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct impl *impl = user_data; + uint32_t i, j; + impl->n_graph = 0; + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &impl->filter_graph[i]; + if (g->graph == NULL || !g->active) + continue; + impl->graph_index[impl->n_graph++] = i; + + for (j = impl->n_graph-1; j > 0; j--) { + if (impl->filter_graph[impl->graph_index[j]].order >= + impl->filter_graph[impl->graph_index[j-1]].order) + break; + SPA_SWAP(impl->graph_index[j], impl->graph_index[j-1]); + } + } + impl->recalc = true; + return 0; +} + +static void clean_filter_handles(struct impl *impl, bool force) +{ + uint32_t i; + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &impl->filter_graph[i]; + if (!g->active || force) { + if (g->graph) + spa_hook_remove(&g->listener); + if (g->handle) + spa_plugin_loader_unload(impl->loader, g->handle); + spa_zero(*g); + } + } +} + +static int load_filter_graph(struct impl *impl, const char *graph, int order) +{ + char qlimit[64]; + int res; + void *iface; + struct spa_handle *new_handle = NULL; + uint32_t i, idx, n_graph; + struct filter_graph *pending, *old_active = NULL; + + if (impl->props.filter_graph_disabled) + return -EPERM; + + /* find graph spot */ + idx = SPA_ID_INVALID; + n_graph = 0; + for (i = 0; i < MAX_GRAPH; i++) { + pending = &impl->filter_graph[i]; + /* find the first free spot for our new filter */ + if (!pending->active && idx == SPA_ID_INVALID) + idx = i; + /* deactivate an existing filter of the same order */ + if (pending->active) { + if (pending->order == order) + old_active = pending; + else + n_graph++; + } + } + /* we can at most have MAX_GRAPH-1 active filters */ + if (n_graph >= MAX_GRAPH-1) + return -ENOSPC; + + pending = &impl->filter_graph[idx]; + pending->impl = impl; + pending->order = order; + + if (graph != NULL && graph[0] != '\0') { + snprintf(qlimit, sizeof(qlimit), "%u", impl->quantum_limit); + + new_handle = spa_plugin_loader_load(impl->loader, "filter.graph", + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_LIBRARY_NAME, "filter-graph/libspa-filter-graph"), + SPA_DICT_ITEM("clock.quantum-limit", qlimit), + SPA_DICT_ITEM("filter.graph", graph))); + if (new_handle == NULL) + goto error; + + res = spa_handle_get_interface(new_handle, SPA_TYPE_INTERFACE_FilterGraph, &iface); + if (res < 0 || iface == NULL) + goto error; + + /* prepare new filter and swap it */ + res = setup_filter_graph(impl, iface); + if (res < 0) + goto error; + pending->graph = iface; + pending->active = true; + spa_log_info(impl->log, "loading filter-graph order:%d in %d active:%d", + order, idx, n_graph + 1); + } else { + pending->active = false; + spa_log_info(impl->log, "removing filter-graph order:%d active:%d", + order, n_graph); + } + if (old_active) + old_active->active = false; + + /* we call this here on the pending_graph so that the n_input/n_output is updated + * before we switch */ + if (pending->active) + spa_filter_graph_add_listener(pending->graph, + &pending->listener, &graph_events, pending); + + spa_loop_invoke(impl->data_loop, do_sync_filter_graph, 0, NULL, 0, true, impl); + + if (pending->active) + pending->handle = new_handle; + + if (impl->in_filter_props == 0) + clean_filter_handles(impl, false); + + impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + impl->params[IDX_PropInfo].user++; + impl->params[IDX_Props].user++; + + return 0; +error: + if (new_handle != NULL) + spa_plugin_loader_unload(impl->loader, new_handle); + return -ENOTSUP; +} + +static int audioconvert_set_param(struct impl *this, const char *k, const char *s) +{ + int res; + + if (spa_streq(k, "monitor.channel-volumes")) + this->monitor_channel_volumes = spa_atob(s); + else if (spa_streq(k, "channelmix.disable")) + this->props.mix_disabled = spa_atob(s); + else if (spa_streq(k, "channelmix.min-volume")) + spa_atof(s, &this->props.min_volume); + else if (spa_streq(k, "channelmix.max-volume")) + spa_atof(s, &this->props.max_volume); + 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 if (spa_streq(k, "debug.wav-path")) { + spa_scnprintf(this->props.wav_path, + sizeof(this->props.wav_path), "%s", s ? s : ""); + } + else if (spa_streq(k, "channelmix.lock-volumes")) + this->props.lock_volumes = spa_atob(s); + else if (spa_strstartswith(k, "audioconvert.filter-graph")) { + int order = atoi(k+ strlen("audioconvert.filter-graph.")); + if ((res = load_filter_graph(this, s, order)) < 0) { + spa_log_warn(this->log, "Can't load filter-graph %d: %s", + order, spa_strerror(res)); + } + } + else + return 0; + return 1; +} + +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[4096]; + + 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 get_ramp_samples(struct impl *this) +{ + struct volume_ramp_params *vrp = &this->props.vrp; + int samples = -1; + + if (vrp->volume_ramp_samples) + samples = vrp->volume_ramp_samples; + else if (vrp->volume_ramp_time) { + struct dir *d = &this->dir[SPA_DIRECTION_OUTPUT]; + unsigned int sample_rate = d->format.info.raw.rate; + samples = (vrp->volume_ramp_time * sample_rate) / 1000; + spa_log_info(this->log, "volume ramp samples calculated from time is %d", samples); + } + if (!samples) + samples = -1; + + return samples; + +} + +static int get_ramp_step_samples(struct impl *this) +{ + struct volume_ramp_params *vrp = &this->props.vrp; + int samples = -1; + + if (vrp->volume_ramp_step_samples) + samples = vrp->volume_ramp_step_samples; + else if (vrp->volume_ramp_step_time) { + struct dir *d = &this->dir[SPA_DIRECTION_OUTPUT]; + int sample_rate = d->format.info.raw.rate; + /* convert the step time which is in nano seconds to seconds */ + samples = (vrp->volume_ramp_step_time/1000) * (sample_rate/1000); + spa_log_debug(this->log, "volume ramp step samples calculated from time is %d", samples); + } + if (!samples) + samples = -1; + + return samples; + +} + +static double get_volume_at_scale(struct impl *this, double value) +{ + struct volume_ramp_params *vrp = &this->props.vrp; + if (vrp->scale == SPA_AUDIO_VOLUME_RAMP_LINEAR || vrp->scale == SPA_AUDIO_VOLUME_RAMP_INVALID) + return value; + else if (vrp->scale == SPA_AUDIO_VOLUME_RAMP_CUBIC) + return (value * value * value); + + return 0.0; +} + +static struct spa_pod *generate_ramp_up_seq(struct impl *this) +{ + struct spa_pod_dynamic_builder b; + struct spa_pod_frame f[1]; + struct props *p = &this->props; + double volume_accum = p->prev_volume; + int ramp_samples = get_ramp_samples(this); + int ramp_step_samples = get_ramp_step_samples(this); + double volume_step = ((p->volume - p->prev_volume) / (ramp_samples / ramp_step_samples)); + uint32_t volume_offs = 0; + + spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); + + spa_pod_builder_push_sequence(&b.b, &f[0], 0); + spa_log_info(this->log, "generating ramp up sequence from %f to %f with a" + " step value %f at scale %d", p->prev_volume, p->volume, volume_step, p->vrp.scale); + do { + spa_log_trace(this->log, "volume accum %f", get_volume_at_scale(this, volume_accum)); + spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties); + spa_pod_builder_add_object(&b.b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, + SPA_POD_Float(get_volume_at_scale(this, volume_accum))); + volume_accum += volume_step; + volume_offs += ramp_step_samples; + } while (volume_accum < p->volume); + return spa_pod_builder_pop(&b.b, &f[0]); +} + +static struct spa_pod *generate_ramp_down_seq(struct impl *this) +{ + struct spa_pod_dynamic_builder b; + struct spa_pod_frame f[1]; + int ramp_samples = get_ramp_samples(this); + int ramp_step_samples = get_ramp_step_samples(this); + struct props *p = &this->props; + double volume_accum = p->prev_volume; + double volume_step = ((p->prev_volume - p->volume) / (ramp_samples / ramp_step_samples)); + uint32_t volume_offs = 0; + + spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); + + spa_pod_builder_push_sequence(&b.b, &f[0], 0); + spa_log_info(this->log, "generating ramp down sequence from %f to %f with a" + " step value %f at scale %d", p->prev_volume, p->volume, volume_step, p->vrp.scale); + do { + spa_log_trace(this->log, "volume accum %f", get_volume_at_scale(this, volume_accum)); + spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties); + spa_pod_builder_add_object(&b.b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, + SPA_POD_Float(get_volume_at_scale(this, volume_accum))); + + volume_accum -= volume_step; + volume_offs += ramp_step_samples; + } while (volume_accum > p->volume); + return spa_pod_builder_pop(&b.b, &f[0]); +} + +static struct volume_ramp_params *reset_volume_ramp_params(struct impl *this) +{ + if (!this->vol_ramp_sequence) { + struct volume_ramp_params *vrp = &this->props.vrp; + spa_zero(this->props.vrp); + return vrp; + } + 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; + bool have_channel_volume = false; + bool have_soft_volume = false; + int changed = 0; + int vol_ramp_params_changed = 0; + struct volume_ramp_params *vrp = reset_volume_ramp_params(this); + uint32_t n; + int32_t value; + uint32_t id; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + p->prev_volume = p->volume; + + if (!p->lock_volumes && + spa_pod_get_float(&prop->value, &p->volume) == 0) { + spa_log_debug(this->log, "%p new volume %f", this, p->volume); + changed++; + } + break; + case SPA_PROP_mute: + if (!p->lock_volumes && + spa_pod_get_bool(&prop->value, &p->channel.mute) == 0) { + have_channel_volume = true; + changed++; + } + break; + case SPA_PROP_volumeRampSamples: + if (this->vol_ramp_sequence) { + spa_log_error(this->log, "%p volume ramp sequence is being " + "applied try again", this); + break; + } + + if (spa_pod_get_int(&prop->value, &value) == 0 && value) { + vrp->volume_ramp_samples = value; + spa_log_info(this->log, "%p volume ramp samples %d", this, value); + vol_ramp_params_changed++; + } + break; + case SPA_PROP_volumeRampStepSamples: + if (this->vol_ramp_sequence) { + spa_log_error(this->log, "%p volume ramp sequence is being " + "applied try again", this); + break; + } + + if (spa_pod_get_int(&prop->value, &value) == 0 && value) { + vrp->volume_ramp_step_samples = value; + spa_log_info(this->log, "%p volume ramp step samples is %d", + this, value); + } + break; + case SPA_PROP_volumeRampTime: + if (this->vol_ramp_sequence) { + spa_log_error(this->log, "%p volume ramp sequence is being " + "applied try again", this); + break; + } + + if (spa_pod_get_int(&prop->value, &value) == 0 && value) { + vrp->volume_ramp_time = value; + spa_log_info(this->log, "%p volume ramp time %d", this, value); + vol_ramp_params_changed++; + } + break; + case SPA_PROP_volumeRampStepTime: + if (this->vol_ramp_sequence) { + spa_log_error(this->log, "%p volume ramp sequence is being " + "applied try again", this); + break; + } + + if (spa_pod_get_int(&prop->value, &value) == 0 && value) { + vrp->volume_ramp_step_time = value; + spa_log_info(this->log, "%p volume ramp time %d", this, value); + } + break; + case SPA_PROP_volumeRampScale: + if (this->vol_ramp_sequence) { + spa_log_error(this->log, "%p volume ramp sequence is being " + "applied try again", this); + break; + } + + if (spa_pod_get_id(&prop->value, &id) == 0 && id) { + vrp->scale = id; + spa_log_info(this->log, "%p volume ramp scale %d", this, id); + } + break; + case SPA_PROP_channelVolumes: + if (!p->lock_volumes && + (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 (!p->lock_volumes && + spa_pod_get_bool(&prop->value, &p->soft.mute) == 0) { + have_soft_volume = true; + changed++; + } + break; + case SPA_PROP_softVolumes: + if (!p->lock_volumes && + (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: + if (spa_pod_get_double(&prop->value, &p->rate) == 0 && + !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: + if (this->filter_props_count == 0) + 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); + this->recalc = true; + } + + if (!p->lock_volumes && vol_ramp_params_changed) { + void *sequence = NULL; + if (p->volume == p->prev_volume) + spa_log_error(this->log, "no change in volume, cannot ramp volume"); + else if (p->volume > p->prev_volume) + sequence = generate_ramp_up_seq(this); + else + sequence = generate_ramp_down_seq(this); + + if (!sequence) + spa_log_error(this->log, "unable to generate sequence"); + + this->vol_ramp_sequence = (struct spa_pod_sequence *) sequence; + this->vol_ramp_offset = 0; + this->recalc = true; + } + return changed; +} + +static int apply_midi(struct impl *this, const struct spa_pod *value) +{ + struct props *p = &this->props; + uint8_t data[8]; + int size; + + size = spa_ump_to_midi(SPA_POD_BODY(value), SPA_POD_BODY_SIZE(value), + data, sizeof(data)); + if (size < 3) + return -EINVAL; + + if ((data[0] & 0xf0) != 0xb0 || data[1] != 7) + return 0; + + p->volume = data[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_debug(this->log, "%p: port config direction:%d monitor:%d " + "control:%d mode:%d %d", this, direction, monitor, + control, mode, dir->n_ports); + + for (i = 0; i < dir->n_ports; i++) { + deinit_port(this, direction, i); + if (this->monitor && direction == SPA_DIRECTION_INPUT) + deinit_port(this, SPA_DIRECTION_OUTPUT, i+1); + } + + this->monitor = monitor; + this->setup = false; + dir->control = control; + dir->have_profile = true; + dir->mode = mode; + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + { + if (info) { + dir->n_ports = 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: + { + uint32_t i; + bool have_graph = false; + this->filter_props_count = 0; + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &this->filter_graph[i]; + if (!g->active) + continue; + + have_graph = true; + + this->in_filter_props++; + spa_filter_graph_set_props(g->graph, + SPA_DIRECTION_INPUT, param); + this->filter_props_count++; + this->in_filter_props--; + } + if (!have_graph && apply_props(this, param) > 0) + emit_node_info(this, false); + + clean_filter_handles(this, false); + break; + } + default: + return -ENOENT; + } + 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 set volume %f have_format:%d", this, this->props.volume, 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] = SPA_CLAMPF(vol->volumes[dir->remap[i]], + this->props.min_volume, this->props.max_volume); + + channelmix_set_volume(&this->mix, + SPA_CLAMPF(this->props.volume, this->props.min_volume, this->props.max_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); + + if (this->props.mix_disabled && + (src_chan != dst_chan || src_mask != dst_mask)) + return -EPERM; + + 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; + uint32_t channels; + + if (this->direction == SPA_DIRECTION_INPUT) + channels = in->format.info.raw.channels; + else + channels = out->format.info.raw.channels; + + 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), + channels, + in->format.info.raw.rate, + spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), + channels, + out->format.info.raw.rate); + + if (this->props.resample_disabled && !this->resample_peaks && + in->format.info.raw.rate != out->format.info.raw.rate) + return -EPERM; + + if (this->resample.free) + resample_free(&this->resample); + + this->resample.channels = 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 void free_tmp(struct impl *this) +{ + uint32_t i; + + spa_log_debug(this->log, "free tmp %d", this->scratch_size); + + free(this->empty); + this->empty = NULL; + this->scratch_size = 0; + this->scratch_ports = 0; + free(this->scratch); + this->scratch = NULL; + free(this->tmp[0]); + this->tmp[0] = NULL; + free(this->tmp[1]); + this->tmp[1] = NULL; + for (i = 0; i < MAX_PORTS; i++) { + this->tmp_datas[0][i] = NULL; + this->tmp_datas[1][i] = NULL; + } +} + +static int ensure_tmp(struct impl *this, uint32_t maxsize, uint32_t maxports) +{ + if (maxsize > this->scratch_size || maxports > this->scratch_ports) { + float *empty, *scratch, *tmp[2]; + uint32_t i; + + spa_log_debug(this->log, "resize tmp %d -> %d", this->scratch_size, maxsize); + + if ((empty = realloc(this->empty, maxsize + MAX_ALIGN)) != NULL) + this->empty = empty; + if ((scratch = realloc(this->scratch, maxsize + MAX_ALIGN)) != NULL) + this->scratch = scratch; + if ((tmp[0] = realloc(this->tmp[0], (maxsize + MAX_ALIGN) * maxports)) != NULL) + this->tmp[0] = tmp[0]; + if ((tmp[1] = realloc(this->tmp[1], (maxsize + MAX_ALIGN) * maxports)) != NULL) + this->tmp[1] = tmp[1]; + + if (empty == NULL || scratch == NULL || tmp[0] == NULL || tmp[1] == NULL) { + free_tmp(this); + return -ENOMEM; + } + memset(this->empty, 0, maxsize + MAX_ALIGN); + this->scratch_size = maxsize; + this->scratch_ports = maxports; + + for (i = 0; i < maxports; i++) { + this->tmp_datas[0][i] = SPA_PTROFF(tmp[0], maxsize * 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(tmp[1], maxsize * i, void); + this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp_datas[1][i], MAX_ALIGN, void); + } + } + return 0; +} + +static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t size, uint32_t queued) +{ + uint32_t delay, match_size; + int32_t delay_frac; + + if (passthrough) { + delay = 0; + delay_frac = 0; + match_size = size; + } else { + /* Only apply rate_scale if we're working in DSP mode (i.e. in driver rate) */ + double scale = this->dir[SPA_DIRECTION_REVERSE(this->direction)].mode == SPA_PARAM_PORT_CONFIG_MODE_dsp ? + this->rate_scale : 1.0; + double rate = scale / this->props.rate; + double fdelay; + + if (this->io_rate_match && + SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE)) + rate *= this->io_rate_match->rate; + resample_update_rate(&this->resample, rate); + fdelay = resample_delay(&this->resample) + resample_phase(&this->resample); + if (this->direction == SPA_DIRECTION_INPUT) { + match_size = resample_in_len(&this->resample, size); + } else { + fdelay *= rate * this->resample.o_rate / this->resample.i_rate; + match_size = resample_out_len(&this->resample, size); + } + + delay = (uint32_t)round(fdelay); + delay_frac = (int32_t)((fdelay - delay) * 1e9); + } + match_size -= SPA_MIN(match_size, queued); + + spa_log_trace_fp(this->log, "%p: next match %u %u %u", this, match_size, size, queued); + + if (this->io_rate_match) { + this->io_rate_match->delay = delay + queued; + this->io_rate_match->delay_frac = delay_frac; + this->io_rate_match->size = match_size; + } + return match_size; +} + +static inline bool resample_is_passthrough(struct impl *this) +{ + if (this->props.resample_disabled) + return true; + if (this->resample.i_rate != this->resample.o_rate) + return false; + if (this->rate_scale != 1.0) + return false; + if (this->rate_adjust) + return false; + if (this->io_rate_match != NULL && + SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE)) + return false; + return true; +} + +static int setup_convert(struct impl *this) +{ + struct dir *in, *out; + uint32_t i, rate, maxsize, maxports, duration; + struct port *p; + 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; + + if (this->io_position != NULL) { + rate = this->io_position->clock.target_rate.denom; + duration = this->io_position->clock.target_duration; + } else { + rate = DEFAULT_RATE; + duration = this->quantum_limit; + } + + /* 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; + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &this->filter_graph[i]; + if (!g->active) + continue; + if ((res = setup_filter_graph(this, g->graph)) < 0) + return res; + } + if ((res = setup_channelmix(this)) < 0) + return res; + if ((res = setup_resample(this)) < 0) + return res; + if ((res = setup_out_convert(this)) < 0) + return res; + + maxsize = this->quantum_limit * sizeof(float); + for (i = 0; i < in->n_ports; i++) { + p = GET_IN_PORT(this, i); + maxsize = SPA_MAX(maxsize, p->maxsize); + } + for (i = 0; i < out->n_ports; i++) { + p = GET_OUT_PORT(this, i); + maxsize = SPA_MAX(maxsize, p->maxsize); + } + maxports = SPA_MAX(in->format.info.raw.channels, out->format.info.raw.channels); + if ((res = ensure_tmp(this, maxsize, maxports)) < 0) + return res; + + resample_update_rate_match(this, resample_is_passthrough(this), duration, 0); + + this->setup = true; + this->recalc = true; + + emit_node_info(this, false); + + return 0; +} + +static void reset_node(struct impl *this) +{ + uint32_t i; + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &this->filter_graph[i]; + if (g->graph) + spa_filter_graph_deactivate(g->graph); + } + if (this->resample.reset) + resample_reset(&this->resample); + this->in_offset = 0; + 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; + struct port *p; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + if ((p = GET_IN_PORT(this, i)) && p->valid) + emit_port_info(this, p, true); + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + if ((p = GET_OUT_PORT(this, i)) && p->valid) + emit_port_info(this, p, true); + } + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + + 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), + SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int( + (1u<io_position ? + this->io_position->clock.target_rate.denom : DEFAULT_RATE; + + 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(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), + 0); + if (!this->props.resample_disabled) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + rate, 1, INT32_MAX), + 0); + } + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_CHANNELS, 1, SPA_AUDIO_MAX_CHANNELS), + 0); + *param = spa_pod_builder_pop(builder, &f[0]); + } + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_log_debug(this->log, "%p: enum params port %d.%d %d %u", + this, direction, port_id, seq, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(object, direction, port_id, result.index, ¶m, &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), + SPA_FORMAT_CONTROL_types, SPA_POD_Int( + (1u<format.info.raw); + break; + case SPA_PARAM_Buffers: + { + uint32_t size; + + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + size = this->quantum_limit; + + if (!PORT_IS_DSP(this, direction, port_id)) { + uint32_t irate, orate; + struct dir *dir = &this->dir[direction]; + + /* Convert ports are scaled so that they can always + * provide one quantum of data. irate is the rate of the + * data before it goes into the resampler. */ + irate = dir->format.info.raw.rate; + /* scale the size for adaptive resampling */ + size += size/2; + + /* collect the other port rate. This is the output of the resampler + * and is usually one quantum. */ + dir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; + if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) + orate = this->io_position ? this->io_position->clock.target_rate.denom : DEFAULT_RATE; + else + orate = dir->format.info.raw.rate; + + /* scale the buffer size when we can. Only do this when we downsample because + * then we need to ask more input data for one quantum. */ + if (irate != 0 && orate != 0 && irate > orate) + size = SPA_SCALE32_UP(size, irate, orate); + } + + 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( + size * port->stride, + 16 * port->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + uint32_t idx = result.index; + param = spa_latency_build(&b, id, &port->latency[idx]); + break; + } + default: + return 0; + } + break; + case SPA_PARAM_Tag: + switch (result.index) { + case 0: case 1: + { + uint32_t idx = result.index; + if (port->is_monitor) + idx = idx ^ 1; + param = this->dir[idx].tag; + if (param == NULL) + goto next; + break; + } + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, "%p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int port_set_latency(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *latency) +{ + struct impl *this = object; + struct port *port, *oport; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct spa_latency_info info; + bool have_latency, emit = false;; + uint32_t i; + + spa_log_debug(this->log, "%p: set latency direction:%d id:%d %p", + this, direction, port_id, latency); + + port = GET_PORT(this, direction, port_id); + if (latency == NULL) { + info = SPA_LATENCY_INFO(other); + have_latency = false; + } else { + if (spa_latency_parse(latency, &info) < 0 || + info.direction != other) + return -EINVAL; + have_latency = true; + } + emit = spa_latency_info_compare(&info, &port->latency[other]) != 0 || + port->have_latency == have_latency; + + port->latency[other] = info; + port->have_latency = have_latency; + + spa_log_debug(this->log, "%p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, + info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + info.min_quantum, info.max_quantum, + info.min_rate, info.max_rate, + info.min_ns, info.max_ns); + + if (this->monitor_passthrough) { + if (port->is_monitor) + oport = GET_PORT(this, other, port_id-1); + else if (this->monitor && direction == SPA_DIRECTION_INPUT) + oport = GET_PORT(this, other, port_id+1); + else + return 0; + + if (oport != NULL && + spa_latency_info_compare(&info, &oport->latency[other]) != 0) { + oport->latency[other] = info; + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Latency].user++; + emit_port_info(this, oport, false); + } + } else { + spa_latency_info_combine_start(&info, other); + for (i = 0; i < this->dir[direction].n_ports; i++) { + oport = GET_PORT(this, direction, i); + if ((oport->is_monitor) || !oport->have_latency) + continue; + spa_log_debug(this->log, "%p: combine %d", this, i); + spa_latency_info_combine(&info, &oport->latency[other]); + } + spa_latency_info_combine_finish(&info); + + spa_log_debug(this->log, "%p: combined %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, + info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + info.min_quantum, info.max_quantum, + info.min_rate, info.max_rate, + info.min_ns, info.max_ns); + + for (i = 0; i < this->dir[other].n_ports; i++) { + oport = GET_PORT(this, other, i); + if (oport->is_monitor) + continue; + spa_log_debug(this->log, "%p: change %d", this, i); + if (spa_latency_info_compare(&info, &oport->latency[other]) != 0) { + oport->latency[other] = info; + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Latency].user++; + emit_port_info(this, oport, false); + } + } + } + if (emit) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].user++; + emit_port_info(this, port, false); + } + return 0; +} + +static int port_set_tag(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *tag) +{ + struct impl *this = object; + struct port *port, *oport; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + uint32_t i; + + spa_log_debug(this->log, "%p: set tag direction:%d id:%d %p", + this, direction, port_id, tag); + + port = GET_PORT(this, direction, port_id); + if (port->is_monitor && !this->monitor_passthrough) + return 0; + + if (tag != NULL) { + struct spa_tag_info info; + void *state = NULL; + if (spa_tag_parse(tag, &info, &state) < 0 || + info.direction != other) + return -EINVAL; + } + if (spa_tag_compare(tag, this->dir[other].tag) != 0) { + free(this->dir[other].tag); + this->dir[other].tag = tag ? spa_pod_copy(tag) : NULL; + + for (i = 0; i < this->dir[other].n_ports; i++) { + oport = GET_PORT(this, other, i); + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Tag].user++; + emit_port_info(this, oport, false); + } + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Tag].user++; + emit_port_info(this, port, false); + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: %d:%d set format", this, direction, port_id); + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_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 || + (!this->props.resample_disabled && 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_Tag: + return port_set_tag(this, direction, port_id, flags, param); + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } +} + +static inline void queue_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d", + this, id, port->id, b->flags); + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); +} + +static inline struct buffer *peek_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_log_trace_fp(this->log, "%p: peek buffer %d/%d on port %d %u", + this, b->id, port->n_buffers, port->id, b->flags); + return b; +} + +static inline void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u", + this, b->id, port->id, b->flags); + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return; + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i, j, maxsize; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", + this, n_buffers, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + maxsize = this->quantum_limit * sizeof(float); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + uint32_t n_datas = buffers[i]->n_datas; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->buf = buffers[i]; + + if (n_datas != port->blocks) { + spa_log_error(this->log, "%p: invalid blocks %d on buffer %d", + this, n_datas, i); + return -EINVAL; + } + + 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); + } + port->maxsize = maxsize; + port->n_buffers = n_buffers; + + return 0; +} + +struct io_data { + struct port *port; + void *data; + size_t size; +}; + +static int do_set_port_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + const struct io_data *d = user_data; + d->port->io = d->data; + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set io %d on port %d:%d %p", + this, id, direction, port_id, data); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + if (this->data_loop) { + struct io_data d = { .port = port, .data = data, .size = size }; + spa_loop_invoke(this->data_loop, do_set_port_io, 0, NULL, 0, true, &d); + } + else + port->io = data; + break; + case SPA_IO_RateMatch: + this->io_rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_OUT_PORT(this, port_id); + queue_buffer(this, port, buffer_id); + + return 0; +} + +static int channelmix_process_apply_sequence(struct impl *this, + const struct spa_pod_sequence *sequence, uint32_t *processed_offset, + void *SPA_RESTRICT dst[], const void *SPA_RESTRICT src[], + 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 = &(sequence)->body; + uint32_t size = SPA_POD_BODY_SIZE(sequence); + 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 <= *processed_offset) { + prev = c; + if (c != NULL) + c = spa_pod_control_next(c); + continue; + } + chunk = SPA_MIN(avail_samples, c->offset - *processed_offset); + spa_log_trace_fp(this->log, "%p: process %d-%d %d/%d", this, + *processed_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_UMP: + 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; + *processed_offset += chunk; + } + return end ? 1 : 0; +} + +static inline 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 uint64_t get_time_ns(struct impl *impl) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + return SPA_TIMESPEC_TO_NSEC(&now); +} + +static void run_wav_stage(struct stage *stage, struct stage_context *c) +{ + struct impl *impl = stage->impl; + const void **src = (const void **)c->datas[stage->in_idx]; + + if (SPA_UNLIKELY(impl->props.wav_path[0])) { + if (impl->wav_file == NULL) { + struct wav_file_info info; + + info.info = impl->dir[impl->direction].format; + + impl->wav_file = wav_file_open(impl->props.wav_path, + "w", &info); + if (impl->wav_file == NULL) + spa_log_warn(impl->log, "can't open wav path: %m"); + } + if (impl->wav_file) { + wav_file_write(impl->wav_file, src, c->n_samples); + } else { + spa_zero(impl->props.wav_path); + } + } else if (impl->wav_file != NULL) { + wav_file_close(impl->wav_file); + impl->wav_file = NULL; + impl->recalc = true; + } +} + +static void add_wav_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->src_idx; + s->data = NULL; + s->run = run_wav_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; +} + +static void run_dst_remap_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT]; + uint32_t i; + for (i = 0; i < dir->conv.n_channels; i++) { + c->datas[s->out_idx][i] = c->datas[s->in_idx][dir->remap[i]]; + spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]); + } +} +static void add_dst_remap_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->dst_idx; + s->out_idx = CTX_DATA_REMAP_DST; + s->data = NULL; + s->run = run_dst_remap_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->dst_idx = CTX_DATA_REMAP_DST; + ctx->final_idx = CTX_DATA_REMAP_DST; +} + +static void run_src_remap_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + struct dir *dir = &impl->dir[SPA_DIRECTION_INPUT]; + uint32_t i; + for (i = 0; i < dir->conv.n_channels; i++) { + c->datas[s->out_idx][dir->remap[i]] = c->datas[s->in_idx][i]; + spa_log_trace_fp(impl->log, "%p: input remap %d -> %d", impl, dir->remap[i], i); + } +} +static void add_src_remap_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = CTX_DATA_REMAP_SRC; + s->data = NULL; + s->run = run_src_remap_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = CTX_DATA_REMAP_SRC; +} + +static void run_src_convert_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + struct dir *dir = &impl->dir[SPA_DIRECTION_INPUT]; + void *remap_src_datas[MAX_PORTS], **dst; + + spa_log_trace_fp(impl->log, "%p: input convert %d", impl, c->n_samples); + if (dir->need_remap) { + uint32_t i; + for (i = 0; i < dir->conv.n_channels; i++) { + remap_src_datas[i] = c->datas[s->out_idx][dir->remap[i]]; + spa_log_trace_fp(impl->log, "%p: input remap %d -> %d", impl, dir->remap[i], i); + } + dst = remap_src_datas; + } else { + dst = c->datas[s->out_idx]; + } + convert_process(&dir->conv, dst, (const void**)c->datas[s->in_idx], c->n_samples); +} +static void add_src_convert_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->dst_idx; + s->data = NULL; + s->run = run_src_convert_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = ctx->dst_idx; +} + +static void run_resample_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + uint32_t in_len = c->n_samples; + uint32_t out_len = c->n_out; + + resample_process(&impl->resample, (const void**)c->datas[s->in_idx], &in_len, + c->datas[s->out_idx], &out_len); + + spa_log_trace_fp(impl->log, "%p: resample %d/%d -> %d/%d", impl, + c->n_samples, in_len, c->n_out, out_len); + c->in_samples = in_len; + c->n_samples = out_len; +} +static void add_resample_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->dst_idx; + s->data = NULL; + s->run = run_resample_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = ctx->dst_idx; +} + +static void run_channelmix_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + void **out_datas = c->datas[s->out_idx]; + const void **in_datas = (const void**)c->datas[s->in_idx]; + struct port *ctrlport = c->ctrlport; + + spa_log_trace_fp(impl->log, "%p: channelmix %d", impl, c->n_samples); + if (ctrlport != NULL && ctrlport->ctrl != NULL) { + if (channelmix_process_apply_sequence(impl, ctrlport->ctrl, + &ctrlport->ctrl_offset, out_datas, in_datas, c->n_samples) == 1) { + ctrlport->io->status = SPA_STATUS_OK; + ctrlport->ctrl = NULL; + } + } else if (impl->vol_ramp_sequence) { + if (channelmix_process_apply_sequence(impl, impl->vol_ramp_sequence, + &impl->vol_ramp_offset, out_datas, in_datas, c->n_samples) == 1) { + free(impl->vol_ramp_sequence); + impl->vol_ramp_sequence = NULL; + } + } else { + channelmix_process(&impl->mix, out_datas, in_datas, c->n_samples); + } +} + +static void run_filter_stage(struct stage *s, struct stage_context *c) +{ + struct filter_graph *fg = s->data; + + spa_log_trace_fp(s->impl->log, "%p: filter-graph %d", s->impl, c->n_samples); + spa_filter_graph_process(fg->graph, (const void **)c->datas[s->in_idx], + c->datas[s->out_idx], c->n_samples); +} +static void add_filter_stage(struct impl *impl, uint32_t i, struct filter_graph *fg, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->dst_idx; + s->data = fg; + s->run = run_filter_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = ctx->dst_idx; +} + +static void add_channelmix_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->dst_idx; + s->data = NULL; + s->run = run_channelmix_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = ctx->dst_idx; +} + +static void run_dst_convert_stage(struct stage *s, struct stage_context *c) +{ + struct impl *impl = s->impl; + struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT]; + void *remap_datas[MAX_PORTS], **src; + + spa_log_trace_fp(impl->log, "%p: output convert %d", impl, c->n_samples); + if (dir->need_remap) { + uint32_t i; + for (i = 0; i < dir->conv.n_channels; i++) { + remap_datas[dir->remap[i]] = c->datas[s->in_idx][i]; + spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]); + } + src = remap_datas; + } else { + src = c->datas[s->in_idx]; + } + convert_process(&dir->conv, c->datas[s->out_idx], (const void **)src, c->n_samples); +} +static void add_dst_convert_stage(struct impl *impl, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->passthrough = false; + s->in_idx = ctx->src_idx; + s->out_idx = ctx->final_idx; + s->data = NULL; + s->run = run_dst_convert_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = s->out_idx; +} + +static void recalc_stages(struct impl *this, struct stage_context *ctx) +{ + struct dir *dir; + bool filter_passthrough, in_passthrough, mix_passthrough, resample_passthrough, out_passthrough; + int tmp = 0; + struct port *ctrlport = ctx->ctrlport; + bool in_need_remap, out_need_remap; + uint32_t i; + + this->recalc = false; + this->n_stages = 0; + + dir = &this->dir[SPA_DIRECTION_INPUT]; + in_passthrough = dir->conv.is_passthrough; + in_need_remap = dir->need_remap; + + dir = &this->dir[SPA_DIRECTION_OUTPUT]; + out_passthrough = dir->conv.is_passthrough; + out_need_remap = dir->need_remap; + + resample_passthrough = resample_is_passthrough(this); + filter_passthrough = this->n_graph == 0; + this->resample_passthrough = resample_passthrough; + mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) && + (ctrlport == NULL || ctrlport->ctrl == NULL) && (this->vol_ramp_sequence == NULL); + + if (in_passthrough && filter_passthrough && mix_passthrough && resample_passthrough) + out_passthrough = false; + + if (out_passthrough && out_need_remap) + add_dst_remap_stage(this, ctx); + + if (this->direction == SPA_DIRECTION_INPUT && + (this->props.wav_path[0] || this->wav_file != NULL)) + add_wav_stage(this, ctx); + + if (!in_passthrough) { + if (filter_passthrough && mix_passthrough && resample_passthrough && out_passthrough) + ctx->dst_idx = ctx->final_idx; + else + ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + + add_src_convert_stage(this, ctx); + } else { + if (in_need_remap) + add_src_remap_stage(this, ctx); + } + + if (this->direction == SPA_DIRECTION_INPUT) { + if (!resample_passthrough) { + if (filter_passthrough && mix_passthrough && out_passthrough) + ctx->dst_idx = ctx->final_idx; + else + ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + + add_resample_stage(this, ctx); + resample_passthrough = true; + } + } + if (!filter_passthrough) { + for (i = 0; i < this->n_graph; i++) { + struct filter_graph *fg = &this->filter_graph[this->graph_index[i]]; + + if (mix_passthrough && resample_passthrough && out_passthrough && + i + 1 == this->n_graph) + ctx->dst_idx = ctx->final_idx; + else + ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + + add_filter_stage(this, i, fg, ctx); + } + } + if (!mix_passthrough) { + if (resample_passthrough && out_passthrough) + ctx->dst_idx = ctx->final_idx; + else + ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + + add_channelmix_stage(this, ctx); + } + if (this->direction == SPA_DIRECTION_OUTPUT) { + if (!resample_passthrough) { + if (out_passthrough) + ctx->dst_idx = ctx->final_idx; + else + ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + + add_resample_stage(this, ctx); + } + } + if (!out_passthrough) { + add_dst_convert_stage(this, ctx); + } + if (this->direction == SPA_DIRECTION_OUTPUT && + (this->props.wav_path[0] || this->wav_file != NULL)) + add_wav_stage(this, ctx); + + spa_log_trace(this->log, "got %u processing stages", this->n_stages); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + const void *src_datas[MAX_PORTS]; + void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS]; + 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 res = 0, suppressed; + bool in_avail = false, flush_in = false, flush_out = false; + bool draining = false, in_empty = this->out_offset == 0; + struct spa_io_buffers *io; + const struct spa_pod_sequence *ctrl = NULL; + uint64_t current_time; + struct stage_context ctx; + + /* calculate quantum scale, this is how many samples we need to produce or + * consume. Also update the rate scale, this is sent to the resampler to adjust + * 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; + + current_time = this->io_position->clock.nsec; + 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 graph:%u in:%u out:%u scale:%f->%f", + this->io_position->clock.rate.denom, + this->resample.i_rate, this->resample.o_rate, + this->rate_scale, r); + this->rate_scale = r; + } + } + else { + current_time = get_time_ns(this); + quant_samples = this->quantum_limit; + } + + dir = &this->dir[SPA_DIRECTION_INPUT]; + 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->scratch_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; + 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; + this->recalc = true; + } + } 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); + } + } + } + } + bool resample_passthrough = resample_is_passthrough(this); + if (this->resample_passthrough != resample_passthrough) + this->recalc = true; + + /* calculate how many samples we are going to produce. */ + if (this->direction == SPA_DIRECTION_INPUT) { + /* 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; + if (!in_avail || this->drained) { + n_out = max_out - SPA_MIN(max_out, this->out_offset); + /* 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; + } + 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); + if (buf == NULL && port->n_buffers > 0 && + (suppressed = spa_ratelimit_test(&this->rate_limit, current_time)) >= 0) { + spa_log_warn(this->log, "%p: (%d suppressed) out of buffers on port %d %d", + this, suppressed, port->id, port->n_buffers); + } + } + 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->scratch_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]; + + volume = SPA_CLAMPF(volume, this->props.min_volume, + this->props.max_volume); + + 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); + + /* calculate how many samples we are going to consume. */ + if (this->direction == SPA_DIRECTION_INPUT) { + /* 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; + } + + ctx.datas[CTX_DATA_SRC] = (void **)src_datas; + ctx.datas[CTX_DATA_DST] = dst_datas; + ctx.datas[CTX_DATA_REMAP_DST] = remap_dst_datas; + ctx.datas[CTX_DATA_REMAP_SRC] = remap_src_datas; + ctx.datas[CTX_DATA_TMP_0] = (void**)this->tmp_datas[0]; + ctx.datas[CTX_DATA_TMP_1] = (void**)this->tmp_datas[1]; + ctx.in_samples = n_samples; + ctx.n_samples = n_samples; + ctx.n_out = n_out; + ctx.ctrlport = ctrlport; + + if (SPA_UNLIKELY(this->recalc)) { + ctx.src_idx = CTX_DATA_SRC; + ctx.dst_idx = CTX_DATA_DST; + ctx.final_idx = CTX_DATA_DST; + recalc_stages(this, &ctx); + } + + for (i = 0; i < this->n_stages; i++) { + struct stage *s = &this->stages[i]; + s->run(s, &ctx); + } + this->in_offset += ctx.in_samples; + this->out_offset += ctx.n_samples; + + spa_log_trace_fp(this->log, "%d/%d %d/%d %d->%d", this->in_offset, max_in, + this->out_offset, max_out, n_samples, n_out); + + 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(this->out_offset > 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); + } + } + { + uint32_t size, queued; + + if (this->direction == SPA_DIRECTION_INPUT) { + size = max_out - this->out_offset; + queued = max_in - this->in_offset; + } else { + size = quant_samples; + queued = 0; + } + if (resample_update_rate_match(this, resample_passthrough, + size, queued) > 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 void free_dir(struct dir *dir) +{ + uint32_t i; + for (i = 0; i < MAX_PORTS; i++) + free(dir->ports[i]); + if (dir->conv.free) + convert_free(&dir->conv); + free(dir->tag); +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + free_dir(&this->dir[SPA_DIRECTION_INPUT]); + free_dir(&this->dir[SPA_DIRECTION_OUTPUT]); + + free_tmp(this); + + clean_filter_handles(this, true); + + if (this->resample.free) + resample_free(&this->resample); + if (this->wav_file != NULL) + wav_file_close(this->wav_file); + free (this->vol_ramp_sequence); + 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; + const char *str; + bool filter_graph_disabled; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + if (this->cpu) { + this->cpu_flags = spa_cpu_get_flags(this->cpu); + this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); + } + this->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); + + props_reset(&this->props); + filter_graph_disabled = this->props.filter_graph_disabled; + + this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + this->rate_limit.burst = 1; + + this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE; + this->mix.upmix = CHANNELMIX_UPMIX_NONE; + this->mix.log = this->log; + this->mix.lfe_cutoff = 0.0f; + this->mix.fc_cutoff = 0.0f; + this->mix.rear_delay = 0.0f; + this->mix.widen = 0.0f; + + if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")) != NULL) + spa_atou32(str, &this->quantum_limit, 0); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "resample.peaks")) + this->resample_peaks = spa_atob(s); + else if (spa_streq(k, "resample.prefill")) + SPA_FLAG_UPDATE(this->resample.options, + RESAMPLE_OPTION_PREFILL, spa_atob(s)); + else if (spa_streq(k, "convert.direction")) { + if (spa_streq(s, "output")) + this->direction = SPA_DIRECTION_OUTPUT; + else + this->direction = SPA_DIRECTION_INPUT; + } + else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + if (s != NULL) + spa_audio_parse_position(s, strlen(s), this->props.channel_map, + &this->props.n_channels); + } + else if (spa_streq(k, SPA_KEY_PORT_IGNORE_LATENCY)) + this->port_ignore_latency = spa_atob(s); + else if (spa_streq(k, SPA_KEY_PORT_GROUP)) + spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s); + else if (spa_streq(k, "monitor.passthrough")) + this->monitor_passthrough = spa_atob(s); + else if (spa_streq(k, "audioconvert.filter-graph.disable")) + filter_graph_disabled = spa_atob(s); + else + audioconvert_set_param(this, k, s); + } + this->props.filter_graph_disabled = filter_graph_disabled; + this->props.channel.n_volumes = this->props.n_channels; + this->props.soft.n_volumes = this->props.n_channels; + this->props.monitor.n_volumes = this->props.n_channels; + + this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT; + this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT; + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_OUT_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + 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..9ea43ec --- /dev/null +++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c @@ -0,0 +1,325 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#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 +#if defined (HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_f32_s16", "rvv", true, true, conv_f32_to_s16_rvv); + run_test("test_f32d_s16d", "rvv", false, false, conv_f32d_to_s16d_rvv); + run_test("test_f32d_s16", "rvv", false, true, conv_f32d_to_s16_rvv); + } +#endif + run_test("test_f32_s16d", "c", true, false, conv_f32_to_s16d_c); + run_test("test_f32d_s16d", "c", false, false, conv_f32d_to_s16d_c); +} + +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 +#if defined (HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_s16_f32d", "rvv", true, false, conv_s16_to_f32d_rvv); + } +#endif + run_test("test_s16d_f32d", "c", false, false, conv_s16d_to_f32d_c); +} + +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 +#if defined (HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_f32d_s32", "rvv", false, true, conv_f32d_to_s32_rvv); + } +#endif + run_test("test_f32_s32d", "c", true, false, conv_f32_to_s32d_c); + run_test("test_f32d_s32d", "c", false, false, conv_f32d_to_s32d_c); +} + +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 +#if defined (HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_s32_f32d", "rvv", true, false, conv_s32_to_f32d_rvv); + } +#endif + run_test("test_s32_f32d", "c", true, false, conv_s32_to_f32d_c); + run_test("test_s32d_f32d", "c", false, false, conv_s32d_to_f32d_c); +} + +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..35814cd --- /dev/null +++ b/spa/plugins/audioconvert/benchmark-resample.c @@ -0,0 +1,184 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#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..d9bb7e3 --- /dev/null +++ b/spa/plugins/audioconvert/biquad.c @@ -0,0 +1,374 @@ +/* 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 +#include "biquad.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +/* Q = 1 / sqrt(2), also resulting Q value when S = 1 */ +#define BIQUAD_DEFAULT_Q 0.707106781186548 + +static void set_coefficient(struct biquad *bq, double b0, double b1, double b2, + double a0, double a1, double a2) +{ + double a0_inv = 1 / a0; + bq->b0 = (float)(b0 * a0_inv); + bq->b1 = (float)(b1 * a0_inv); + bq->b2 = (float)(b2 * a0_inv); + bq->a1 = (float)(a1 * a0_inv); + bq->a2 = (float)(a2 * a0_inv); +} + +static void biquad_lowpass(struct biquad *bq, double cutoff, double Q) +{ + /* 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; + } + + /* Set Q to a sane default value if not set */ + if (Q <= 0) + Q = BIQUAD_DEFAULT_Q; + + /* Compute biquad coefficients for lowpass filter */ + /* H(s) = 1 / (s^2 + s/Q + 1) */ + double w0 = M_PI * cutoff; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = (1 - k) / 2; + double b1 = 1 - k; + double b2 = (1 - k) / 2; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_highpass(struct biquad *bq, double cutoff, double Q) +{ + /* 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; + } + + /* Set Q to a sane default value if not set */ + if (Q <= 0) + Q = BIQUAD_DEFAULT_Q; + + /* Compute biquad coefficients for highpass filter */ + /* H(s) = s^2 / (s^2 + s/Q + 1) */ + double w0 = M_PI * cutoff; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = (1 + k) / 2; + double b1 = -(1 + k); + double b2 = (1 + k) / 2; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_bandpass(struct biquad *bq, double frequency, double Q) +{ + /* No negative frequencies allowed. */ + frequency = fmax(0.0, frequency); + + /* Don't let Q go negative, which causes an unstable filter. */ + Q = fmax(0.0, Q); + + if (frequency <= 0 || frequency >= 1) { + /* When the cutoff is zero, the z-transform approaches 0, if Q + * > 0. When both Q and cutoff are zero, the z-transform is + * pretty much undefined. What should we do in this case? + * For now, just make the filter 0. When the cutoff is 1, the + * z-transform also approaches 0. + */ + set_coefficient(bq, 0, 0, 0, 1, 0, 0); + return; + } + if (Q <= 0) { + /* When Q = 0, the above formulas have problems. If we + * look at the z-transform, we can see that the limit + * as Q->0 is 1, so set the filter that way. + */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = alpha; + double b1 = 0; + double b2 = -alpha; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_lowshelf(struct biquad *bq, double frequency, double Q, + double db_gain) +{ + /* Clip frequencies to between 0 and 1, inclusive. */ + frequency = fmax(0.0, fmin(frequency, 1.0)); + + double A = pow(10.0, db_gain / 40); + + if (frequency == 1) { + /* The z-transform is a constant gain. */ + set_coefficient(bq, A * A, 0, 0, 1, 0, 0); + return; + } + if (frequency <= 0) { + /* When frequency is 0, the z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + + /* Set Q to an equivalent value to S = 1 if not specified */ + if (Q <= 0) + Q = BIQUAD_DEFAULT_Q; + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + double k2 = 2 * sqrt(A) * alpha; + double a_plus_one = A + 1; + double a_minus_one = A - 1; + + double b0 = A * (a_plus_one - a_minus_one * k + k2); + double b1 = 2 * A * (a_minus_one - a_plus_one * k); + double b2 = A * (a_plus_one - a_minus_one * k - k2); + double a0 = a_plus_one + a_minus_one * k + k2; + double a1 = -2 * (a_minus_one + a_plus_one * k); + double a2 = a_plus_one + a_minus_one * k - k2; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_highshelf(struct biquad *bq, double frequency, double Q, + double db_gain) +{ + /* Clip frequencies to between 0 and 1, inclusive. */ + frequency = fmax(0.0, fmin(frequency, 1.0)); + + double A = pow(10.0, db_gain / 40); + + if (frequency == 1) { + /* The z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + if (frequency <= 0) { + /* When frequency = 0, the filter is just a gain, A^2. */ + set_coefficient(bq, A * A, 0, 0, 1, 0, 0); + return; + } + + /* Set Q to an equivalent value to S = 1 if not specified */ + if (Q <= 0) + Q = BIQUAD_DEFAULT_Q; + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + double k2 = 2 * sqrt(A) * alpha; + double a_plus_one = A + 1; + double a_minus_one = A - 1; + + double b0 = A * (a_plus_one + a_minus_one * k + k2); + double b1 = -2 * A * (a_minus_one + a_plus_one * k); + double b2 = A * (a_plus_one + a_minus_one * k - k2); + double a0 = a_plus_one - a_minus_one * k + k2; + double a1 = 2 * (a_minus_one - a_plus_one * k); + double a2 = a_plus_one - a_minus_one * k - k2; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_peaking(struct biquad *bq, double frequency, double Q, + double db_gain) +{ + /* Clip frequencies to between 0 and 1, inclusive. */ + frequency = fmax(0.0, fmin(frequency, 1.0)); + + /* Don't let Q go negative, which causes an unstable filter. */ + Q = fmax(0.0, Q); + + double A = pow(10.0, db_gain / 40); + + if (frequency <= 0 || frequency >= 1) { + /* When frequency is 0 or 1, the z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + if (Q <= 0) { + /* When Q = 0, the above formulas have problems. If we + * look at the z-transform, we can see that the limit + * as Q->0 is A^2, so set the filter that way. + */ + set_coefficient(bq, A * A, 0, 0, 1, 0, 0); + return; + } + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = 1 + alpha * A; + double b1 = -2 * k; + double b2 = 1 - alpha * A; + double a0 = 1 + alpha / A; + double a1 = -2 * k; + double a2 = 1 - alpha / A; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_notch(struct biquad *bq, double frequency, double Q) +{ + /* Clip frequencies to between 0 and 1, inclusive. */ + frequency = fmax(0.0, fmin(frequency, 1.0)); + + /* Don't let Q go negative, which causes an unstable filter. */ + Q = fmax(0.0, Q); + + if (frequency <= 0 || frequency >= 1) { + /* When frequency is 0 or 1, the z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + if (Q <= 0) { + /* When Q = 0, the above formulas have problems. If we + * look at the z-transform, we can see that the limit + * as Q->0 is 0, so set the filter that way. + */ + set_coefficient(bq, 0, 0, 0, 1, 0, 0); + return; + } + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = 1; + double b1 = -2 * k; + double b2 = 1; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +static void biquad_allpass(struct biquad *bq, double frequency, double Q) +{ + /* Clip frequencies to between 0 and 1, inclusive. */ + frequency = fmax(0.0, fmin(frequency, 1.0)); + + /* Don't let Q go negative, which causes an unstable filter. */ + Q = fmax(0.0, Q); + + if (frequency <= 0 || frequency >= 1) { + /* When frequency is 0 or 1, the z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + return; + } + + if (Q <= 0) { + /* When Q = 0, the above formulas have problems. If we + * look at the z-transform, we can see that the limit + * as Q->0 is -1, so set the filter that way. + */ + set_coefficient(bq, -1, 0, 0, 1, 0, 0); + return; + } + + double w0 = M_PI * frequency; + double alpha = sin(w0) / (2 * Q); + double k = cos(w0); + + double b0 = 1 - alpha; + double b1 = -2 * k; + double b2 = 1 + alpha; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + set_coefficient(bq, b0, b1, b2, a0, a1, a2); +} + +void biquad_set(struct biquad *bq, enum biquad_type type, double freq, double Q, + double gain) +{ + /* Clear history values. */ + bq->type = type; + bq->x1 = 0; + bq->x2 = 0; + + switch (type) { + case BQ_LOWPASS: + biquad_lowpass(bq, freq, 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, Q, gain); + break; + case BQ_HIGHSHELF: + biquad_highshelf(bq, freq, Q, gain); + break; + case BQ_PEAKING: + biquad_peaking(bq, freq, Q, gain); + break; + case BQ_NOTCH: + biquad_notch(bq, freq, Q); + break; + case BQ_ALLPASS: + biquad_allpass(bq, freq, Q); + break; + case BQ_NONE: + case BQ_RAW: + /* Default is an identity filter. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + break; + } +} diff --git a/spa/plugins/audioconvert/biquad.h b/spa/plugins/audioconvert/biquad.h new file mode 100644 index 0000000..3344598 --- /dev/null +++ b/spa/plugins/audioconvert/biquad.h @@ -0,0 +1,58 @@ +/* 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 type of the biquad filters */ +enum biquad_type { + BQ_NONE, + BQ_LOWPASS, + BQ_HIGHPASS, + BQ_BANDPASS, + BQ_LOWSHELF, + BQ_HIGHSHELF, + BQ_PEAKING, + BQ_NOTCH, + BQ_ALLPASS, + BQ_RAW, +}; + +/* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1) + * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)). The previous two inputs + * are stored in x1 and x2, and the previous two outputs are stored in y1 and + * y2. + * + * We use double during the coefficients calculation for better accuracy, but + * float is used during the actual filtering for faster computation. + */ +struct biquad { + enum biquad_type type; + float b0, b1, b2; + float a1, a2; + float x1, x2; +}; + +/* 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/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c new file mode 100644 index 0000000..2f03df8 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops-c.c @@ -0,0 +1,584 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#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) +{ + if (d != s) + spa_memcpy(d, s, n_samples * sizeof(float)); +} + +static inline void vol_c(float *d, const float *s, float vol, uint32_t n_samples) +{ + 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); +} + +static void lr4_process_c(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples) +{ + float x1 = lr4->x1; + float x2 = lr4->x2; + float y1 = lr4->y1; + float y2 = lr4->y2; + float b0 = lr4->bq.b0; + float b1 = lr4->bq.b1; + float b2 = lr4->bq.b2; + float a1 = lr4->bq.a1; + float a2 = lr4->bq.a2; + float x, y, z; + int i; + + if (vol == 0.0f || !lr4->active) { + vol_c(dst, src, vol, samples); + return; + } + + for (i = 0; i < samples; i++) { + x = src[i]; + y = b0 * x + x1; + x1 = b1 * x - a1 * y + x2; + x2 = b2 * x - a2 * y; + z = b0 * y + y1; + y1 = b1 * y - a1 * z + y2; + y2 = b2 * y - a2 * z; + dst[i] = z * vol; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + lr4->x1 = F(x1); + lr4->x2 = F(x2); + lr4->y1 = F(y1); + lr4->y2 = F(y2); +#undef F +} + +static inline void delay_convolve_run_c(float *buffer, uint32_t *pos, + uint32_t n_buffer, uint32_t delay, + const float *taps, uint32_t n_taps, + float *dst, const float *src, const float vol, uint32_t n_samples) +{ + uint32_t i, j; + uint32_t w = *pos; + uint32_t o = n_buffer - delay - n_taps-1; + + if (n_taps == 1) { + for (i = 0; i < n_samples; i++) { + buffer[w] = buffer[w + n_buffer] = src[i]; + dst[i] = buffer[w + o] * vol; + w = w + 1 >= n_buffer ? 0 : w + 1; + } + } else { + for (i = 0; i < n_samples; i++) { + float sum = 0.0f; + + buffer[w] = buffer[w + n_buffer] = src[i]; + for (j = 0; j < n_taps; j++) + sum += taps[j] * buffer[w+o+j]; + dst[i] = sum * vol; + + w = w + 1 >= n_buffer ? 0 : w + 1; + } + } + *pos = w; +} + +#define _M(ch) (1UL << SPA_AUDIO_CHANNEL_ ## ch) + +void +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_c(&mix->lr4[i], di, sj[0], mj[0], n_samples); + } else { + conv_c(di, sj, mj, n_j, n_samples); + lr4_process_c(&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_c(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[3], d[2], -v3, n_samples); + delay_convolve_run_c(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_c(&mix->lr4[3], d[3], d[2], v3, n_samples); + lr4_process_c(&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_c(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples); + delay_convolve_run_c(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_c(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples); + delay_convolve_run_c(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..1058959 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops-sse.c @@ -0,0 +1,691 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "channelmix-ops.h" + +#include +#include +#include + +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); +} + +static void lr4_process_sse(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples) +{ + __m128 x, y, z; + __m128 b012; + __m128 a12; + __m128 x12, y12, v; + int i; + + if (vol == 0.0f || !lr4->active) { + vol_sse(dst, src, vol, samples); + return; + } + + b012 = _mm_setr_ps(lr4->bq.b0, lr4->bq.b1, lr4->bq.b2, 0.0f); /* b0 b1 b2 0 */ + a12 = _mm_setr_ps(0.0f, lr4->bq.a1, lr4->bq.a2, 0.0f); /* 0 a1 a2 0 */ + x12 = _mm_setr_ps(lr4->x1, lr4->x2, 0.0f, 0.0f); /* x1 x2 0 0 */ + y12 = _mm_setr_ps(lr4->y1, lr4->y2, 0.0f, 0.0f); /* y1 y2 0 0 */ + v = _mm_setr_ps(vol, vol, 0.0f, 0.0f); + + for (i = 0; i < samples; i++) { + x = _mm_load1_ps(&src[i]); /* x x x x */ + + z = _mm_mul_ps(x, b012); /* b0*x b1*x b2*x 0 */ + z = _mm_add_ps(z, x12); /* b0*x+x1 b1*x+x2 b2*x 0 */ + y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0)); /* b0*x+x1 b0*x+x1 b0*x+x1 b0*x+x1 = y*/ + x = _mm_mul_ps(y, a12); /* 0 a1*y a2*y 0 */ + x = _mm_sub_ps(z, x); /* y x1 x2 0 */ + x12 = _mm_shuffle_ps(x, x, _MM_SHUFFLE(3,3,2,1)); /* x1 x2 0 0*/ + + z = _mm_mul_ps(y, b012); + z = _mm_add_ps(z, y12); + x = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0)); + y = _mm_mul_ps(x, a12); + y = _mm_sub_ps(z, y); + y12 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1)); + + x = _mm_mul_ps(x, v); + _mm_store_ss(&dst[i], x); + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + lr4->x1 = F(x12[0]); + lr4->x2 = F(x12[1]); + lr4->y1 = F(y12[0]); + lr4->y2 = F(y12[1]); +#undef F +} + +static void lr4_process_2_sse(struct lr4 *lr40, struct lr4 *lr41, float *dst0, float *dst1, + const float *src0, const float *src1, const float vol0, const float vol1, uint32_t samples) +{ + __m128 x, y, z; + __m128 b0, b1, b2; + __m128 a1, a2; + __m128 x1, x2; + __m128 y1, y2, v; + uint32_t i; + + b0 = _mm_setr_ps(lr40->bq.b0, lr41->bq.b0, 0.0f, 0.0f); + b1 = _mm_setr_ps(lr40->bq.b1, lr41->bq.b1, 0.0f, 0.0f); + b2 = _mm_setr_ps(lr40->bq.b2, lr41->bq.b2, 0.0f, 0.0f); + a1 = _mm_setr_ps(lr40->bq.a1, lr41->bq.a1, 0.0f, 0.0f); + a2 = _mm_setr_ps(lr40->bq.a2, lr41->bq.a2, 0.0f, 0.0f); + x1 = _mm_setr_ps(lr40->x1, lr41->x1, 0.0f, 0.0f); + x2 = _mm_setr_ps(lr40->x2, lr41->x2, 0.0f, 0.0f); + y1 = _mm_setr_ps(lr40->y1, lr41->y1, 0.0f, 0.0f); + y2 = _mm_setr_ps(lr40->y2, lr41->y2, 0.0f, 0.0f); + v = _mm_setr_ps(vol0, vol1, 0.0f, 0.0f); + + for (i = 0; i < samples; i++) { + x = _mm_setr_ps(src0[i], src1[i], 0.0f, 0.0f); + + y = _mm_mul_ps(x, b0); /* y = x * b0 */ + y = _mm_add_ps(y, x1); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a1); /* z = a1 * y */ + x1 = _mm_mul_ps(x, b1); /* x1 = x * b1 */ + x1 = _mm_add_ps(x1, x2); /* x1 = x * b1 + x2*/ + x1 = _mm_sub_ps(x1, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a2); /* z = a2 * y */ + x2 = _mm_mul_ps(x, b2); /* x2 = x * b2 */ + x2 = _mm_sub_ps(x2, z); /* x2 = x * b2 - a2 * y*/ + + x = _mm_mul_ps(y, b0); /* y = x * b0 */ + x = _mm_add_ps(x, y1); /* y = x * b0 + x1*/ + z = _mm_mul_ps(x, a1); /* z = a1 * y */ + y1 = _mm_mul_ps(y, b1); /* x1 = x * b1 */ + y1 = _mm_add_ps(y1, y2); /* x1 = x * b1 + x2*/ + y1 = _mm_sub_ps(y1, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(x, a2); /* z = a2 * y */ + y2 = _mm_mul_ps(y, b2); /* x2 = x * b2 */ + y2 = _mm_sub_ps(y2, z); /* x2 = x * b2 - a2 * y*/ + + x = _mm_mul_ps(x, v); + dst0[i] = x[0]; + dst1[i] = x[1]; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + lr40->x1 = F(x1[0]); + lr40->x2 = F(x2[0]); + lr40->y1 = F(y1[0]); + lr40->y2 = F(y2[0]); + lr41->x1 = F(x1[1]); + lr41->x2 = F(x2[1]); + lr41->y1 = F(y1[1]); + lr41->y2 = F(y2[1]); +#undef F +} + +static inline void convolver_run(const float *src, float *dst, + const float *taps, uint32_t n_taps, const __m128 vol) +{ + __m128 t[1], sum[4]; + uint32_t i; + + sum[0] = _mm_setzero_ps(); + for(i = 0; i < n_taps; i+=4) { + t[0] = _mm_loadu_ps(&src[i]); + sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(_mm_load_ps(&taps[i]), t[0])); + } + sum[0] = _mm_add_ps(sum[0], _mm_movehl_ps(sum[0], sum[0])); + sum[0] = _mm_add_ss(sum[0], _mm_shuffle_ps(sum[0], sum[0], 0x55)); + t[0] = _mm_mul_ss(sum[0], vol); + _mm_store_ss(dst, t[0]); +} + +static inline void delay_convolve_run_sse(float *buffer, uint32_t *pos, + uint32_t n_buffer, uint32_t delay, + const float *taps, uint32_t n_taps, + float *dst, const float *src, const float vol, uint32_t n_samples) +{ + __m128 t[1]; + const __m128 v = _mm_set1_ps(vol); + uint32_t i; + uint32_t w = *pos; + uint32_t o = n_buffer - delay - n_taps-1; + uint32_t n, unrolled; + + if (SPA_IS_ALIGNED(src, 16) && + SPA_IS_ALIGNED(dst, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + if (n_taps == 1) { + for(n = 0; n < unrolled; n += 4) { + t[0] = _mm_load_ps(&src[n]); + _mm_storeu_ps(&buffer[w], t[0]); + _mm_storeu_ps(&buffer[w+n_buffer], t[0]); + t[0] = _mm_loadu_ps(&buffer[w+o]); + t[0] = _mm_mul_ps(t[0], v); + _mm_store_ps(&dst[n], t[0]); + w += 4; + if (w >= n_buffer) { + w -= n_buffer; + t[0] = _mm_load_ps(&buffer[n_buffer]); + _mm_store_ps(&buffer[0], t[0]); + } + } + for(; n < n_samples; n++) { + t[0] = _mm_load_ss(&src[n]); + _mm_store_ss(&buffer[w], t[0]); + _mm_store_ss(&buffer[w+n_buffer], t[0]); + t[0] = _mm_load_ss(&buffer[w+o]); + t[0] = _mm_mul_ss(t[0], v); + _mm_store_ss(&dst[n], t[0]); + w = w + 1 >= n_buffer ? 0 : w + 1; + } + } else { + for(n = 0; n < unrolled; n += 4) { + t[0] = _mm_load_ps(&src[n]); + _mm_storeu_ps(&buffer[w], t[0]); + _mm_storeu_ps(&buffer[w+n_buffer], t[0]); + for(i = 0; i < 4; i++) + convolver_run(&buffer[w+o+i], &dst[n+i], taps, n_taps, v); + w += 4; + if (w >= n_buffer) { + w -= n_buffer; + t[0] = _mm_load_ps(&buffer[n_buffer]); + _mm_store_ps(&buffer[0], t[0]); + } + } + for(; n < n_samples; n++) { + t[0] = _mm_load_ss(&src[n]); + _mm_store_ss(&buffer[w], t[0]); + _mm_store_ss(&buffer[w+n_buffer], t[0]); + convolver_run(&buffer[w+o], &dst[n], taps, n_taps, v); + w = w + 1 >= n_buffer ? 0 : w + 1; + } + } + *pos = w; +} + +void +channelmix_f32_n_m_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + 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) { + lr4_process_sse(&mix->lr4[i], di, sj[0], mj[0], n_samples); + } else { + conv_sse(di, sj, mj, n_j, n_samples); + lr4_process_sse(&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_2_sse(&mix->lr4[3], &mix->lr4[2], d[3], d[2], d[2], d[2], v3, 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_sse(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples); + delay_convolve_run_sse(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_sse(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples); + delay_convolve_run_sse(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..d774112 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -0,0 +1,799 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 _SH 2 +#define _CH(ch) ((SPA_AUDIO_CHANNEL_ ## ch)-_SH) +#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 uint32_t mask_to_ch(struct channelmix *mix, uint64_t mask) +{ + uint32_t ch = 0; + while (mask > 1) { + ch++; + mask >>= 1; + } + return ch; +} + +static void distribute_mix(struct channelmix *mix, + float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + uint64_t mask) +{ + uint32_t i, ch = mask_to_ch(mix, mask); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][ch]= 1.0f; +} +static void average_mix(struct channelmix *mix, + float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + uint64_t mask) +{ + uint32_t i, ch = mask_to_ch(mix, mask); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[ch][i]= 1.0f; +} +static void pair_mix(float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]) +{ + uint32_t i; + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][i]= 1.0f; +} +static bool match_mix(struct channelmix *mix, + float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + uint64_t src_mask, uint64_t dst_mask) +{ + bool matched = false; + uint32_t i; + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + if ((src_mask & dst_mask & (1ULL << i))) { + spa_log_info(mix->log, "matched channel %u (%f)", i, 1.0f); + matrix[i][i] = 1.0f; + matched = true; + } + } + return matched; +} + +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, src_paired; + uint64_t dst_mask = mix->dst_mask, dst_paired; + 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, matched = false, normalize; +#define _MATRIX(s,d) matrix[_CH(s)][_CH(d)] + + normalize = SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_NORMALIZE); + + spa_log_debug(mix->log, "src-mask:%08"PRIx64" dst-mask:%08"PRIx64 + " options:%08x", src_mask, dst_mask, mix->options); + + /* shift so that bit 0 is MONO */ + src_mask >>= _SH; + dst_mask >>= _SH; + + if (src_chan > 1 && (src_mask & _MASK(MONO))) + src_mask = 0; + if (dst_chan > 1 && (dst_mask & _MASK(MONO))) + dst_mask = 0; + + src_paired = src_mask; + dst_paired = dst_mask; + + /* unknown channels */ + if (src_mask == 0 || dst_mask == 0) { + if (src_chan == 1) { + /* one src channel goes everywhere */ + spa_log_info(mix->log, "distribute UNK (%f) %"PRIu64, 1.0f, src_mask); + distribute_mix(mix, matrix, src_mask); + } else if (dst_chan == 1) { + /* one dst channel get average of everything */ + spa_log_info(mix->log, "average UNK (%f) %"PRIu64, 1.0f / src_chan, dst_mask); + average_mix(mix, matrix, dst_mask); + normalize = true; + } else { + /* just pair channels */ + spa_log_info(mix->log, "pairing UNK channels (%f)", 1.0f); + if (src_mask == 0) + src_paired = dst_mask; + else if (dst_mask == 0) + dst_paired = src_mask; + pair_mix(matrix); + } + goto done; + } else { + spa_log_debug(mix->log, "matching channels"); + matched = match_mix(mix, matrix, src_mask, dst_mask); + } + + unassigned = src_mask & ~dst_mask; + keep = dst_mask & ~src_mask; + + if (!SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_UPMIX)) { + /* upmix completely disabled */ + keep = 0; + } else { + /* some upmixing (FC and LFE) enabled. */ + if (mix->upmix == CHANNELMIX_UPMIX_NONE) + keep = 0; + if (mix->fc_cutoff > 0.0f) + keep |= FRONT; + else + keep &= ~FRONT; + if (mix->lfe_cutoff > 0.0f) + keep |= _MASK(LFE); + else + keep &= ~_MASK(LFE); + } + /* if we have no channel matched, try to upmix or keep the stereo + * pair or else we might end up with silence. */ + if (dst_mask & STEREO && !matched) + keep |= STEREO; + + spa_log_info(mix->log, "unassigned downmix %08" PRIx64 " %08" PRIx64, unassigned, keep); + + if (unassigned & _MASK(MONO)) { + if ((dst_mask & STEREO) == STEREO) { + spa_log_info(mix->log, "assign MONO to STEREO (%f)", 1.0f); + _MATRIX(FL,MONO) += 1.0f; + _MATRIX(FR,MONO) += 1.0f; + keep &= ~STEREO; + } else if ((dst_mask & FRONT) == FRONT) { + spa_log_info(mix->log, "assign MONO to FRONT (%f)", 1.0f); + _MATRIX(FC,MONO) += 1.0f; + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign MONO"); + } + } + + if (unassigned & FRONT) { + if ((dst_mask & STEREO) == STEREO){ + if (src_mask & STEREO) { + spa_log_info(mix->log, "assign FC to STEREO (%f)", clev); + _MATRIX(FL,FC) += clev; + _MATRIX(FR,FC) += clev; + } else { + spa_log_info(mix->log, "assign FC to STEREO (%f)", SQRT1_2); + _MATRIX(FL,FC) += SQRT1_2; + _MATRIX(FR,FC) += SQRT1_2; + } + keep &= ~STEREO; + } else if (dst_mask & _MASK(MONO)){ + spa_log_info(mix->log, "assign FC to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][_CH(FC)]= 1.0f; + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign FC"); + } + } + + if (unassigned & STEREO){ + if (dst_mask & FRONT) { + spa_log_info(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_info(mix->log, "assign FC to FC (%f)", clev * SQRT2); + _MATRIX(FC,FC) = clev * SQRT2; + } + keep &= ~FRONT; + } else if ((dst_mask & _MASK(MONO))){ + spa_log_info(mix->log, "assign STEREO to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + matrix[i][_CH(FL)]= 1.0f; + matrix[i][_CH(FR)]= 1.0f; + } + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign STEREO"); + } + } + + if (unassigned & _MASK(RC)) { + if (dst_mask & REAR){ + spa_log_info(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_info(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_info(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_info(mix->log, "assign RC to FC (%f)", slev * SQRT1_2); + _MATRIX(FC,RC) += slev * SQRT1_2; + } else if (dst_mask & _MASK(MONO)){ + spa_log_info(mix->log, "assign RC to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][_CH(RC)]= 1.0f; + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign RC"); + } + } + + if (unassigned & REAR) { + if (dst_mask & _MASK(RC)) { + spa_log_info(mix->log, "assign RL+RR to RC"); + _MATRIX(RC,RL) += SQRT1_2; + _MATRIX(RC,RR) += SQRT1_2; + } else if (dst_mask & SIDE) { + spa_log_info(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_info(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_info(mix->log, "assign RL+RR to FC (%f)", + slev * SQRT1_2); + _MATRIX(FC,RL)+= slev * SQRT1_2; + _MATRIX(FC,RR)+= slev * SQRT1_2; + } else if (dst_mask & _MASK(MONO)){ + spa_log_info(mix->log, "assign RL+RR to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + matrix[i][_CH(RL)]= 1.0f; + matrix[i][_CH(RR)]= 1.0f; + } + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign RL"); + } + } + + if (unassigned & SIDE) { + if (dst_mask & REAR) { + if (src_mask & _MASK(RL)) { + spa_log_info(mix->log, "assign SL+SR to RL+RR (%f)", SQRT1_2); + _MATRIX(RL,SL) += SQRT1_2; + _MATRIX(RR,SR) += SQRT1_2; + } else { + spa_log_info(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_info(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_info(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_info(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_info(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_info(mix->log, "assign SL+SR to FC (%f)", slev * SQRT1_2); + _MATRIX(FC,SL) += slev * SQRT1_2; + _MATRIX(FC,SR) += slev * SQRT1_2; + } else if (dst_mask & _MASK(MONO)){ + spa_log_info(mix->log, "assign SL+SR to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + matrix[i][_CH(SL)]= 1.0f; + matrix[i][_CH(SR)]= 1.0f; + } + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign SL"); + } + } + + if (unassigned & _MASK(FLC)) { + if (dst_mask & STEREO) { + spa_log_info(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_info(mix->log, "assign FLC+FRC to FC (%f)", SQRT1_2); + _MATRIX(FC,FLC)+= SQRT1_2; + _MATRIX(FC,FRC)+= SQRT1_2; + } else if (dst_mask & _MASK(MONO)){ + spa_log_info(mix->log, "assign FLC+FRC to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + matrix[i][_CH(FLC)]= 1.0f; + matrix[i][_CH(FRC)]= 1.0f; + } + normalize = true; + } 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_info(mix->log, "assign LFE to FC (%f)", llev); + _MATRIX(FC,LFE) += llev; + } else if (dst_mask & STEREO) { + spa_log_info(mix->log, "assign LFE to FL+FR (%f)", + llev * SQRT1_2); + _MATRIX(FL,LFE) += llev * SQRT1_2; + _MATRIX(FR,LFE) += llev * SQRT1_2; + } else if ((dst_mask & _MASK(MONO))){ + spa_log_info(mix->log, "assign LFE to MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][_CH(LFE)]= 1.0f; + normalize = true; + } else { + spa_log_warn(mix->log, "can't assign LFE"); + } + } + + unassigned = dst_mask & ~src_mask & keep; + + spa_log_info(mix->log, "unassigned upmix %08"PRIx64" lfe:%f", + unassigned, mix->lfe_cutoff); + + if (unassigned & STEREO) { + if ((src_mask & FRONT) == FRONT) { + spa_log_info(mix->log, "produce STEREO from FC (%f)", clev); + _MATRIX(FL,FC) += clev; + _MATRIX(FR,FC) += clev; + } else if (src_mask & _MASK(MONO)) { + spa_log_info(mix->log, "produce STEREO from MONO (%f)", 1.0f); + _MATRIX(FL,MONO) += 1.0f; + _MATRIX(FR,MONO) += 1.0f; + } else { + spa_log_warn(mix->log, "can't produce STEREO"); + } + } + if (unassigned & FRONT) { + if ((src_mask & STEREO) == STEREO) { + spa_log_info(mix->log, "produce FC from STEREO (%f)", clev); + _MATRIX(FC,FL) += clev; + _MATRIX(FC,FR) += clev; + filter_fc = true; + } else if (src_mask & _MASK(MONO)) { + spa_log_info(mix->log, "produce FC from MONO (%f)", 1.0f); + _MATRIX(FC,MONO) += 1.0f; + filter_fc = true; + } else { + spa_log_warn(mix->log, "can't produce FC"); + } + } + if (unassigned & _MASK(LFE)) { + if ((src_mask & STEREO) == STEREO) { + spa_log_info(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_info(mix->log, "produce LFE from FC (%f)", llev); + _MATRIX(LFE,FC) += llev; + filter_lfe = true; + } else if (src_mask & _MASK(MONO)) { + spa_log_info(mix->log, "produce LFE from MONO (%f)", 1.0f); + _MATRIX(LFE,MONO) += 1.0f; + filter_lfe = true; + } else { + spa_log_warn(mix->log, "can't produce LFE"); + } + } + if (unassigned & SIDE) { + if ((src_mask & REAR) == REAR) { + spa_log_info(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_info(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_info(mix->log, "produce SIDE from FC (%f)", clev); + _MATRIX(SL,FC) += clev; + _MATRIX(SR,FC) += clev; + } else if (src_mask & _MASK(MONO) && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_info(mix->log, "produce SIDE from MONO (%f)", 1.0f); + _MATRIX(SL,MONO) += 1.0f; + _MATRIX(SR,MONO) += 1.0f; + } else { + spa_log_info(mix->log, "won't produce SIDE"); + } + } + if (unassigned & REAR) { + if ((src_mask & SIDE) == SIDE) { + spa_log_info(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_info(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_info(mix->log, "produce REAR from FC (%f)", clev); + _MATRIX(RL,FC) += clev; + _MATRIX(RR,FC) += clev; + } else if (src_mask & _MASK(MONO) && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_info(mix->log, "produce REAR from MONO (%f)", 1.0f); + _MATRIX(RL,MONO) += 1.0f; + _MATRIX(RR,MONO) += 1.0f; + } else { + spa_log_info(mix->log, "won't produce SIDE"); + } + } + if (unassigned & _MASK(RC)) { + if ((src_mask & REAR) == REAR) { + spa_log_info(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_info(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_info(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_info(mix->log, "produce RC from FC (%f)", slev); + _MATRIX(RC,FC) += slev; + } else if (src_mask & _MASK(MONO) && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_info(mix->log, "produce RC from MONO (%f)", 1.0f); + _MATRIX(RC,MONO) += 1.0f; + } else { + spa_log_info(mix->log, "won't produce RC"); + } + } + +done: + if (dst_paired == 0) + dst_paired = ~0LU; + if (src_paired == 0) + src_paired = ~0LU; + + for (jc = 0, ic = 0, i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + float sum = 0.0f; + char str1[1024], str2[1024]; + struct spa_strbuf sb1, sb2; + + spa_strbuf_init(&sb1, str1, sizeof(str1)); + spa_strbuf_init(&sb2, str2, sizeof(str2)); + + if ((dst_paired & (1UL << i)) == 0) + continue; + for (jc = 0, j = 0; j < SPA_AUDIO_MAX_CHANNELS; j++) { + if ((src_paired & (1UL << j)) == 0) + continue; + if (ic >= dst_chan || jc >= src_chan) + continue; + + if (ic == 0) + spa_strbuf_append(&sb2, "%-4.4s ", + src_mask == 0 ? "UNK" : + spa_debug_type_find_short_name(spa_type_audio_channel, j + _SH)); + + mix->matrix_orig[ic][jc++] = matrix[i][j]; + sum += fabsf(matrix[i][j]); + + if (matrix[i][j] == 0.0f) + spa_strbuf_append(&sb1, " "); + else + spa_strbuf_append(&sb1, "%1.3f ", matrix[i][j]); + } + if (sb2.pos > 0) + spa_log_info(mix->log, " %s", str2); + if (sb1.pos > 0) { + spa_log_info(mix->log, "%-4.4s %s %f", + dst_mask == 0 ? "UNK" : + spa_debug_type_find_short_name(spa_type_audio_channel, i + _SH), + str1, 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 { + lr4_set(&mix->lr4[ic], BQ_NONE, mix->fc_cutoff / mix->freq); + } + ic++; + } + if (normalize && maxsum > 1.0f) { + spa_log_info(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 = (uint32_t)(mix->rear_delay * mix->freq / 1000.0f); + mix->func_name = info->name; + + spa_zero(mix->taps_mem); + mix->taps = SPA_PTR_ALIGN(mix->taps_mem, CHANNELMIX_OPS_MAX_ALIGN, float); + mix->buffer[0] = SPA_PTR_ALIGN(&mix->buffer_mem[0], CHANNELMIX_OPS_MAX_ALIGN, float); + mix->buffer[1] = SPA_PTR_ALIGN(&mix->buffer_mem[2*BUFFER_SIZE], CHANNELMIX_OPS_MAX_ALIGN, float); + + if (mix->hilbert_taps > 0) { + mix->n_taps = SPA_CLAMP(mix->hilbert_taps, 15u, MAX_TAPS) | 1; + blackman_window(mix->taps, mix->n_taps); + hilbert_generate(mix->taps, mix->n_taps); + reverse_taps(mix->taps, mix->n_taps); + } else { + mix->n_taps = 1; + mix->taps[0] = 1.0f; + } + if (mix->delay + mix->n_taps > BUFFER_SIZE) + mix->delay = BUFFER_SIZE - mix->n_taps; + + spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay, + mix->options); + + return make_matrix(mix); +} diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h new file mode 100644 index 0000000..26e2efc --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops.h @@ -0,0 +1,141 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include +#include + +#include "crossover.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 255u + +#define CHANNELMIX_OPS_MAX_ALIGN 16 + +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_mem[2 * BUFFER_SIZE*2 + CHANNELMIX_OPS_MAX_ALIGN/4]; + float *buffer[2]; + uint32_t pos[2]; + uint32_t delay; + float taps_mem[MAX_TAPS + CHANNELMIX_OPS_MAX_ALIGN/4]; + float *taps; + uint32_t n_taps; + + void (*process) (struct channelmix *mix, void * SPA_RESTRICT dst[], + 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_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..537d493 --- /dev/null +++ b/spa/plugins/audioconvert/crossover.c @@ -0,0 +1,19 @@ +/* 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 +#include + +#include "crossover.h" + +void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq) +{ + biquad_set(&lr4->bq, type, freq, 0, 0); + lr4->x1 = 0; + lr4->x2 = 0; + lr4->y1 = 0; + lr4->y2 = 0; + lr4->active = type != BQ_NONE; +} diff --git a/spa/plugins/audioconvert/crossover.h b/spa/plugins/audioconvert/crossover.h new file mode 100644 index 0000000..159d959 --- /dev/null +++ b/spa/plugins/audioconvert/crossover.h @@ -0,0 +1,28 @@ +/* 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 + +#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; + bool active; +}; + +void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq); + +#endif /* CROSSOVER_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..a939da4 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-avx2.c @@ -0,0 +1,1187 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "fmt-ops.h" + +#include +// 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) + +#define _MM256_BSWAP_EPI16(x) \ +({ \ + _mm256_or_si256( \ + _mm256_slli_epi16(x, 8), \ + _mm256_srli_epi16(x, 8)); \ +}) + +static void +conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + 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); +} + + +static void +conv_s16s_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const uint16_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m256i in = _mm256_setzero_si256(); + __m256 out, factor = _mm256_set1_ps(1.0f / S16_SCALE); + + if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 32))) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in = _mm256_insert_epi16(in, s[0*n_channels], 1); + in = _mm256_insert_epi16(in, s[1*n_channels], 3); + in = _mm256_insert_epi16(in, s[2*n_channels], 5); + in = _mm256_insert_epi16(in, s[3*n_channels], 7); + in = _mm256_insert_epi16(in, s[4*n_channels], 9); + in = _mm256_insert_epi16(in, s[5*n_channels], 11); + in = _mm256_insert_epi16(in, s[6*n_channels], 13); + in = _mm256_insert_epi16(in, s[7*n_channels], 15); + in = _MM256_BSWAP_EPI16(in); + + in = _mm256_srai_epi32(in, 16); + out = _mm256_cvtepi32_ps(in); + out = _mm256_mul_ps(out, factor); + _mm256_store_ps(&d0[n], out); + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE); + out = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0])); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s16s_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const uint16_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_s16s_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +void +conv_s16_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + 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_s16s_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const uint16_t *s = src[0]; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m256i in[2], t[4]; + __m256 out[4], factor = _mm256_set1_ps(1.0f / S16_SCALE); + + if (SPA_IS_ALIGNED(s, 32) && + SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + in[0] = _mm256_load_si256((__m256i*)(s + 0)); + in[1] = _mm256_load_si256((__m256i*)(s + 16)); + in[0] = _MM256_BSWAP_EPI16(in[0]); + in[1] = _MM256_BSWAP_EPI16(in[1]); + + t[0] = _mm256_slli_epi32(in[0], 16); + t[0] = _mm256_srai_epi32(t[0], 16); + out[0] = _mm256_cvtepi32_ps(t[0]); + out[0] = _mm256_mul_ps(out[0], factor); + + t[1] = _mm256_srai_epi32(in[0], 16); + out[1] = _mm256_cvtepi32_ps(t[1]); + out[1] = _mm256_mul_ps(out[1], factor); + + t[2] = _mm256_slli_epi32(in[1], 16); + t[2] = _mm256_srai_epi32(t[2], 16); + out[2] = _mm256_cvtepi32_ps(t[2]); + out[2] = _mm256_mul_ps(out[2], factor); + + t[3] = _mm256_srai_epi32(in[1], 16); + out[3] = _mm256_cvtepi32_ps(t[3]); + out[3] = _mm256_mul_ps(out[3], factor); + + _mm256_store_ps(&d0[n + 0], out[0]); + _mm256_store_ps(&d1[n + 0], out[1]); + _mm256_store_ps(&d0[n + 8], out[2]); + _mm256_store_ps(&d1[n + 8], out[3]); + + s += 32; + } + for(; n < n_samples; n++) { + __m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE); + out[0] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0])); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[1])); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += 2; + } +} + +static void +conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + 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); +} + +static 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 / S32_SCALE_I2F); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + 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); + + 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 / S32_SCALE_I2F); + out[0] = _mm_cvtsi32_ss(factor, s[0]); + out[1] = _mm_cvtsi32_ss(factor, s[1]); + out[2] = _mm_cvtsi32_ss(factor, s[2]); + out[3] = _mm_cvtsi32_ss(factor, s[3]); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + out[2] = _mm_mul_ss(out[2], factor); + 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; + } +} + +static 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 / S32_SCALE_I2F); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + 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); + + 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 / S32_SCALE_I2F); + out[0] = _mm_cvtsi32_ss(factor, s[0]); + out[1] = _mm_cvtsi32_ss(factor, s[1]); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += n_channels; + } +} + +static 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 / S32_SCALE_I2F); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + 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); + + 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 / S32_SCALE_I2F); + out = _mm_cvtsi32_ss(factor, s[0]); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + 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[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]); + d += n_channels; + } +} + +#define spa_write_unaligned(ptr, type, val) \ +__extension__ ({ \ + __typeof__(type) _val = (val); \ + memcpy((ptr), &_val, sizeof(_val)); \ +}) + +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(S32_SCALE_F2I); + __m256 int_min = _mm256_set1_ps(S32_MIN_F2I); + __m256 int_max = _mm256_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32)) + 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 */ + + 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__ + spa_write_unaligned(d + 0*n_channels, uint64_t, _mm256_extract_epi64(t[0], 0)); + spa_write_unaligned(d + 1*n_channels, uint64_t, _mm256_extract_epi64(t[0], 1)); + spa_write_unaligned(d + 2*n_channels, uint64_t, _mm256_extract_epi64(t[1], 0)); + spa_write_unaligned(d + 3*n_channels, uint64_t, _mm256_extract_epi64(t[1], 1)); + spa_write_unaligned(d + 4*n_channels, uint64_t, _mm256_extract_epi64(t[0], 2)); + spa_write_unaligned(d + 5*n_channels, uint64_t, _mm256_extract_epi64(t[0], 3)); + spa_write_unaligned(d + 6*n_channels, uint64_t, _mm256_extract_epi64(t[1], 2)); + spa_write_unaligned(d + 7*n_channels, uint64_t, _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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + + 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]); + _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(S32_SCALE_F2I); + __m256 int_min = _mm256_set1_ps(S32_MIN_F2I); + __m256 int_max = _mm256_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32) && + SPA_IS_ALIGNED(s2, 32) && + SPA_IS_ALIGNED(s3, 32)) + 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 */ + + 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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + 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]); + _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 */ + + spa_write_unaligned(d + 0*n_channels, uint32_t, _mm256_extract_epi32(out[0],0)); + spa_write_unaligned(d + 1*n_channels, uint32_t, _mm256_extract_epi32(out[0],1)); + spa_write_unaligned(d + 2*n_channels, uint32_t, _mm256_extract_epi32(out[0],2)); + spa_write_unaligned(d + 3*n_channels, uint32_t, _mm256_extract_epi32(out[0],3)); + spa_write_unaligned(d + 4*n_channels, uint32_t, _mm256_extract_epi32(out[0],4)); + spa_write_unaligned(d + 5*n_channels, uint32_t, _mm256_extract_epi32(out[0],5)); + spa_write_unaligned(d + 6*n_channels, uint32_t, _mm256_extract_epi32(out[0],6)); + spa_write_unaligned(d + 7*n_channels, uint32_t, _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__ + spa_write_unaligned(d + 0*n_channels, uint64_t, _mm256_extract_epi64(out[2], 0)); /* a0 b0 c0 d0 */ + spa_write_unaligned(d + 1*n_channels, uint64_t, _mm256_extract_epi64(out[2], 1)); /* a1 b1 c1 d1 */ + spa_write_unaligned(d + 2*n_channels, uint64_t, _mm256_extract_epi64(out[3], 0)); /* a2 b2 c2 d2 */ + spa_write_unaligned(d + 3*n_channels, uint64_t, _mm256_extract_epi64(out[3], 1)); /* a3 b3 c3 d3 */ + spa_write_unaligned(d + 4*n_channels, uint64_t, _mm256_extract_epi64(out[2], 2)); /* a4 b4 c4 d4 */ + spa_write_unaligned(d + 5*n_channels, uint64_t, _mm256_extract_epi64(out[2], 3)); /* a5 b5 c5 d5 */ + spa_write_unaligned(d + 6*n_channels, uint64_t, _mm256_extract_epi64(out[3], 2)); /* a6 b6 c6 d6 */ + spa_write_unaligned(d + 7*n_channels, uint64_t, _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; + } +} + +void +conv_f32d_to_s16s_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + uint16_t *d = dst[0]; + uint32_t n, unrolled; + __m256 in[4]; + __m256i out[4], t[4]; + __m256 int_scale = _mm256_set1_ps(S16_SCALE); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale); + in[2] = _mm256_mul_ps(_mm256_load_ps(&s0[n+8]), int_scale); + in[3] = _mm256_mul_ps(_mm256_load_ps(&s1[n+8]), int_scale); + + out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + out[2] = _mm256_cvtps_epi32(in[2]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[3] = _mm256_cvtps_epi32(in[3]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + t[2] = _mm256_unpacklo_epi32(out[2], out[3]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[3] = _mm256_unpackhi_epi32(out[2], out[3]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + + out[0] = _mm256_packs_epi32(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + out[1] = _mm256_packs_epi32(t[2], t[3]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + out[0] = _MM256_BSWAP_EPI16(out[0]); + out[1] = _MM256_BSWAP_EPI16(out[1]); + + _mm256_store_si256((__m256i*)(d+0), out[0]); + _mm256_store_si256((__m256i*)(d+16), out[1]); + + d += 32; + } + for(; n < n_samples; n++) { + __m128 in[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = bswap_16((uint16_t)_mm_cvtss_si32(in[0])); + d[1] = bswap_16((uint16_t)_mm_cvtss_si32(in[1])); + d += 2; + } +} diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c new file mode 100644 index 0000000..e92b5bf --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-c.c @@ -0,0 +1,413 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include + +#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..55be55f --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-neon.c @@ -0,0 +1,467 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#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 { v1.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-rvv.c b/spa/plugins/audioconvert/fmt-ops-rvv.c new file mode 100644 index 0000000..02b0172 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-rvv.c @@ -0,0 +1,259 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright (c) 2023 Institue of Software Chinese Academy of Sciences (ISCAS). */ +/* SPDX-License-Identifier: MIT */ + +#include "fmt-ops.h" + +#if HAVE_RVV +void +f32_to_s16(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_samples) +{ + asm __volatile__ ( + ".option arch, +v \n\t" + "li t0, 1191182336 \n\t" + "fmv.w.x fa5, t0 \n\t" + "1: \n\t" + "vsetvli t0, %[n_samples], e32, m8, ta, ma \n\t" + "vle32.v v8, (%[src]) \n\t" + "sub %[n_samples], %[n_samples], t0 \n\t" + "vfmul.vf v8, v8, fa5 \n\t" + "vsetvli zero, zero, e16, m4, ta, ma \n\t" + "vfncvt.x.f.w v8, v8 \n\t" + "slli t0, t0, 1 \n\t" + "vse16.v v8, (%[dst]) \n\t" + "add %[src], %[src], t0 \n\t" + "add %[dst], %[dst], t0 \n\t" + "add %[src], %[src], t0 \n\t" + "bnez %[n_samples], 1b \n\t" + : [n_samples] "+r" (n_samples), + [src] "+r" (src), + [dst] "+r" (dst) + : + : "cc", "memory" + ); +} + +void +conv_f32_to_s16_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples * conv->n_channels <= 4) { + conv_f32_to_s16_c(conv, dst, src, n_samples); + return; + } + + f32_to_s16(conv, *dst, *src, n_samples * conv -> n_channels); +} + +void +conv_f32d_to_s16d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples <= 4) { + conv_f32d_to_s16d_c(conv, dst, src, n_samples); + return; + } + + uint32_t i = 0, n_channels = conv->n_channels; + for(i = 0; i < n_channels; i++) { + f32_to_s16(conv, dst[i], src[i], n_samples); + } +} + +static void +f32d_to_s16(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src[0]; + uint32_t stride = n_channels << 1; + + asm __volatile__ ( + ".option arch, +v \n\t" + "li t0, 1191182336 \n\t" + "fmv.w.x fa5, t0 \n\t" + "1: \n\t" + "vsetvli t0, %[n_samples], e32, m8, ta, ma \n\t" + "vle32.v v8, (%[s]) \n\t" + "sub %[n_samples], %[n_samples], t0 \n\t" + "vfmul.vf v8, v8, fa5 \n\t" + "vsetvli zero, zero, e16, m4, ta, ma \n\t" + "vfncvt.x.f.w v8, v8 \n\t" + "slli t2, t0, 2 \n\t" + "mul t3, t0, %[stride] \n\t" + "vsse16.v v8, (%[dst]), %[stride] \n\t" + "add %[s], %[s], t2 \n\t" + "add %[dst], %[dst], t3 \n\t" + "bnez %[n_samples], 1b \n\t" + : [n_samples] "+r" (n_samples), + [s] "+r" (s), + [dst] "+r" (dst) + : [stride] "r" (stride) + : "cc", "memory" + ); +} + +void +conv_f32d_to_s16_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples <= 4) { + conv_f32d_to_s16_c(conv, dst, src, n_samples); + return; + } + + int16_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(i = 0; i < n_channels; i++) + f32d_to_s16(conv, &d[i], &src[i], n_channels, n_samples); +} + +static void +s16_to_f32d(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + float *d = dst[0]; + uint32_t stride = n_channels << 1; + + asm __volatile__ ( + ".option arch, +v \n\t" + "li t0, 939524096 \n\t" + "fmv.w.x fa5, t0 \n\t" + "1: \n\t" + "vsetvli t0, %[n_samples], e16, m4, ta, ma \n\t" + "vlse16.v v8, (%[src]), %[stride] \n\t" + "sub %[n_samples], %[n_samples], t0 \n\t" + "vfwcvt.f.x.v v16, v8 \n\t" + "vsetvli zero, zero, e32, m8, ta, ma \n\t" + "mul t4, t0, %[stride] \n\t" + "vfmul.vf v8, v16, fa5 \n\t" + "slli t3, t0, 2 \n\t" + "vse32.v v8, (%[d]) \n\t" + "add %[src], %[src], t4 \n\t" + "add %[d], %[d], t3 \n\t" + "bnez %[n_samples], 1b \n\t" + : [n_samples] "+r" (n_samples), + [src] "+r" (src), + [d] "+r" (d) + : [stride] "r" (stride) + : "cc", "memory" + ); + +} + +void +conv_s16_to_f32d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples <= 4) { + conv_s16_to_f32d_c(conv, dst, src, n_samples); + return; + } + + const int16_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(i = 0; i < n_channels; i++) + s16_to_f32d(conv, &dst[i], &s[i], n_channels, n_samples); +} + +static void +s32_to_f32d(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + float *d = dst[0]; + uint32_t stride = n_channels << 2; + + asm __volatile__ ( + ".option arch, +v \n\t" + "li t0, 805306368 \n\t" + "fmv.w.x fa5, t0 \n\t" + "1: \n\t" + "vsetvli t0, %[n_samples], e32, m8, ta, ma \n\t" + "vlse32.v v8, (%[src]), %[stride] \n\t" + "sub %[n_samples], %[n_samples], t0 \n\t" + "vfcvt.f.x.v v8, v8 \n\t" + "mul t4, t0, %[stride] \n\t" + "vfmul.vf v8, v8, fa5 \n\t" + "slli t3, t0, 2 \n\t" + "vse32.v v8, (%[d]) \n\t" + "add %[src], %[src], t4 \n\t" + "add %[d], %[d], t3 \n\t" + "bnez %[n_samples], 1b \n\t" + : [n_samples] "+r" (n_samples), + [src] "+r" (src), + [d] "+r" (d) + : [stride] "r" (stride) + : "cc", "memory" + ); + +} + +void +conv_s32_to_f32d_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples <= 4) { + conv_s32_to_f32d_c(conv, dst, src, n_samples); + return; + } + + const int32_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(i = 0; i < n_channels; i++) + s32_to_f32d(conv, &dst[i], &s[i], n_channels, n_samples); + return; +} + +static void +f32d_to_s32(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src[0]; + uint32_t stride = n_channels << 2; + + asm __volatile__ ( + ".option arch, +v \n\t" + "li t0, 1325400064 \n\t" + "li t2, 1325400063 \n\t" + "fmv.w.x fa5, t0 \n\t" + "fmv.w.x fa4, t2 \n\t" + "1: \n\t" + "vsetvli t0, %[n_samples], e32, m8, ta, ma \n\t" + "vle32.v v8, (%[s]) \n\t" + "sub %[n_samples], %[n_samples], t0 \n\t" + "vfmul.vf v8, v8, fa5 \n\t" + "vfmin.vf v8, v8, fa4 \n\t" + "vfcvt.x.f.v v8, v8 \n\t" + "slli t2, t0, 2 \n\t" + "mul t3, t0, %[stride] \n\t" + "vsse32.v v8, (%[dst]), %[stride] \n\t" + "add %[s], %[s], t2 \n\t" + "add %[dst], %[dst], t3 \n\t" + "bnez %[n_samples], 1b \n\t" + : [n_samples] "+r" (n_samples), + [s] "+r" (s), + [dst] "+r" (dst) + : [stride] "r" (stride) + : "cc", "memory" + ); +} + +void +conv_f32d_to_s32_rvv(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + if (n_samples <= 4) { + conv_f32d_to_s32_c(conv, dst, src, n_samples); + return; + } + + int32_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(i = 0; i < n_channels; i++) + f32d_to_s32(conv, &d[i], &src[i], n_channels, n_samples); +} +#endif diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c new file mode 100644 index 0000000..fe6af66 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-sse2.c @@ -0,0 +1,1641 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "fmt-ops.h" + +#include + +#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) + +#define _MM_BSWAP_EPI16(x) \ +({ \ + _mm_or_si128( \ + _mm_slli_epi16(x, 8), \ + _mm_srli_epi16(x, 8)); \ +}) + +#define _MM_BSWAP_EPI32(x) \ +({ \ + __m128i a = _MM_BSWAP_EPI16(x); \ + a = _mm_shufflelo_epi16(a, _MM_SHUFFLE(2, 3, 0, 1)); \ + a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(2, 3, 0, 1)); \ +}) + +static void +conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + 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); +} + +static void +conv_s16s_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const uint16_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in = _mm_setzero_si128(); + __m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE); + + if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 16))) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_insert_epi16(in, s[0*n_channels], 1); + in = _mm_insert_epi16(in, s[1*n_channels], 3); + in = _mm_insert_epi16(in, s[2*n_channels], 5); + in = _mm_insert_epi16(in, s[3*n_channels], 7); + in = _MM_BSWAP_EPI16(in); + in = _mm_srai_epi32(in, 16); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 4*n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0])); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s16s_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const uint16_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_s16s_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +void +conv_s16_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + 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_s16s_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const uint16_t *s = src[0]; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m128i in[2], t[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE); + + if (SPA_IS_ALIGNED(s, 16) && + SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_load_si128((__m128i*)(s + 0)); + in[1] = _mm_load_si128((__m128i*)(s + 8)); + in[0] = _MM_BSWAP_EPI16(in[0]); + in[1] = _MM_BSWAP_EPI16(in[1]); + + t[0] = _mm_slli_epi32(in[0], 16); + t[0] = _mm_srai_epi32(t[0], 16); + out[0] = _mm_cvtepi32_ps(t[0]); + out[0] = _mm_mul_ps(out[0], factor); + + t[1] = _mm_srai_epi32(in[0], 16); + out[1] = _mm_cvtepi32_ps(t[1]); + out[1] = _mm_mul_ps(out[1], factor); + + t[2] = _mm_slli_epi32(in[1], 16); + t[2] = _mm_srai_epi32(t[2], 16); + out[2] = _mm_cvtepi32_ps(t[2]); + out[2] = _mm_mul_ps(out[2], factor); + + t[3] = _mm_srai_epi32(in[1], 16); + out[3] = _mm_cvtepi32_ps(t[3]); + out[3] = _mm_mul_ps(out[3], factor); + + _mm_store_ps(&d0[n + 0], out[0]); + _mm_store_ps(&d1[n + 0], out[1]); + _mm_store_ps(&d0[n + 4], out[2]); + _mm_store_ps(&d1[n + 4], out[3]); + + s += 16; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[0])); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_cvtsi32_ss(factor, (int16_t)bswap_16(s[1])); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += 2; + } +} + +#define spa_read_unaligned(ptr, type) \ +__extension__ ({ \ + __typeof__(type) _val; \ + memcpy(&_val, (ptr), sizeof(_val)); \ + _val; \ +}) + +#define spa_write_unaligned(ptr, type, val) \ +__extension__ ({ \ + __typeof__(type) _val = (val); \ + memcpy((ptr), &_val, sizeof(_val)); \ +}) +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( + spa_read_unaligned(&s[0 * n_channels], uint32_t), + spa_read_unaligned(&s[1 * n_channels], uint32_t), + spa_read_unaligned(&s[2 * n_channels], uint32_t), + spa_read_unaligned(&s[3 * n_channels], uint32_t)); + 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( + spa_read_unaligned(&s[0 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 3*n_channels], uint32_t)); + in[1] = _mm_setr_epi32( + spa_read_unaligned(&s[1 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 3*n_channels], uint32_t)); + + 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( + spa_read_unaligned(&s[0 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[0 + 3*n_channels], uint32_t)); + in[1] = _mm_setr_epi32( + spa_read_unaligned(&s[1 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[1 + 3*n_channels], uint32_t)); + in[2] = _mm_setr_epi32( + spa_read_unaligned(&s[2 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[2 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[2 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[2 + 3*n_channels], uint32_t)); + in[3] = _mm_setr_epi32( + spa_read_unaligned(&s[3 + 0*n_channels], uint32_t), + spa_read_unaligned(&s[3 + 1*n_channels], uint32_t), + spa_read_unaligned(&s[3 + 2*n_channels], uint32_t), + spa_read_unaligned(&s[3 + 3*n_channels], uint32_t)); + + 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); +} + +static 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 / S32_SCALE_I2F); + + 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]); + 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_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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + 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[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]); + 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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + 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]); + + t[0] = _mm_unpacklo_epi32(out[0], out[1]); + t[1] = _mm_unpackhi_epi32(out[0], out[1]); + + _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)t[0]); + _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)t[0]); + _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)t[1]); + _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)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]); + _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(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16) && + SPA_IS_ALIGNED(s2, 16) && + SPA_IS_ALIGNED(s3, 16)) + 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]); + + _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]); + _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]); +} + +// FIXME: this function is not covered with tests. +static void +conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + float *noise, uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[1]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S32_SCALE_F2I); + __m128 int_min = _mm_set1_ps(S32_MIN_F2I); + __m128 int_max = _mm_set1_ps(S32_MAX_F2I); + + if (SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~3; + 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[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]); + 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); +} + +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++) { + uint32_t *di = (uint32_t*)&d0[n], *si = (uint32_t*)s; + *di = bswap_32(*si); + 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++) { + *((uint32_t*)&d0[n]) = bswap_32(*((uint32_t*)&s[0])); + *((uint32_t*)&d1[n]) = bswap_32(*((uint32_t*)&s[1])); + *((uint32_t*)&d2[n]) = bswap_32(*((uint32_t*)&s[2])); + *((uint32_t*)&d3[n]) = bswap_32(*((uint32_t*)&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)); + + spa_write_unaligned(d + 0*n_channels, uint32_t, _mm_cvtsi128_si32(out[0])); + spa_write_unaligned(d + 1*n_channels, uint32_t, _mm_cvtsi128_si32(out[1])); + spa_write_unaligned(d + 2*n_channels, uint32_t, _mm_cvtsi128_si32(out[2])); + spa_write_unaligned(d + 3*n_channels, uint32_t, _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_s16s_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0]; + uint16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + out[0] = _MM_BSWAP_EPI16(out[0]); + + d[0*n_channels] = _mm_extract_epi16(out[0], 0); + d[1*n_channels] = _mm_extract_epi16(out[0], 1); + d[2*n_channels] = _mm_extract_epi16(out[0], 2); + d[3*n_channels] = _mm_extract_epi16(out[0], 3); + d[4*n_channels] = _mm_extract_epi16(out[0], 4); + d[5*n_channels] = _mm_extract_epi16(out[0], 5); + d[6*n_channels] = _mm_extract_epi16(out[0], 6); + d[7*n_channels] = _mm_extract_epi16(out[0], 7); + d += 8*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = bswap_16((uint16_t)_mm_cvtss_si32(in[0])); + d += n_channels; + } +} + +void +conv_f32d_to_s16s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + uint16_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_f32d_to_s16s_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples); +} + +static void +conv_f32d_to_s16_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + const float *noise, uint32_t n_channels, uint32_t n_samples) +{ + 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; + } +} + +void +conv_f32d_to_s16s_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + uint16_t *d = dst[0]; + uint32_t n, unrolled; + __m128 in[4]; + __m128i out[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n+0]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n+0]), int_scale); + in[2] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + in[3] = _mm_mul_ps(_mm_load_ps(&s1[n+4]), int_scale); + + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[2] = _mm_cvtps_epi32(in[2]); + out[3] = _mm_cvtps_epi32(in[3]); + + out[0] = _mm_packs_epi32(out[0], out[2]); + out[1] = _mm_packs_epi32(out[1], out[3]); + + out[2] = _mm_unpacklo_epi16(out[0], out[1]); + out[3] = _mm_unpackhi_epi16(out[0], out[1]); + + out[2] = _MM_BSWAP_EPI16(out[2]); + out[3] = _MM_BSWAP_EPI16(out[3]); + + _mm_storeu_si128((__m128i*)(d+0), out[2]); + _mm_storeu_si128((__m128i*)(d+8), out[3]); + + d += 16; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = bswap_16((uint16_t)_mm_cvtss_si32(in[0])); + d[1] = bswap_16((uint16_t)_mm_cvtss_si32(in[1])); + d += 2; + } +} diff --git a/spa/plugins/audioconvert/fmt-ops-sse41.c b/spa/plugins/audioconvert/fmt-ops-sse41.c new file mode 100644 index 0000000..c218762 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-sse41.c @@ -0,0 +1,73 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "fmt-ops.h" + +#include + +#define spa_read_unaligned(ptr, type) \ +__extension__ ({ \ + __typeof__(type) _val; \ + memcpy(&_val, (ptr), sizeof(_val)); \ + _val; \ +}) + +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, spa_read_unaligned(&s[0 * n_channels], uint32_t), 0); + in = _mm_insert_epi32(in, spa_read_unaligned(&s[1 * n_channels], uint32_t), 1); + in = _mm_insert_epi32(in, spa_read_unaligned(&s[2 * n_channels], uint32_t), 2); + in = _mm_insert_epi32(in, spa_read_unaligned(&s[3 * n_channels], uint32_t), 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..5879aa7 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-ssse3.c @@ -0,0 +1,91 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "fmt-ops.h" + +#include + +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..4df3132 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops.c @@ -0,0 +1,578 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include + +#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 +#if defined (HAVE_RVV) + MAKE(S16, F32P, 0, conv_s16_to_f32d_rvv, SPA_CPU_FLAG_RISCV_V), +#endif + MAKE(S16, F32P, 0, conv_s16_to_f32d_c), + MAKE(S16P, F32, 0, conv_s16d_to_f32_c), + +#if defined (HAVE_AVX2) + MAKE(S16_OE, F32P, 2, conv_s16s_to_f32d_2_avx2, SPA_CPU_FLAG_AVX2), + MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_avx2, SPA_CPU_FLAG_AVX2), +#endif +#if defined (HAVE_SSE2) + MAKE(S16_OE, F32P, 2, conv_s16s_to_f32d_2_sse2, SPA_CPU_FLAG_SSE2), + MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_c), + + MAKE(F32, F32, 0, conv_copy32_c), + 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 +#if defined (HAVE_RVV) + MAKE(S32, F32P, 0, conv_s32_to_f32d_rvv, SPA_CPU_FLAG_RISCV_V), +#endif + MAKE(S32, F32, 0, conv_s32_to_f32_c), + MAKE(S32P, F32P, 0, conv_s32d_to_f32d_c), + 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 +#if defined (HAVE_RVV) + MAKE(F32, S16, 0, conv_f32_to_s16_rvv, SPA_CPU_FLAG_RISCV_V), + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_rvv, SPA_CPU_FLAG_RISCV_V), + MAKE(F32P, S16, 0, conv_f32d_to_s16_rvv, SPA_CPU_FLAG_RISCV_V), +#endif + MAKE(F32, S16, 0, conv_f32_to_s16_c), + + 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), +#if defined (HAVE_SSE2) + MAKE(F32P, S16_OE, 2, conv_f32d_to_s16s_2_sse2, SPA_CPU_FLAG_SSE2), + MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_c), + + MAKE(F32, U32, 0, conv_f32_to_u32_c), + 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 +#if defined (HAVE_RVV) + MAKE(F32P, S32, 0, conv_f32d_to_s32_rvv, SPA_CPU_FLAG_RISCV_V), +#endif + MAKE(F32P, S32, 0, conv_f32d_to_s32_c), + + 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]; + + /* we generate int32 bits of random values. With this scale + * factor, we bring this in the [-1.0, 1.0] range */ + 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; + /* the pattern method does not use a random number + * but flips the noise between [-(1<<(N-1)), 0] every + * 1024 samples. */ + 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: + /* Amplify the random noise, with additional + * N-1 bits of noise. */ + conv->scale *= (1 << (conv->noise_bits-1)); + break; + } + } + /* RECTANGULAR dither goes from [-0.5, 0.5] */ + 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..7aed0bc --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops.h @@ -0,0 +1,492 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include + +#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 32u + +#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 S25_MIN -16777216 +#define S25_MAX 16777215 +#define S25_SCALE 16777216.0f +#define S25_32_TO_F32(v) ITOF(int32_t, v, S25_SCALE, 0.0f) +#define S25_32S_TO_F32(v) S25_32_TO_F32(bswap_32(v)) +#define F32_TO_S25_32_D(v,d) FTOI(int32_t, v, S25_SCALE, 0.0f, d, S25_MIN, S25_MAX) +#define F32_TO_S25_32(v) F32_TO_S25_32_D(v, 0.0f) +#define F32_TO_S25_32S(v) bswap_32(F32_TO_S25_32(v)) +#define F32_TO_S25_32S_D(v,d) bswap_32(F32_TO_S25_32_D(v,d)) +#define S25_32_TO_S32(v) ((int32_t)(((uint32_t)(v)) << 7)) +#define S32_TO_S25_32(v) (((int32_t)(v)) >> 7) + +#define U32_MIN 0u +#define U32_MAX 4294967295u +#define U32_SCALE 2147483648.f +#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 -2147483648 +#define S32_MAX 2147483647 +#define S32_SCALE_I2F 2147483648.0f +#define S32_TO_F32(v) ITOF(int32_t, v, S32_SCALE_I2F, 0.0f) +#define S32S_TO_F32(v) S32_TO_F32(bswap_32(v)) + +#define S32_MIN_F2I ((int32_t)(((uint32_t)(S25_MIN)) << 7)) +#define S32_MAX_F2I ((S25_MAX) << 7) +#define S32_SCALE_F2I (-((float)(S32_MIN_F2I))) +#define F32_TO_S32_D(v,d) FTOI(int32_t, v, S32_SCALE_F2I, 0.0f, d, S32_MIN_F2I, S32_MAX_F2I) +#define F32_TO_S32(v) F32_TO_S32_D(v, 0.0f) +#define F32_TO_S32S(v) bswap_32(F32_TO_S32(v)) +#define F32_TO_S32S_D(v,d) bswap_32(F32_TO_S32_D(v,d)) + +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 ((uint32_t)((int32_t)src.v1 & 0xFFFF) << 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_RVV) +DEFINE_FUNCTION(f32d_to_s32, rvv); +DEFINE_FUNCTION(f32_to_s16, rvv); +DEFINE_FUNCTION(f32d_to_s16d, rvv); +DEFINE_FUNCTION(f32d_to_s16, rvv); +DEFINE_FUNCTION(s16_to_f32d, rvv); +DEFINE_FUNCTION(s32_to_f32d, rvv); +#endif +#if defined(HAVE_SSE2) +DEFINE_FUNCTION(s16_to_f32d_2, sse2); +DEFINE_FUNCTION(s16_to_f32d, sse2); +DEFINE_FUNCTION(s16s_to_f32d, sse2); +DEFINE_FUNCTION(s16s_to_f32d_2, sse2); +DEFINE_FUNCTION(s24_to_f32d, sse2); +DEFINE_FUNCTION(s32_to_f32d, sse2); +DEFINE_FUNCTION(f32d_to_s32, sse2); +DEFINE_FUNCTION(f32d_to_s32_noise, sse2); +DEFINE_FUNCTION(f32_to_s16, sse2); +DEFINE_FUNCTION(f32d_to_s16_2, sse2); +DEFINE_FUNCTION(f32d_to_s16, sse2); +DEFINE_FUNCTION(f32d_to_s16s_2, sse2); +DEFINE_FUNCTION(f32d_to_s16s, sse2); +DEFINE_FUNCTION(f32d_to_s16_noise, sse2); +DEFINE_FUNCTION(f32d_to_s16d, sse2); +DEFINE_FUNCTION(f32d_to_s16d_noise, sse2); +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(s16s_to_f32d, avx2); +DEFINE_FUNCTION(s16s_to_f32d_2, avx2); +DEFINE_FUNCTION(s24_to_f32d, avx2); +DEFINE_FUNCTION(s32_to_f32d, avx2); +DEFINE_FUNCTION(f32d_to_s32, avx2); +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..aa00940 --- /dev/null +++ b/spa/plugins/audioconvert/hilbert.h @@ -0,0 +1,57 @@ +/* Hilbert function */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef HILBERT_H +#define HILBERT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + + +static inline void blackman_window(float *taps, int n_taps) +{ + int n; + for (n = 0; n < n_taps; n++) { + float w = 2.0f * (float)M_PI * n / (n_taps-1); + taps[n] = 0.3635819f - 0.4891775f * cosf(w) + + 0.1365995f * cosf(2 * w) - 0.0106411f * cosf(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 = (float)M_PI * k; + taps[i] *= (1.0f - cosf(pk)) / pk; + } else { + taps[i] = 0.0f; + } + } + return 0; +} + +static inline void reverse_taps(float *taps, int n_taps) +{ + int i; + for (i = 0; i < n_taps/2; i++) + SPA_SWAP(taps[i], taps[n_taps-1-i]); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* HILBERT_H */ diff --git a/spa/plugins/audioconvert/law.h b/spa/plugins/audioconvert/law.h new file mode 100644 index 0000000..43a7b26 --- /dev/null +++ b/spa/plugins/audioconvert/law.h @@ -0,0 +1,2143 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +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..394bc11 --- /dev/null +++ b/spa/plugins/audioconvert/meson.build @@ -0,0 +1,257 @@ +audioconvert_sources = [ + 'audioadapter.c', + 'audioconvert.c', + 'plugin.c' +] + +simd_cargs = [] +simd_dependencies = [] + +opt_flags = [] +if host_machine.cpu_family() != 'alpha' + opt_flags += '-Ofast' +else + opt_flags += '-O3' +endif + +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 : [ opt_flags ], + 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, opt_flags, '-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 + +if have_rvv + audioconvert_rvv = static_library('audioconvert_rvv', + ['fmt-ops-rvv.c' ], + c_args : ['-O3', '-DHAVE_RVV'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_RVV'] + simd_dependencies += audioconvert_rvv +endif + +sparesampledumpcoeffs_sources = [ + 'resample-native.c', + 'resample-native-c.c', + 'spa-resample-dump-coeffs.c', +] + +sparesampledumpcoeffs = executable( + 'spa-resample-dump-coeffs', + sparesampledumpcoeffs_sources, + c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ], + dependencies : [ spa_dep, mathlib_native ], + install : false, + native : true, +) + +precomptuples = [] +foreach tuple : get_option('resampler-precomp-tuples') + precomptuples += '-t ' + tuple +endforeach + +resample_native_precomp_h = custom_target( + 'resample-native-precomp.h', + output : 'resample-native-precomp.h', + capture : true, + command : [ + sparesampledumpcoeffs, + ] + precomptuples +) + +audioconvert_lib = static_library('audioconvert', + ['fmt-ops.c', + 'channelmix-ops.c', + 'peaks-ops.c', + resample_native_precomp_h, + 'resample-native.c', + 'resample-peaks.c', + 'wavfile.c', + '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_inc = include_directories('../test') + +test_apps = [ + 'test-audioadapter', + 'test-audioconvert', + 'test-channelmix', + 'test-fmt-ops', + 'test-peaks', + 'test-resample', + 'test-resample-delay', + ] + +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, test_inc ], + 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, test_inc ], + 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..e944f13 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops-c.c @@ -0,0 +1,30 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#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..fc63b67 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops-sse.c @@ -0,0 +1,102 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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..29b93a0 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops.c @@ -0,0 +1,69 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +#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..1629e8e --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops.h @@ -0,0 +1,52 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +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..4d1a7f6 --- /dev/null +++ b/spa/plugins/audioconvert/plugin.c @@ -0,0 +1,33 @@ +/* Spa Audioconvert plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_audioconvert_factory; +extern const struct spa_handle_factory spa_audioadapter_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..a57d668 --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-avx.c @@ -0,0 +1,74 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "resample-native-impl.h" + +#include +#include + +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 = _mm256_loadu_ps(s + i + 0); + sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(taps + i + 0), sy[0]); + ty = _mm256_loadu_ps(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 = _mm_loadu_ps(s + i + 0); + sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(taps + i + 0), sx[0]); + tx = _mm_loadu_ps(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 = _mm256_loadu_ps(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 = _mm256_loadu_ps(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 = _mm_loadu_ps(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 = _mm_loadu_ps(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..7f4a862 --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-c.c @@ -0,0 +1,45 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..e3ad54d --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-impl.h @@ -0,0 +1,160 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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; + float 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, ch = r->channels; \ + \ + 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 < ch; 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, ch = r->channels; \ + \ + index = ioffs; \ + phase = (uint32_t)data->phase; \ + for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ + float *filter = &data->filter[phase * stride]; \ + for (c = 0; c < ch; c++) { \ + const float *s = src[c]; \ + float *d = dst[c]; \ + inner_product_##arch(&d[o], &s[index], \ + filter, 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, 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, ch = r->channels; \ + float phase; \ + \ + index = ioffs; \ + phase = data->phase; \ + for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ + float ph = phase * n_phases / out_rate; \ + uint32_t offset = (uint32_t)floorf(ph); \ + float *filter0 = &data->filter[(offset+0) * stride]; \ + float *filter1 = &data->filter[(offset+1) * stride]; \ + float pho = ph - offset; \ + for (c = 0; c < ch; c++) { \ + const float *s = src[c]; \ + float *d = dst[c]; \ + inner_product_ip_##arch(&d[o], &s[index], \ + filter0, filter1, pho, 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..9176acc --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-neon.c @@ -0,0 +1,198 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "resample-native-impl.h" + +#include + +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..241d2b1 --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-sse.c @@ -0,0 +1,74 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "resample-native-impl.h" + +#include + +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..c4f1d37 --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-ssse3.c @@ -0,0 +1,95 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "resample-native-impl.h" + +#include + +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..f393e3d --- /dev/null +++ b/spa/plugins/audioconvert/resample-native.c @@ -0,0 +1,440 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include "resample-native-impl.h" +#ifndef RESAMPLE_DISABLE_PRECOMP +#include "resample-native-precomp.h" +#endif + +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)] = (float) + (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, gcd, old_out_rate; + float phase; + + if (SPA_LIKELY(data->rate == rate)) + return; + + old_out_rate = data->out_rate; + in_rate = (uint32_t)(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 / (float)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 && rate == 1.0) { + 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 gcd:%d phase:%f inc:%d frac:%d", r, + rate, r->i_rate, r->o_rate, gcd, 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 = (uint32_t)((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 uint32_t impl_native_out_len(struct resample *r, uint32_t in_len) +{ + struct native_data *data = r->data; + uint32_t out_len; + + in_len = in_len - SPA_MIN(in_len, (data->n_taps - data->hist) + 1); + out_len = (uint32_t)(in_len * data->out_rate - data->phase); + out_len = (out_len + data->in_rate - 1) / data->in_rate; + + spa_log_trace_fp(r->log, "native %p: hist:%d %d->%d", r, data->hist, in_len, out_len); + + return out_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; + d->phase = 0; +} + +static uint32_t impl_native_delay (struct resample *r) +{ + struct native_data *d = r->data; + return d->n_taps / 2 - 1; +} + +static float impl_native_phase (struct resample *r) +{ + struct native_data *d = r->data; + float pho = 0; + + if (d->func == d->info->process_full) { + pho = -(float)((int32_t)d->phase) / d->out_rate; + + /* XXX: this is how it seems to behave, but not clear why */ + if (d->hist >= d->n_taps - 1) + pho += 1.0f; + } else if (d->func == d->info->process_inter) { + pho = -d->phase / d->out_rate; + + /* XXX: this is how it seems to behave, but not clear why */ + if (d->hist >= d->n_taps - 1) + pho += 1.0f; + } + + return pho; +} + +int resample_native_init(struct resample *r) +{ + 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->out_len = impl_native_out_len; + r->process = impl_native_process; + r->reset = impl_native_reset; + r->delay = impl_native_delay; + r->phase = impl_native_phase; + + q = &window_qualities[r->quality]; + + 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); + +#ifndef RESAMPLE_DISABLE_PRECOMP + /* See if we have precomputed coefficients */ + for (c = 0; precomp_coeffs[c].filter; c++) { + if (precomp_coeffs[c].in_rate == r->i_rate && + precomp_coeffs[c].out_rate == r->o_rate && + precomp_coeffs[c].quality == r->quality) + break; + } + + if (precomp_coeffs[c].filter) { + spa_log_debug(r->log, "using precomputed filter for %u->%u(%u)", + r->i_rate, r->o_rate, r->quality); + spa_memcpy(d->filter, precomp_coeffs[c].filter, filter_size); + } else { +#endif + build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale); +#ifndef RESAMPLE_DISABLE_PRECOMP + } +#endif + + d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags); + if (SPA_UNLIKELY(d->info == NULL)) { + 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 gcd:%d n_taps:%d n_phases:%d features:%08x:%08x", + r, r->quality, r->i_rate, r->o_rate, gcd, 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..aade771 --- /dev/null +++ b/spa/plugins/audioconvert/resample-peaks.c @@ -0,0 +1,141 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +#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 - i); + + m = peaks_abs_max(&pd->peaks, &s[i], chunk, m); + + i += chunk; + i_count += chunk; + + if (chunk == 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; + + while (pd->i_count >= r->i_rate && pd->o_count >= r->o_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 uint32_t impl_peaks_out_len(struct resample *r, uint32_t in_len) +{ + return in_len; +} + +static void impl_peaks_reset (struct resample *r) +{ + struct peaks_data *d = r->data; + d->i_count = d->o_count = 0; +} + +static float impl_peaks_phase (struct resample *r) +{ + return 0; +} + +int resample_peaks_init(struct resample *r) +{ + struct peaks_data *d; + 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; + r->out_len = impl_peaks_out_len; + r->phase = impl_peaks_phase; + + spa_log_debug(r->log, "peaks %p: in:%d out:%d features:%08x:%08x", r, + r->i_rate, r->o_rate, r->cpu_flags, d->peaks.cpu_flags); + + 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..5308fa8 --- /dev/null +++ b/spa/plugins/audioconvert/resample.h @@ -0,0 +1,54 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef RESAMPLE_H +#define RESAMPLE_H + +#include +#include + +#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); + + /** Fractional part of delay (in input samples) */ + float (*phase) (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) +#define resample_phase(r) (r)->phase(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-dump-coeffs.c b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c new file mode 100644 index 0000000..154792f --- /dev/null +++ b/spa/plugins/audioconvert/spa-resample-dump-coeffs.c @@ -0,0 +1,200 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2024 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +SPA_LOG_IMPL(logger); + +#include "resample.h" +#include "resample-native-impl.h" + +#define OPTIONS "ht:" +static const struct option long_options[] = { + { "help", no_argument, NULL, 'h'}, + + { "tuple", required_argument, NULL, 't' }, + + { NULL, 0, NULL, 0 } +}; + +static void show_usage(const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, "%s [options]\n", name); + fprintf(fp, + " -h, --help Show this help\n" + "\n" + " -t --tuple Sample rate tuple (as \"in_rate,out_rate[,quality]\")\n" + "\n"); +} + +static void parse_tuple(const char *arg, int *in, int *out, int *quality) +{ + char tuple[256]; + char *token; + + strncpy(tuple, arg, sizeof(tuple) - 1); + *in = 0; + *out = 0; + + token = strtok(tuple, ","); + if (!token || !spa_atoi32(token, in, 10)) + return; + + token = strtok(NULL, ","); + if (!token || !spa_atoi32(token, out, 10)) + return; + + token = strtok(NULL, ","); + if (!token) { + *quality = RESAMPLE_DEFAULT_QUALITY; + } else if (!spa_atoi32(token, quality, 10)) { + *quality = -1; + return; + } + + /* first, second now contain zeroes on error, or the numbers on success, + * third contains a quality or -1 on error, default value if unspecified */ +} + +#define PREFIX "__precomp_coeff" + +static void dump_header(void) +{ + printf("/* This is a generated file, see spa-resample-dump-coeffs.c */"); + printf("\n#include \n"); + printf("\n#include \n"); + printf("\n"); + printf("struct resample_coeffs {\n"); + printf("\tuint32_t in_rate;\n"); + printf("\tuint32_t out_rate;\n"); + printf("\tint quality;\n"); + printf("\tconst float *filter;\n"); + printf("};\n"); +} + +static void dump_footer(const uint32_t *ins, const uint32_t *outs, const int *qualities) +{ + printf("\n"); + printf("static const struct resample_coeffs precomp_coeffs[] = {\n"); + while (*ins && *outs) { + printf("\t{ .in_rate = %u, .out_rate = %u, .quality = %u, " + ".filter = %s_%u_%u_%u },\n", + *ins, *outs, *qualities, PREFIX, *ins, *outs, *qualities); + ins++; + outs++; + qualities++; + } + printf("\t{ .in_rate = 0, .out_rate = 0, .quality = 0, .filter = NULL },\n"); + printf("};\n"); +} + +static void dump_coeffs(unsigned int in_rate, unsigned int out_rate, int quality) +{ + struct resample r = { 0, }; + struct native_data *d; + unsigned int i, filter_size; + int ret; + + r.log = &logger.log; + r.i_rate = in_rate; + r.o_rate = out_rate; + r.quality = quality; + r.channels = 1; /* irrelevant for generated taps */ + + if ((ret = resample_native_init(&r)) < 0) { + fprintf(stderr, "can't init converter: %s\n", spa_strerror(ret)); + return; + } + + d = r.data; + filter_size = d->filter_stride * (d->n_phases + 1); + + printf("\n"); + printf("static const float %s_%u_%u_%u[] = {", PREFIX, in_rate, out_rate, quality); + for (i = 0; i < filter_size; i++) { + printf("%a", d->filter[i]); + if (i != filter_size - 1) + printf(","); + } + printf("};\n"); + + if (r.free) + r.free(&r); +} + +int main(int argc, char* argv[]) +{ + unsigned int ins[256] = { 0, }, outs[256] = { 0, }; + int qualities[256] = { 0, }; + int in_rate = 0, out_rate = 0, quality = 0; + int c, longopt_index = 0, i = 0; + + while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) { + switch (c) { + case 'h': + show_usage(argv[0], false); + return EXIT_SUCCESS; + case 't': + parse_tuple(optarg, &in_rate, &out_rate, &quality); + if (in_rate <= 0) { + fprintf(stderr, "error: bad input rate %d\n", in_rate); + goto error; + } + if (out_rate <= 0) { + fprintf(stderr, "error: bad output rate %d\n", out_rate); + goto error; + } + if (quality < 0 || quality > 14) { + fprintf(stderr, "error: bad quality value %s\n", optarg); + goto error; + } + ins[i] = in_rate; + outs[i] = out_rate; + qualities[i] = quality; + i++; + break; + default: + fprintf(stderr, "error: unknown option\n"); + goto error_usage; + } + } + + if (optind != argc) { + fprintf(stderr, "error: got %d extra argument(s))\n", + optind - argc); + goto error_usage; + } + if (in_rate == 0) { + fprintf(stderr, "error: input rate must be specified\n"); + goto error; + } + if (out_rate == 0) { + fprintf(stderr, "error: input rate must be specified\n"); + goto error; + } + + dump_header(); + while (i--) { + dump_coeffs(ins[i], outs[i], qualities[i]); + } + dump_footer(ins, outs, qualities); + + return EXIT_SUCCESS; + +error_usage: + show_usage(argv[0], true); +error: + return EXIT_FAILURE; +} diff --git a/spa/plugins/audioconvert/spa-resample.c b/spa/plugins/audioconvert/spa-resample.c new file mode 100644 index 0000000..ec752c6 --- /dev/null +++ b/spa/plugins/audioconvert/spa-resample.c @@ -0,0 +1,347 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +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] \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) +{ + int i, count = 0, format = -1; + + d->ifile = sf_open(d->iname, SFM_READ, &d->iinfo); + if (d->ifile == NULL) { + fprintf(stderr, "error: failed to open input file \"%s\": %s\n", + 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 & SF_FORMAT_SUBMASK); + + /* try to guess the format from the extension */ + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0) + count = 0; + + for (i = 0; i < count; i++) { + SF_FORMAT_INFO fi; + + spa_zero(fi); + fi.format = i; + if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0) + continue; + + if (spa_strendswith(d->oname, fi.extension)) { + format = fi.format; + break; + } + } + if (format == -1) + /* use the same format as the input file otherwise */ + format = d->iinfo.format & ~SF_FORMAT_SUBMASK; + if (format == SF_FORMAT_WAV && d->oinfo.channels > 2) + format = SF_FORMAT_WAVEX; + + d->oinfo.format |= format; + + d->ofile = sf_open(d->oname, SFM_WRITE, &d->oinfo); + if (d->ofile == NULL) { + 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..5dfa7d2 --- /dev/null +++ b/spa/plugins/audioconvert/test-audioadapter.c @@ -0,0 +1,282 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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..bc6e2a5 --- /dev/null +++ b/spa/plugins/audioconvert/test-audioconvert.c @@ -0,0 +1,1146 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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[6]; + 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"); + items[1] = SPA_DICT_ITEM_INIT("channelmix.upmix", "true"); + items[2] = SPA_DICT_ITEM_INIT("channelmix.upmix-method", "psd"); + items[3] = SPA_DICT_ITEM_INIT("channelmix.lfe-cutoff", "150"); + items[4] = SPA_DICT_ITEM_INIT("channelmix.fc-cutoff", "12000"); + items[5] = SPA_DICT_ITEM_INIT("channelmix.rear-delay", "12.0"); + + res = spa_handle_factory_init(factory, + ctx->convert_handle, + &SPA_DICT_INIT(items, 6), + 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_log_mem(&logger.log, SPA_LOG_LEVEL_WARN, + 0, b->datas[j].data, out_data->size); + spa_debug_log_mem(&logger.log, SPA_LOG_LEVEL_WARN, + 2, 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..b68c956 --- /dev/null +++ b/spa/plugins/audioconvert/test-channelmix.c @@ -0,0 +1,396 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +static uint32_t cpu_flags; + +SPA_LOG_IMPL(logger); + +#define MATRIX(...) (double[]) { __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, double *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, (float)*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, double *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; + mix.fc_cutoff = 120.0f; + mix.lfe_cutoff = 12000.0f; + + 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, 0.0)); + test_mix(1, _M(MONO), 3, _M(FL)|_M(FR)|_M(LFE), CHANNELMIX_OPTION_UPMIX, + 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, 0.0, 0.0)); + test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), CHANNELMIX_OPTION_UPMIX, + 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, 0.0, 0.0)); + test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), CHANNELMIX_OPTION_UPMIX, + MATRIX(1.0, 1.0, 0.0, 0.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.5, 0.5)); + 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_2_N(void) +{ + test_mix(2, _M(FL)|_M(FR), 1, _M(MONO), 0, MATRIX(0.5, 0.5)); + test_mix(2, _M(FL)|_M(FR), 1, 0, 0, MATRIX(0.5, 0.5)); + test_mix(2, _M(FL)|_M(FR), 2, 0, 0, MATRIX(1.0, 0.0, 0.0, 1.0)); + test_mix(2, _M(FL)|_M(FR), 2, _M(MONO), 0, MATRIX(1.0, 0.0, 0.0, 1.0)); + test_mix(2, _M(FL)|_M(FR), 2, _M(FL)|_M(FR), 0, MATRIX(1.0, 0.0, 0.0, 1.0)); + test_mix(2, _M(FL)|_M(FR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(1.0, 0.0, + 0.0, 1.0, + 0.0, 0.0, + 0.0, 0.0)); + test_mix(2, _M(FL)|_M(FR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), CHANNELMIX_OPTION_UPMIX, + MATRIX(1.0, 0.0, + 0.0, 1.0, + 0.707107, 0.707107, + 0.5, 0.5)); + test_mix(2, _M(FL)|_M(FR), 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0, + MATRIX(1.0, 0.0, + 0.0, 1.0, + 0.0, 0.0, + 0.0, 0.0, + 0.0, 0.0, + 0.0, 0.0)); + test_mix(2, _M(FL)|_M(FR), 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), CHANNELMIX_OPTION_UPMIX, + MATRIX(1.0, 0.0, + 0.0, 1.0, + 0.707107, 0.707107, + 0.5, 0.5, + 0.0, 0.0, + 0.0, 0.0)); +} + +static void test_3p1_N(void) +{ + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 1, _M(MONO), 0, + MATRIX(0.333333, 0.333333, 0.333333, 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.25, 0.25, 0.25, 0.25)); + test_mix(4, _M(FL)|_M(FR)|_M(SL)|_M(SR), 1, _M(MONO), 0, + MATRIX(0.25, 0.25, 0.25, 0.25)); + 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.5, 0.5, 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.20, 0.20, 0.20, 0.0, 0.20, 0.20)); + 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.166667, 0.166667, 0.166667, 0.0, 0.166667, 0.166667, 0.166667)); + 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.142857, 0.142857, 0.142857, 0.0, 0.142857, 0.142857, 0.142857, 0.142857)); + 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] = (float)((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] = (float)(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_2_N(); + 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..17a26a3 --- /dev/null +++ b/spa/plugins/audioconvert/test-fmt-ops.c @@ -0,0 +1,905 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#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 +#if defined(HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_f32_s16_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_s16_rvv); + run_test("test_f32d_s16d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_f32d_to_s16d_rvv); + run_test("test_f32d_s16_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s16_rvv); + } +#endif +} + +static void test_s16_f32(void) +{ + 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 +#if defined(HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_s16_f32d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s16_to_f32d_rvv); + } +#endif +} + +static void test_f32_u32(void) +{ + 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/0xa00000, 1.0f/0x800000, -1.0f/0x800000, + 1.0f/0x1000000, -1.0f/0x1000000, 1.0f/0x2000000, -1.0f/0x2000000, + 1.0f/0x4000000, -1.0f/0x4000000, 1.0f/0x8000000, -1.0f/0x8000000, + 1.0f/0x10000000, -1.0f/0x10000000, 1.0f/0x20000000, -1.0f/0x20000000, + 1.0f/0x40000000, -1.0f/0x40000000, 1.0f/0x80000000, -1.0f/0x80000000, + 1.0f/0x100000000, -1.0f/0x100000000, 1.0f/0x200000000, -1.0f/0x200000000, + }; + static const int32_t out[] = { 0x00000000, 0x7fffff80, 0x80000000, + 0x40000000, 0xc0000000, 0x7fffff80, 0x80000000, 0x000000cd, + 0xffffff33, 0x00000100, 0xffffff00, 0x00000080, 0xffffff80, + 0x00000040, 0xffffffc0, 0x00000020, 0xffffffe0, 0x00000010, + 0xfffffff0, 0x00000008, 0xfffffff8, 0x00000004, 0xfffffffc, + 0x00000002, 0xfffffffe, 0x00000001, 0xffffffff, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, + }; + + run_test("test_f32_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_s32_c); + 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 +#if defined(HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_f32d_s32_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s32_rvv); + } +#endif +} + +static void test_s32_f32(void) +{ + static const int32_t in[] = { 0, 0x7FFFFFFF, 0x80000000, 0x7fffff00, + 0x80000100, 0x40000000, 0xc0000000, 0x0080, 0xFFFFFF80, 0x0100, + 0xFFFFFF00, 0x0200, 0xFFFFFE00 + }; + + static const float out[] = { 0.e+00f, 1.e+00f, -1.e+00f, + 9.9999988079071044921875e-01f, -9.9999988079071044921875e-01f, 5.e-01f, + -5.e-01f, 5.9604644775390625e-08f, -5.9604644775390625e-08f, + 1.1920928955078125e-07f, -1.1920928955078125e-07f, + 2.384185791015625e-07f, -2.384185791015625e-07f + }; + + run_test("test_s32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s32_to_f32d_c); + 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 +#if defined(HAVE_RVV) + if (cpu_flags & SPA_CPU_FLAG_RISCV_V) { + run_test("test_s32_f32d_rvv", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s32_to_f32d_rvv); + } +#endif +} + +static void test_f32_u24(void) +{ + 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) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S16_MIN; i <= S16_MAX; i+=3) { + float v = S16_TO_F32((int16_t)i); + int16_t t = F32_TO_S16(v); + spa_assert_se(i == t); + + int32_t t2 = F32_TO_S32(v); + spa_assert_se((int32_t)(((uint32_t)i)<<16) == t2); + spa_assert_se(i == t2>>16); + } +} + +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((uint16_t)i); + uint16_t t = F32_TO_U16(v); + spa_assert_se(i == t); + + uint32_t t2 = F32_TO_U32(v); + spa_assert_se(i<<16 == t2); + spa_assert_se(i == t2>>16); + } +} + +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_s25_32_to_f32_to_s25_32(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S25_MIN; i <= S25_MAX; i+=11) { + float v = S25_32_TO_F32(i); + int32_t t = F32_TO_S25_32(v); + spa_assert_se(i == t); + } +} + +static void test_lossless_s25_32_to_s32_to_f32_to_s25_32(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S25_MIN; i <= S25_MAX; i+=13) { + float v = S32_TO_F32(S25_32_TO_S32(i)); + int32_t t = F32_TO_S25_32(v); + spa_assert_se(i == t); + } +} + +static void test_lossless_s25_32_to_s32_to_f32_to_s32_to_s25_32(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S25_MIN; i <= S25_MAX; i+=11) { + float v = S32_TO_F32(S25_32_TO_S32(i)); + int32_t t = S32_TO_S25_32(F32_TO_S32(v)); + spa_assert_se(i == t); + } +} + +static void test_lossless_s25_32_to_f32_to_s32_to_s25_32(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S25_MIN; i <= S25_MAX; i+=11) { + float v = S25_32_TO_F32(i); + int32_t t = S32_TO_S25_32(F32_TO_S32(v)); + spa_assert_se(i == t); + } +} + +static void test_lossless_s32(void) +{ + int64_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S32_MIN; i < S32_MAX; i += 63) { + float v = S32_TO_F32(i); + int32_t t = F32_TO_S32(v); + spa_assert_se(SPA_ABS(i - t) <= 126); + // NOTE: 126 is the maximal absolute error given step=1, + // for wider steps it may (errneously) be lower, + // because we may not check some integer that would bump it. + } +} + +static void test_lossless_s32_lossless_subset(void) +{ + int32_t i, j; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S25_MIN; i <= S25_MAX; i+=11) { + for(j = 0; j < 8; ++j) { + int32_t s = i * (1< 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_s25_32_to_f32_to_s25_32(); + test_lossless_s25_32_to_s32_to_f32_to_s25_32(); + test_lossless_s25_32_to_s32_to_f32_to_s32_to_s25_32(); + test_lossless_s25_32_to_f32_to_s32_to_s25_32(); + test_lossless_s32(); + test_lossless_s32_lossless_subset(); + test_lossless_u32(); + + test_swaps(); + + test_noise(); + + return 0; +} diff --git a/spa/plugins/audioconvert/test-peaks.c b/spa/plugins/audioconvert/test-peaks.c new file mode 100644 index 0000000..9225bda --- /dev/null +++ b/spa/plugins/audioconvert/test-peaks.c @@ -0,0 +1,108 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +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] = (float)((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-delay.c b/spa/plugins/audioconvert/test-resample-delay.c new file mode 100644 index 0000000..91a04a5 --- /dev/null +++ b/spa/plugins/audioconvert/test-resample-delay.c @@ -0,0 +1,456 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +SPA_LOG_IMPL(logger); + +#include "resample.h" + + +static float samp_in[65536]; +static float samp_out[65536]; +static bool force_print; + +static void assert_test(bool check) +{ + if (!check) + fprintf(stderr, "FAIL\n\n"); +#if 1 + spa_assert_se(check); +#endif +} + +static double difference(double delay, const float *a, size_t len_a, const float *b, size_t len_b, double b_rate) +{ + size_t i; + float c = 0, wa = 0, wb = 0; + + /* Difference measure: sum((a-b)^2) / sqrt(sum a^2 sum b^2); restricted to overlap */ + for (i = 0; i < len_a; ++i) { + double jf = (i + delay) * b_rate; + int j; + double x; + float bv; + + j = (int)floor(jf); + if (j < 0 || j + 1 >= (int)len_b) + continue; + + x = jf - j; + bv = (float)((1 - x) * b[j] + x * b[j + 1]); + + c += (a[i] - bv) * (a[i] - bv); + wa += a[i]*a[i]; + wb = bv*bv; + } + + if (wa == 0 || wb == 0) + return 1e30; + + return c / sqrt(wa * wb); +} + +struct find_delay_data { + const float *a; + const float *b; + size_t len_a; + size_t len_b; + double b_rate; +}; + +static double find_delay_func(double x, void *user_data) +{ + const struct find_delay_data *data = user_data; + + return difference((float)x, data->a, data->len_a, data->b, data->len_b, data->b_rate); +} + +static double minimum(double x1, double x4, double (*func)(double, void *), void *user_data, double tol) +{ + /* Find minimum with golden section search */ + const double phi = (1 + sqrt(5)) / 2; + double x2, x3; + double f2, f3; + + spa_assert(x4 >= x1); + + x2 = x4 - (x4 - x1) / phi; + x3 = x1 + (x4 - x1) / phi; + + f2 = func(x2, user_data); + f3 = func(x3, user_data); + + while (x4 - x1 > tol) { + if (f2 > f3) { + x1 = x2; + x2 = x3; + x3 = x1 + (x4 - x1) / phi; + f2 = f3; + f3 = func(x3, user_data); + } else { + x4 = x3; + x3 = x2; + x2 = x4 - (x4 - x1) / phi; + f3 = f2; + f2 = func(x2, user_data); + } + } + + return (f2 < f3) ? x2 : x3; +} + +static double find_delay(const float *a, size_t len_a, const float *b, size_t len_b, double b_rate, int maxdelay, double tol) +{ + struct find_delay_data data = { .a = a, .len_a = len_a, .b = b, .len_b = len_b, .b_rate = b_rate }; + double best_x, best_f; + int i; + + best_x = 0; + best_f = find_delay_func(best_x, &data); + + for (i = -maxdelay; i <= maxdelay; ++i) { + double f = find_delay_func(i, &data); + + if (f < best_f) { + best_x = i; + best_f = f; + } + } + + return minimum(best_x - 2, best_x + 2, find_delay_func, &data, tol); +} + +static void test_find_delay(void) +{ + float v1[1024]; + float v2[1024]; + const double tol = 0.001; + double delay, expect; + int i; + + fprintf(stderr, "\n\n-- test_find_delay\n\n"); + + for (i = 0; i < 1024; ++i) { + v1[i] = sinf(0.1f * i); + v2[i] = sinf(0.1f * (i - 3.1234f)); + } + + delay = find_delay(v1, SPA_N_ELEMENTS(v1), v2, SPA_N_ELEMENTS(v2), 1, 50, tol); + expect = 3.1234f; + fprintf(stderr, "find_delay = %g (exact %g)\n", delay, expect); + assert_test(expect - 2*tol < delay && delay < expect + 2*tol); + + for (i = 0; i < 1024; ++i) { + v1[i] = sinf(0.1f * i); + v2[i] = sinf(0.1f * (i*3.0f/4 - 3.1234f)); + } + + delay = find_delay(v1, SPA_N_ELEMENTS(v1), v2, SPA_N_ELEMENTS(v2), 4.0/3.0, 50, tol); + expect = 3.1234f; + fprintf(stderr, "find_delay = %g (exact %g)\n", delay, expect); + assert_test(expect - 2*tol < delay && delay < expect + 2*tol); +} + +static uint32_t feed_sine(struct resample *r, uint32_t in, uint32_t *inp, uint32_t *phase, bool print) +{ + uint32_t i, out; + const void *src[1]; + void *dst[1]; + + for (i = 0; i < in; ++i) + samp_in[i] = sinf(0.01f * (*phase + i)) * expf(-0.001f * (*phase + i)); + + src[0] = samp_in; + dst[0] = samp_out; + out = SPA_N_ELEMENTS(samp_out); + + *inp = in; + resample_process(r, src, inp, dst, &out); + spa_assert_se(*inp == in); + + if (print || force_print) { + fprintf(stderr, "inp(%u) = ", in); + for (uint32_t i = 0; i < in; ++i) + fprintf(stderr, "%g, ", samp_in[i]); + fprintf(stderr, "\n\n"); + + fprintf(stderr, "out(%u) = ", out); + for (uint32_t i = 0; i < out; ++i) + fprintf(stderr, "%g, ", samp_out[i]); + fprintf(stderr, "\n\n"); + } else { + fprintf(stderr, "inp(%u) = ...\n", in); + fprintf(stderr, "out(%u) = ...\n", out); + } + + *phase += in; + *inp = in; + return out; +} + +static void check_delay(double rate, uint32_t out_rate, uint32_t options) +{ + const double tol = 0.001; + struct resample r; + uint32_t in_phase = 0; + uint32_t in, out; + double expect, got; + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 48000; + r.o_rate = out_rate; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = options; + resample_native_init(&r); + + resample_update_rate(&r, rate); + + feed_sine(&r, 512, &in, &in_phase, false); + + /* Test delay */ + expect = resample_delay(&r) + (double)resample_phase(&r); + out = feed_sine(&r, 256, &in, &in_phase, true); + got = find_delay(samp_in, in, samp_out, out, out_rate / 48000.0, 100, tol); + + fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got); + assert_test(expect - 4*tol < got && got < expect + 4*tol); + + resample_free(&r); +} + +static void test_delay_copy(void) +{ + fprintf(stderr, "\n\n-- test_delay_copy (no prefill)\n\n"); + check_delay(1, 48000, 0); + + fprintf(stderr, "\n\n-- test_delay_copy (prefill)\n\n"); + check_delay(1, 48000, RESAMPLE_OPTION_PREFILL); +} + +static void test_delay_full(void) +{ + const uint32_t rates[] = { 16000, 32000, 44100, 48000, 88200, 96000, 144000, 192000 }; + unsigned int i; + + for (i = 0; i < SPA_N_ELEMENTS(rates); ++i) { + fprintf(stderr, "\n\n-- test_delay_full(%u, no prefill)\n\n", rates[i]); + check_delay(1, rates[i], 0); + fprintf(stderr, "\n\n-- test_delay_full(%u, prefill)\n\n", rates[i]); + check_delay(1, rates[i], RESAMPLE_OPTION_PREFILL); + } +} + +static void test_delay_interp(void) +{ + fprintf(stderr, "\n\n-- test_delay_interp(no prefill)\n\n"); + check_delay(1 + 1e-12, 48000, 0); + + fprintf(stderr, "\n\n-- test_delay_interp(prefill)\n\n"); + check_delay(1 + 1e-12, 48000, RESAMPLE_OPTION_PREFILL); +} + +static void check_delay_vary_rate(double rate, double end_rate, uint32_t out_rate, uint32_t options) +{ + const double tol = 0.001; + struct resample r; + uint32_t in_phase = 0; + uint32_t in, out; + double expect, got; + + fprintf(stderr, "\n\n-- check_delay_vary_rate(%g, %.14g, %u, %s)\n\n", rate, end_rate, out_rate, + (options & RESAMPLE_OPTION_PREFILL) ? "prefill" : "no prefill"); + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 48000; + r.o_rate = out_rate; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = options; + resample_native_init(&r); + + /* Cause nonzero resampler phase */ + resample_update_rate(&r, rate); + feed_sine(&r, 128, &in, &in_phase, false); + + resample_update_rate(&r, 1.7); + feed_sine(&r, 128, &in, &in_phase, false); + + resample_update_rate(&r, end_rate); + feed_sine(&r, 128, &in, &in_phase, false); + feed_sine(&r, 255, &in, &in_phase, false); + + /* Test delay */ + expect = (double)resample_delay(&r) + (double)resample_phase(&r); + out = feed_sine(&r, 256, &in, &in_phase, true); + got = find_delay(samp_in, in, samp_out, out, out_rate/48000.0, 100, tol); + + fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got); + assert_test(expect - 4*tol < got && got < expect + 4*tol); + + resample_free(&r); +} + + +static void test_delay_interp_vary_rate(void) +{ + const uint32_t rates[] = { 32000, 44100, 48000, 88200, 96000 }; + const double factors[] = { 1.0123456789, 1.123456789, 1.203883, 1.23456789, 1.3456789 }; + unsigned int i, j; + + for (i = 0; i < SPA_N_ELEMENTS(rates); ++i) { + for (j = 0; j < SPA_N_ELEMENTS(factors); ++j) { + /* Interp at end */ + check_delay_vary_rate(factors[j], 1 + 1e-12, rates[i], 0); + + /* Copy/full at end */ + check_delay_vary_rate(factors[j], 1, rates[i], 0); + + /* Interp at end */ + check_delay_vary_rate(factors[j], 1 + 1e-12, rates[i], RESAMPLE_OPTION_PREFILL); + + /* Copy/full at end */ + check_delay_vary_rate(factors[j], 1, rates[i], RESAMPLE_OPTION_PREFILL); + } + } +} + +static void run(uint32_t in_rate, uint32_t out_rate, double end_rate, double mid_rate, uint32_t options) +{ + const double tol = 0.001; + struct resample r; + uint32_t in_phase = 0; + uint32_t in, out; + double expect, got; + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = in_rate; + r.o_rate = out_rate; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = options; + resample_native_init(&r); + + /* Cause nonzero resampler phase */ + if (mid_rate != 0.0) { + resample_update_rate(&r, mid_rate); + feed_sine(&r, 128, &in, &in_phase, true); + + resample_update_rate(&r, 1.7); + feed_sine(&r, 128, &in, &in_phase, true); + } + + resample_update_rate(&r, end_rate); + feed_sine(&r, 128, &in, &in_phase, true); + feed_sine(&r, 255, &in, &in_phase, true); + + /* Test delay */ + expect = (double)resample_delay(&r) + (double)resample_phase(&r); + out = feed_sine(&r, 256, &in, &in_phase, true); + got = find_delay(samp_in, in, samp_out, out, ((double)out_rate)/in_rate, 100, tol); + + fprintf(stderr, "delay: expect = %g, got = %g\n", expect, got); + if (!(expect - 4*tol < got && got < expect + 4*tol)) + fprintf(stderr, "FAIL\n\n"); + + resample_free(&r); +} + +int main(int argc, char *argv[]) +{ + static const struct option long_options[] = { + { "in-rate", required_argument, NULL, 'i' }, + { "out-rate", required_argument, NULL, 'o' }, + { "end-full", no_argument, NULL, 'f' }, + { "end-interp", no_argument, NULL, 'p' }, + { "mid-rate", required_argument, NULL, 'm' }, + { "prefill", no_argument, NULL, 'r' }, + { "print", no_argument, NULL, 'P' }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0} + }; + const char *help = "%s [options]\n" + "\n" + "Check resampler delay. If no arguments, run tests.\n" + "\n" + "-i | --in-rate INRATE input rate\n" + "-o | --out-rate OUTRATE output rate\n" + "-f | --end-full force full (or copy) resampler\n" + "-p | --end-interp force interp resampler\n" + "-m | --mid-rate RELRATE force rate adjustment in the middle\n" + "-r | --prefill enable prefill\n" + "-P | --print force printing\n" + "\n"; + uint32_t in_rate = 0, out_rate = 0; + double end_rate = 1, mid_rate = 0; + uint32_t options = 0; + int c; + + logger.log.level = SPA_LOG_LEVEL_TRACE; + + while ((c = getopt_long(argc, argv, "i:o:fpm:rPh", long_options, NULL)) != -1) { + switch (c) { + case 'h': + fprintf(stderr, help, argv[0]); + return 0; + case 'i': + if (!spa_atou32(optarg, &in_rate, 0)) + goto error_arg; + break; + case 'o': + if (!spa_atou32(optarg, &out_rate, 0)) + goto error_arg; + break; + case 'f': + end_rate = 1; + break; + case 'p': + end_rate = 1 + 1e-12; + break; + case 'm': + if (!spa_atod(optarg, &mid_rate)) + goto error_arg; + break; + case 'r': + options = RESAMPLE_OPTION_PREFILL; + break; + case 'P': + force_print = true; + break; + default: + goto error_arg; + } + } + + if (in_rate && out_rate) { + run(in_rate, out_rate, end_rate, mid_rate, options); + return 0; + } + + test_find_delay(); + test_delay_copy(); + test_delay_full(); + test_delay_interp(); + test_delay_interp_vary_rate(); + + return 0; + +error_arg: + fprintf(stderr, "Invalid arguments\n"); + return 1; +} diff --git a/spa/plugins/audioconvert/test-resample.c b/spa/plugins/audioconvert/test-resample.c new file mode 100644 index 0000000..1d77a3b --- /dev/null +++ b/spa/plugins/audioconvert/test-resample.c @@ -0,0 +1,157 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +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..a11861c --- /dev/null +++ b/spa/plugins/audioconvert/test-source.c @@ -0,0 +1,913 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, "%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, ¶m, &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, "%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, "%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, "%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, "%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, "%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, "%p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + if (!SPA_IS_ALIGNED(d[j].data, 16)) { + spa_log_warn(this->log, "%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, "%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, "%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..3419b45 --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops-c.c @@ -0,0 +1,25 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..c1fc5f1 --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops-sse.c @@ -0,0 +1,46 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "volume-ops.h" + +#include + +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..bf6aa69 --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops.c @@ -0,0 +1,64 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include + +#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..a50ee9a --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops.h @@ -0,0 +1,48 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include + +#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/audioconvert/wavfile.c b/spa/plugins/audioconvert/wavfile.c new file mode 100644 index 0000000..1d16278 --- /dev/null +++ b/spa/plugins/audioconvert/wavfile.c @@ -0,0 +1,250 @@ +/* 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 +#include +#include + +#include + +#include "wavfile.h" + +#define BLOCK_SIZE 4096 + +struct wav_file { + struct spa_audio_info info; + int fd; + const struct format_info *fi; + + uint32_t length; + + uint32_t stride; + uint32_t blocks; +}; + +static inline ssize_t write_data(struct wav_file *wf, const void *data, size_t size) +{ + ssize_t len; + len = write(wf->fd, data, size); + if (len > 0) + wf->length += len; + return len; +} + +static ssize_t writei(struct wav_file *wf, const void **data, size_t samples) +{ + return write_data(wf, data[0], samples * wf->stride); +} + +typedef struct { + uint8_t v[3]; +} __attribute__ ((packed)) uint24_t; + +#define MAKE_WRITEN_FUNC(name, type) \ +static ssize_t name (struct wav_file *wf, const void **data, size_t samples) \ +{ \ + uint32_t b, n, k, blocks = wf->blocks, chunk; \ + uint8_t buf[BLOCK_SIZE]; \ + ssize_t res = 0; \ + type **d = (type**)data; \ + uint32_t chunk_size = sizeof(buf) / (blocks * sizeof(type)); \ + for (n = 0; n < samples; ) { \ + type *p = (type*)buf; \ + chunk = SPA_MIN(samples - n, chunk_size); \ + for (k = 0; k < chunk; k++, n++) { \ + for (b = 0; b < blocks; b++) \ + *p++ = d[b][n]; \ + } \ + res += write_data(wf, buf, \ + chunk * blocks * sizeof(type)); \ + } \ + return res; \ +} + +MAKE_WRITEN_FUNC(writen_8, uint8_t); +MAKE_WRITEN_FUNC(writen_16, uint16_t); +MAKE_WRITEN_FUNC(writen_24, uint24_t); +MAKE_WRITEN_FUNC(writen_32, uint32_t); +MAKE_WRITEN_FUNC(writen_64, uint64_t); + +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_le16(int fd, uint16_t val) +{ + uint8_t buf[2] = { val, val >> 8 }; + return write_n(fd, buf, 2); +} + +static inline int write_le32(int fd, uint32_t val) +{ + uint8_t buf[4] = { val, val >> 8, val >> 16, val >> 24 }; + return write_n(fd, buf, 4); +} + +#define MAKE_AUDIO_RAW(format,bits,planar,fmt,...) \ + { SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw, format, bits, planar, fmt, __VA_ARGS__ } + +static struct format_info { + uint32_t media_type; + uint32_t media_subtype; + uint32_t format; + uint32_t bits; + bool planar; + uint32_t fmt; + ssize_t (*write) (struct wav_file *wf, const void **data, size_t samples); +} format_info[] = { + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_U8P, 8, true, 1, writen_8), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_U8, 8, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S16P, 16, true, 1, writen_16), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S16_LE, 16, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24P, 24, true, 1, writen_24), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_LE, 24, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_32P, 32, true, 1, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S32P, 32, true, 1, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_32_LE, 32, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S32_LE, 32, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F32P, 32, true, 3, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F32_LE, 32, false, 3, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F64P, 64, true, 3, writen_64), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F64_LE, 32, false, 3, writei), +}; + +#define CHECK_RES(expr) if ((res = (expr)) < 0) return res + +static int write_headers(struct wav_file *wf) +{ + int res; + uint32_t channels, rate, bps, bits; + const struct format_info *fi = wf->fi; + + lseek(wf->fd, 0, SEEK_SET); + + rate = wf->info.info.raw.rate; + channels = wf->info.info.raw.channels; + bits = fi->bits; + bps = channels * bits / 8; + + CHECK_RES(write_n(wf->fd, "RIFF", 4)); + CHECK_RES(write_le32(wf->fd, wf->length == 0 ? (uint32_t)-1 : wf->length + 12 + 8 + 16)); + CHECK_RES(write_n(wf->fd, "WAVE", 4)); + CHECK_RES(write_n(wf->fd, "fmt ", 4)); + CHECK_RES(write_le32(wf->fd, 16)); + CHECK_RES(write_le16(wf->fd, fi->fmt)); /* format */ + CHECK_RES(write_le16(wf->fd, channels)); /* channels */ + CHECK_RES(write_le32(wf->fd, rate)); /* rate */ + CHECK_RES(write_le32(wf->fd, bps * rate)); /* bytes per sec */ + CHECK_RES(write_le16(wf->fd, bps)); /* bytes per samples */ + CHECK_RES(write_le16(wf->fd, bits)); /* bits per sample */ + CHECK_RES(write_n(wf->fd, "data", 4)); + CHECK_RES(write_le32(wf->fd, wf->length == 0 ? (uint32_t)-1 : wf->length)); + + return 0; +} + +static const struct format_info *find_info(struct wav_file_info *info) +{ + uint32_t i; + + if (info->info.media_type != SPA_MEDIA_TYPE_audio || + info->info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return NULL; + + for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) { + if (format_info[i].format == info->info.info.raw.format) + return &format_info[i]; + } + return NULL; +} + +static int open_write(struct wav_file *wf, const char *filename, struct wav_file_info *info) +{ + int res; + const struct format_info *fi; + + fi = find_info(info); + if (fi == NULL) + return -ENOTSUP; + + if ((wf->fd = open(filename, O_WRONLY | O_CREAT | O_CLOEXEC | O_TRUNC, 0660)) < 0) { + res = -errno; + goto exit; + } + wf->info = info->info; + wf->fi = fi; + if (fi->planar) { + wf->stride = fi->bits / 8; + wf->blocks = info->info.info.raw.channels; + } else { + wf->stride = info->info.info.raw.channels * (fi->bits / 8); + wf->blocks = 1; + } + + res = write_headers(wf); +exit: + return res; +} + +struct wav_file * +wav_file_open(const char *filename, const char *mode, struct wav_file_info *info) +{ + int res; + struct wav_file *wf; + + wf = calloc(1, sizeof(struct wav_file)); + if (wf == NULL) + return NULL; + + if (spa_streq(mode, "w")) { + if ((res = open_write(wf, filename, info)) < 0) + goto exit_free; + } else { + res = -EINVAL; + goto exit_free; + } + return wf; + +exit_free: + free(wf); + errno = -res; + return NULL; +} + +int wav_file_close(struct wav_file *wf) +{ + int res; + + CHECK_RES(write_headers(wf)); + + close(wf->fd); + free(wf); + return 0; +} + +ssize_t wav_file_write(struct wav_file *wf, const void **data, size_t samples) +{ + return wf->fi->write(wf, data, samples); +} diff --git a/spa/plugins/audioconvert/wavfile.h b/spa/plugins/audioconvert/wavfile.h new file mode 100644 index 0000000..d0c300f --- /dev/null +++ b/spa/plugins/audioconvert/wavfile.h @@ -0,0 +1,39 @@ +/* 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 +#include + +struct wav_file; + +struct wav_file_info { + struct spa_audio_info info; +}; + +struct wav_file * +wav_file_open(const char *filename, const char *mode, struct wav_file_info *info); + +int wav_file_close(struct wav_file *wf); + +ssize_t wav_file_write(struct wav_file *wf, const void **data, size_t size); diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c new file mode 100644 index 0000000..bab3007 --- /dev/null +++ b/spa/plugins/audiomixer/audiomixer.c @@ -0,0 +1,1040 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mix-ops.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audiomixer"); + +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 + +#define MAX_BUFFERS 64 +#define MAX_PORTS 512 +#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[2]; + + 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; + + struct spa_loop *data_loop; + + 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_io_position *position; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + struct buffer *mix_buffers[MAX_PORTS]; + const void *mix_datas[MAX_PORTS]; + + 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_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) +#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 CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_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 GET_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) ? NULL : GET_PORT(this,d,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) +{ + struct impl *this = object; + + switch (id) { + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOTSUP; + } + 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; + 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, struct port *port, + 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_ANY(this, direction, port_id), -EINVAL); + + port = GET_PORT_ANY(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, port, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (port == NULL || !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 == NULL || !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; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_AsyncBuffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_async_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); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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; +} + +struct io_info { + struct port *port; + void *data; + size_t size; +}; + +static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct io_info *info = user_data; + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + info->port->io[0] = &ab->buffers[info->port->direction]; + info->port->io[1] = &ab->buffers[info->port->direction^1]; + } else if (info->size >= sizeof(struct spa_io_buffers)) { + info->port->io[0] = info->data; + info->port->io[1] = info->data; + } else { + info->port->io[0] = NULL; + info->port->io[1] = NULL; + } + 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; + struct io_info info; + + 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); + info.port = port; + info.data = data; + info.size = size; + + switch (id) { + case SPA_IO_Buffers: + case SPA_IO_AsyncBuffers: + spa_loop_invoke(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + 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; + uint32_t cycle = this->position->clock.cycle & 1; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io[cycle]) == 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 = this->mix_buffers; + datas = this->mix_datas; + 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[cycle]) == NULL)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", + this, i, PORT_VALID(inport), + inport->io[0], inport->io[1], cycle); + continue; + } + if (inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, inio, inio->status, inio->buffer_id, 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/%d", this, + i, inio, outio, inio->status, inio->buffer_id, + offs, size, this->stride); + + 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)) { + if (outport->n_buffers > 0) + spa_log_warn(this->log, "%p: out of buffers (%d)", this, + outport->n_buffers); + 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->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->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..1870356 --- /dev/null +++ b/spa/plugins/audiomixer/benchmark-mix-ops.c @@ -0,0 +1,202 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#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..65aafee --- /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, test_inc ], + 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, test_inc ], + 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..af717d1 --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-avx.c @@ -0,0 +1,68 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#include "mix-ops.h" + +#include + +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..933951b --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-c.c @@ -0,0 +1,48 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#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..ea4dcc6 --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-sse.c @@ -0,0 +1,67 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#include "mix-ops.h" + +#include + +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..ce1112d --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-sse2.c @@ -0,0 +1,67 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#include "mix-ops.h" + +#include + +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..bebd66f --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops.c @@ -0,0 +1,116 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include + +#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..0d95596 --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops.h @@ -0,0 +1,149 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +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 ((uint32_t)((int32_t)src.v1 & 0xFFFF) << 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 32u + +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..fb396c1 --- /dev/null +++ b/spa/plugins/audiomixer/mixer-dsp.c @@ -0,0 +1,977 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mix-ops.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.mixer-dsp"); + +#define MAX_BUFFERS 64 +#define MAX_PORTS 512 +#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[2]; + + 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; + + struct spa_loop *data_loop; + + 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_io_position *position; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + struct buffer *mix_buffers[MAX_PORTS]; + const void *mix_datas[MAX_PORTS]; + + 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_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) +#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 CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_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 GET_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) ? NULL : GET_PORT(this,d,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) +{ + struct impl *this = object; + + switch (id) { + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOTSUP; + } + 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; + 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, struct port *port, + 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_ANY(this, direction, port_id), -EINVAL); + + port = GET_PORT_ANY(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, port, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (port == NULL || !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 == NULL || !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; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_AsyncBuffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_async_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); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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; +} + +struct io_info { + struct port *port; + void *data; + size_t size; +}; + +static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct io_info *info = user_data; + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + info->port->io[0] = &ab->buffers[info->port->direction]; + info->port->io[1] = &ab->buffers[info->port->direction^1]; + } else if (info->size >= sizeof(struct spa_io_buffers)) { + info->port->io[0] = info->data; + info->port->io[1] = info->data; + } else { + info->port->io[0] = NULL; + info->port->io[1] = NULL; + } + 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; + struct io_info info; + + 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); + info.port = port; + info.data = data; + info.size = size; + + switch (id) { + case SPA_IO_Buffers: + case SPA_IO_AsyncBuffers: + spa_loop_invoke(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + 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; + uint32_t cycle = this->position->clock.cycle & 1; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io[cycle]) == 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 = this->mix_buffers; + datas = this->mix_datas; + 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[cycle]) == NULL)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", + this, i, PORT_VALID(inport), + inport->io[0], inport->io[1], cycle); + continue; + } + if (inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, inio, inio->status, inio->buffer_id, 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:%d/%d %u", this, + i, inio, outio, inio->status, inio->buffer_id, inport->n_buffers, + offs, size, (int)sizeof(float), + bd->chunk->flags); + + 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)) { + if (outport->n_buffers > 0) + spa_log_warn(this->log, "%p: out of buffers (%d)", this, + outport->n_buffers); + 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->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->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..a4ddf35 --- /dev/null +++ b/spa/plugins/audiomixer/plugin.c @@ -0,0 +1,33 @@ +/* Spa Audiomixer plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_audiomixer_factory; +extern const struct spa_handle_factory spa_mixer_dsp_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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-mix-ops.c b/spa/plugins/audiomixer/test-mix-ops.c new file mode 100644 index 0000000..2092de4 --- /dev/null +++ b/spa/plugins/audiomixer/test-mix-ops.c @@ -0,0 +1,273 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#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..273e4a5 --- /dev/null +++ b/spa/plugins/audiotestsrc/audiotestsrc.c @@ -0,0 +1,1161 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, "%p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return 0; +} + +static void update_target(struct impl *this) +{ + if (this->position) { + this->position->clock.duration = this->position->clock.target_duration; + this->position->clock.rate = this->position->clock.target_rate; + } +} + +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, "%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, "%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; + + update_target(this); + + 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, ¶m, &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, "%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, "%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, "%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); + if (this->data_loop == NULL) { + spa_log_error(this->log, "%p: could not find a data loop", this); + return -EINVAL; + } + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + if (this->data_system == NULL) { + spa_log_error(this->log, "%p: could not find a data system", this); + return -EINVAL; + } + + 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, "%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 " }, + { 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, + "audiotestsrc", + &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..cf3d95e --- /dev/null +++ b/spa/plugins/audiotestsrc/plugin.c @@ -0,0 +1,29 @@ +/* Spa Volume plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_audiotestsrc_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..16ff52b --- /dev/null +++ b/spa/plugins/audiotestsrc/render.c @@ -0,0 +1,44 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#define M_PI_M2f (float)(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_M2f * 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_M2f) \ + this->port.accumulator -= M_PI_M2f; \ + val = (type) (sin (this->port.accumulator) * amp); \ + for (c = 0; c < channels; ++c) \ + *samples++ = val; \ + } \ +} + +DEFINE_SINE(int16_t, 32767.0f); +DEFINE_SINE(int32_t, 2147483647.0f); +DEFINE_SINE(float, 1.0f); +DEFINE_SINE(double, 1.0f); + +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..6b9e485 --- /dev/null +++ b/spa/plugins/avb/avb-pcm-sink.c @@ -0,0 +1,892 @@ +/* Spa AVB PCM Sink */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(¶ms)); + + 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 = 0; + + 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 (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; + 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 " }, + { 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..9811c24 --- /dev/null +++ b/spa/plugins/avb/avb-pcm-source.c @@ -0,0 +1,892 @@ +/* Spa AVB PCM Source */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(¶ms)); + + 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 = 0; + + 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 (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; + 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 " }, + { 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..3f608d7 --- /dev/null +++ b/spa/plugins/avb/avb-pcm.c @@ -0,0 +1,1216 @@ +/* Spa AVB PCM */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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_type_audio_format_from_short_name(s); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + spa_audio_parse_position(s, strlen(s), state->default_pos.pos, + &state->default_pos.channels); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) { + state->n_allowed_rates = spa_avb_parse_rates(state->allowed_rates, + 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 void update_position(struct state *state) +{ + 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; + } +} + +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]; + + update_position(state); + + 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) +{ + update_position(state); + + 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; + + update_position(state); + + 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; + struct spa_fraction 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.target_duration; + rate = state->position->clock.target_rate; + } else { + duration = 1024; + rate = SPA_FRACTION(1, 48000); + } + + state->next_time = current_time + duration * SPA_NSEC_PER_SEC / rate.denom; + + if (state->ports[0].direction == SPA_DIRECTION_INPUT) + handle_play(state, current_time); + else + handle_capture(state, current_time); + + if (SPA_LIKELY(state->clock)) { + state->clock->nsec = current_time; + state->clock->rate = rate; + state->clock->position += state->clock->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; + + update_position(state); + + 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); + set_timeout(state, 0); + + 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; + + return 0; +} diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h new file mode 100644 index 0000000..d4dfa03 --- /dev/null +++ b/spa/plugins/avb/avb-pcm.h @@ -0,0 +1,287 @@ +/* Spa AVB PCM */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AVB_PCM_H +#define SPA_AVB_PCM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len) +{ + struct spa_json it[1]; + char v[256]; + uint32_t count; + + if (spa_json_begin_array_relax(&it[0], val, len) <= 0) + return 0; + + count = 0; + while (spa_json_get_string(&it[0], 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..f193924 --- /dev/null +++ b/spa/plugins/avb/avb.c @@ -0,0 +1,37 @@ +/* Spa AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +#include "avb.h" + +extern const struct spa_handle_factory spa_avb_sink_factory; +extern const struct spa_handle_factory spa_avb_source_factory; + +SPA_LOG_TOPIC_DEFINE(avb_log_topic, "spa.avb"); + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..e6cbf41 --- /dev/null +++ b/spa/plugins/avb/avb.h @@ -0,0 +1,19 @@ +/* Spa AVB */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AVB_H +#define SPA_AVB_H + +#include + +#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..a70bb3d --- /dev/null +++ b/spa/plugins/avb/avbtp/packets.h @@ -0,0 +1,200 @@ +/* Spa AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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 +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/README-Telephony.md b/spa/plugins/bluez5/README-Telephony.md new file mode 100644 index 0000000..93910b0 --- /dev/null +++ b/spa/plugins/bluez5/README-Telephony.md @@ -0,0 +1,345 @@ +# PipeWire Bluetooth Telephony service + +The Telephony service is a D-Bus service that allows applications to communicate +with the HFP native backend in order to control phone calls. Phone call features +are a core part of the HFP specification and are available when a mobile phone +is paired (therefore, PipeWire acts as the Hands-Free and the phone is the Audio +Gateway). + +The service is exposed on the user session bus by default, but there is an +option to make it available on the system bus instead. + +The service implements its own interfaces alongside the standard DBus +Introspectable, ObjectManager and Properties interfaces, where needed. +These interfaces are mostly compatible with the ofono Manager, VoiceCallManager +and VoiceCall interfaces. For compatibility, the `org.ofono.Manager`, +`org.ofono.VoiceCallManager` and `org.ofono.VoiceCall` are also implemented +with any additional compatibility methods & signals are necessary to allow +ofono-based applications to be able to work just by modifying the service name, +the manager object path and the operating bus (session vs system). + +In addition, to the compatibility interfaces, there is a runtime option to +also register the service as `org.ofono` on the system bus, making it a drop-in +replacement for ofono. Note, however, that this service is not a full replacement, +but only for the Bluetooth-based voice calls. + +## Manager Object + +``` +Service org.pipewire.Telephony + or org.ofono +Object path /org/pipewire/Telephony + or / +Implements org.ofono.Manager + org.freedesktop.DBus.Introspectable + org.freedesktop.DBus.ObjectManager +``` + +The manager object is always available and allows applications to get access to +the connected audio gateways. + +The object path is set to `/` when ofono service compatibility is enabled, +in which case the service name `org.ofono` is also registered instead of +`org.pipewire.Telephony`. + +The methods and signals below are made available on the `org.ofono.Manager` +interface, for compatibility. AudioGateway objects are normally announced via +the standard DBus ObjectManager interface. + +### Methods + +`array{object,dict} GetModems()` + +Get an array of AudioGateway objects and properties that represents the +currently connected audio gateways. + +### Signals + +`ModemAdded(object path, dict properties)` + +Signal that is sent when a new audio gateway is added. It contains the object +path of the new audio gateway and also its properties. + +`ModemRemoved(object path)` + +Signal that is sent when an audio gateway has been removed. The object path is +no longer accessible after this signal and only emitted for reference. + +## AudioGateway Object + +``` +Service org.pipewire.Telephony + or org.ofono +Object path /org/pipewire/Telephony/{ag0,ag1,...} +Implements org.pipewire.Telephony.AudioGateway1 + org.ofono.VoiceCallManager + org.freedesktop.DBus.Introspectable + org.freedesktop.DBus.ObjectManager +``` + +Audio gateway objects represent the currently connected AG devices (typically +mobile phones). + +The methods, signals and properties listed below are made available on both +`org.pipewire.Telephony.AudioGateway1` and +`org.ofono.VoiceCallManager` interfaces, unless explicitly documented otherwise. + +Call objects are announced via both the standard DBus ObjectManager interface +and via the `org.ofono.VoiceCallManager` interface, for compatibility. + +### Methods + +`array{object,dict} GetCalls()` + +Get an array of call object paths and properties that represents the currently +present calls. + +This method call should only be used once when an application starts up. +Further call additions and removal shall be monitored via CallAdded and +CallRemoved signals. + +NOTE: This method is implemented only on the `org.ofono.VoiceCallManager` +interface, for compatibility. Call announcements are normally made available via +the standard `org.freedesktop.DBus.ObjectManager` interface. + +`object Dial(string number)` + +Initiates a new outgoing call. Returns the object path to the newly created +call. + +The number must be a string containing the following characters: +`[0-9+*#,ABCD]{1,80}` In other words, it must be a non-empty string consisting +of 1 to 80 characters. The character set can contain numbers, `+`, `*`, `#`, `,` +and the letters `A` to `D`. Besides this sanity checking no further number +validation is performed. It is assumed that the gateway and/or the network will +perform further validation. + +NOTE: If an active call (single or multiparty) exists, then it is automatically +put on hold if the dial procedure is successful. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.InvalidArgs + * org.freedesktop.DBus.Error.Failed + +`void SwapCalls()` + +Swaps Active and Held calls. The effect of this is that all calls (0 or more +including calls in a multi-party conversation) that were Active are now Held, +and all calls (0 or more) that were Held are now Active. + +GSM specification does not allow calls to be swapped in the case where Held, +Active and Waiting calls exist. Some modems implement this anyway, thus it is +manufacturer specific whether this method will succeed in the case of Held, +Active and Waiting calls. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void ReleaseAndAnswer()` + +Releases currently active call (0 or more) and answers the currently waiting +call. Please note that if the current call is a multiparty call, then all +parties in the multi-party call will be released. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void ReleaseAndSwap()` + +Releases currently active call (0 or more) and activates any currently held +calls. Please note that if the current call is a multiparty call, then all +parties in the multi-party call will be released. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void HoldAndAnswer()` + +Puts the current call (including multi-party calls) on hold and answers the +currently waiting call. Calling this function when a user already has a both +Active and Held calls is invalid, since in GSM a user can have only a single +Held call at a time. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void HangupAll()` + +Releases all calls except waiting calls. This includes multiparty calls. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`array{object} CreateMultiparty()` + +Joins active and held calls together into a multi-party call. If one of the +calls is already a multi-party call, then the other call is added to the +multiparty conversation. Returns the new list of calls participating in the +multiparty call. + +There can only be one subscriber controlled multi-party call according to the +GSM specification. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void SendTones(string tones)` + +Sends the DTMF tones to the network. The tones have a fixed duration. Tones +can be one of: '0' - '9', '*', '#', 'A', 'B', 'C', 'D'. The last four are +typically not used in normal circumstances. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.InvalidArgs + * org.freedesktop.DBus.Error.Failed + +### Signals + +`CallAdded(object path, dict properties)` + +Signal that is sent when a new call is added. It contains the object path of +the new voice call and also its properties. + +NOTE: This method is implemented only on the `org.ofono.VoiceCallManager` +interface, for compatibility. Call announcements are normally made available via +the standard `org.freedesktop.DBus.ObjectManager` interface. + +`CallRemoved(object path)` + +Signal that is sent when a voice call has been released. The object path is no +longer accessible after this signal and only emitted for reference. + +NOTE: This method is implemented only on the `org.ofono.VoiceCallManager` +interface, for compatibility. Call announcements are normally made available via +the standard `org.freedesktop.DBus.ObjectManager` interface. + +## Call Object + +``` +Service org.pipewire.Telephony + or org.ofono +Object path /org/pipewire/Telephony/{ag0,ag1,...}/{call0,call1,...} +Implements org.pipewire.Telephony.Call1 + org.ofono.VoiceCall + org.freedesktop.DBus.Introspectable + org.freedesktop.DBus.Properties +``` + +Call objects represent active calls and allow managing them. + +The methods, signals and properties listed below are made available on both +`org.pipewire.Telephony.Call1` and `org.ofono.VoiceCall` +interfaces, unless explicitly documented otherwise. + +### Methods + +`dict GetProperties()` + +Returns all properties for this object. See the properties section for available +properties. + +NOTE: This method is implemented only on the `org.ofono.VoiceCall` interface, +for compatibility. Properties are normally made available via the standard +`org.freedesktop.DBus.Properties` interface. + +`void Answer()` + +Answers an incoming call. Only valid if the state of the call is "incoming". + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +`void Hangup()` + +Hangs up the call. + +For an incoming call, the call is hung up using ATH or equivalent. For a +waiting call, the remote party is notified by using the User Determined User +Busy (UDUB) condition. This is generally implemented using CHLD=0. + +Please note that the GSM specification does not allow the release of a held call +when a waiting call exists. This is because 27.007 allows CHLD=1X to operate +only on active calls. Hence a held call cannot be hung up without affecting the +state of the incoming call (e.g. using other CHLD alternatives). Most +manufacturers provide vendor extensions that do allow the state of the held call +to be modified using CHLD=1X or equivalent. It should be noted that Bluetooth +HFP specifies the classic 27.007 behavior and does not allow CHLD=1X to modify +the state of held calls. + +Based on the discussion above, it should also be noted that releasing a +particular party of a held multiparty call might not be possible on some +implementations. It is recommended for the applications to structure their UI +accordingly. + +NOTE: Releasing active calls does not produce side-effects. That is the state +of held or waiting calls is not affected. As an exception, in the case where a +single active call and a waiting call are present, releasing the active call +will result in the waiting call transitioning to the 'incoming' state. + +Possible Errors: + * org.pipewire.Telephony.Error.InvalidState + * org.freedesktop.DBus.Error.Failed + +### Signals + +`PropertyChanged(string property, variant value)` + +Signal is emitted whenever a property has changed. The new value is passed as +the signal argument. + +NOTE: This method is implemented only on the `org.ofono.VoiceCall` interface, +for compatibility. Properties are normally made available via the standard +`org.freedesktop.DBus.Properties` interface. + +### Properties + +`string LineIdentification [readonly]` + +Contains the Line Identification information returned by the network, if +present. For incoming calls this is effectively the CLIP. For outgoing calls +this attribute will hold the dialed number, or the COLP if received by the +audio gateway. + +Please note that COLP may be different from the dialed number. A special +"withheld" value means the remote party refused to provide caller ID and the +"override category" option was not provisioned for the current subscriber. + +`string IncomingLine [readonly, optional]` + +Contains the Called Line Identification information returned by the network. +This is only available for incoming calls and indicates the local subscriber +number which was dialed by the remote party. This is useful for subscribers +which have a multiple line service with their network provider and would like to +know what line the call is coming in on. + +`string Name [readonly]` + +Contains the Name Identification information returned by the network, if +present. + +`boolean Multiparty [readonly]` + +Contains the indication if the call is part of a multiparty call or not. + +Notifications if a call becomes part or leaves a multiparty call are sent. + +`string State [readonly]` + +Contains the state of the current call. The state can be one of: + - "active" - The call is active + - "held" - The call is on hold + - "dialing" - The call is being dialed + - "alerting" - The remote party is being alerted + - "incoming" - Incoming call in progress + - "waiting" - Call is waiting + - "disconnected" - No further use of this object is allowed, it will be + destroyed shortly diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c new file mode 100644 index 0000000..bf0b735 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -0,0 +1,736 @@ +/* Spa A2DP AAC codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "rtp.h" +#include "media-codecs.h" + +static struct spa_log *log; + +#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; + + uint32_t enc_delay; + uint32_t dec_delay; +}; + +static bool eld_supported(void) +{ + static bool supported = false, checked = false; + HANDLE_AACENCODER aacenc = NULL; + + if (checked) + return supported; + + if (aacEncOpen(&aacenc, 0, 2) != AACENC_OK) + goto done; + if (aacEncoder_SetParam(aacenc, AACENC_AOT, AOT_ER_AAC_ELD) != AACENC_OK) + goto done; + if (aacEncoder_SetParam(aacenc, AACENC_SBR_MODE, 1) != AACENC_OK) + goto done; + + supported = true; + +done: + if (aacenc) + aacEncClose(&aacenc); + checked = true; + spa_log_debug(log, "FDK-AAC AAC-ELD support:%d", (int)supported); + return supported; +} + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + 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 | + (eld_supported() ? AAC_OBJECT_TYPE_MPEG4_AAC_ELD : 0), + 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 (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD) { + if (!eld_supported()) + return -ENOTSUP; + + if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) + conf.object_type = AAC_OBJECT_TYPE_MPEG4_AAC_ELD; + else + return -ENOTSUP; + } else { + 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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (i == 0) + return -EINVAL; + + 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 | + AAC_OBJECT_TYPE_MPEG4_AAC_ELD))) + 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 object type has multiple bits set (invalid per spec, see above), + * assume the device usually means AAC-LC. + */ + if (conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC)) { + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); + if (res != AACENC_OK) + goto error; + } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) { + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_ER_AAC_ELD); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_SBR_MODE, 1); + if (res != AACENC_OK) + goto error; + } else { + res = -EINVAL; + 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->enc_delay = enc_info.nDelay; + + 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 + + this->dec_delay = 0; + + 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_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) + *encoder = this->enc_delay; + + if (decoder) { + CStreamInfo *info = aacDecoder_GetStreamInfo(this->aacdec); + if (info) + this->dec_delay = info->outputDelay; + *decoder = this->dec_delay; + } +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_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, + .get_delay = codec_get_delay, +}; + +const struct media_codec a2dp_codec_aac_eld = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD, + .codec_id = A2DP_CODEC_MPEG24, + .name = "aac_eld", + .description = "AAC-ELD", + .endpoint_name = "aac", + .fill_caps = NULL, + .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, + .get_delay = codec_get_delay, +}; + +MEDIA_CODEC_EXPORT_DEF( + "aac", + &a2dp_codec_aac, + &a2dp_codec_aac_eld +); diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c new file mode 100644 index 0000000..e10691e --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-aptx.c @@ -0,0 +1,756 @@ +/* Spa A2DP aptX codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#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, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + size_t actual_conf_size = codec_get_caps_size(codec); + const a2dp_aptx_t a2dp_aptx = { + .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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (i == 0) + return -EINVAL; + + 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 = NULL; + a2dp_aptx_t conf; + int res; + int frequency; + + if (config_len < sizeof(conf)) { + res = -EINVAL; + goto error; + } + + memcpy(&conf, config, sizeof(conf)); + + 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; + + frequency = media_codec_get_config(aptx_frequencies, SPA_N_ELEMENTS(aptx_frequencies), conf.frequency); + if (frequency < 0) { + res = -EINVAL; + goto error; + } + + if (this->hd) + this->max_frames = (this->mtu - sizeof(struct rtp_header)) / this->frame_length; + else if (codec_is_ll(codec)) + /* try to make 7.5ms packets */ + this->max_frames = SPA_MIN((unsigned)frequency * 75u/10000u / 4u, 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; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + if (encoder) + *encoder = 90; + if (decoder) + *decoder = 0; +} + +/* + * 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, + .get_delay = codec_get_delay, +}; + + +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, + .get_delay = codec_get_delay, +}; + +#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, \ + .get_delay = codec_get_delay + + +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..d775ce1 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-caps.h @@ -0,0 +1,491 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2018 Pali Rohár + * + * 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 +#include + +#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_OBJECT_TYPE_MPEG4_AAC_ELD 0x02 + +#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) + + +#define OPUS_G_VENDOR_ID 0x000000e0 +#define OPUS_G_CODEC_ID 0x0001 + +#define OPUS_G_FREQUENCY_MASK 0x80 +#define OPUS_G_FREQUENCY_48000 0x80 + +#define OPUS_G_DURATION_MASK 0x18 +#define OPUS_G_DURATION_100 0x08 +#define OPUS_G_DURATION_200 0x10 + +#define OPUS_G_CHANNELS_MASK 0x07 +#define OPUS_G_CHANNELS_MONO 0x01 +#define OPUS_G_CHANNELS_STEREO 0x02 +#define OPUS_G_CHANNELS_MONO_2 0x04 + +#define OPUS_G_GET_FREQUENCY(a) ((a).data & OPUS_G_FREQUENCY_MASK) +#define OPUS_G_GET_DURATION(a) ((a).data & OPUS_G_DURATION_MASK) +#define OPUS_G_GET_CHANNELS(a) ((a).data & OPUS_G_CHANNELS_MASK) + +#define OPUS_G_SET(a, freq, dur, ch) \ + (a).data = ((freq) & OPUS_G_FREQUENCY_MASK) | ((dur) & OPUS_G_DURATION_MASK) | ((ch) & OPUS_G_CHANNELS_MASK) + + +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; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t data; +} __attribute__ ((packed)) a2dp_opus_g_t; + +#define ASHA_CODEC_G722 0x63 + +#endif diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c new file mode 100644 index 0000000..bbf9e96 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-faststream.c @@ -0,0 +1,627 @@ +/* Spa A2DP FastStream codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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, + const struct spa_dict *settings, 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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + if (i == 0) + return -EINVAL; + + 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; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + if (encoder) + *encoder = 73; + if (decoder) + *decoder = 0; +} + +/* Voice channel SBC, not a real A2DP codec */ +static const struct media_codec duplex_codec = { + .codec_id = A2DP_CODEC_VENDOR, + .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, \ + .get_delay = codec_get_delay + +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..05a3aa2 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c @@ -0,0 +1,768 @@ +/* Spa A2DP LC3plus HR codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +#ifdef HAVE_LC3PLUS_H +#include +#else +#include +#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, + const struct spa_dict *settings, 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..f5d0b15 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-ldac.c @@ -0,0 +1,606 @@ +/* Spa A2DP LDAC codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#ifdef ENABLE_LDAC_ABR +#include +#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, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + static const a2dp_ldac_t a2dp_ldac = { + .info.vendor_id = LDAC_VENDOR_ID, + .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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (i == 0) + return -EINVAL; + + 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; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) { + switch (this->frequency) { + case 96000: + case 88200: + *encoder = 256; + break; + default: + *encoder = 128; + break; + } + } + if (decoder) + *decoder = 0; +} + +const struct media_codec a2dp_codec_ldac = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + .codec_id = A2DP_CODEC_VENDOR, + .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, + .get_delay = codec_get_delay, +}; + +MEDIA_CODEC_EXPORT_DEF( + "ldac", + &a2dp_codec_ldac +); diff --git a/spa/plugins/bluez5/a2dp-codec-opus-g.c b/spa/plugins/bluez5/a2dp-codec-opus-g.c new file mode 100644 index 0000000..85a36bf --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-opus-g.c @@ -0,0 +1,542 @@ +/* Spa A2DP Opus Codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "rtp.h" +#include "media-codecs.h" + +static struct spa_log *log; + +struct dec_data { + int32_t delay; +}; + +struct enc_data { + struct rtp_header *header; + struct rtp_payload *payload; + + int samples; + int codesize; + int frame_dms; + int bitrate; + int packet_size; + + int32_t delay; +}; + +struct impl { + OpusEncoder *enc; + OpusDecoder *dec; + + int mtu; + int samplerate; + int channels; + int application; + + struct dec_data d; + struct enc_data e; +}; + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_opus_g_t conf = { + .info = codec->vendor, + }; + + OPUS_G_SET(conf, + OPUS_G_FREQUENCY_48000, + OPUS_G_DURATION_100 | OPUS_G_DURATION_200, + OPUS_G_CHANNELS_MONO | OPUS_G_CHANNELS_STEREO | OPUS_G_CHANNELS_MONO_2); + + memcpy(caps, &conf, sizeof(conf)); + return sizeof(conf); +} + +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]) +{ + a2dp_opus_g_t conf; + int frequency, duration, channels; + + 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 (OPUS_G_GET_FREQUENCY(conf) & OPUS_G_FREQUENCY_48000) + frequency = OPUS_G_FREQUENCY_48000; + else + return -EINVAL; + + if (OPUS_G_GET_DURATION(conf) & OPUS_G_DURATION_200) + duration = OPUS_G_DURATION_200; + else if (OPUS_G_GET_DURATION(conf) & OPUS_G_DURATION_100) + duration = OPUS_G_DURATION_100; + else + return -EINVAL; + + if (OPUS_G_GET_CHANNELS(conf) & OPUS_G_CHANNELS_STEREO) + channels = OPUS_G_CHANNELS_STEREO; + else if (OPUS_G_GET_CHANNELS(conf) & OPUS_G_CHANNELS_MONO) + channels = OPUS_G_CHANNELS_MONO; + else if (OPUS_G_GET_CHANNELS(conf) & OPUS_G_CHANNELS_MONO_2) + channels = OPUS_G_CHANNELS_MONO_2; + else + return -EINVAL; + + OPUS_G_SET(conf, frequency, duration, channels); + + 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_g_t conf1, conf2, cap1, cap2; + a2dp_opus_g_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_g_t)) ? 1 : 0; + b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_opus_g_t)) ? 1 : 0; + if (!a || !b) + return b - a; + + memcpy(&cap1, caps1, sizeof(cap1)); + memcpy(&cap2, caps2, sizeof(cap2)); + + PREFER_EXPR(OPUS_G_GET_CHANNELS(*conf) & OPUS_G_CHANNELS_STEREO); + + 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_opus_g_t conf; + struct spa_pod_frame f[1]; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + int channels; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + switch (OPUS_G_GET_CHANNELS(conf)) { + case OPUS_G_CHANNELS_STEREO: + channels = 2; + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + break; + case OPUS_G_CHANNELS_MONO: + channels = 1; + position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + case OPUS_G_CHANNELS_MONO_2: + channels = 2; + position[0] = SPA_AUDIO_CHANNEL_AUX0; + position[1] = SPA_AUDIO_CHANNEL_AUX1; + break; + default: + 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(channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 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) +{ + a2dp_opus_g_t conf; + + 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_F32; + info->info.raw.rate = 0; /* not specified by config */ + + switch (OPUS_G_GET_FREQUENCY(conf)) { + case OPUS_G_FREQUENCY_48000: + break; + default: + return -EINVAL; + } + + switch (OPUS_G_GET_DURATION(conf)) { + case OPUS_G_DURATION_100: + case OPUS_G_DURATION_200: + break; + default: + return -EINVAL; + } + + switch (OPUS_G_GET_CHANNELS(conf)) { + case OPUS_G_CHANNELS_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; + case OPUS_G_CHANNELS_MONO: + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + case OPUS_G_CHANNELS_MONO_2: + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_AUX0; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_AUX1; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int parse_frame_dms(int value) +{ + switch (value) { + case OPUS_G_DURATION_100: + return 100; + case OPUS_G_DURATION_200: + return 200; + default: + return -EINVAL; + } +} + +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_opus_g_t conf; + struct impl *this = NULL; + struct spa_audio_info config_info; + int res; + + if (config_len < sizeof(conf)) { + res = -EINVAL; + goto error; + } + memcpy(&conf, config, sizeof(conf)); + + 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; + + if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 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; + + /* + * Setup encoder + */ + this->enc = opus_encoder_create(this->samplerate, this->channels, this->application, &res); + if (this->enc == NULL) { + res = -EINVAL; + goto error; + } + + if ((this->e.frame_dms = parse_frame_dms(OPUS_G_GET_DURATION(conf))) < 0) { + res = -EINVAL; + goto error; + } + + this->e.samples = this->e.frame_dms * this->samplerate / 10000; + this->e.codesize = this->e.samples * (int)this->channels * sizeof(float); + + int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + this->e.bitrate = SPA_MIN(128000 * this->channels, + (int64_t)8 * (this->mtu - header_size) * 10000 / this->e.frame_dms); + + opus_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate)); + + opus_encoder_ctl(this->enc, OPUS_GET_LOOKAHEAD(&this->e.delay)); + + /* + * Setup decoder + */ + this->dec = opus_decoder_create(this->samplerate, this->channels, &res); + if (this->dec == NULL) { + res = -EINVAL; + goto error; + } + + opus_decoder_ctl(this->dec, OPUS_GET_LOOKAHEAD(&this->d.delay)); + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (this && this->enc) + opus_encoder_destroy(this->enc); + if (this && this->dec) + opus_decoder_destroy(this->dec); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + + opus_encoder_destroy(this->enc); + opus_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_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; + + 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; + int res; + + if (src_size < (size_t)this->e.codesize) { + *dst_out = 0; + return 0; + } + if (this->e.packet_size >= this->mtu) + return -EINVAL; + + dst_size = SPA_MIN(dst_size, (size_t)(this->mtu - this->e.packet_size)); + + res = opus_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++; + + *need_flush = NEED_FLUSH_ALL; + + return this->e.codesize; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl SPA_UNUSED *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) + return -EINVAL; /* fragmentation not supported */ + if (payload->frame_count != 1) + return -EINVAL; /* wrong number of frames in packet */ + + 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 SPA_UNUSED *this = data; + int consumed = src_size; + int res; + int dst_samples; + + dst_samples = dst_size / (sizeof(float) * this->channels); + res = opus_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) +{ + return -ENOTSUP; +} + +static int codec_reduce_bitpool(void *data) +{ + return 0; +} + +static int codec_increase_bitpool(void *data) +{ + return 0; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) + *encoder = this->e.delay; + if (decoder) + *decoder = this->d.delay; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_log_topic); +} + +const struct media_codec a2dp_codec_opus_g = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = OPUS_G_VENDOR_ID, + .codec_id = OPUS_G_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, + .start_encode = codec_start_encode, + .encode = codec_encode, + .abr_process = codec_abr_process, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, + .set_log = codec_set_log, + .start_decode = codec_start_decode, + .decode = codec_decode, + .name = "opus_g", + .description = "Opus", + .fill_caps = codec_fill_caps, + .get_delay = codec_get_delay, +}; + +MEDIA_CODEC_EXPORT_DEF( + "opus-g", + &a2dp_codec_opus_g +); diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c new file mode 100644 index 0000000..e65b62f --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-opus.c @@ -0,0 +1,1437 @@ +/* Spa A2DP Opus Codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rtp.h" +#include "media-codecs.h" + +static struct spa_log *log; + +#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]; + + int32_t delay; +}; + +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; + + int32_t delay; +}; + +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 /* Front 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_not_reached(); + }; + + 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, + const struct spa_dict *settings, 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); + + opus_multistream_encoder_ctl(this->enc, OPUS_GET_LOOKAHEAD(&this->e.delay)); + + /* + * 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; + } + + opus_multistream_decoder_ctl(this->dec, OPUS_GET_LOOKAHEAD(&this->d.delay)); + + 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_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) + *encoder = this->e.delay; + if (decoder) + *decoder = this->d.delay; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_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, \ + .get_delay = codec_get_delay + +#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 05", + .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 05 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 05 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 05 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 05 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 05 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..fc55a03 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-sbc.c @@ -0,0 +1,686 @@ +/* Spa A2DP SBC codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include + +#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; + + uint32_t enc_delay; +}; + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + static const a2dp_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; + this->enc_delay = 37; + break; + case SBC_SUBBANDS_8: + this->sbc.subbands = SBC_SB_8; + this->enc_delay = 73; + 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; +} + +static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) +{ + struct impl *this = data; + + if (encoder) + *encoder = this->enc_delay; + if (decoder) + *decoder = 0; +} + +const struct media_codec a2dp_codec_sbc = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC, + .codec_id = A2DP_CODEC_SBC, + .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, + .get_delay = codec_get_delay, +}; + +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", + .endpoint_name = "sbc", + .fill_caps = NULL, + .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, + .get_delay = codec_get_delay, +}; + +MEDIA_CODEC_EXPORT_DEF( + "sbc", + &a2dp_codec_sbc, + &a2dp_codec_sbc_xq +); diff --git a/spa/plugins/bluez5/asha-codec-g722.c b/spa/plugins/bluez5/asha-codec-g722.c new file mode 100644 index 0000000..1043b4f --- /dev/null +++ b/spa/plugins/bluez5/asha-codec-g722.c @@ -0,0 +1,176 @@ +/* Spa ASHA G722 codec */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include "rtp.h" +#include "media-codecs.h" +#include "g722/g722_enc_dec.h" + +#define ASHA_HEADER_SZ 1 /* 1 byte sequence number */ +#define ASHA_ENCODED_PKT_SZ 160 + +static struct spa_log *spalog; + +struct impl { + g722_encode_state_t encode; + unsigned int codesize; +}; + +static int codec_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + /* Payload for ASHA must be preceded by 1-byte sequence number */ + *(uint8_t *)dst = seqnum % 256; + + return 1; +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_pod_frame f[1]; + uint32_t position[1]; + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(16000), + 0); + + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + 0); + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + + return *param == NULL ? -EIO : 1; +} + +static void codec_deinit(void *data) +{ + return; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this; + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + return NULL; + + g722_encode_init(&this->encode, 64000, G722_PACKED); + + /* + * G722 has a compression ratio of 4. Considering 160 bytes of encoded + * payload, we need 640 bytes for generating an encoded frame. + */ + this->codesize = ASHA_ENCODED_PKT_SZ * 4; + + spa_log_debug(spalog, "Codec initialized"); + + return this; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + size_t src_sz; + int ret; + + if (src_size < this->codesize) { + spa_log_trace(spalog, "Insufficient bytes for encoding, %zd", src_size); + return 0; + } + + if (dst_size < (ASHA_HEADER_SZ + ASHA_ENCODED_PKT_SZ)) { + spa_log_trace(spalog, "No space for encoded output, %zd", dst_size); + return 0; + } + + src_sz = (src_size > this->codesize) ? this->codesize : src_size; + + ret = g722_encode(&this->encode, dst, src, src_sz / 2 /* S16LE */); + if (ret < 0) { + spa_log_error(spalog, "encode error: %d", ret); + return -EIO; + } + + *dst_out = ret; + *need_flush = NEED_FLUSH_ALL; + + return src_sz; +} + +static void codec_set_log(struct spa_log *global_log) +{ + spalog = global_log; + spa_log_topic_init(spalog, &codec_plugin_log_topic); +} + +const struct media_codec asha_codec_g722 = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_G722, + .codec_id = ASHA_CODEC_G722, + .name = "g722", + .asha = true, + .description = "G722", + .fill_caps = NULL, + .enum_config = codec_enum_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .abr_process = codec_abr_process, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, + .set_log = codec_set_log, +}; + +MEDIA_CODEC_EXPORT_DEF( + "g722", + &asha_codec_g722 +); diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c new file mode 100644 index 0000000..ddbae1c --- /dev/null +++ b/spa/plugins/bluez5/backend-hsphfpd.c @@ -0,0 +1,1521 @@ +/* Spa hsphfpd backend */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ +/* Based on previous work for pulseaudio by: Pali Rohár */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 \ + "\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\n" + +#define AUDIO_AGENT_ENDPOINT_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\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); + free(endpoint->local_address); + free(endpoint->remote_address); + free(endpoint); +} + +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) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + DBusMessageIter iter; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + if (r == NULL) { + spa_log_error(backend->log, "Transport Set() failed for transport %s (%s)", path, err.message); + 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; + } + + 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; + spa_autoptr(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; + + 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; + spa_autoptr(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, &(const char *){ "AgentCodec" }); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &data); + dbus_message_iter_append_basic(&data, DBUS_TYPE_STRING, &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; + + 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; + 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; + spa_autoptr(DBusMessage) r = NULL; + spa_autoclose int fd = -1; + + 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)) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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 = spa_steal_fd(fd); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + +fail: + if (r) { + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + + 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; + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + 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; + + 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, &(const char *) { "AgentCodec" }); + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void hsphfpd_audio_acquire_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_transport *transport = user_data; + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + const char *transport_path; + const char *service_id; + const char *agent_path; + spa_auto(DBusError) error = DBUS_ERROR_INIT; + int ret = 0; + + backend->acquire_in_progress = false; + + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&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)); + ret = -EIO; + 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"); + ret = -EIO; + goto finish; + } + + if (!check_signature(r, "oso")) { + spa_log_error(backend->log, "Invalid reply signature for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio()"); + ret = -EIO; + 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); + ret = -EIO; + 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"); + ret = -EIO; + goto finish; + } + + spa_log_debug(backend->log, "hsphfpd audio acquired"); + +finish: + if (ret < 0) + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ERROR); + else + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE); +} + +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); + spa_autoptr(DBusMessage) m = NULL; + const char *air_codec = HSPHFP_AIR_CODEC_CVSD; + const char *agent_codec = HSPHFP_AGENT_CODEC_PCM; + + 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); + + if (!send_with_reply(backend->conn, m, hsphfpd_audio_acquire_reply, transport)) + return -EIO; + + backend->acquire_in_progress = true; + + 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); + + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE); + + 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: + { + dbus_bool_t 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; + DBusMessageIter i, array_i; + + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&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)); + return; + } + + if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) { + spa_log_error(backend->log, "Reply for GetManagedObjects() from invalid sender"); + return; + } + + if (!dbus_message_iter_init(r, &i) || !check_signature(r, "a{oa{sa{sv}}}")) { + spa_log_error(backend->log, "Invalid arguments in GetManagedObjects() reply"); + return; + } + + 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; +} + +static int hsphfpd_register(struct impl *backend) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + const char *path = APPLICATION_OBJECT_MANAGER_PATH; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + 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); + return -ENOTSUP; + } else { + spa_log_warn(backend->log, "Registering application %s failed: %s (%s)", + path, err.message, err.name); + return -EIO; + } + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterApplication() failed: %s", + dbus_message_get_error_name(r)); + return -EIO; + } + + backend->hsphfpd_service_id = strdup(dbus_message_get_sender(r)); + + spa_log_debug(backend->log, "Registered to hsphfpd"); + + return 0; +} + +static int hsphfpd_get_endpoints(struct impl *backend) +{ + spa_autoptr(DBusMessage) m = NULL; + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/", + DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects"); + if (m == NULL) + return -ENOMEM; + + if (!send_with_reply(backend->conn, m, hsphfpd_get_endpoints_reply, backend)) + return -EIO; + + return 0; +} + +static int backend_hsphfpd_register(void *data) +{ + int ret = hsphfpd_register(data); + if (ret < 0) + return ret; + + ret = hsphfpd_get_endpoints(data); + if (ret < 0) + return ret; + + return 0; +} + +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; + + 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; + + if (backend->filters_added) + return 0; + + if (!dbus_connection_add_filter(backend->conn, hsphfpd_filter_cb, backend, NULL)) { + spa_log_error(backend->log, "failed to add filter function"); + return -EIO; + } + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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; +} + +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) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/", + DBUS_INTERFACE_INTROSPECTABLE, "Introspect"); + if (m == NULL) + return false; + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN) + return true; + + return false; +} + +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..8d5354d --- /dev/null +++ b/spa/plugins/bluez5/backend-native.c @@ -0,0 +1,4053 @@ +/* Spa HSP/HFP native backend */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" + +#ifdef HAVE_LIBUSB +#include +#endif + +#include "modemmanager.h" +#include "upower.h" +#include "telephony.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define PROP_KEY_ROLES "bluez5.roles" +#define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles" +#define PROP_KEY_HFP_DISABLE_NREC "bluez5.hfp-hf.disable-nrec" + +#define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000 +#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000 + +#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; + bool hfp_disable_nrec; + + 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 spa_bt_telephony *telephony; +}; + +struct transport_data { + struct rfcomm *rfcomm; + struct spa_source sco; + int err; + bool requesting; +}; + +enum hfp_hf_state { + hfp_hf_brsf, + hfp_hf_bac, + hfp_hf_cind1, + hfp_hf_cind2, + hfp_hf_cmer, + hfp_hf_chld, + hfp_hf_clip, + hfp_hf_ccwa, + hfp_hf_cmee, + hfp_hf_nrec, + hfp_hf_slc1, + hfp_hf_slc2, + hfp_hf_vgs, + 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_call_data { + struct rfcomm *rfcomm; + struct spa_bt_telephony_call *call; +}; + +struct rfcomm_cmd { + struct spa_list link; + char* cmd; +}; + +struct rfcomm { + struct spa_list link; + struct spa_source source; + 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 lc3_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; + unsigned int hfp_hf_3way:1; + unsigned int hfp_hf_nrec:1; + unsigned int hfp_hf_clcc:1; + unsigned int hfp_hf_cme:1; + unsigned int hfp_hf_cmd_in_progress:1; + unsigned int hfp_hf_in_progress:1; + unsigned int chld_supported:1; + enum hfp_hf_state hf_state; + enum hsp_hs_state hs_state; + unsigned int codec; + uint32_t cind_enabled_indicators; + char *hf_indicators[MAX_HF_INDICATORS]; + struct spa_bt_telephony_ag *telephony_ag; + struct spa_list hfp_hf_commands; +#endif +}; + +static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + 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 void transport_state_changed (void *data, enum spa_bt_transport_state old, + enum spa_bt_transport_state state) +{ + struct rfcomm *rfcomm = data; + if (rfcomm->telephony_ag) { + rfcomm->telephony_ag->transport.state = state; + telephony_ag_transport_notify_updated_props(rfcomm->telephony_ag); + } +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, + .state_changed = transport_state_changed, +}; + +static const struct spa_bt_transport_implementation sco_transport_impl; + +static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec) +{ + struct impl *backend = rfcomm->backend; + struct spa_bt_transport *t = NULL; + struct transport_data *td; + char* pathfd; + + if (rfcomm->transport) { + spa_hook_remove(&rfcomm->transport_listener); + spa_bt_transport_free(rfcomm->transport); + rfcomm->transport = NULL; + } + + if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL) + goto fail; + + t = spa_bt_transport_create(backend->monitor, pathfd, sizeof(struct transport_data)); + if (t == NULL) { + free(pathfd); + goto fail; + } + 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; + t->codec = codec; + + 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 = (float) + 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); + + if (rfcomm->telephony_ag) { + rfcomm->telephony_ag->transport.codec = codec; + rfcomm->telephony_ag->transport.state = SPA_BT_TRANSPORT_STATE_IDLE; + telephony_ag_transport_notify_updated_props(rfcomm->telephony_ag); + } + + rfcomm->transport = t; + return 0; + +fail: + spa_log_warn(backend->log, "failed to create transport"); + return -ENOMEM; +} + +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); + if (rfcomm->telephony_ag) { + telephony_ag_destroy(rfcomm->telephony_ag); + rfcomm->telephony_ag = NULL; + } + for (int i = 0; i < MAX_HF_INDICATORS; i++) { + if (rfcomm->hf_indicators[i]) { + free(rfcomm->hf_indicators[i]); + } + } + 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(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; + + if (rfcomm->hfp_hf_cmd_in_progress) { + spa_log_debug(backend->log, "Command in progress, postponing: %s", message); + struct rfcomm_cmd *cmd = calloc(1, sizeof(struct rfcomm_cmd)); + cmd->cmd = strndup(message, len); + spa_list_append(&rfcomm->hfp_hf_commands, &cmd->link); + return 0; + } + + spa_log_debug(backend->log, "RFCOMM >> %s", message); + + /* + * The format of an AT command from the HF to the AG shall be: + * - HFP 1.8, 4.34.1 + * + * The format for a command from the HS to the AG is thus: AT= + * - 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)); + } + + rfcomm->hfp_hf_cmd_in_progress = true; + + 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: OK + * The format of the generic ERROR code from the AG to the HF shall be: ERROR + * The format of an unsolicited result code from the AG to the HF shall be: + * - HFP 1.8, 4.34.1 + * + * If the command is processed successfully, the resulting response from the AG to the HS is: OK + * If the command is not processed successfully, or is not recognized, + * the resulting response from the AG to the HS is: ERROR + * The format for an unsolicited result code (such as RING) from the AG to the HS is: + * - 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 = (float) + 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; + + /* 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 (spa_strstartswith(buf, "AT+CKPD=200") == 1) { + rfcomm_send_reply(rfcomm, "OK"); + spa_bt_device_emit_switch_profile(rfcomm->device); + } 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, "+VGS=%d", &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, "+VGM=%d", &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_streq(buf, "OK")) { + 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_codec(struct impl *backend, struct spa_bt_device *device, int codec) +{ + int res; + bool alt6_ok = true, alt1_ok = true; + bool msbc_alt6_ok = true, msbc_alt1_ok = true; + 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_alt1_ok = (bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL)); + msbc_alt6_ok = (bt_features & SPA_BT_FEATURE_MSBC); + } + + switch (codec) { + case HFP_AUDIO_CODEC_CVSD: + return true; + case HFP_AUDIO_CODEC_MSBC: + alt1_ok = msbc_alt1_ok; + alt6_ok = msbc_alt6_ok; + break; + case HFP_AUDIO_CODEC_LC3_SWB: +#ifdef HAVE_LC3 + /* LC3-SWB has same transport requirements as msbc. + * However, ALT1/ALT5 modes don't appear to work, seem + * to lose frame sync so output is garbled. + */ + alt1_ok = false; + alt6_ok = msbc_alt6_ok; + break; +#else + return false; +#endif + default: + return false; + } + + spa_log_info(backend->log, + "bluez-monitor/hardware.conf: alt6:%d alt1/5:%d", (int)alt6_ok, (int)alt1_ok); + + if (!alt6_ok && !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 && !alt1_ok && alt6_ok) { +#ifdef HAVE_LIBUSB + if (device->adapter->source_id == SOURCE_ID_USB) { + alt6_ok = check_usb_altsetting_6(backend, device->adapter->vendor_id, + device->adapter->product_id); + } else { + alt6_ok = false; + } + if (!alt6_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, assuming no"); + alt6_ok = false; +#endif + } + if (device->adapter->bus_type != BUS_TYPE_USB) + alt1_ok = false; + + return alt6_ok || alt1_ok; +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec); + +static void process_xevent_indicator(struct rfcomm *rfcomm, unsigned int level, unsigned int nlevels) +{ + struct impl *backend = rfcomm->backend; + uint8_t perc; + + spa_log_debug(backend->log, "AT+XEVENT level:%u nlevels:%u", level, nlevels); + + if (nlevels <= 1) + return; + + /* 0 <= level < nlevels */ + perc = SPA_MIN(level, nlevels - 1) * 100 / (nlevels - 1); + spa_bt_device_report_battery_level(rfcomm->device, perc); +} + +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; + unsigned int xevent_level; + unsigned int xevent_nlevels; + int xapl_vendor; + int xapl_product; + int xapl_features; + + if (sscanf(buf, "AT+BRSF=%u", &features) == 1) { + unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE; + bool codecs = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC) || + device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); + + /* + * 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 codec negotiation + This should be done when the computers bluetooth adapter supports the necessary transport mode */ + if (codecs) { + /* 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; + rfcomm->lc3_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; + rfcomm->lc3_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 | SPA_BT_HFP_AG_FEATURE_ESCO_S4; + rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BAC=")) { + /* retrieve supported codecs */ + /* response has the form AT+BAC=,, + 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 = + device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC); + else if (codec_id == HFP_AUDIO_CODEC_LC3_SWB) + rfcomm->lc3_supported_by_hfp = + device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); + } + cntr++; + } + + if (rfcomm->msbc_supported_by_hfp) + spa_log_debug(backend->log, "mSBC codec is supported"); + if (rfcomm->lc3_supported_by_hfp) + spa_log_debug(backend->log, "LC3 codec is supported"); + + 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; + bool have_codecs = rfcomm->msbc_supported_by_hfp || rfcomm->lc3_supported_by_hfp; + + 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 && have_codecs) { + spa_log_debug(backend->log, "RFCOMM initial codec setup"); + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND; + if (rfcomm->lc3_supported_by_hfp) + rfcomm_send_reply(rfcomm, "+BCS: 3"); + else + rfcomm_send_reply(rfcomm, "+BCS: 2"); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC); + } else { + if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) { + // TODO: We should manage the missing transport + } else { + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID); + } + } + } else if (spa_streq(buf, "")) { + /* 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 && + selected_codec != HFP_AUDIO_CODEC_LC3_SWB) { + 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_new_transport(rfcomm, selected_codec) < 0) { + // TODO: We should manage the missing transport + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM); + return true; + } + 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+BCC")) { + if (!rfcomm->codec_negotiation_supported) + return false; + + rfcomm_send_reply(rfcomm, "OK"); + rfcomm_send_reply(rfcomm, "+BCS: %u", rfcomm->codec); + rfcomm->hfp_ag_switching_codec = true; + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + } 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+CCWA=")) { + /* + * Claim that call waiting notifications are supported. + * Required for some devices (e.g. Soundcore Motion 300), + * as they stop sending commands if the reply to CCWA is not OK. + */ + 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); + rfcomm_send_reply(rfcomm, "OK"); + } 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 (spa_strstartswith(buf, "AT+XEVENT=USER-AGENT")) { + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+XEVENT=BATTERY,%u,%u,%*u,%*u", &xevent_level, &xevent_nlevels) == 2) { + process_xevent_indicator(rfcomm, xevent_level, xevent_nlevels); + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+XEVENT=BATTERY,%u", &xevent_level) == 1) { + process_xevent_indicator(rfcomm, xevent_level + 1, 11); + 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; + } + rfcomm_send_reply(rfcomm, "OK"); + } 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 access 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, + * access 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[2]; + enum cmee_error error; + + 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* token); + +static bool hfp_hf_wait_for_reply(struct rfcomm *rfcomm, char *buf, size_t len) +{ + struct impl *backend = rfcomm->backend; + struct pollfd fds[1]; + bool reply_found = false; + + fds[0].fd = rfcomm->source.fd; + fds[0].events = POLLIN; + while (!reply_found) { + int ret; + char tmp_buf[512]; + ssize_t tmp_len; + char *ptr, *token; + + ret = poll(fds, 1, 2000); + if (ret < 0) { + spa_log_error(backend->log, "RFCOMM poll error: %s", strerror(errno)); + goto done; + } else if (ret == 0) { + spa_log_error(backend->log, "RFCOMM poll timeout"); + goto done; + } + + if (fds[0].revents & (POLLHUP | POLLERR)) { + spa_log_info(backend->log, "lost RFCOMM connection."); + rfcomm_free(rfcomm); + return false; + } + + if (fds[0].revents & POLLIN) { + tmp_len = read(rfcomm->source.fd, tmp_buf, sizeof(tmp_buf) - 1); + if (tmp_len < 0) { + spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno)); + goto done; + } + tmp_buf[tmp_len] = '\0'; + + /* Relaxed parsing of \r\n\r\n */ + ptr = tmp_buf; + while ((token = strsep(&ptr, "\r"))) { + size_t ptr_len; + + /* Skip leading and trailing \n */ + while (*token == '\n') + ++token; + for (ptr_len = strlen(token); ptr_len > 0 && token[ptr_len - 1] == '\n'; --ptr_len) + token[ptr_len - 1] = '\0'; + + /* Skip empty */ + if (*token == '\0' /*&& buf == NULL*/) + continue; + + spa_log_debug(backend->log, "RFCOMM event: %s", token); + if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") || + spa_strstartswith(token, "+CME ERROR:")) { + spa_log_debug(backend->log, "RFCOMM reply found: %s", token); + reply_found = true; + strncpy(buf, token, len); + buf[len-1] = '\0'; + } else if (!rfcomm_hfp_hf(rfcomm, token)) { + spa_log_debug(backend->log, "RFCOMM received unsupported event: %s", token); + } + } + } + } + +done: + rfcomm->hfp_hf_cmd_in_progress = false; + if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) { + struct rfcomm_cmd *cmd; + cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link); + spa_list_remove(&cmd->link); + spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd); + rfcomm_send_cmd(rfcomm, "%s", cmd->cmd); + free(cmd->cmd); + free(cmd); + } + + return reply_found; +} + +static void hfp_hf_get_error_from_reply(char *reply, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + if (spa_strstartswith(reply, "+CME ERROR:")) { + *cme_error = atoi(reply + strlen("+CME ERROR:")); + *err = BT_TELEPHONY_ERROR_CME; + } else { + *err = BT_TELEPHONY_ERROR_FAILED; + } +} + +static void hfp_hf_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm_call_data *call_data = data; + struct rfcomm *rfcomm = call_data->rfcomm; + struct impl *backend = rfcomm->backend; + char reply[20]; + bool res; + + if (call_data->call->state != CALL_STATE_INCOMING) { + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "ATA"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to answer call"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_hangup(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm_call_data *call_data = data; + struct rfcomm *rfcomm = call_data->rfcomm; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call, *tcall; + bool found_held = false; + bool hfp_hf_in_progress = false; + char reply[20]; + bool res; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) + found_held = true; + } + + switch (call_data->call->state) { + case CALL_STATE_ACTIVE: + case CALL_STATE_DIALING: + case CALL_STATE_ALERTING: + case CALL_STATE_INCOMING: + if (found_held) { + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); + hfp_hf_in_progress = true; + } else { + rfcomm_send_cmd(rfcomm, "AT+CHUP"); + } + break; + case CALL_STATE_WAITING: + if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + rfcomm_send_cmd(rfcomm, "AT+CHLD=0"); + hfp_hf_in_progress = true; + break; + default: + spa_log_info(backend->log, "Call not incoming, waiting or active: skip hangup"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to hangup call"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + if (hfp_hf_in_progress) { + if (call_data->call->state != CALL_STATE_WAITING) { + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) { + call->state = CALL_STATE_ACTIVE; + telephony_call_notify_updated_props(call); + } + } + } + rfcomm->hfp_hf_in_progress = true; + } + *err = BT_TELEPHONY_ERROR_NONE; +} + +static const struct spa_bt_telephony_call_callbacks telephony_call_callbacks = { + SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS, + .answer = hfp_hf_answer, + .hangup = hfp_hf_hangup, +}; + +static struct spa_bt_telephony_call *hfp_hf_add_call(struct rfcomm *rfcomm, struct spa_bt_telephony_ag *ag, enum spa_bt_telephony_call_state state, + const char *number) +{ + struct spa_bt_telephony_call *call; + struct rfcomm_call_data *data; + + call = telephony_call_new(ag, sizeof(*data)); + if (!call) + return NULL; + call->state = state; + if (number) + call->line_identification = strdup(number); + data = telephony_call_get_user_data(call); + data->rfcomm = rfcomm; + data->call = call; + telephony_call_set_callbacks(call, &telephony_call_callbacks, data); + telephony_call_register(call); + + return call; +} + +static void hfp_hf_dial(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + char reply[20]; + bool res; + + spa_log_info(backend->log, "Dialing: \"%s\"", number); + rfcomm_send_cmd(rfcomm, "ATD%s;", number); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (res && spa_strstartswith(reply, "OK")) { + struct spa_bt_telephony_call *call; + call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, number); + *err = call ? BT_TELEPHONY_ERROR_NONE : BT_TELEPHONY_ERROR_FAILED; + } else { + spa_log_info(backend->log, "Failed to dial: \"%s\"", number); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + } +} + +static void hfp_hf_swap_calls(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_held = false; + char reply[20]; + bool res; + + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_WAITING) { + spa_log_debug(backend->log, "call waiting before swapping"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } else if (call->state == CALL_STATE_ACTIVE) + found_active = true; + else if (call->state == CALL_STATE_HELD) + found_held = true; + } + + if (!found_active || !found_held) { + spa_log_debug(backend->log, "no active and held calls"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=2"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to swap calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + rfcomm->hfp_hf_in_progress = true; + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_release_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_waiting = false; + char reply[20]; + bool res; + + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) + found_active = true; + else if (call->state == CALL_STATE_WAITING) + found_waiting = true; + } + + if (!found_active || !found_waiting) { + spa_log_debug(backend->log, "no active and waiting calls"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to release and answer calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + rfcomm->hfp_hf_in_progress = true; + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_release_and_swap(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_held = false; + char reply[20]; + bool res; + + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_WAITING) { + spa_log_debug(backend->log, "call waiting before release and swap"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } else if (call->state == CALL_STATE_ACTIVE) + found_active = true; + else if (call->state == CALL_STATE_HELD) + found_held = true; + } + + if (!found_active || !found_held) { + spa_log_debug(backend->log, "no active and held calls"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to release and swap calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + rfcomm->hfp_hf_in_progress = true; + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_hold_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_waiting = false; + char reply[20]; + bool res; + + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) + found_active = true; + else if (call->state == CALL_STATE_WAITING) + found_waiting = true; + } + + if (!found_active || !found_waiting) { + spa_log_debug(backend->log, "no active and waiting calls"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=2"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to hold and answer calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + rfcomm->hfp_hf_in_progress = true; + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_hangup_all(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_held = false; + char reply[20]; + bool res; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + switch (call->state) { + case CALL_STATE_ACTIVE: + case CALL_STATE_DIALING: + case CALL_STATE_ALERTING: + case CALL_STATE_INCOMING: + found_active = true; + break; + case CALL_STATE_HELD: + case CALL_STATE_WAITING: + found_held = true; + break; + default: + break; + } + } + + *err = BT_TELEPHONY_ERROR_NONE; + + /* Hangup held calls */ + if (found_held) { + rfcomm_send_cmd(rfcomm, "AT+CHLD=0"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to hangup held calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + } + } + + /* Hangup active calls */ + if (found_active) { + rfcomm_send_cmd(rfcomm, "AT+CHUP"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to hangup active calls"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + } + } +} + +static void hfp_hf_create_multiparty(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found_active = false; + bool found_held = false; + char reply[20]; + bool res; + + if (!rfcomm->chld_supported) { + *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + return; + } else if (rfcomm->hfp_hf_in_progress) { + *err = BT_TELEPHONY_ERROR_IN_PROGRESS; + return; + } + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_WAITING) { + spa_log_debug(backend->log, "call waiting before creating multiparty"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } else if (call->state == CALL_STATE_ACTIVE) + found_active = true; + else if (call->state == CALL_STATE_HELD) + found_held = true; + } + + if (!found_active || !found_held) { + spa_log_debug(backend->log, "no active and held calls"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+CHLD=3"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to create multiparty"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + rfcomm->hfp_hf_in_progress = true; + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_send_tones(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call; + bool found = false; + char reply[20]; + bool res; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) { + found = true; + break; + } + } + + if (!found) { + spa_log_debug(backend->log, "no active call"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+VTS=%s", tones); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to send tones: %s", tones); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + *err = BT_TELEPHONY_ERROR_NONE; +} + +static void hfp_hf_transport_activate(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + char reply[20]; + bool res; + + if (spa_list_is_empty(&rfcomm->telephony_ag->call_list)) { + spa_log_debug(backend->log, "no ongoing call"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + if (rfcomm->transport->fd > 0) { + spa_log_debug(backend->log, "transport is already active; SCO socket exists"); + *err = BT_TELEPHONY_ERROR_INVALID_STATE; + return; + } + + rfcomm_send_cmd(rfcomm, "AT+BCC"); + res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); + if (!res || !spa_strstartswith(reply, "OK")) { + spa_log_info(backend->log, "Failed to send AT+BCC"); + if (res) + hfp_hf_get_error_from_reply(reply, err, cme_error); + else + *err = BT_TELEPHONY_ERROR_FAILED; + return; + } + + *err = BT_TELEPHONY_ERROR_NONE; +} + +static const struct spa_bt_telephony_ag_callbacks telephony_ag_callbacks = { + SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS, + .dial = hfp_hf_dial, + .swap_calls = hfp_hf_swap_calls, + .release_and_answer = hfp_hf_release_and_answer, + .release_and_swap = hfp_hf_release_and_swap, + .hold_and_answer = hfp_hf_hold_and_answer, + .hangup_all = hfp_hf_hangup_all, + .create_multiparty = hfp_hf_create_multiparty, + .send_tones = hfp_hf_send_tones, + .transport_activate = hfp_hf_transport_activate, +}; + +static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) +{ + struct impl *backend = rfcomm->backend; + unsigned int features, gain, selected_codec, indicator, value, type; + char number[17]; + + if (sscanf(token, "+BRSF:%u", &features) == 1) { + if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) && + (rfcomm->msbc_supported_by_hfp || rfcomm->lc3_supported_by_hfp)) + rfcomm->codec_negotiation_supported = true; + rfcomm->hfp_hf_3way = (features & SPA_BT_HFP_AG_FEATURE_3WAY) != 0; + rfcomm->hfp_hf_nrec = (features & SPA_BT_HFP_AG_FEATURE_ECNR) != 0; + rfcomm->hfp_hf_clcc = (features & SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS) != 0; + rfcomm->hfp_hf_cme = (features & SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE) != 0; + } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) { + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC && + selected_codec != HFP_AUDIO_CODEC_LC3_SWB) { + 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_new_transport(rfcomm, selected_codec) < 0) { + // TODO: We should manage the missing transport + } else { + 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 (spa_strstartswith(token, "+CHLD: (")) { + int chlds = 0; + token[strcspn(token, "\r")] = 0; + token[strcspn(token, "\n")] = 0; + token[strcspn(token, ")")] = 0; + token += strlen("+CHLD: ("); + while (strlen(token)) { + token[strcspn(token, ",")] = 0; + if (spa_streq(token, "0")) + chlds |= 1 << 0; + else if (spa_streq(token, "1")) + chlds |= 1 << 1; + else if (spa_streq(token, "2")) + chlds |= 1 << 2; + else if (spa_streq(token, "3")) + chlds |= 1 << 3; + token += strcspn(token, "\0") + 1; + } + rfcomm->chld_supported = (chlds == 0x0F); + spa_log_debug(backend->log, "AT+CHLD supported: %d (0x%X)", rfcomm->chld_supported, chlds); + } else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) { + if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) { + spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator); + } 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_streq(rfcomm->hf_indicators[indicator], "callsetup")) { + if (value == CIND_CALLSETUP_NONE) { + struct spa_bt_telephony_call *call, *tcall; + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING || + call->state == CALL_STATE_INCOMING) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + } else if (value == CIND_CALLSETUP_INCOMING) { + struct spa_bt_telephony_call *call; + bool found = false; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_INCOMING || call->state == CALL_STATE_WAITING) { + spa_log_info(backend->log, "incoming call already in progress (%d)", call->state); + found = true; + break; + } + } + + if (!found && !rfcomm->hfp_hf_clcc) { + spa_log_info(backend->log, "Incoming call"); + if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_INCOMING, NULL) == NULL) + spa_log_warn(backend->log, "failed to create incoming call"); + } + } else if (value == CIND_CALLSETUP_DIALING) { + struct spa_bt_telephony_call *call; + bool found = false; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING) { + spa_log_info(backend->log, "dialing call already in progress (%d)", call->state); + found = true; + break; + } + } + + if (!found && !rfcomm->hfp_hf_clcc) { + spa_log_info(backend->log, "Dialing call"); + if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, NULL) == NULL) + spa_log_warn(backend->log, "failed to create dialing call"); + } + } else if (value == CIND_CALLSETUP_ALERTING) { + struct spa_bt_telephony_call *call; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_DIALING) { + call->state = CALL_STATE_ALERTING; + telephony_call_notify_updated_props(call); + } + } + } + + if (rfcomm->hfp_hf_clcc) + rfcomm_send_cmd(rfcomm, "AT+CLCC"); + else + rfcomm->hfp_hf_in_progress = false; + } else if (spa_streq(rfcomm->hf_indicators[indicator], "call")) { + if (value == 0) { + struct spa_bt_telephony_call *call, *tcall; + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + } else if (value == 1) { + struct spa_bt_telephony_call *call; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING || + call->state == CALL_STATE_INCOMING) { + call->state = CALL_STATE_ACTIVE; + telephony_call_notify_updated_props(call); + } + } + } + + if (rfcomm->hfp_hf_clcc) + rfcomm_send_cmd(rfcomm, "AT+CLCC"); + else + rfcomm->hfp_hf_in_progress = false; + } else if (spa_streq(rfcomm->hf_indicators[indicator], "callheld")) { + if (value == 0) { /* Reject waiting call or no held calls */ + struct spa_bt_telephony_call *call, *tcall; + bool found_waiting = false; + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_WAITING) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + found_waiting = true; + break; + } + } + if (!found_waiting) { + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) { + call->state = CALL_STATE_DISCONNECTED; + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + } + } else if (value == 1) { /* Swap calls */ + struct spa_bt_telephony_call *call; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + bool changed = false; + if (call->state == CALL_STATE_ACTIVE) { + call->state = CALL_STATE_HELD; + changed = true; + } else if (call->state == CALL_STATE_HELD) { + call->state = CALL_STATE_ACTIVE; + changed = true; + } + + if (changed) + telephony_call_notify_updated_props(call); + } + } else if (value == 2) { /* No active calls, place waiting on hold */ + struct spa_bt_telephony_call *call; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + bool changed = false; + if (call->state == CALL_STATE_ACTIVE || call->state == CALL_STATE_WAITING) { + call->state = CALL_STATE_HELD; + changed = true; + } + + if (changed) + telephony_call_notify_updated_props(call); + } + } + + if (rfcomm->hfp_hf_clcc) + rfcomm_send_cmd(rfcomm, "AT+CLCC"); + else + rfcomm->hfp_hf_in_progress = false; + } + } + } else if (sscanf(token, "+CLIP: \"%16[^\"]\",%u", number, &type) == 2) { + struct spa_bt_telephony_call *call; + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_INCOMING && !spa_streq(number, call->line_identification)) { + if (call->line_identification) + free(call->line_identification); + call->line_identification = strdup(number); + telephony_call_notify_updated_props(call); + break; + } + } + } else if (sscanf(token, "+CCWA: \"%16[^\"]\",%u", number, &type) == 2) { + struct spa_bt_telephony_call *call; + bool found = false; + + spa_log_info(backend->log, "Waiting call"); + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_WAITING) { + spa_log_info(backend->log, "waiting call already in progress (id: %d)", call->id); + found = true; + break; + } + } + if (!found) { + call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_WAITING, number); + if (call == NULL) + spa_log_warn(backend->log, "failed to create waiting call"); + } + } else if (spa_strstartswith(token, "+CLCC:")) { + struct spa_bt_telephony_call *call; + size_t pos; + char *token_end; + int idx; + unsigned int status, mpty; + bool parsed = false, found = false; + + number[0] = '\0'; + + token[strcspn(token, "\r")] = 0; + token[strcspn(token, "\n")] = 0; + token_end = token + strlen(token); + token += strlen("+CLCC:"); + + if (token < token_end) { + pos = strcspn(token, ","); + token[pos] = '\0'; + idx = atoi(token); + token += pos + 1; + } + if (token < token_end) { + // Skip direction + pos = strcspn(token, ","); + token += pos + 1; + } + if (token < token_end) { + pos = strcspn(token, ","); + token[pos] = '\0'; + status = atoi(token); + token += pos + 1; + } + if (token < token_end) { + // Skip mode + pos = strcspn(token, ","); + token += pos + 1; + } + if (token < token_end) { + pos = strcspn(token, ","); + token[pos] = '\0'; + mpty = atoi(token); + token += pos + 1; + parsed = true; + } + if (token < token_end) { + if (sscanf(token, "\"%16[^\"]\",%u", number, &type) != 2) { + spa_log_warn(backend->log, "Failed to parse number: %s", token); + number[0] = '\0'; + } + } + + if (SPA_LIKELY (parsed)) { + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->id == idx) { + bool changed = false; + + found = true; + + if (call->state != status) { + call->state =status; + changed = true; + } + if (call->multiparty != mpty) { + call->multiparty = mpty; + changed = true; + } + if (strlen(number) && !spa_streq(number, call->line_identification)) { + if (call->line_identification) + free(call->line_identification); + call->line_identification = strdup(number); + changed = true; + } + + if (changed) + telephony_call_notify_updated_props(call); + } + } + + if (!found) { + spa_log_info(backend->log, "New call, initial state: %u", status); + call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, status, strlen(number) ? number : NULL); + if (call == NULL) + spa_log_warn(backend->log, "failed to create call"); + else if (call->id != idx) + spa_log_warn(backend->log, "wrong call index: %d, expected: %d", call->id, idx); + } + } else { + spa_log_warn(backend->log, "malformed +CLCC command received from AG"); + } + + rfcomm->hfp_hf_in_progress = false; + } else if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") || + spa_strstartswith(token, "+CME ERROR:")) { + rfcomm->hfp_hf_cmd_in_progress = false; + if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) { + struct rfcomm_cmd *cmd; + cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link); + spa_list_remove(&cmd->link); + spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd); + rfcomm_send_cmd(rfcomm, "%s", cmd->cmd); + free(cmd->cmd); + free(cmd); + } + + if (spa_strstartswith(token, "OK")) { + switch(rfcomm->hf_state) { + case hfp_hf_brsf: + if (rfcomm->codec_negotiation_supported) { + char buf[64]; + struct spa_strbuf str; + + spa_strbuf_init(&str, buf, sizeof(buf)); + spa_strbuf_append(&str, "1"); + if (rfcomm->msbc_supported_by_hfp) + spa_strbuf_append(&str, ",2"); + if (rfcomm->lc3_supported_by_hfp) + spa_strbuf_append(&str, ",3"); + + rfcomm_send_cmd(rfcomm, "AT+BAC=%s", buf); + rfcomm->hf_state = hfp_hf_bac; + } else { + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + } + break; + case hfp_hf_bac: + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + break; + case hfp_hf_cind1: + rfcomm_send_cmd(rfcomm, "AT+CIND?"); + rfcomm->hf_state = hfp_hf_cind2; + break; + case hfp_hf_cind2: + rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1"); + rfcomm->hf_state = hfp_hf_cmer; + break; + case hfp_hf_cmer: + if (rfcomm->hfp_hf_3way) { + rfcomm_send_cmd(rfcomm, "AT+CHLD=?"); + rfcomm->hf_state = hfp_hf_chld; + break; + } + SPA_FALLTHROUGH; + case hfp_hf_chld: + rfcomm->slc_configured = true; + + if (!rfcomm->codec_negotiation_supported) { + if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) { + // TODO: We should manage the missing transport + } else { + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + + rfcomm->telephony_ag = telephony_ag_new(backend->telephony, 0); + rfcomm->telephony_ag->address = strdup(rfcomm->device->address); + telephony_ag_set_callbacks(rfcomm->telephony_ag, + &telephony_ag_callbacks, rfcomm); + if (rfcomm->transport) { + rfcomm->telephony_ag->transport.codec = rfcomm->transport->codec; + rfcomm->telephony_ag->transport.state = rfcomm->transport->state; + } + telephony_ag_register(rfcomm->telephony_ag); + + rfcomm_send_cmd(rfcomm, "AT+CLIP=1"); + rfcomm->hf_state = hfp_hf_clip; + break; + case hfp_hf_clip: + if (rfcomm->chld_supported) { + rfcomm_send_cmd(rfcomm, "AT+CCWA=1"); + rfcomm->hf_state = hfp_hf_ccwa; + break; + } + SPA_FALLTHROUGH; + case hfp_hf_ccwa: + if (rfcomm->hfp_hf_cme) { + rfcomm_send_cmd(rfcomm, "AT+CMEE=1"); + rfcomm->hf_state = hfp_hf_cmee; + break; + } + SPA_FALLTHROUGH; + case hfp_hf_cmee: + if (backend->hfp_disable_nrec && rfcomm->hfp_hf_nrec) { + rfcomm_send_cmd(rfcomm, "AT+NREC=0"); + rfcomm->hf_state = hfp_hf_nrec; + break; + } + SPA_FALLTHROUGH; + case hfp_hf_nrec: + rfcomm->hf_state = hfp_hf_slc1; + + if (rfcomm->hfp_hf_clcc) { + rfcomm_send_cmd(rfcomm, "AT+CLCC"); + rfcomm->hf_state = hfp_hf_slc2; + break; + } else { + // TODO: Create calls if CIND reports one during SLC setup + } + + /* Report volume on SLC establishment */ + SPA_FALLTHROUGH; + case hfp_hf_slc2: + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + break; + case hfp_hf_vgs: + rfcomm->hf_state = hfp_hf_slc1; + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) + rfcomm->hf_state = hfp_hf_vgm; + break; + default: + break; + } + } + } + + return true; +} + +#endif + +static void rfcomm_process_events(struct rfcomm *rfcomm, char *buf, bool ag, bool (*handler)(struct rfcomm *, char *)) +{ + struct impl *backend = rfcomm->backend; + char *token; + + /* Relaxed parsing of both \r (AG) and \r\n\r\n (HF) */ + + while ((token = strsep(&buf, "\r"))) { + size_t len; + + /* Skip leading and trailing \n */ + while (*token == '\n') + ++token; + for (len = strlen(token); len > 0 && token[len - 1] == '\n'; --len) + token[len - 1] = '\0'; + + /* Skip empty (only last one if AG) */ + if (*token == '\0' && (buf == NULL || !ag)) + continue; + + spa_log_debug(backend->log, "RFCOMM event: %s", token); + + if (!handler(rfcomm, token)) { + spa_log_debug(backend->log, "RFCOMM received unsupported event: %s", token); + if (ag) + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED); + } + } +} + +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; + + len = read(source->fd, buf, sizeof(buf) - 1); + 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); + spa_debug_log_mem(backend->log, SPA_LOG_LEVEL_DEBUG, 2, buf, strlen(buf)); + + switch (rfcomm->profile) { +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + case SPA_BT_PROFILE_HSP_HS: + rfcomm_process_events(rfcomm, buf, true, rfcomm_hsp_ag); + break; + case SPA_BT_PROFILE_HSP_AG: + rfcomm_process_events(rfcomm, buf, false, rfcomm_hsp_hs); + break; +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + case SPA_BT_PROFILE_HFP_HF: + rfcomm_process_events(rfcomm, buf, true, rfcomm_hfp_ag); + break; + case SPA_BT_PROFILE_HFP_AG: + rfcomm_process_events(rfcomm, buf, false, rfcomm_hfp_hf); + break; +#endif + default: + break; + } + } +} + +static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapter, bool transparent) +{ + struct sockaddr_sco addr; + socklen_t len; + bdaddr_t src; + + spa_autoclose int sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK, BTPROTO_SCO); + if (sock < 0) { + spa_log_error(backend->log, "socket(SEQPACKET, SCO) %s", strerror(errno)); + return -1; + } + + 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)); + return -1; + } + + spa_log_debug(backend->log, "transparent=%d", (int)transparent); + if (transparent) { + /* set correct socket options for mSBC/LC3 */ + 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)); + return -1; + } + } + + return spa_steal_fd(sock); +} + +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; + int err; + + spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u", + t, t->codec); + + td->err = -EIO; + + if (d->adapter == NULL) + return -EIO; + + spa_zero(addr); + addr.sco_family = AF_BLUETOOTH; + str2ba(d->address, &addr.sco_bdaddr); + + for (int retry = 2;;) { + bool transparent = (t->codec == HFP_AUDIO_CODEC_MSBC || t->codec == HFP_AUDIO_CODEC_LC3_SWB); + spa_autoclose int sock = sco_create_socket(backend, d->adapter, transparent); + if (sock < 0) + return -1; + + spa_log_debug(backend->log, "transport %p: doing connect", t); + err = connect(sock, (struct sockaddr *) &addr, sizeof(addr)); + if (err < 0 && errno == ECONNABORTED && retry-- > 0) { + spa_log_warn(backend->log, "connect(): %s. Remaining retry:%d", + strerror(errno), retry); + continue; + } 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_CVSD && + td->rfcomm->codec_negotiation_supported) { + /* Adapter doesn't support msbc/lc3. Renegotiate. */ + d->adapter->msbc_probed = true; + d->adapter->has_msbc = false; + td->rfcomm->msbc_supported_by_hfp = false; + td->rfcomm->lc3_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 + return -1; + } + + td->err = -EINPROGRESS; + + return spa_steal_fd(sock); + } +} + +static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later); + +static void sco_ready(struct spa_bt_transport *t) +{ + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + struct transport_data *td = t->user_data; + struct sco_options sco_opt; + socklen_t len; + int err; + + spa_log_debug(backend->log, "transport %p: ready", t); + + /* Read socket error status */ + if (t->fd >= 0) { + if (td->err == -EINPROGRESS) { + len = sizeof(err); + memset(&err, 0, len); + if (getsockopt(t->fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) + td->err = -errno; + else + td->err = -err; + } + } else { + td->err = -EIO; + } + + if (!td->requesting) + return; + + td->requesting = false; + + if (td->err) + goto done; + + /* XXX: The MTU as currently reported by kernel (6.2) here is not a valid packet size, + * XXX: for USB adapters, see sco-io. + */ + 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: %d (%m)", errno); + t->read_mtu = 144; + t->write_mtu = 144; + } else { + spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu); + t->read_mtu = sco_opt.mtu; + t->write_mtu = sco_opt.mtu; + } + + /* Clear nonblocking flag we set for connect() */ + err = fcntl(t->fd, F_GETFL, O_NONBLOCK); + if (err < 0) { + td->err = -errno; + goto done; + } + err &= ~O_NONBLOCK; + err = fcntl(t->fd, F_SETFL, O_NONBLOCK, err); + if (err < 0) { + td->err = -errno; + goto done; + } + +done: + if (td->err) { + spa_log_debug(backend->log, "transport %p: acquire failed: %s (%d)", + t, strerror(-td->err), td->err); + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_ERROR); + return; + } + + spa_log_debug(backend->log, "transport %p: acquire complete, read_mtu=%u, write_mtu=%u", + t, t->read_mtu, t->write_mtu); + + /* + * 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. + */ + rfcomm_ag_sync_volume(td->rfcomm, false); + rfcomm_ag_sync_volume(td->rfcomm, true); + + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_ACTIVE); +} + +static void sco_start_source(struct spa_bt_transport *t); + +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; + + 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 + + t->fd = sock; + + td->requesting = true; + + sco_start_source(t); + + if (td->err != -EINPROGRESS) + sco_ready(t); + + return 0; + +fail: + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_ERROR); + return -1; +} + +static int sco_destroy_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); + + if (t->sco_io) { + spa_bt_sco_io_destroy(t->sco_io); + t->sco_io = NULL; + } + + if (td->sco.loop) + spa_loop_remove_source(backend->main_loop, &td->sco); + + if (t->fd > 0) { + /* Shutdown and close the socket */ + shutdown(t->fd, SHUT_RDWR); + close(t->fd); + t->fd = -1; + } + + return 0; +} + +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); + + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE); + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + rfcomm_hfp_ag_set_cind(td->rfcomm, false); +#endif + + sco_destroy_cb(t); + + 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)); + sco_ready(t); + if (source->loop) + spa_loop_remove_source(source->loop, source); + if (t->fd >= 0) { + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE); + shutdown(t->fd, SHUT_RDWR); + close(t->fd); + t->fd = -1; + } + } + + if (source->rmask & (SPA_IO_OUT | SPA_IO_IN)) { + SPA_FLAG_CLEAR(source->mask, SPA_IO_OUT | SPA_IO_IN); + spa_loop_update_source(backend->main_loop, source); + sco_ready(t); + } +} + +static void sco_start_source(struct spa_bt_transport *t) +{ + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + struct transport_data *td = t->user_data; + + if (td->sco.loop) + return; + + td->err = -EINPROGRESS; + + td->sco.func = sco_event; + td->sco.data = t; + td->sco.fd = t->fd; + td->sco.mask = SPA_IO_HUP | SPA_IO_ERR; + td->sco.rmask = 0; + + switch (t->device->adapter->bus_type) { + case BUS_TYPE_USB: + /* With USB controllers, we have to determine packet size from incoming + * packets before we can send. Wait for POLLIN when connecting (not + * POLLOUT as usual). + */ + td->sco.mask |= SPA_IO_IN; + break; + default: + td->sco.mask |= SPA_IO_OUT; + break; + } + + spa_loop_add_source(backend->main_loop, &td->sco); +} + +static void sco_listen_event(struct spa_source *source) +{ + struct impl *backend = source->data; + struct sockaddr_sco addr; + socklen_t addrlen; + char local_address[18], remote_address[18]; + struct rfcomm *rfcomm; + struct spa_bt_transport *t = NULL; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_error(backend->log, "error listening SCO connection: %s", strerror(errno)); + return; + } + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + spa_log_debug(backend->log, "doing accept"); + spa_autoclose int sock = accept(source->fd, (struct sockaddr *) &addr, &addrlen); + if (sock < 0) { + if (errno != EAGAIN) + spa_log_error(backend->log, "SCO accept(): %s", strerror(errno)); + return; + } + + 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)); + return; + } + + ba2str(&addr.sco_bdaddr, local_address); + + /* Find transport for local and remote address */ + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if ((rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) && + rfcomm->transport && + spa_streq(rfcomm->device->address, remote_address) && + spa_streq(rfcomm->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); + return; + } + + spa_assert(t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + + if (rfcomm->telephony_ag && rfcomm->telephony_ag->transport.rejectSCO) { + spa_log_info(backend->log, "rejecting SCO, AudioGatewayTransport1.RejectSCO=true"); + return; + } + + if (t->fd >= 0) { + spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t); + return; + } + + 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 || t->codec == HFP_AUDIO_CODEC_LC3_SWB) { + /* set correct socket options for mSBC/LC3 */ + 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)); + return; + } + } + + /* 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)); + return; + } + } + + t->fd = spa_steal_fd(sock); + + sco_start_source(t); + + 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); +} + +static void sco_listen(struct impl *backend) +{ + struct sockaddr_sco addr; + uint32_t defer = 1; + + spa_autoclose int 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; + } + + /* 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"); + return; + } + + 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"); + return; + } + + backend->sco.func = sco_listen_event; + backend->sco.data = backend; + backend->sco.fd = spa_steal_fd(sock); + backend->sco.mask = SPA_IO_IN; + backend->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &backend->sco); + + return; +} + +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, + .destroy = sco_destroy_cb, +}; + +static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device, + enum spa_bt_profile profile) +{ + struct rfcomm *rfcomm; + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->device == device && (rfcomm->profile & profile)) + 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, SPA_BT_PROFILE_HFP_HF); + if (rfcomm == NULL) + return -ENOTSUP; + + if (codec == HFP_AUDIO_CODEC_CVSD) + return 1; + + if (!rfcomm->codec_negotiation_supported) + return 0; + + if (codec == HFP_AUDIO_CODEC_MSBC) + return rfcomm->msbc_supported_by_hfp; + else if (codec == HFP_AUDIO_CODEC_LC3_SWB) + return rfcomm->lc3_supported_by_hfp; + + return 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) { + if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) == 0) { + 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 res; + else if (!res) + return -EINVAL; + + rfcomm = device_find_rfcomm(backend, device, SPA_BT_PROFILE_HFP_HF); + 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; + spa_autoptr(DBusMessage) r = NULL; + DBusMessageIter it; + const char *handler, *path; + enum spa_bt_profile profile; + struct rfcomm *rfcomm; + struct spa_bt_device *d; + spa_autoclose int fd = -1; + + 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); + dbus_message_iter_get_basic(&it, &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); + dbus_message_iter_get_basic(&it, &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 = spa_steal_fd(fd); + rfcomm->source.mask = SPA_IO_IN; + rfcomm->source.rmask = 0; + spa_list_init(&rfcomm->hfp_hf_commands); + /* By default all indicators are enabled */ + rfcomm->cind_enabled_indicators = 0xFFFFFFFF; + memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators); + + 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) { + if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) + goto fail_need_memory; + + 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(rfcomm->device, profile); + + spa_log_debug(backend->log, "Transport %s available for profile %s", + rfcomm->transport->path, handler); + } else if (profile == SPA_BT_PROFILE_HFP_AG) { + /* Start SLC connection */ + unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_CLIP | SPA_BT_HFP_HF_FEATURE_3WAY | + SPA_BT_HFP_HF_FEATURE_ECNR | + SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS | + SPA_BT_HFP_HF_FEATURE_ESCO_S4; + bool has_msbc = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC); + bool has_lc3 = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); + + /* Decide if we want to signal that the HF supports mSBC/LC3 negotiation + This should be done when the bluetooth adapter supports the necessary transport mode */ + if (has_msbc || has_lc3) { + /* set the feature bit that indicates HF supports codec negotiation */ + hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION; + rfcomm->msbc_supported_by_hfp = has_msbc; + rfcomm->lc3_supported_by_hfp = has_lc3; + rfcomm->codec_negotiation_supported = false; + } else { + rfcomm->msbc_supported_by_hfp = false; + rfcomm->lc3_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; + + 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; + spa_autoptr(DBusMessage) r = NULL; + 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; + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + 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; + + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + spa_log_warn(backend->log, "Register profile not supported"); + return; + } + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(backend->log, "Error registering profile"); + return; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterProfile() failed: %s", + dbus_message_get_error_name(r)); + return; + } +} + +static int register_profile(struct impl *backend, const char *profile, const char *uuid) +{ + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter it[4]; + dbus_bool_t autoconnect; + dbus_uint16_t version, chan, features; + const char *str; + + 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; +#ifdef HAVE_LC3 + features |= SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH; +#endif + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + 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.9 */ + str = "Version"; + version = 0x0109; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + 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; +#ifdef HAVE_LC3 + features |= SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH; +#endif + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + 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.9 */ + str = "Version"; + version = 0x0109; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + 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]); + + if (!send_with_reply(backend->conn, m, register_profile_reply, backend)) + return -EIO; + + return 0; +} + +static void unregister_profile(struct impl *backend, const char *profile) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + if (r == NULL) { + spa_log_info(backend->log, "Unregistering Profile %s failed", profile); + 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; + } +} + +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->profile == SPA_BT_PROFILE_HFP_HF && 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->profile == SPA_BT_PROFILE_HFP_HF && 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); + } +} + +static 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->modemmanager); + backend->modemmanager = NULL; + } + + if (backend->upower) { + upower_unregister(backend->upower); + backend->upower = NULL; + } + + spa_clear_ptr(backend->telephony, telephony_free); + + if (backend->ring_timer) + spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer); + +#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) + goto fallback; + + if ((str = spa_dict_lookup(info, PROP_KEY_ROLES)) == 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 void parse_hfp_disable_nrec(struct impl *backend, const struct spa_dict *info) +{ + const char *str; + + if ((str = spa_dict_lookup(info, PROP_KEY_HFP_DISABLE_NREC)) != NULL) + backend->hfp_disable_nrec = spa_atob(str); + else + backend->hfp_disable_nrec = false; +} + +static const struct spa_bt_backend_implementation backend_impl = { + SPA_VERSION_BT_BACKEND_IMPLEMENTATION, + .free = backend_native_free, + .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; + + parse_hfp_disable_nrec(backend, info); + +#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); + backend->telephony = telephony_new(backend->log, backend->dbus, info); + + 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..42dd0ec --- /dev/null +++ b/spa/plugins/bluez5/backend-ofono.c @@ -0,0 +1,901 @@ +/* Spa oFono backend */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" + +#define INITIAL_INTERVAL_NSEC (500 * SPA_NSEC_PER_MSEC) +#define ACTION_INTERVAL_NSEC (3000 * SPA_NSEC_PER_MSEC) + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +#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 = 144; + t->write_mtu = 144; + + 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: %d (%m)", errno); + 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) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + int ret = 0; + + m = dbus_message_new_method_call(OFONO_SERVICE, path, + OFONO_HF_AUDIO_CARD_INTERFACE, + "Acquire"); + if (m == NULL) + return -ENOMEM; + + + /* + * 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); + if (r == NULL) { + spa_log_error(backend->log, "Transport Acquire() failed for transport %s (%s)", + path, err.message); + 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)); + return -EIO; + } + + 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); + return -EIO; + } + + 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); + + ret = -EIO; + goto finish; + } + + 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: + if (ret < 0) + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ERROR); + else + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE); + + 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); + + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE); + + 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, +}; + +static 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; + + spa_log_warn(backend->log, "release"); + + if (!reply_with_error(conn, m, OFONO_HF_AUDIO_AGENT_INTERFACE ".Error.NotImplemented", "Method not implemented")) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + 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; + spa_autoptr(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) { + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult ofono_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + 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; + DBusMessageIter i, array_i, struct_i, props_i; + + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&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)); + return; + } + + 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"); + return; + } + + 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); + } +} + +static int ofono_register(struct impl *backend) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + const char *path = OFONO_AUDIO_CLIENT; + uint8_t codecs[2]; + const uint8_t *pcodecs = codecs; + int ncodecs = 0; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + 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); + return -ENOTSUP; + } else { + spa_log_warn(backend->log, "Registering Profile %s failed: %s (%s)", + path, err.message, err.name); + return -EIO; + } + } + + if (dbus_message_is_error(r, OFONO_ERROR_INVALID_ARGUMENTS)) { + spa_log_warn(backend->log, "invalid arguments"); + return -EIO; + } + if (dbus_message_is_error(r, OFONO_ERROR_IN_USE)) { + spa_log_warn(backend->log, "already in use"); + return -EIO; + } + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(backend->log, "Error registering profile"); + return -EIO; + } + if (dbus_message_is_error(r, DBUS_ERROR_SERVICE_UNKNOWN)) { + spa_log_info(backend->log, "oFono not available, disabling"); + return -EIO; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Register() failed: %s", + dbus_message_get_error_name(r)); + return -EIO; + } + + spa_log_debug(backend->log, "registered"); + + return 0; +} + +static int ofono_getcards(struct impl *backend) +{ + spa_autoptr(DBusMessage) m = NULL; + + m = dbus_message_new_method_call(OFONO_SERVICE, "/", + OFONO_HF_AUDIO_MANAGER_INTERFACE, "GetCards"); + if (m == NULL) + return -ENOMEM; + + if (!send_with_reply(backend->conn, m, ofono_getcards_reply, backend)) + return -EIO; + + return 0; +} + +static int backend_ofono_register(void *data) +{ + int ret = ofono_register(data); + if (ret < 0) + return ret; + + ret = ofono_getcards(data); + if (ret < 0) + return ret; + + return 0; +} + +static DBusHandlerResult ofono_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct impl *backend = user_data; + + 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; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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) +{ + if (backend->filters_added) + return 0; + + if (!dbus_connection_add_filter(backend->conn, ofono_filter_cb, backend, NULL)) { + spa_log_error(backend->log, "failed to add filter function"); + return -EIO; + } + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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; +} + +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) +{ + spa_autoptr(DBusMessage) m = NULL, r = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + m = dbus_message_new_method_call(OFONO_SERVICE, "/", + DBUS_INTERFACE_INTROSPECTABLE, "Introspect"); + if (m == NULL) + return false; + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN) + return true; + + return false; +} + +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..9d1da42 --- /dev/null +++ b/spa/plugins/bluez5/bap-codec-caps.h @@ -0,0 +1,178 @@ +/* Spa BAP codec API */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#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_VAL_FREQ_8KHZ 8000 +#define LC3_VAL_FREQ_11KHZ 11025 +#define LC3_VAL_FREQ_16KHZ 16000 +#define LC3_VAL_FREQ_22KHZ 22050 +#define LC3_VAL_FREQ_24KHZ 24000 +#define LC3_VAL_FREQ_32KHZ 32000 +#define LC3_VAL_FREQ_44KHZ 44100 +#define LC3_VAL_FREQ_48KHZ 48000 + +#define LC3_TYPE_DUR 0x02 +#define LC3_DUR_7_5 (1 << 0) +#define LC3_DUR_10 (1 << 1) +#define LC3_DUR_ANY (LC3_DUR_7_5 | \ + LC3_DUR_10) + +#define LC3_VAL_DUR_7_5 7.5 +#define LC3_VAL_DUR_10 10 + +#define LC3_TYPE_CHAN 0x03 +#define LC3_CHAN_1 (1 << 0) +#define LC3_CHAN_2 (1 << 1) +#define LC3_CHAN_3 (1 << 2) +#define LC3_CHAN_4 (1 << 3) +#define LC3_CHAN_5 (1 << 4) +#define LC3_CHAN_6 (1 << 5) +#define LC3_CHAN_7 (1 << 6) +#define LC3_CHAN_8 (1 << 7) + +#define LC3_VAL_CHAN_1 1 +#define LC3_VAL_CHAN_2 2 +#define LC3_VAL_CHAN_3 3 +#define LC3_VAL_CHAN_4 4 +#define LC3_VAL_CHAN_5 5 +#define LC3_VAL_CHAN_6 6 +#define LC3_VAL_CHAN_7 7 +#define LC3_VAL_CHAN_8 8 + +#define LC3_TYPE_FRAMELEN 0x04 +#define LC3_TYPE_BLKS 0x05 + +/* 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_MAX_CHANNELS 28 + +#define BAP_CHANNEL_MONO 0x00000000 /* mono */ +#define BAP_CHANNEL_FL 0x00000001 /* front left */ +#define BAP_CHANNEL_FR 0x00000002 /* front right */ +#define BAP_CHANNEL_FC 0x00000004 /* front center */ +#define BAP_CHANNEL_LFE 0x00000008 /* LFE */ +#define BAP_CHANNEL_BL 0x00000010 /* back left */ +#define BAP_CHANNEL_BR 0x00000020 /* back right */ +#define BAP_CHANNEL_FLC 0x00000040 /* front left center */ +#define BAP_CHANNEL_FRC 0x00000080 /* front right center */ +#define BAP_CHANNEL_BC 0x00000100 /* back center */ +#define BAP_CHANNEL_LFE2 0x00000200 /* LFE 2 */ +#define BAP_CHANNEL_SL 0x00000400 /* side left */ +#define BAP_CHANNEL_SR 0x00000800 /* side right */ +#define BAP_CHANNEL_TFL 0x00001000 /* top front left */ +#define BAP_CHANNEL_TFR 0x00002000 /* top front right */ +#define BAP_CHANNEL_TFC 0x00004000 /* top front center */ +#define BAP_CHANNEL_TC 0x00008000 /* top center */ +#define BAP_CHANNEL_TBL 0x00010000 /* top back left */ +#define BAP_CHANNEL_TBR 0x00020000 /* top back right */ +#define BAP_CHANNEL_TSL 0x00040000 /* top side left */ +#define BAP_CHANNEL_TSR 0x00080000 /* top side right */ +#define BAP_CHANNEL_TBC 0x00100000 /* top back center */ +#define BAP_CHANNEL_BFC 0x00200000 /* bottom front center */ +#define BAP_CHANNEL_BFL 0x00400000 /* bottom front left */ +#define BAP_CHANNEL_BFR 0x00800000 /* bottom front right */ +#define BAP_CHANNEL_FLW 0x01000000 /* front left wide */ +#define BAP_CHANNEL_FRW 0x02000000 /* front right wide */ +#define BAP_CHANNEL_LS 0x04000000 /* left surround */ +#define BAP_CHANNEL_RS 0x08000000 /* right surround */ + +#define BAP_CHANNEL_ALL 0x0fffffff /* mask of all */ + +#define BAP_CONTEXT_PROHIBITED 0x0000 /* Prohibited */ +#define BAP_CONTEXT_UNSPECIFIED 0x0001 /* Unspecified */ +#define BAP_CONTEXT_CONVERSATIONAL 0x0002 /* Telephony, video calls, ... */ +#define BAP_CONTEXT_MEDIA 0x0004 /* Music, radio, podcast, movie soundtrack, TV */ +#define BAP_CONTEXT_GAME 0x0008 /* Gaming media, game effects, music, in-game voice chat */ +#define BAP_CONTEXT_INSTRUCTIONAL 0x0010 /* Instructional audio, navigation, announcements, user guidance */ +#define BAP_CONTEXT_VOICE 0x0020 /* Man-machine communication, voice recognition, virtual assistants */ +#define BAP_CONTEXT_LIVE 0x0040 /* Live audio, perceived both via direct acoustic path and via BAP */ +#define BAP_CONTEXT_SOUND_EFFECTS 0x0080 /* Keyboard and touch feedback, menu, UI, other system sounds */ +#define BAP_CONTEXT_NOTIFICATIONS 0x0100 /* Attention-seeking, message arrival, reminders */ +#define BAP_CONTEXT_RINGTONE 0x0200 /* Incoming call alert audio */ +#define BAP_CONTEXT_ALERTS 0x0400 /* Alarms and timers, critical battery, alarm clock, toaster */ +#define BAP_CONTEXT_EMERGENCY 0x0800 /* Fire alarm, other urgent alerts */ + +#define BAP_CONTEXT_ALL 0x0fff + +#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 __attribute__((packed)) ltv { + uint8_t len; + uint8_t type; + uint8_t value[]; +}; + +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; + uint32_t locations; + uint16_t supported_context; + uint16_t context; + uint32_t channel_allocation; +}; + +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; +}; + +struct bap_codec_qos_full { + uint8_t cig; + uint8_t cis; + uint8_t big; + uint8_t bis; + struct bap_codec_qos qos; +}; + +#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..e9b7723 --- /dev/null +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -0,0 +1,1394 @@ +/* Spa BAP LC3 codec */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "media-codecs.h" +#include "bap-codec-caps.h" + +#define MAX_PACS 64 + +static struct spa_log *log_; + +struct impl { + lc3_encoder_t enc[LC3_MAX_CHANNELS]; + lc3_decoder_t dec[LC3_MAX_CHANNELS]; + + int samplerate; + int channels; + int frame_dus; + int framelen; + int samples; + unsigned int codesize; +}; + +struct pac_data { + const uint8_t *data; + size_t size; + int index; + uint32_t locations; + uint32_t channel_allocation; + bool sink; + bool duplex; +}; + +struct bap_qos { + char *name; + uint8_t rate; + uint8_t frame_duration; + bool framing; + uint16_t framelen; + uint8_t retransmission; + uint16_t latency; + uint32_t delay; + unsigned int priority; +}; + +typedef struct { + uint8_t rate; + uint8_t frame_duration; + uint32_t channels; + uint16_t framelen; + uint8_t n_blks; + bool sink; + bool duplex; + unsigned int priority; +} bap_lc3_t; + +static const struct { + uint32_t bit; + enum spa_audio_channel channel; +} channel_bits[] = { + { BAP_CHANNEL_MONO, SPA_AUDIO_CHANNEL_MONO }, + { BAP_CHANNEL_FL, SPA_AUDIO_CHANNEL_FL }, + { BAP_CHANNEL_FR, SPA_AUDIO_CHANNEL_FR }, + { BAP_CHANNEL_FC, SPA_AUDIO_CHANNEL_FC }, + { BAP_CHANNEL_LFE, SPA_AUDIO_CHANNEL_LFE }, + { BAP_CHANNEL_BL, SPA_AUDIO_CHANNEL_RL }, + { BAP_CHANNEL_BR, SPA_AUDIO_CHANNEL_RR }, + { BAP_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FLC }, + { BAP_CHANNEL_FRC, SPA_AUDIO_CHANNEL_FRC }, + { BAP_CHANNEL_BC, SPA_AUDIO_CHANNEL_BC }, + { BAP_CHANNEL_LFE2, SPA_AUDIO_CHANNEL_LFE2 }, + { BAP_CHANNEL_SL, SPA_AUDIO_CHANNEL_SL }, + { BAP_CHANNEL_SR, SPA_AUDIO_CHANNEL_SR }, + { BAP_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFL }, + { BAP_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TFR }, + { BAP_CHANNEL_TFC, SPA_AUDIO_CHANNEL_TFC }, + { BAP_CHANNEL_TC, SPA_AUDIO_CHANNEL_TC }, + { BAP_CHANNEL_TBL, SPA_AUDIO_CHANNEL_TRL }, + { BAP_CHANNEL_TBR, SPA_AUDIO_CHANNEL_TRR }, + { BAP_CHANNEL_TSL, SPA_AUDIO_CHANNEL_TSL }, + { BAP_CHANNEL_TSR, SPA_AUDIO_CHANNEL_TSR }, + { BAP_CHANNEL_TBC, SPA_AUDIO_CHANNEL_TRC }, + { BAP_CHANNEL_BFC, SPA_AUDIO_CHANNEL_BC }, + { BAP_CHANNEL_BFL, SPA_AUDIO_CHANNEL_BLC }, + { BAP_CHANNEL_BFR, SPA_AUDIO_CHANNEL_BRC }, + { BAP_CHANNEL_FLW, SPA_AUDIO_CHANNEL_FLW }, + { BAP_CHANNEL_FRW, SPA_AUDIO_CHANNEL_FRW }, + { BAP_CHANNEL_LS, SPA_AUDIO_CHANNEL_SL }, /* is it the right mapping? */ + { BAP_CHANNEL_RS, SPA_AUDIO_CHANNEL_SR }, /* is it the right mapping? */ +}; + +#define BAP_QOS(name_, rate_, duration_, framing_, framelen_, rtn_, latency_, delay_, priority_) \ + ((struct bap_qos){ .name = (name_), .rate = (rate_), .frame_duration = (duration_), .framing = (framing_), \ + .framelen = (framelen_), .retransmission = (rtn_), .latency = (latency_), \ + .delay = (delay_), .priority = (priority_) }) + +static const struct bap_qos bap_qos_configs[] = { + /* Priority: low-latency > high-reliability, 7.5ms > 10ms, + * bigger frequency and sdu better */ + + /* BAP v1.0.1 Table 5.2; low-latency */ + BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30), /* 8_1_1 */ + BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20), /* 8_2_1 */ + BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31), /* 16_1_1 */ + BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21), /* 16_2_1 (mandatory) */ + BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32), /* 24_1_1 */ + BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22), /* 24_2_1 */ + BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33), /* 32_1_1 */ + BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23), /* 32_2_1 */ + BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 5, 24, 40000, 34), /* 441_1_1 */ + BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 5, 31, 40000, 24), /* 441_2_1 */ + BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 5, 15, 40000, 35), /* 48_1_1 */ + BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 5, 20, 40000, 25), /* 48_2_1 */ + BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 5, 15, 40000, 36), /* 48_3_1 */ + BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 5, 20, 40000, 26), /* 48_4_1 */ + BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 5, 15, 40000, 37), /* 48_5_1 */ + BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 5, 20, 40000, 27), /* 48_6_1 */ + + /* BAP v1.0.1 Table 5.2; high-reliability */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 13, 75, 40000, 10), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 13, 95, 40000, 0), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 13, 75, 40000, 11), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 13, 95, 40000, 1), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 13, 75, 40000, 12), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 14), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 4), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 15), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 5), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 16), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 6), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 17), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 7), /* 48_6_2 */ +}; + +static const struct bap_qos bap_bcast_qos_configs[] = { + /* Priority: low-latency > high-reliability, 7.5ms > 10ms, + * bigger frequency and sdu better */ + + /* BAP v1.0.1 Table 6.4; low-latency */ + BAP_QOS("8_1_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 2, 8, 40000, 30), /* 8_1_1 */ + BAP_QOS("8_2_1", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 2, 10, 40000, 20), /* 8_2_1 */ + BAP_QOS("16_1_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 2, 8, 40000, 31), /* 16_1_1 */ + BAP_QOS("16_2_1", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 2, 10, 40000, 21), /* 16_2_1 (mandatory) */ + BAP_QOS("24_1_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 2, 8, 40000, 32), /* 24_1_1 */ + BAP_QOS("24_2_1", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 2, 10, 40000, 22), /* 24_2_1 */ + BAP_QOS("32_1_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 2, 8, 40000, 33), /* 32_1_1 */ + BAP_QOS("32_2_1", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 2, 10, 40000, 23), /* 32_2_1 */ + BAP_QOS("441_1_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 24, 40000, 34), /* 441_1_1 */ + BAP_QOS("441_2_1", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 31, 40000, 24), /* 441_2_1 */ + BAP_QOS("48_1_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 15, 40000, 35), /* 48_1_1 */ + BAP_QOS("48_2_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 20, 40000, 25), /* 48_2_1 */ + BAP_QOS("48_3_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 15, 40000, 36), /* 48_3_1 */ + BAP_QOS("48_4_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 20, 40000, 26), /* 48_4_1 */ + BAP_QOS("48_5_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 15, 40000, 37), /* 48_5_1 */ + BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 20, 40000, 27), /* 48_6_1 */ + + /* BAP v1.0.1 Table 6.4; high-reliability */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 4, 45, 40000, 10), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 4, 60, 40000, 0), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 4, 45, 40000, 11), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 4, 60, 40000, 1), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 4, 45, 40000, 12), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 4, 60, 40000, 2), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 4, 45, 40000, 13), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 4, 60, 40000, 3), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 54, 40000, 14), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 60, 40000, 4), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 50, 40000, 15), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 65, 40000, 5), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 50, 40000, 16), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 65, 40000, 6), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 50, 40000, 17), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 65, 40000, 7), /* 48_6_2 */ +}; + +static unsigned int get_rate_mask(uint8_t rate) { + switch (rate) { + case LC3_CONFIG_FREQ_8KHZ: return LC3_FREQ_8KHZ; + case LC3_CONFIG_FREQ_16KHZ: return LC3_FREQ_16KHZ; + case LC3_CONFIG_FREQ_24KHZ: return LC3_FREQ_24KHZ; + case LC3_CONFIG_FREQ_32KHZ: return LC3_FREQ_32KHZ; + case LC3_CONFIG_FREQ_44KHZ: return LC3_FREQ_44KHZ; + case LC3_CONFIG_FREQ_48KHZ: return LC3_FREQ_48KHZ; + } + return 0; +} + +static unsigned int get_duration_mask(uint8_t rate) { + switch (rate) { + case LC3_CONFIG_DURATION_7_5: return LC3_DUR_7_5; + case LC3_CONFIG_DURATION_10: return LC3_DUR_10; + } + return 0; +} + +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 uint16_t parse_rates(const char *str) +{ + struct spa_json it; + uint16_t rate_mask = 0; + int value; + + if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0) + return rate_mask; + + while (spa_json_get_int(&it, &value) > 0) { + switch (value) { + case LC3_VAL_FREQ_8KHZ: + rate_mask |= LC3_FREQ_8KHZ; + break; + case LC3_VAL_FREQ_16KHZ: + rate_mask |= LC3_FREQ_16KHZ; + break; + case LC3_VAL_FREQ_24KHZ: + rate_mask |= LC3_FREQ_24KHZ; + break; + case LC3_VAL_FREQ_32KHZ: + rate_mask |= LC3_FREQ_32KHZ; + break; + case LC3_VAL_FREQ_44KHZ: + rate_mask |= LC3_FREQ_44KHZ; + break; + case LC3_VAL_FREQ_48KHZ: + rate_mask |= LC3_FREQ_48KHZ; + break; + default: + break; + } + } + + return rate_mask; +} + +static uint8_t parse_durations(const char *str) +{ + struct spa_json it; + uint8_t duration_mask = 0; + float value; + + if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0) + return duration_mask; + + while (spa_json_get_float(&it, &value) > 0) { + if (value == (float)LC3_VAL_DUR_7_5) + duration_mask |= LC3_DUR_7_5; + else if (value == (float)LC3_VAL_DUR_10) + duration_mask |= LC3_DUR_10; + } + + return duration_mask; +} + +static uint8_t parse_channel_counts(const char *str) +{ + struct spa_json it; + uint8_t channel_counts = 0; + int value; + + if (spa_json_begin_array_relax(&it, str, strlen(str)) <= 0) + return channel_counts; + + while (spa_json_get_int(&it, &value) > 0) { + switch (value) { + case LC3_VAL_CHAN_1: + channel_counts |= LC3_CHAN_1; + break; + case LC3_VAL_CHAN_2: + channel_counts |= LC3_CHAN_2; + break; + case LC3_VAL_CHAN_3: + channel_counts |= LC3_CHAN_3; + break; + case LC3_VAL_CHAN_4: + channel_counts |= LC3_CHAN_4; + break; + case LC3_VAL_CHAN_5: + channel_counts |= LC3_CHAN_5; + break; + case LC3_VAL_CHAN_6: + channel_counts |= LC3_CHAN_6; + break; + case LC3_VAL_CHAN_7: + channel_counts |= LC3_CHAN_7; + break; + case LC3_VAL_CHAN_8: + channel_counts |= LC3_CHAN_8; + break; + default: + break; + } + } + + return channel_counts; +} + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + uint8_t *data = caps; + const char *str; + uint16_t framelen[2]; + uint16_t rate_mask = LC3_FREQ_48KHZ | LC3_FREQ_32KHZ | \ + LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ; + uint8_t duration_mask = LC3_DUR_ANY; + uint8_t channel_counts = LC3_CHAN_1 | LC3_CHAN_2; + uint16_t framelen_min = LC3_MIN_FRAME_BYTES; + uint16_t framelen_max = LC3_MAX_FRAME_BYTES; + uint8_t max_frames = 2; + uint32_t value; + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.rates"))) + rate_mask = parse_rates(str); + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.durations"))) + duration_mask = parse_durations(str); + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.channels"))) + channel_counts = parse_channel_counts(str); + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.framelen_min"))) + if (spa_atou32(str, &value, 0)) + framelen_min = value; + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.framelen_max"))) + if (spa_atou32(str, &value, 0)) + framelen_max = value; + + if (settings && (str = spa_dict_lookup(settings, "bluez5.bap-server-capabilities.max_frames"))) + if (spa_atou32(str, &value, 0)) + max_frames = value; + + framelen[0] = htobs(framelen_min); + framelen[1] = htobs(framelen_max); + + data += write_ltv_uint16(data, LC3_TYPE_FREQ, htobs(rate_mask)); + data += write_ltv_uint8(data, LC3_TYPE_DUR, duration_mask); + data += write_ltv_uint8(data, LC3_TYPE_CHAN, channel_counts); + data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen)); + /* XXX: we support only one frame block -> max 2 frames per SDU */ + if (max_frames > 2) + max_frames = 2; + + data += write_ltv_uint8(data, LC3_TYPE_BLKS, max_frames); + + return data - caps; +} + +static void debugc_ltv(struct spa_debug_context *debug_ctx, int pac, struct ltv *ltv) +{ + switch (ltv->len) { + case 0: + spa_debugc(debug_ctx, "PAC %d: --", pac); + break; + case 2: + spa_debugc(debug_ctx, "PAC %d: 0x%02x %x", pac, ltv->type, ltv->value[0]); + break; + case 3: + spa_debugc(debug_ctx, "PAC %d: 0x%02x %x %x", pac, ltv->type, ltv->value[0], ltv->value[1]); + break; + case 5: + spa_debugc(debug_ctx, "PAC %d: 0x%02x %x %x %x %x", pac, ltv->type, + ltv->value[0], ltv->value[1], ltv->value[2], ltv->value[3]); + break; + default: + spa_debugc(debug_ctx, "PAC %d: 0x%02x", pac, ltv->type); + spa_debugc_mem(debug_ctx, 7, ltv->value, ltv->len - 1); + break; + } +} + +static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS], + struct spa_debug_context *debug_ctx) +{ + /* + * BlueZ capabilities 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, pac }; + } else if (ltv->len >= data_size) { + return -EINVAL; + } else { + debugc_ltv(debug_ctx, pac, ltv); + pacs[pac].size += ltv->len + 1; + } + data_size -= ltv->len + 1; + data += ltv->len + 1; + } + + return pac + 1; +} + +static uint8_t get_channel_count(uint32_t channels) +{ + uint8_t num; + + channels &= BAP_CHANNEL_ALL; + + if (channels == 0) + return 1; /* MONO */ + + for (num = 0; channels; channels >>= 1) + if (channels & 0x1) + ++num; + + return num; +} + +static bool supports_channel_count(uint8_t mask, uint8_t count) +{ + if (count == 0 || count > 8) + return false; + return mask & (1u << (count - 1)); +} + +static const struct bap_qos *select_bap_qos(unsigned int rate_mask, unsigned int duration_mask, uint16_t framelen_min, uint16_t framelen_max) +{ + const struct bap_qos *best = NULL; + unsigned int best_priority = 0; + + SPA_FOR_EACH_ELEMENT_VAR(bap_qos_configs, c) { + if (c->priority < best_priority) + continue; + if (!(get_rate_mask(c->rate) & rate_mask)) + continue; + if (!(get_duration_mask(c->frame_duration) & duration_mask)) + continue; + if (c->framing) + continue; /* XXX: framing not supported */ + if (c->framelen < framelen_min || c->framelen > framelen_max) + continue; + + best = c; + best_priority = c->priority; + } + + return best; +} + +static int select_channels(uint8_t channel_counts, uint32_t locations, uint32_t channel_allocation, + uint32_t *allocation) +{ + unsigned int i, num; + + locations &= BAP_CHANNEL_ALL; + + if (!channel_counts) + return -1; + + if (!locations) { + *allocation = 0; /* mono (omit Audio_Channel_Allocation) */ + return 0; + } + + if (channel_allocation) { + channel_allocation &= locations; + + /* sanity check channel allocation */ + while (!supports_channel_count(channel_counts, get_channel_count(channel_allocation))) { + for (i = 32; i > 0; --i) { + uint32_t mask = (1u << (i-1)); + if (channel_allocation & mask) { + channel_allocation &= ~mask; + break; + } + } + if (i == 0) + break; + } + + *allocation = channel_allocation; + return 0; + } + + /* XXX: select some channels, but upper level should tell us what */ + if ((channel_counts & LC3_CHAN_2) && get_channel_count(locations) >= 2) + num = 2; + else if ((channel_counts & LC3_CHAN_1) && get_channel_count(locations) >= 1) + num = 1; + else + return -1; + + *allocation = 0; + for (i = 0; i < SPA_N_ELEMENTS(channel_bits); ++i) { + if (locations & channel_bits[i].bit) { + *allocation |= channel_bits[i].bit; + --num; + if (num == 0) + break; + } + } + + return 0; +} + +static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct spa_debug_context *debug_ctx) +{ + const uint8_t *data = pac->data; + size_t data_size = pac->size; + uint16_t framelen_min = 0, framelen_max = 0; + int max_frames = -1; + uint8_t channel_counts = LC3_CHAN_1; /* Default: 1 channel (BAP v1.0.1 Sec 4.3.1) */ + uint8_t max_channels = 0; + uint8_t duration_mask = 0; + uint16_t rate_mask = 0; + const struct bap_qos *bap_qos = NULL; + unsigned int i; + + if (!data_size) + return false; + memset(conf, 0, sizeof(*conf)); + + conf->sink = pac->sink; + conf->duplex = pac->duplex; + + /* XXX: we always use one frame block */ + conf->n_blks = 1; + + while (data_size > 0) { + struct ltv *ltv = (struct ltv *)data; + + if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size) { + spa_debugc(debug_ctx, "invalid LTV data"); + return false; + } + + switch (ltv->type) { + case LC3_TYPE_FREQ: + spa_return_val_if_fail(ltv->len == 3, false); + rate_mask = ltv->value[0] + (ltv->value[1] << 8); + break; + case LC3_TYPE_DUR: + spa_return_val_if_fail(ltv->len == 2, false); + duration_mask = ltv->value[0]; + break; + case LC3_TYPE_CHAN: + spa_return_val_if_fail(ltv->len == 2, false); + channel_counts = ltv->value[0]; + 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); + max_frames = ltv->value[0]; + break; + default: + spa_debugc(debug_ctx, "unknown LTV type: 0x%02x", ltv->type); + break; + } + data_size -= ltv->len + 1; + data += ltv->len + 1; + } + + for (i = 0; i < 8; ++i) + if (channel_counts & (1u << i)) + max_channels = i + 1; + + /* Default: 1 frame per channel (BAP v1.0.1 Sec 4.3.1) */ + if (max_frames < 0) + max_frames = max_channels; + + /* + * Workaround: + * Creative Zen Hybrid Pro sets Supported_Max_Codec_Frames_Per_SDU == 1 + * but channels == 0x3, and the 2-channel audio stream works. + */ + if (max_frames < max_channels) { + spa_debugc(debug_ctx, "workaround: fixing bad Supported_Max_Codec_Frames_Per_SDU: %u->%u", + max_frames, max_channels); + max_frames = max_channels; + } + + if (select_channels(channel_counts, pac->locations, pac->channel_allocation, &conf->channels) < 0) { + spa_debugc(debug_ctx, "invalid channel configuration: 0x%02x %u", + channel_counts, max_frames); + return false; + } + + if (max_frames < get_channel_count(conf->channels)) { + spa_debugc(debug_ctx, "invalid max frames per SDU: %u", max_frames); + return false; + } + + /* + * Select supported rate + frame length combination. + * + * Frame length is not limited by ISO MTU, as kernel will fragment + * and reassemble SDUs as needed. + */ + if (pac->sink && pac->duplex) { + /* 16KHz input is mandatory in BAP v1.0.1 Table 3.5, so prefer + * it for now for input rate in duplex configuration. + * + * Devices may list other values but not certain they will work properly. + */ + bap_qos = select_bap_qos(rate_mask & LC3_FREQ_16KHZ, duration_mask, framelen_min, framelen_max); + } + if (!bap_qos) + bap_qos = select_bap_qos(rate_mask, duration_mask, framelen_min, framelen_max); + + if (!bap_qos) { + spa_debugc(debug_ctx, "no compatible configuration found, rate:0x%08x, duration:0x%08x frame:%u-%u", + rate_mask, duration_mask, framelen_min, framelen_max); + return false; + } + + conf->rate = bap_qos->rate; + conf->frame_duration = bap_qos->frame_duration; + conf->framelen = bap_qos->framelen; + conf->priority = bap_qos->priority; + + 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; + + /* Absent Codec_Frame_Blocks_Per_SDU means 0x1 (BAP v1.0.1 Sec 4.3.2) */ + conf->n_blks = 1; + + 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]; + /* XXX: we only support 1 frame block for now */ + if (conf->n_blks != 1) + 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->channels & LC3_CHAN_1); + + if (conf->sink && conf->duplex) + PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_16KHZ); + + PREFER_EXPR(conf->priority); + + 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; + struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_TRACE); + bap_lc3_t conf1, conf2; + int res1, res2; + + res1 = select_config(&conf1, pac1, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL; + res2 = select_config(&conf2, pac2, &debug_ctx.ctx) ? (int)sizeof(bap_lc3_t) : -EINVAL; + + return conf_cmp(&conf1, res1, &conf2, res2); +} + +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; + uint32_t locations = 0; + uint32_t channel_allocation = 0; + bool sink = false, duplex = false; + struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_TRACE); + int i; + + if (caps == NULL) + return -EINVAL; + + if (settings) { + for (i = 0; i < (int)settings->n_items; ++i) { + if (spa_streq(settings->items[i].key, "bluez5.bap.locations")) + sscanf(settings->items[i].value, "%"PRIu32, &locations); + if (spa_streq(settings->items[i].key, "bluez5.bap.channel-allocation")) + sscanf(settings->items[i].value, "%"PRIu32, &channel_allocation); + } + + if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug"))) + debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_DEBUG); + + /* Is remote endpoint sink or source */ + sink = spa_atob(spa_dict_lookup(settings, "bluez5.bap.sink")); + + /* Is remote endpoint duplex */ + duplex = spa_atob(spa_dict_lookup(settings, "bluez5.bap.duplex")); + } + + /* Select best conf from those possible */ + npacs = parse_bluez_pacs(caps, caps_size, pacs, &debug_ctx.ctx); + if (npacs < 0) { + spa_debugc(&debug_ctx.ctx, "malformed PACS"); + return npacs; + } else if (npacs == 0) { + spa_debugc(&debug_ctx.ctx, "no PACS"); + return -EINVAL; + } + + for (i = 0; i < npacs; ++i) { + pacs[i].locations = locations; + pacs[i].channel_allocation = channel_allocation; + pacs[i].sink = sink; + pacs[i].duplex = duplex; + } + + qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp); + + spa_debugc(&debug_ctx.ctx, "selected PAC %d", pacs[0].index); + + if (!select_config(&conf, &pacs[0], &debug_ctx.ctx)) + return -ENOTSUP; + + data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate); + data += write_ltv_uint8(data, LC3_TYPE_DUR, conf.frame_duration); + + /* Indicate MONO with absent Audio_Channel_Allocation (BAP v1.0.1 Sec. 4.3.2) */ + if (conf.channels != 0) + 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, global_settings, (uint8_t *)&conf1); + res2 = codec->select_config(codec, 0, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2); + + return conf_cmp(&conf1, res1, &conf2, res2); +} + +static uint8_t channels_to_positions(uint32_t channels, uint32_t *position) +{ + uint8_t n_channels = get_channel_count(channels); + uint8_t n_positions = 0; + + spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS); + + if (channels == 0) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + n_positions = 1; + } else { + unsigned int i; + + for (i = 0; i < SPA_N_ELEMENTS(channel_bits); ++i) + if (channels & channel_bits[i].bit) + position[n_positions++] = channel_bits[i].channel; + } + + if (n_positions != n_channels) + return 0; /* error */ + + 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_32KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 32000); + spa_pod_builder_int(b, 32000); + } + 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 > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (i == 0) + return -EINVAL; + + res = channels_to_positions(conf.channels, position); + if (res == 0) + return -EINVAL; + 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_32KHZ: + info->info.raw.rate = 32000U; + 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, 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) +{ + const struct bap_qos *bap_qos; + bap_lc3_t conf; + + spa_zero(*qos); + + if (!parse_conf(&conf, config, config_size)) + return -EINVAL; + + bap_qos = select_bap_qos(get_rate_mask(conf.rate), get_duration_mask(conf.frame_duration), + conf.framelen, conf.framelen); + if (!bap_qos) { + /* shouldn't happen: select_config should pick existing one */ + spa_log_error(log_, "no QoS settings found"); + 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->sdu = conf.framelen * conf.n_blks * get_channel_count(conf.channels); + qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); + qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED; + + qos->delay = bap_qos->delay; + qos->latency = bap_qos->latency; + qos->retransmission = bap_qos->retransmission; + + /* Clamp to ASE values (if known) */ + 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); + + /* + * We ignore endpoint suggested latency and RTN. On current devices + * these do not appear to be very useful numbers, so it's better + * to just pick one from the table in the spec. + */ + + 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)) { + spa_log_error(log_, "invalid LC3 config"); + res = -ENOTSUP; + goto error; + } + + 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; + } + + spa_log_info(log_, "LC3 rate:%d frame_duration:%d channels:%d framelen:%d nblks:%d", + this->samplerate, this->frame_dus, this->channels, this->framelen, conf.n_blks); + + res = lc3_frame_samples(this->frame_dus, this->samplerate); + if (res < 0) { + spa_log_error(log_, "invalid LC3 frame samples"); + res = -EINVAL; + goto error; + } + this->samples = res; + this->codesize = (size_t)this->samples * this->channels * conf.n_blks * 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 uint64_t codec_get_interval(void *data) +{ + struct impl *this = data; + + return (uint64_t)this->frame_dus * 1000; +} + +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 ich, res; + int size, processed; + + processed = 0; + size = 0; + + if (src_size < (size_t)this->codesize) + return -EINVAL; + if (dst_size < (size_t)this->framelen * this->channels) + return -EINVAL; + + 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; + + *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; + + consumed = 0; + + if (src_size < (size_t)this->framelen * this->channels) + 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; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log_ = global_log; + spa_log_topic_init(log_, &codec_plugin_log_topic); +} + +static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, + uint8_t *caps_size, struct spa_dict *settings, + struct bap_codec_qos *qos) +{ + int index = 0x0; + bool preset_found = false; + const char *preset = NULL; + int channel_allocation = 0; + uint8_t *data = caps; + *caps_size = 0; + int i; + + if (settings) { + for (i = 0; i < (int)settings->n_items; ++i) { + if (spa_streq(settings->items[i].key, "channel_allocation")) + sscanf(settings->items[i].value, "%"PRIu32, &channel_allocation); + if (spa_streq(settings->items[i].key, "preset")) + preset = spa_dict_lookup(settings, "preset"); + } + } + + if (preset == NULL) + return -EINVAL; + + SPA_FOR_EACH_ELEMENT_VAR(bap_bcast_qos_configs, c) { + if (spa_streq(c->name, preset)) { + preset_found = true; + break; + } + index++; + } + + if (!preset_found) + return -EINVAL; + + switch (bap_bcast_qos_configs[index].rate) { + case LC3_CONFIG_FREQ_48KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_48KHZ); + break; + case LC3_CONFIG_FREQ_32KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_32KHZ); + break; + case LC3_CONFIG_FREQ_24KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_24KHZ); + break; + case LC3_CONFIG_FREQ_16KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_16KHZ); + break; + case LC3_CONFIG_FREQ_8KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_8KHZ); + break; + default: + return -EINVAL; + } + *caps_size += 3; + + data += write_ltv_uint16(data, LC3_TYPE_FRAMELEN, htobs(bap_bcast_qos_configs[index].framelen)); + *caps_size += 4; + data += write_ltv_uint8(data, LC3_TYPE_DUR, bap_bcast_qos_configs[index].frame_duration); + *caps_size += 3; + data += write_ltv_uint32(data, LC3_TYPE_CHAN, htobl(channel_allocation)); + *caps_size += 6; + + if(bap_bcast_qos_configs[index].framing) + qos->framing = 1; + else + qos->framing = 0; + qos->sdu = bap_bcast_qos_configs[index].framelen * get_channel_count(channel_allocation); + qos->retransmission = bap_bcast_qos_configs[index].retransmission; + qos->latency = bap_bcast_qos_configs[index].latency; + qos->delay = bap_bcast_qos_configs[index].delay; + qos->phy = 2; + qos->interval = (bap_bcast_qos_configs[index].frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); + + return true; +} + +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, + .get_interval = codec_get_interval, + .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, + .set_log = codec_set_log, + .get_bis_config = codec_get_bis_config +}; + +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..d08ff0b --- /dev/null +++ b/spa/plugins/bluez5/bluez-hardware.conf @@ -0,0 +1,109 @@ +# 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 = "Audio Pro_A26", address = "~^7c:96:d2:", no-features = [ hw-volume ]}, # doesn't remember volume, #pipewire-3225 + { 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 = "PMK True Wireless Earbuds" no-features = [ sbc-xq ] }, # Primark earbud + { name = "Rockbox Brick", no-features = [ hw-volume ] }, # #pipewire-3786 + { 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 mini2", no-features = [ hw-volume ] }, # #pipewire-2927 + { 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 ] }, + { name = "WorkTunes Connect", no-features = [ hw-volume ] }, # 3M WorkTunes Connect + + { address = "~^44:5e:cd:", no-features = [ faststream, a2dp-duplex ]}, # #pipewire-1756 + { address = "~^2c:53:d7:", no-features = [ sbc-xq ] }, # Phonak hearing aids #pipewire-3939 + + { 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..e4495f0 --- /dev/null +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -0,0 +1,6786 @@ +/* Spa V4l2 dbus */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "codec-loader.h" +#include "player.h" +#include "iso-io.h" +#include "bap-codec-caps.h" +#include "defs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 + +/* How many times to retry acquire on errors, and how long delay to require before we can + * try again. + */ +#define TRANSPORT_ERROR_MAX_RETRY 3 +#define TRANSPORT_ERROR_TIMEOUT (2*BLUEZ_ACTION_RATE_MSEC*SPA_NSEC_PER_MSEC) + + +struct spa_bt_monitor { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_loop *data_loop; + struct spa_system *main_system; + struct spa_system *data_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; + + enum spa_bt_profile enabled_profiles; + + unsigned int connection_info_supported:1; + unsigned int dummy_avrcp_player:1; + + struct spa_list bcast_source_config_list; + + 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; +}; + +/* 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 *transport_path; + + char *uuid; + unsigned int codec; + struct spa_bt_device *device; + uint8_t *capabilities; + int capabilities_len; + bool delay_reporting; + bool acceptor; +}; + +#define METADATA_MAX_LEN 255 +#define CC_MAX_LEN 255 + +/* + * This structure stores metadata as defined + * in Assigned Numbers chapter 6.12.6 Metadata + * LTV structures. Length contains the size of + * type and value. + */ +struct spa_bt_metadata { + struct spa_list link; + int length; + int type; + uint8_t value[METADATA_MAX_LEN - 1]; +}; + +struct spa_bt_bis { + struct spa_list link; + char qos_preset[255]; + int channel_allocation; + struct spa_list metadata_list; +}; + +#define BROADCAST_CODE_LEN 16 + +struct spa_bt_big { + struct spa_list link; + char broadcast_code[BROADCAST_CODE_LEN]; + bool encryption; + int presentation_delay; + struct spa_list bis_list; + int big_id; + int sync_factor; +}; + +/* + * 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 | \ + SPA_BT_PROFILE_BAP_AUDIO) + +#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. + * + * Avoiding unnecessary release+reacquire also makes sense for ISO. + */ +#define TRANSPORT_RELEASE_TIMEOUT_MSEC 1000 + +#define TRANSPORT_VOLUME_TIMEOUT_MSEC 200 + +#define SPA_BT_TRANSPORT_IS_A2DP(transport) ((transport)->profile & (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK)) + +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 void spa_bt_transport_commit_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); + +static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw); + +// 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) +{ + return spa_aprintf(PIPEWIRE_BATTERY_PROVIDER "%s", device_path); +} + +// Unregister virtual battery of device +static void battery_remove(struct spa_bt_device *device) +{ + DBusMessageIter i, entry; + spa_autoptr(DBusMessage) m = NULL; + const char *interface; + + cancel_and_unref(&device->battery_pending_call); + + 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"); + } + + 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); + + spa_autoptr(DBusMessage) msg = NULL; + 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"); +} + +// Create new virtual battery with value stored in current device object +static void battery_create(struct spa_bt_device *device) +{ + spa_autoptr(DBusMessage) msg = NULL; + 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; + } + + 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) +{ + struct spa_bt_device *device = data; + + spa_assert(device->battery_pending_call == pending_call); + spa_autoptr(DBusMessage) reply = steal_reply_and_unref(&device->battery_pending_call); + + 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; + return; + } + + spa_log_debug(device->monitor->log, "Registered Battery Provider"); + + device->adapter->has_battery_provider = true; + + if (!device->has_battery) + battery_create(device); +} + +// Register Battery Provider for adapter and then create virtual battery for device +static void register_battery_provider(struct spa_bt_device *device) +{ + spa_autoptr(DBusMessage) method_call = NULL; + 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); + + device->battery_pending_call = send_with_reply(device->monitor->conn, method_call, + on_battery_provider_registered, device); + if (!device->battery_pending_call) { + spa_log_error(device->monitor->log, "Failed to register battery provider"); + return; + } +} + +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 if (direction == SPA_BT_MEDIA_SINK) + endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT; + else if (direction == SPA_BT_MEDIA_SOURCE_BROADCAST) + endpoint = BAP_BROADCAST_SOURCE_ENDPOINT; + else if (direction == SPA_BT_MEDIA_SINK_BROADCAST) + endpoint = BAP_BROADCAST_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 if (spa_strstartswith(endpoint, BAP_BROADCAST_SOURCE_ENDPOINT "/")) { + ep_name = endpoint + strlen(BAP_BROADCAST_SOURCE_ENDPOINT "/"); + *sink = false; + } else if (spa_strstartswith(endpoint, BAP_BROADCAST_SINK_ENDPOINT "/")) { + ep_name = endpoint + strlen(BAP_BROADCAST_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 (!preferred && !codec->fill_caps) + continue; + 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 if (spa_strstartswith(endpoint, BAP_BROADCAST_SINK_ENDPOINT "/")) + return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + else if (spa_strstartswith(endpoint, BAP_BROADCAST_SOURCE_ENDPOINT "/")) + return SPA_BT_PROFILE_BAP_BROADCAST_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: + case SPA_BT_MEDIA_SOURCE_BROADCAST: + return codec->encode; + case SPA_BT_MEDIA_SINK: + case SPA_BT_MEDIA_SINK_BROADCAST: + return codec->decode; + default: + spa_assert_not_reached(); + } +} + +static enum spa_bt_profile get_codec_profile(const struct media_codec *codec, + enum spa_bt_media_direction direction) +{ + switch (direction) { + case SPA_BT_MEDIA_SOURCE: + return codec->bap ? SPA_BT_PROFILE_BAP_SOURCE : SPA_BT_PROFILE_A2DP_SOURCE; + case SPA_BT_MEDIA_SINK: + if (codec->asha) + return SPA_BT_PROFILE_ASHA_SINK; + return codec->bap ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_A2DP_SINK; + case SPA_BT_MEDIA_SOURCE_BROADCAST: + return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + case SPA_BT_MEDIA_SINK_BROADCAST: + return SPA_BT_PROFILE_BAP_BROADCAST_SINK; + default: + spa_assert_not_reached(); + } +} + +static enum spa_bt_profile swap_profile(enum spa_bt_profile profile) +{ + switch (profile) { + case SPA_BT_PROFILE_A2DP_SOURCE: + return SPA_BT_PROFILE_A2DP_SINK; + case SPA_BT_PROFILE_A2DP_SINK: + return SPA_BT_PROFILE_A2DP_SOURCE; + case SPA_BT_PROFILE_BAP_SOURCE: + return SPA_BT_PROFILE_BAP_SINK; + case SPA_BT_PROFILE_BAP_SINK: + return SPA_BT_PROFILE_BAP_SOURCE; + case SPA_BT_PROFILE_BAP_BROADCAST_SOURCE: + return SPA_BT_PROFILE_BAP_BROADCAST_SINK; + case SPA_BT_PROFILE_BAP_BROADCAST_SINK: + return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + default: + return SPA_BT_PROFILE_NULL; + } +} + +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 && + (get_codec_profile(codec, direction) & monitor->enabled_profiles); +} + +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; + spa_autoptr(DBusMessage) r = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + int size, res; + const struct media_codec *codec; + bool sink; + + 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); + 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 : ""); + + 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; + + 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 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 void parse_codec_qos(struct spa_bt_monitor *monitor, DBusMessageIter *iter, struct bap_codec_qos_full *qos) +{ + DBusMessageIter dict_iter = *iter; + + memset(qos, 0, sizeof(*qos)); + qos->cig = 0xff; + qos->cis = 0xff; + qos->big = 0xff; + qos->bis = 0xff; + + if (!check_iter_signature(&dict_iter, "{sv}")) { + spa_log_warn(monitor->log, "Invalid BAP QoS in DBus"); + return; + } + + while (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(&dict_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_BYTE) { + uint8_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "qos: %s=%d", key, (int)value); + + if (spa_streq(key, "PHY")) + qos->qos.phy = value; + else if (spa_streq(key, "Retransmissions")) + qos->qos.retransmission = value; + else if (spa_streq(key, "CIG")) + qos->cig = value; + else if (spa_streq(key, "CIS")) + qos->cis = value; + else if (spa_streq(key, "BIG")) + qos->big = value; + else if (spa_streq(key, "BIS")) + qos->bis = value; + else if (spa_streq(key, "TargetLatency")) + qos->qos.target_latency = value; + else if (spa_streq(key, "Framing")) + qos->qos.framing = value; + } + else if (type == DBUS_TYPE_UINT16) { + dbus_uint16_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "qos: %s=%d", key, (int)value); + + if (spa_streq(key, "SDU")) + qos->qos.sdu = value; + else if (spa_streq(key, "Latency") || spa_streq(key, "MaximumLatency")) + qos->qos.latency = value; + } + else if (type == DBUS_TYPE_UINT32) { + dbus_uint32_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "qos: %s=%d", key, (int)value); + + if (spa_streq(key, "Interval")) + qos->qos.interval = value; + else if (spa_streq(key, "PresentationDelay")) + qos->qos.delay = value; + } + + dbus_message_iter_next(&dict_iter); + } +} + +static void parse_endpoint_qos(struct spa_bt_monitor *monitor, DBusMessageIter *iter, + struct bap_endpoint_qos *qos) +{ + DBusMessageIter dict_iter = *iter; + + if (!check_iter_signature(&dict_iter, "{sv}")) { + spa_log_warn(monitor->log, "Invalid BAP Endpoint QoS in DBus"); + return; + } + + while (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(&dict_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_BYTE) { + uint8_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + + if (spa_streq(key, "Framing")) + qos->framing = value; + else if (spa_streq(key, "PHY")) + qos->phy = value; + else if (spa_streq(key, "Retransmissions")) + qos->retransmission = value; + } else if (type == DBUS_TYPE_UINT16) { + dbus_uint16_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + + if (spa_streq(key, "Latency") || spa_streq(key, "MaximumLatency")) + qos->latency = value; + else if (spa_streq(key, "Context")) + qos->context = value; + else if (spa_streq(key, "SupportedContext")) + qos->supported_context = value; + } else if (type == DBUS_TYPE_UINT32) { + dbus_uint32_t value; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + + if (spa_streq(key, "MinimumDelay")) + qos->delay_min = value; + else if (spa_streq(key, "MaximumDelay")) + qos->delay_max = value; + else if (spa_streq(key, "PreferredMinimumDelay")) + qos->preferred_delay_min = value; + else if (spa_streq(key, "PreferredMaximumDelay")) + qos->preferred_delay_max = value; + } + + dbus_message_iter_next(&dict_iter); + } +} + +static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter *iter, + uint8_t caps[A2DP_MAX_CAPS_SIZE], int *caps_size, const char **endpoint_path, + struct bap_endpoint_qos *qos) +{ + DBusMessageIter dict_iter = *iter; + const char *key = NULL; + int type = 0; + + memset(caps, 0, A2DP_MAX_CAPS_SIZE); + *endpoint_path = NULL; + memset(qos, 0, sizeof(*qos)); + + if (!check_iter_signature(&dict_iter, "{sv}")) { + spa_log_warn(monitor->log, "Invalid BAP Endpoint QoS in DBus"); + return -EINVAL; + } + + while (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[3]; + + dbus_message_iter_recurse(&dict_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 (spa_streq(key, "Capabilities")) { + uint8_t *buf; + + if (type != DBUS_TYPE_ARRAY) + goto bad_property; + + dbus_message_iter_recurse(&it[1], &it[2]); + type = dbus_message_iter_get_arg_type(&it[2]); + if (type != DBUS_TYPE_BYTE) + goto bad_property; + + dbus_message_iter_get_fixed_array(&it[2], &buf, caps_size); + if (*caps_size > A2DP_MAX_CAPS_SIZE) { + spa_log_error(monitor->log, "%s size:%d too large", key, (int)*caps_size); + return -EINVAL; + } + memcpy(caps, buf, *caps_size); + + spa_log_info(monitor->log, "%p: %s size:%d", monitor, 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) + goto bad_property; + + dbus_message_iter_get_basic(&it[1], endpoint_path); + + spa_log_info(monitor->log, "%p: %s %s", monitor, key, *endpoint_path); + } else if (spa_streq(key, "QoS")) { + if (!check_iter_signature(&it[1], "a{sv}")) + goto bad_property; + + dbus_message_iter_recurse(&it[1], &it[2]); + parse_endpoint_qos(monitor, &it[2], qos); + } else if (spa_streq(key, "Locations") || spa_streq(key, "Location")) { + dbus_uint32_t value; + + if (type != DBUS_TYPE_UINT32) + goto bad_property; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + qos->locations = value; + } else if (spa_streq(key, "ChannelAllocation")) { + dbus_uint32_t value; + + if (type != DBUS_TYPE_UINT32) + goto bad_property; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + qos->channel_allocation = value; + } + + dbus_message_iter_next(&dict_iter); + } + + return 0; + +bad_property: + spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type); + return -EINVAL; +} + +static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + const char *path; + DBusMessageIter args, props, iter; + spa_autoptr(DBusMessage) r = NULL; + int res; + const struct media_codec *codec; + struct spa_bt_remote_endpoint *ep; + bool sink, duplex; + const char *err_msg = "Unknown error"; + struct spa_dict settings; + struct spa_dict_item setting_items[SPA_N_ELEMENTS(monitor->global_setting_items) + 5]; + int i; + + const char *endpoint_path = NULL; + uint8_t caps[A2DP_MAX_CAPS_SIZE]; + uint8_t config[A2DP_MAX_CAPS_SIZE]; + char locations[64] = {0}; + char channel_allocation[64] = {0}; + 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 : ""); + if (!codec || !codec->bap || !codec->get_qos) { + spa_log_error(monitor->log, "Unsupported codec"); + err_msg = "Unsupported codec"; + goto error; + } + + /* Parse endpoint properties */ + if (parse_endpoint_props(monitor, &props, caps, &caps_size, &endpoint_path, &endpoint_qos) < 0) + goto error_invalid; + if (endpoint_qos.locations) + spa_scnprintf(locations, sizeof(locations), "%"PRIu32, endpoint_qos.locations); + if (endpoint_qos.channel_allocation) + spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, endpoint_qos.channel_allocation); + + ep = remote_endpoint_find(monitor, endpoint_path); + if (!ep || !ep->device) { + spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path); + goto error_invalid; + } + + duplex = SPA_FLAG_IS_SET(ep->device->profiles, SPA_BT_PROFILE_BAP_DUPLEX); + + /* Call of SelectProperties means that local device acts as an initiator + * and therefore remote endpoint is an acceptor + */ + ep->acceptor = true; + + for (i = 0; i < (int)monitor->global_settings.n_items; ++i) + setting_items[i] = monitor->global_settings.items[i]; + setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.locations", locations); + setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.channel-allocation", channel_allocation); + setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.sink", sink ? "true" : "false"); + setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.duplex", duplex ? "true" : "false"); + setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.debug", "true"); + settings = SPA_DICT_INIT(setting_items, i); + spa_assert((size_t)i <= SPA_N_ELEMENTS(setting_items)); + + conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, &settings, 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); + + { + struct bap_codec_qos qos; + DBusMessageIter entry, variant, qos_dict; + const char *entry_key = "QoS"; + uint8_t cig = 0xff; + + 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; + } + + if (ep->device->settings) { + const char *str = spa_dict_lookup(ep->device->settings, "bluez5.bap.cig"); + uint32_t value; + + if (spa_atou32(str, &value, 0)) + cig = value; + } + + spa_log_debug(monitor->log, "select qos: interval:%d framing:%d phy:%d sdu:%d " + "rtn:%d latency:%d delay:%d target_latency:%d cig:%u", + qos.interval, qos.framing, qos.phy, qos.sdu, qos.retransmission, + qos.latency, (int)qos.delay, qos.target_latency, cig); + + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &entry_key); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "a{sv}", &variant); + + dbus_message_iter_open_container(&variant, 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, + &qos_dict); + + append_basic_variant_dict_entry(&qos_dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval); + append_basic_variant_dict_entry(&qos_dict, "Framing", DBUS_TYPE_BYTE, "y", &qos.framing); + append_basic_variant_dict_entry(&qos_dict, "PHY", DBUS_TYPE_BYTE, "y", &qos.phy); + append_basic_variant_dict_entry(&qos_dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu); + append_basic_variant_dict_entry(&qos_dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission); + append_basic_variant_dict_entry(&qos_dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency); + append_basic_variant_dict_entry(&qos_dict, "PresentationDelay", DBUS_TYPE_UINT32, "u", &qos.delay); + append_basic_variant_dict_entry(&qos_dict, "TargetLatency", DBUS_TYPE_BYTE, "y", &qos.target_latency); + + if (cig < 0xf0) + append_basic_variant_dict_entry(&qos_dict, "CIG", DBUS_TYPE_BYTE, "y", &cig); + + dbus_message_iter_close_container(&variant, &qos_dict); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + dbus_message_iter_close_container(&iter, &dict); + + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + return DBUS_HANDLER_RESULT_HANDLED; + +error_invalid: + err_msg = "Invalid property"; + goto error; + +error: + if (!reply_with_error(conn, m, "org.bluez.Error.InvalidArguments", err_msg)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + 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 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; + spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_BROADCAST_SOURCE); + adapter->profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_BROADCAST_SINK); + adapter->profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SINK; + } + 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 int adapter_media_update_props(struct spa_bt_adapter *adapter, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + /* Handle org.bluez.Media1 interface properties of .Adapter1 objects */ + 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; + + 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)) { + adapter->le_audio_supported = true; + spa_log_info(monitor->log, "Adapter %s: LE Audio supported", + adapter->path); + } + + if (spa_streq(uuid, SPA_BT_UUID_BAP_BROADCAST_SOURCE) || + spa_streq(uuid, SPA_BT_UUID_BAP_BROADCAST_SINK)) { + adapter->le_audio_bcast_supported = true; + spa_log_info(monitor->log, "Adapter %s: LE Broadcast Audio supported", + adapter->path); + } + + 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 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]; + int vendor_id, product_id; + const char *str; + + /* Lookup vendor/product id for the device, if present */ + str = strrchr(d->path, '/'); /* hciXX */ + if (str == NULL) + return -EINVAL; + + snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/modalias", str); + + spa_autoptr(FILE) f = fopen(path, "rbe"); + if (f == NULL) + return -errno; + + if (fscanf(f, "usb:v%04Xp%04X", &vendor_id, &product_id) != 2) + return -EINVAL; + + d->source_id = SOURCE_ID_USB; + d->vendor_id = vendor_id; + d->product_id = product_id; + + spa_log_debug(monitor->log, "adapter %p: usb vendor:%04x product:%04x", + d, vendor_id, product_id); + return 0; +} + +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 void metadata_entry_free(struct spa_bt_metadata *metadata_entry) +{ + spa_list_remove(&metadata_entry->link); + free(metadata_entry); +} + +static void bis_entry_free(struct spa_bt_bis *bis_entry) +{ + struct spa_bt_metadata *m; + + spa_list_consume(m, &bis_entry->metadata_list, link) + metadata_entry_free(m); + spa_list_remove(&bis_entry->link); + free(bis_entry); +} + +static void big_entry_free(struct spa_bt_big *big_entry) +{ + struct spa_bt_bis *b; + + spa_list_consume(b, &big_entry->bis_list, link) + bis_entry_free(b); + spa_list_remove(&big_entry->link); + free(big_entry); +} + +static uint32_t adapter_connectable_profiles(struct spa_bt_adapter *adapter) +{ + struct spa_bt_monitor *monitor = adapter->monitor; + 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_BAP_BROADCAST_SINK) + mask |= SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + if (profiles & SPA_BT_PROFILE_BAP_BROADCAST_SOURCE) + mask |= SPA_BT_PROFILE_BAP_BROADCAST_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; + + if (monitor->backend_selection == BACKEND_NONE) + mask &= ~SPA_BT_PROFILE_HEADSET_AUDIO; + + 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; +} + +static uint64_t get_time_now(struct spa_bt_monitor *monitor) +{ + struct timespec ts; + + spa_system_clock_gettime(monitor->main_system, CLOCK_MONOTONIC, &ts); + return SPA_TIMESPEC_TO_NSEC(&ts); +} + +void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device) +{ + device->last_bluez_action_time = get_time_now(device->monitor); +} + +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_list_init(&d->set_membership_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 void device_clear_sub(struct spa_bt_device *device) +{ + battery_remove(device); + spa_bt_device_release_transports(device); + device->preferred_codec = NULL; +} + +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; + struct spa_bt_set_membership *s; + + 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_consume(s, &device->set_membership_list, link) { + spa_list_remove(&s->link); + spa_list_remove(&s->others); + free(s->path); + free(s); + } + + 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); +} + +static struct spa_bt_set_membership *device_set_find(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_device *d; + + spa_list_for_each(d, &monitor->device_list, link) { + struct spa_bt_set_membership *s; + + spa_list_for_each(s, &d->set_membership_list, link) { + if (spa_streq(s->path, path)) + return s; + } + } + + return NULL; +} + +static int device_add_device_set(struct spa_bt_device *device, const char *path, uint8_t rank) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_set_membership *s, *set; + + spa_list_for_each(s, &device->set_membership_list, link) { + if (spa_streq(s->path, path)) { + if (rank) + s->rank = rank; + return 0; + } + } + + s = calloc(1, sizeof(struct spa_bt_set_membership)); + if (s == NULL) + return -ENOMEM; + + s->path = strdup(path); + if (!s->path) { + free(s); + return -ENOMEM; + } + + s->device = device; + s->rank = rank; + + spa_list_init(&s->others); + + /* Join with other set members, if any */ + set = device_set_find(monitor, path); + if (set) + spa_list_append(&set->others, &s->others); + + spa_list_append(&device->set_membership_list, &s->link); + + spa_log_debug(monitor->log, "device %p: add %s to device set %s", device, + device->path, path); + + return 1; +} + +static bool device_remove_device_set(struct spa_bt_device *device, const char *path) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_set_membership *s; + + spa_list_for_each(s, &device->set_membership_list, link) { + if (spa_streq(s->path, path)) { + spa_log_debug(monitor->log, + "device %p: remove %s from device set %s", device, + device->path, path); + spa_list_remove(&s->link); + spa_list_remove(&s->others); + free(s->path); + free(s); + return true; + } + } + + return false; +} + +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) +{ + const 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; + spa_autoptr(DBusMessage) m = NULL; + + 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)) + return -EIO; + + 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); + if (reconnect & SPA_BT_PROFILE_BAP_BROADCAST_SINK) + device_try_connect_profile(device, SPA_BT_UUID_BAP_BROADCAST_SINK); + if (reconnect & SPA_BT_PROFILE_BAP_BROADCAST_SOURCE) + device_try_connect_profile(device, SPA_BT_UUID_BAP_BROADCAST_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; + struct spa_bt_set_membership *s, *set; + uint32_t connected_profiles = device->connected_profiles; + uint32_t connectable_profiles = + device->adapter ? adapter_connectable_profiles(device->adapter) : 0; + uint32_t direction_masks[4] = { + SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT, + SPA_BT_PROFILE_MEDIA_SOURCE, + SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY, + SPA_BT_PROFILE_ASHA_SINK, + }; + bool direction_connected = false; + bool set_connected = true; + 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 & connectable_profiles) + == (device->profiles & connectable_profiles)); + + spa_list_for_each(set, &device->set_membership_list, link) + spa_bt_for_each_set_member(s, set) + if ((s->device->connected_profiles & s->device->profiles) != s->device->profiles) + set_connected = false; + + spa_log_debug(monitor->log, "device %p: profiles %08x %08x connectable:%08x added:%d all:%d dir:%d set:%d", + device, device->profiles, connected_profiles, connectable_profiles, + device->added, all_connected, direction_connected, set_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) && set_connected && connected_profiles)) { + 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_quirks_log_features(monitor->quirks, device->adapter, device); + 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); + } +} + +static void device_update_set_status(struct spa_bt_device *device, bool force, const char *path); + +int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile) +{ + device->connected_profiles |= profile; + if (profile & SPA_BT_PROFILE_BAP_DUPLEX) + device_update_set_status(device, true, NULL); + spa_bt_device_check_profiles(device, false); + spa_bt_device_emit_profiles_changed(device, profile); + 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 bool device_set_update_leader(struct spa_bt_set_membership *set) +{ + struct spa_bt_set_membership *s, *leader = NULL; + + /* Make minimum rank device the leader, so that device set nodes always + * appear under a specific device. + */ + spa_bt_for_each_set_member(s, set) { + if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX)) + continue; + + if (leader == NULL || s->rank < leader->rank || + (s->rank == leader->rank && s->leader)) + leader = s; + } + + if (leader == NULL || (leader && leader->leader)) + return false; + + spa_bt_for_each_set_member(s, set) + s->leader = false; + + leader->leader = true; + + spa_log_debug(leader->device->monitor->log, + "device set %p %s: leader is %s", + set, leader->path, leader->device->path); + + return true; +} + +static void device_update_set_status(struct spa_bt_device *device, bool force, const char *path) +{ + struct spa_bt_set_membership *s, *set; + + spa_list_for_each(set, &device->set_membership_list, link) { + if (path && !spa_streq(set->path, path)) + continue; + + if (device_set_update_leader(set) || force) { + spa_bt_for_each_set_member(s, set) + if (!s->leader) + spa_bt_device_emit_device_set_changed(s->device); + spa_bt_for_each_set_member(s, set) + if (s->leader) + spa_bt_device_emit_device_set_changed(s->device); + } + } +} + +static int device_set_update_props(struct spa_bt_monitor *monitor, + const char *path, DBusMessageIter *props_iter, DBusMessageIter *invalidated_iter) +{ + struct spa_bt_device *old[256]; + struct spa_bt_device *new[256]; + struct spa_bt_set_membership *set; + size_t num_old = 0, num_new = 0; + size_t i; + + if (!props_iter) + goto done; + + /* Find current devices */ + 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, "Devices")) { + DBusMessageIter iter; + int i = 0; + + 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) { + struct spa_bt_device *d; + const char *dev_path; + + dbus_message_iter_get_basic(&iter, &dev_path); + + spa_log_debug(monitor->log, "device set %s: Devices[%d]=%s", + path, i++, dev_path); + + if (num_new >= SPA_N_ELEMENTS(new)) + break; + d = spa_bt_device_find(monitor, dev_path); + if (d) + new[num_new++] = d; + + dbus_message_iter_next(&iter); + } + + } + else + spa_log_debug(monitor->log, "device set %s: unhandled key %s", + path, key); + +next: + dbus_message_iter_next(props_iter); + } + +done: + /* Find devices to remove */ + set = device_set_find(monitor, path); + if (set) { + struct spa_bt_set_membership *s; + + spa_bt_for_each_set_member(s, set) { + for (i = 0; i < num_new; ++i) + if (s->device == new[i]) + break; + if (i == num_new) { + if (num_old >= SPA_N_ELEMENTS(old)) + break; + old[num_old++] = s->device; + } + } + } + + /* Remove old devices */ + for (i = 0; i < num_old; ++i) + device_remove_device_set(old[i], path); + + /* Add new devices */ + for (i = 0; i < num_new; ++i) + device_add_device_set(new[i], path, 0); + + /* Emit signals & update set leader */ + for (i = 0; i < num_old; ++i) + spa_bt_device_emit_device_set_changed(old[i]); + + if (num_new > 0) + device_update_set_status(new[0], true, path); + + return 0; +} + +static int device_update_device_sets_prop(struct spa_bt_device *device, + DBusMessageIter *iter) +{ + struct spa_bt_monitor *monitor = device->monitor; + DBusMessageIter it[5]; + bool changed = false; + + if (!check_iter_signature(iter, "a{oa{sv}}")) + return -EINVAL; + + dbus_message_iter_recurse(iter, &it[0]); + + while (dbus_message_iter_get_arg_type(&it[0]) != DBUS_TYPE_INVALID) { + uint8_t rank = 0; + const char *set_path; + + dbus_message_iter_recurse(&it[0], &it[1]); + dbus_message_iter_get_basic(&it[1], &set_path); + dbus_message_iter_next(&it[1]); + dbus_message_iter_recurse(&it[1], &it[2]); + + while (dbus_message_iter_get_arg_type(&it[2]) != DBUS_TYPE_INVALID) { + const char *key; + int type; + + dbus_message_iter_recurse(&it[2], &it[3]); + dbus_message_iter_get_basic(&it[3], &key); + dbus_message_iter_next(&it[3]); + dbus_message_iter_recurse(&it[3], &it[4]); + + type = dbus_message_iter_get_arg_type(&it[4]); + + if (spa_streq(key, "Rank") && type == DBUS_TYPE_BYTE) + dbus_message_iter_get_basic(&it[4], &rank); + + dbus_message_iter_next(&it[2]); + } + + spa_log_debug(monitor->log, "device %p: path %s device set %s rank %d", + device, device->path, set_path, (int)rank); + + /* Only add. Removals are handled in device set updates. */ + if (device_add_device_set(device, set_path, rank) == 1) + changed = true; + + dbus_message_iter_next(&it[0]); + } + + /* Emit change signals */ + device_update_set_status(device, changed, NULL); + + return 0; +} + +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); + + 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, 0); + } + else if (spa_streq(key, "Sets")) { + device_update_device_sets_prop(device, &it[1]); + } + 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, enum spa_bt_profile profile) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_remote_endpoint *ep; + enum spa_bt_profile codec_target_profile; + struct spa_bt_transport *t; + 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 }, + }; + bool is_a2dp = !codec->bap && !codec->asha; + size_t i; + + if (!is_media_codec_enabled(device->monitor, codec)) + return false; + + if (!device->adapter->a2dp_application_registered && is_a2dp) { + /* Codec switching not supported: only plain SBC allowed */ + return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc") && + device->adapter->legacy_endpoints_registered); + } + if (!device->adapter->bap_application_registered && codec->bap) + return false; + + /* 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; + } + + for (i = 0, codec_target_profile = 0; i < (size_t)SPA_BT_MEDIA_DIRECTION_LAST; ++i) + if (codec_has_direction(codec, i)) + codec_target_profile |= swap_profile(get_codec_profile(codec, i)); + + spa_list_for_each(ep, &device->remote_endpoint_list, device_link) { + enum spa_bt_profile ep_profile = spa_bt_profile_from_uuid(ep->uuid); + + if (!(ep_profile & codec_target_profile & profile)) + continue; + + if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len, + &ep->monitor->default_audio_info, &monitor->global_settings)) + return true; + } + + /* Codecs on configured transports are always supported. + * + * Remote BAP endpoints correspond to capabilities of the remote + * BAP Server, not to remote BAP Client, and need not be the same. + * BAP Clients may not have any remote endpoints. In this case we + * can only know that the currently configured codec is supported. + */ + spa_list_for_each(t, &device->transport_list, device_link) { + if (!(t->profile & codec_target_profile & profile)) + continue; + + if (codec == t->media_codec) + return true; + } + + return false; +} + +const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count) +{ + struct spa_bt_monitor *monitor = device->monitor; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + spa_autofree const struct media_codec **supported_codecs = NULL; + 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], device->connected_profiles)) { + supported_codecs[j] = media_codecs[i]; + ++j; + } + + if (j >= size) { + const struct media_codec **p; + size = size * 2; +#ifdef HAVE_REALLOCARRAY + 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) + return NULL; + + supported_codecs = p; + } + } + + supported_codecs[j] = NULL; + *count = j; + + return spa_steal_ptr(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 struct spa_bt_device *create_bcast_device(struct spa_bt_monitor *monitor, const char *adapter_path, + const char *transport_path, const char *address) +{ + struct spa_bt_device *d; + struct spa_bt_adapter *adapter; + + adapter = adapter_find(monitor, adapter_path); + if (adapter == NULL) { + spa_log_warn(monitor->log, "unknown adapter %s", adapter_path); + return NULL; + } + + d = device_create(monitor, transport_path); + if (d == NULL) { + spa_log_warn(monitor->log, "can't create Bluetooth device %s: %m", + transport_path); + return NULL; + } + + d->adapter = adapter; + d->adapter_path = strdup(adapter->path); + d->address = spa_aprintf("%s.%d", address, d->id); + d->alias = strdup(d->address); + d->name = strdup(d->address); + d->reconnect_state = BT_DEVICE_RECONNECT_STOP; + + device_update_hw_volume_profiles(d); + + spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL); + + return d; +} + +static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, struct spa_bt_monitor *monitor); + +static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_endpoint, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + 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); + + if(spa_streq(remote_endpoint->uuid, SPA_BT_UUID_BAP_BROADCAST_SINK)) + /* Set remote endpoint as an acceptor for a broadcast sink. + * So the transport is an initiator. + */ + remote_endpoint->acceptor = true; + } + 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); + } + } + /* For ASHA */ + else if (spa_streq(key, "Transport")) { + free(remote_endpoint->transport_path); + remote_endpoint->transport_path = strdup(value); + } + } + else if (type == DBUS_TYPE_BOOLEAN) { + int value; + + 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; + } + } + /* Codecs property is present for ASHA */ + else if (type == DBUS_TYPE_UINT16) { + uint16_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + if (spa_streq(key, "Codecs")) { + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value); + } + } + else if (spa_streq(key, "Capabilities")) { + DBusMessageIter iter; + uint8_t *value; + 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; + } + } + /* HiSyncId property is present for ASHA */ + else if (spa_streq(key, "HiSyncId")) { + /* + * TODO: Required for Stereo support in ASHA, for now just log. + */ + DBusMessageIter iter; + uint8_t *value; + int len; + + if (!check_iter_signature(&it[1], "ay")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + dbus_message_iter_get_fixed_array(&iter, &value, &len); + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len); + } + else + spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key); + +next: + dbus_message_iter_next(props_iter); + } + + /* BAP profile UUIDs do not appear in device UUID list. + * Instead, we detect these capabilities based on available + * endpoints (i.e. PACs). + */ + if (remote_endpoint->uuid && remote_endpoint->device) { + enum spa_bt_profile profile; + + profile = spa_bt_profile_from_uuid(remote_endpoint->uuid); + if (profile & SPA_BT_PROFILE_BAP_AUDIO) + spa_bt_device_add_profile(remote_endpoint->device, profile); + + if (spa_streq(remote_endpoint->uuid, SPA_BT_UUID_ASHA_SINK)) { + if (profile & SPA_BT_PROFILE_ASHA_SINK) + setup_asha_transport(remote_endpoint, monitor); + } + } + + return 0; +} + +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->transport_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_us = SPA_BT_UNKNOWN_DELAY; + t->latency_us = SPA_BT_UNKNOWN_DELAY; + t->bap_cig = 0xff; + t->bap_cis = 0xff; + t->bap_big = 0xff; + t->bap_bis = 0xff; + 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); + + if (state < SPA_BT_TRANSPORT_STATE_ACTIVE) { + /* If transport becomes inactive, do any pending releases + * immediately, since the fd is not usable any more. + */ + spa_bt_transport_commit_release_timer(transport); + } + + if (state == SPA_BT_TRANSPORT_STATE_ERROR) { + uint64_t now = get_time_now(monitor); + + if (now > transport->last_error_time + TRANSPORT_ERROR_TIMEOUT) { + spa_log_error(monitor->log, "Failure in Bluetooth audio transport %s", + transport->path); + } + + transport->last_error_time = now; + ++transport->error_count; + } + } +} + +void spa_bt_transport_free(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_device *device = transport->device; + + 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; + } + + if (transport->iso_io) + spa_bt_iso_io_destroy(transport->iso_io); + + spa_bt_transport_destroy(transport); + + cancel_and_unref(&transport->acquire_call); + cancel_and_unref(&transport->volume_call); + + if (transport->fd >= 0) { + if (device) + spa_bt_player_set_state(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 (device) { + struct spa_bt_transport *t; + uint32_t disconnected = transport->profile; + + spa_list_remove(&transport->device_link); + + spa_list_for_each(t, &device->transport_list, device_link) + disconnected &= ~t->profile; + device->connected_profiles &= ~disconnected; + + if (transport->profile & SPA_BT_PROFILE_BAP_DUPLEX) + device_update_set_status(device, true, NULL); + + spa_bt_device_emit_profiles_changed(device, transport->profile); + } + + spa_list_remove(&transport->bap_transport_linked); + + free(transport->configuration); + 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; + spa_bt_transport_emit_state_changed(transport, transport->state, transport->state); + return 0; + } + spa_assert(transport->acquire_refcount == 0); + + /* If we are getting into error state too often, stop trying */ + if (get_time_now(monitor) > transport->last_error_time + TRANSPORT_ERROR_TIMEOUT) + transport->error_count = 0; + if (transport->error_count >= TRANSPORT_ERROR_MAX_RETRY) + return -EIO; + + 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; +} + +static void spa_bt_transport_do_release(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + + spa_assert(transport->acquire_refcount >= 1); + spa_assert(transport->acquired); + + 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; +} + +int spa_bt_transport_release(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + + if (transport->acquire_refcount > 1) { + spa_log_debug(monitor->log, "transport %p: decref %s", transport, transport->path); + transport->acquire_refcount -= 1; + spa_bt_transport_emit_state_changed(transport, transport->state, transport->state); + 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); + + /* Postpone active transport releases, since we might need it again soon. + * If not active, release now since it has to be reacquired before using again. + */ + if (transport->state == SPA_BT_TRANSPORT_STATE_ACTIVE && + !SPA_BT_TRANSPORT_IS_A2DP(transport)) { + return spa_bt_transport_start_release_timer(transport); + } else { + spa_bt_transport_do_release(transport); + return 0; + } +} + +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; + + spa_bt_transport_stop_release_timer(transport); + spa_bt_transport_do_release(transport); +} + +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, + 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_commit_release_timer(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + + /* Do release now if it is pending */ + if (transport->release_timer.data) { + spa_log_debug(monitor->log, "transport %p: commit pending release", transport); + spa_bt_transport_release_timer_event(&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 if (transport->profile & SPA_BT_PROFILE_ASHA_SINK) + volume_id = SPA_BT_VOLUME_ID_TX; + 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 = (float)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(t, data_loop, t->monitor->log); + 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_us != SPA_BT_UNKNOWN_DELAY) { + /* end-to-end delay = (presentation) delay + transport latency + * + * For BAP, see Core v5.3 Vol 6/G Sec 3.2.2 Fig. 3.2 & + * BAP v1.0 Sec 7.1.1. + */ + int64_t delay = t->delay_us; + if (t->latency_us != SPA_BT_UNKNOWN_DELAY) + delay += t->latency_us; + return delay * SPA_NSEC_PER_USEC; + } + + /* Fallback values when device does not provide information */ + + if (t->media_codec == NULL) + return 20 * SPA_NSEC_PER_MSEC; + + switch (t->media_codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_SBC: + case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ: + case SPA_BLUETOOTH_AUDIO_CODEC_MPEG: + case SPA_BLUETOOTH_AUDIO_CODEC_AAC: + case SPA_BLUETOOTH_AUDIO_CODEC_APTX: + case SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD: + case SPA_BLUETOOTH_AUDIO_CODEC_LDAC: + return 125 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD: + 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 125 * 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")) { + transport->profile = swap_profile(spa_bt_profile_from_uuid(value)); + if (transport->profile == SPA_BT_PROFILE_NULL) + spa_log_warn(monitor->log, "unknown profile %s", value); + } + else if (spa_streq(key, "State")) { + enum spa_bt_transport_state state = spa_bt_transport_state_from_string(value); + + /* Transition to active emitted only from acquire callback. */ + if (state != SPA_BT_TRANSPORT_STATE_ACTIVE) + spa_bt_transport_set_state(transport, state); + } + else if (spa_streq(key, "Device")) { + char *pos; + struct spa_bt_device *device = spa_bt_device_find(monitor, value); + if ((device == NULL) && + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK)) { + /* + * If a transport with profile broadcast source is detected (over DBus) + * and no device is found for it, a new device will be created. + * This device will be our simulated remote device. + * This is done because BlueZ sets the adapter as the device + * that is connected to a broadcast sink endpoint/transport. + */ + device = spa_bt_device_find(monitor, transport->path); + if (device == NULL) { + device = create_bcast_device(monitor, value, transport->path, "00:00:00:00:00:00"); + if (device == NULL) { + spa_log_warn(monitor->log, "could not find device %s", value); + } else + device_set_connected(device, 1); + } + } if ((device != NULL) && + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + /* + * For each transport that has a broadcast source profile, + * we need to create a new node for each BIS. + * example of transport path = /org/bluez/hci0/dev_2D_9D_93_F9_D7_5E/bis1/fd0 + * Create new devices only for a case of a big with multiple BISes, + * for this case will have the scanned device to the transport + * "/fd0" and create new devices for the other transports from this device + * that appear only in case of multiple BISes per BIG. + */ + pos = strstr(transport->path, "/fd0"); + if (pos == NULL) { + device = create_bcast_device(monitor, device->adapter_path, transport->path, device->address); + if (device == NULL) { + spa_log_warn(monitor->log, "could not find device created"); + } else + device_set_connected(device, 1); + } + } + 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 if (transport->profile & SPA_BT_PROFILE_ASHA_SINK) + t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX]; + else if (transport->profile & SPA_BT_PROFILE_BAP_SINK) + t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX]; + else if (transport->profile & SPA_BT_PROFILE_BAP_SOURCE) + t_volume = &transport->volumes[SPA_BT_VOLUME_ID_RX]; + else + goto next; + + 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=%d", transport, key, (int)value); + + transport->delay_us = value * 100; + + spa_bt_transport_emit_delay_changed(transport); + } + else if (spa_streq(key, "QoS")) { + struct bap_codec_qos_full qos; + DBusMessageIter value; + + if (!check_iter_signature(&it[1], "a{sv}")) + goto next; + + dbus_message_iter_recurse(&it[1], &value); + parse_codec_qos(monitor, &value, &qos); + + transport->bap_cig = qos.cig; + transport->bap_cis = qos.cis; + transport->bap_big = qos.big; + transport->bap_bis = qos.bis; + transport->delay_us = qos.qos.delay; + transport->latency_us = (unsigned int)qos.qos.latency * 1000; + + spa_bt_transport_emit_delay_changed(transport); + } + else if (spa_streq(key, "Links")) { + DBusMessageIter iter; + + if (!check_iter_signature(&it[1], "ao")) + goto next; + + spa_list_remove(&transport->bap_transport_linked); + spa_list_init(&transport->bap_transport_linked); + + dbus_message_iter_recurse(&it[1], &iter); + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *transport_path; + 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 void transport_set_property_volume_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_transport *transport = user_data; + struct spa_bt_monitor *monitor = transport->monitor; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + spa_assert(transport->volume_call == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&transport->volume_call); + + if (dbus_set_error_from_message(&err, r)) { + spa_log_info(monitor->log, "transport %p: set volume failed for transport %s: %s", + transport, transport->path, err.message); + } else { + spa_log_debug(monitor->log, "transport %p: set volume complete", + transport); + } +} + +static void transport_set_property_volume(struct spa_bt_transport *transport, uint16_t value) +{ + struct spa_bt_monitor *monitor = transport->monitor; + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter it[2]; + const char *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE; + const char *name = "Volume"; + int res = 0; + + cancel_and_unref(&transport->volume_call); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + DBUS_INTERFACE_PROPERTIES, + "Set"); + if (m == NULL) { + res = -ENOMEM; + goto fail; + } + + 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]); + + transport->volume_call = send_with_reply(monitor->conn, m, transport_set_property_volume_reply, transport); + if (!transport->volume_call) { + res = -EIO; + goto fail; + } + + spa_log_debug(monitor->log, "transport %p: setting volume to %d", transport, value); + return; + +fail: + spa_log_debug(monitor->log, "transport %p: failed to set volume %d: %s", + transport, value, spa_strerror(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, t_volume->hw_volume_max); + 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_create_iso_io(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_transport *t; + + if (!(transport->profile & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE | + SPA_BT_PROFILE_BAP_BROADCAST_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))) + return 0; + + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + if (transport->bap_big == 0xff || transport->bap_bis == 0xff) + return -EINVAL; + } else { + if (transport->bap_cig == 0xff || transport->bap_cis == 0xff) + return -EINVAL; + } + + if (transport->iso_io) { + spa_log_debug(monitor->log, "transport %p: remove ISO IO", transport); + spa_bt_iso_io_destroy(transport->iso_io); + transport->iso_io = NULL; + } + + /* Transports in same connected iso group share the same i/o */ + spa_list_for_each(t, &monitor->transport_list, link) { + if (!(t->profile & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE | + SPA_BT_PROFILE_BAP_BROADCAST_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))) + continue; + + if (t->device->adapter != transport->device->adapter) + continue; + + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + if (t->bap_big != transport->bap_big) + continue; + } else { + if (t->bap_cig != transport->bap_cig) + continue; + } + + if (t->iso_io) { + spa_log_debug(monitor->log, "transport %p: attach ISO IO to %p", + transport, t); + transport->iso_io = spa_bt_iso_io_attach(t->iso_io, transport); + if (transport->iso_io == NULL) + return -errno; + return 0; + } + } + + spa_log_debug(monitor->log, "transport %p: new ISO IO", transport); + transport->iso_io = spa_bt_iso_io_create(transport, monitor->log, monitor->data_loop, monitor->data_system); + if (transport->iso_io == NULL) + return -errno; + + return 0; +} + +static bool transport_in_same_cig(struct spa_bt_transport *transport, struct spa_bt_transport *other) +{ + return (other->profile & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE)) && + other->bap_cig == transport->bap_cig && + other->bap_initiator; +} + +static void transport_acquire_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_transport *transport = user_data; + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_device *device = transport->device; + int ret = 0; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + struct spa_bt_transport *t, *t_linked; + + spa_assert(transport->acquire_call == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&transport->acquire_call); + + spa_bt_device_update_last_bluez_action_time(device); + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "Acquire %s returned error: %s", + transport->path, + dbus_message_get_error_name(r)); + ret = -EIO; + goto finish; + } + + if (transport->fd >= 0) { + spa_log_error(monitor->log, "transport %p: invalid duplicate acquire", transport); + ret = -EINVAL; + 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 Acquire %s reply: %s", + transport->path, err.message); + ret = -EIO; + goto finish; + } + + spa_log_debug(monitor->log, "transport %p: Acquired %s, fd %d MTU %d:%d", transport, + 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 (ret < 0) { + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ERROR); + + /* For broadcast, skip handling links. Each link acquire + * is handled separately. + */ + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) + return; + } else { + if (transport_create_iso_io(transport) < 0) + spa_log_error(monitor->log, "transport %p: transport_create_iso_io failed", + transport); + /* For broadcast, each transport has a different fd, so it needs to be + * acquired independently from others. Each transport moves to + * SPA_BT_TRANSPORT_STATE_ACTIVE after acquire is completed. + */ + /* TODO: handling multiple BIGs support */ + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE); + return; + } + + if (!transport->bap_initiator) + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_ACTIVE); + } + + /* For LE Audio, multiple transport from the same device may share the same + * stream (CIS) and group (CIG) but for different direction, e.g. a speaker and + * a microphone. In this case they are linked, and we need to set the values + * for all of them here. + */ + spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) { + if (ret < 0) { + spa_bt_transport_set_state(t_linked, SPA_BT_TRANSPORT_STATE_ERROR); + continue; + } + + t_linked->fd = transport->fd; + t_linked->read_mtu = transport->read_mtu; + t_linked->write_mtu = transport->write_mtu; + spa_log_debug(monitor->log, "transport %p: linked Acquired %s, fd %d MTU %d:%d", t_linked, + t_linked->path, t_linked->fd, t_linked->read_mtu, t_linked->write_mtu); + + if (transport_create_iso_io(t_linked) < 0) + spa_log_error(monitor->log, "transport %p: transport_create_iso_io failed", + t_linked); + + /* For broadcast there initiator moves the transport state to SPA_BT_TRANSPORT_STATE_ACTIVE */ + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + spa_bt_transport_set_state(t_linked, SPA_BT_TRANSPORT_STATE_ACTIVE); + } else { + if (!transport->bap_initiator) + spa_bt_transport_set_state(t_linked, SPA_BT_TRANSPORT_STATE_ACTIVE); + } + } + + /* + * Transports in same CIG emit state change events at the same time, + * after all pending acquires complete. + */ + if (transport->bap_initiator) { + spa_list_for_each(t, &monitor->transport_list, link) { + if (!transport_in_same_cig(transport, t)) + continue; + if (t->acquire_call) + return; + } + spa_list_for_each(t, &monitor->transport_list, link) { + if (!transport_in_same_cig(transport, t)) + continue; + if (t->fd >= 0) + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_ACTIVE); + } + } +} + +static int do_transport_acquire(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + spa_autoptr(DBusMessage) m = NULL; + struct spa_bt_transport *t_linked; + + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) + /* For Broadcast, all linked transports need to be + * acquired independently, since they have different fds. + */ + goto acquire; + + spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) { + /* If a linked transport has been acquired, it will do all the work */ + if (t_linked->acquire_call || t_linked->acquired) { + spa_log_debug(monitor->log, "Acquiring %s: use linked transport %s", + transport->path, t_linked->path); + spa_bt_transport_emit_state_changed(transport, transport->state, transport->state); + return 0; + } + } + +acquire: + if (transport->acquire_call) + return -EBUSY; + + spa_log_info(monitor->log, "Acquiring transport %s", transport->path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + BLUEZ_MEDIA_TRANSPORT_INTERFACE, + "Acquire"); + if (m == NULL) + return -ENOMEM; + + transport->acquire_call = send_with_reply(monitor->conn, m, transport_acquire_reply, transport); + if (!transport->acquire_call) + return -EIO; + + return 0; +} + +static bool another_cig_transport_active(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_transport *t; + + spa_list_for_each(t, &monitor->transport_list, link) { + if (!transport_in_same_cig(transport, t) || t == transport) + continue; + if (t->acquired) + return true; + } + + return false; +} + +static int transport_acquire(void *data, bool optional) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_monitor *monitor = transport->monitor; + + /* + * XXX: When as BAP Central, all CIS in a CIG must be acquired at the same time. + * XXX: This is because of kernel ISO socket limitations, which does not handle + * XXX: currently starting streams in the group one by one. + */ + if (transport->bap_initiator && !another_cig_transport_active(transport)) { + struct spa_bt_transport *t; + + spa_list_for_each(t, &monitor->transport_list, link) { + if (!transport_in_same_cig(transport, t) || t == transport) + continue; + + spa_log_debug(monitor->log, "Acquire CIG %d: transport %s", + transport->bap_cig, t->path); + + do_transport_acquire(t); + } + + spa_log_debug(monitor->log, "Acquire CIG %d: transport %s", + transport->bap_cig, transport->path); + } + if (transport->bap_initiator && + (transport->fd >= 0 || transport->acquire_call)) { + /* Already acquired/acquiring */ + spa_log_debug(monitor->log, "Acquiring %s: was in acquired CIG", transport->path); + spa_bt_transport_emit_state_changed(transport, transport->state, transport->state); + return 0; + } + + return do_transport_acquire(data); +} + +static int do_transport_release(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + spa_autoptr(DBusMessage) m = NULL, r = NULL; + struct spa_bt_transport *t_linked; + bool is_idle = (transport->state == SPA_BT_TRANSPORT_STATE_IDLE); + 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); + + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE); + + cancel_and_unref(&transport->acquire_call); + + if (transport->iso_io) { + spa_log_debug(monitor->log, "transport %p: remove ISO IO", transport); + spa_bt_iso_io_destroy(transport->iso_io); + transport->iso_io = NULL; + } + + /* For Unicast LE Audio, multiple transport stream (CIS) can be linked together (CIG). + * If they are part of the same device they reuse the same fd, and call to + * release should be done for the last one only. + * + * For Broadcast LE Audio, since linked transports have different fds, they + * should be released independently. + */ + if ((transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) || + (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) + goto release; + + spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) { + if (t_linked->acquire_call || t_linked->acquired) { + linked = true; + break; + } + } + if (linked) { + spa_log_info(monitor->log, "Linked transport %s released", transport->path); + transport->fd = -1; + return 0; + } + +release: + if (transport->fd >= 0) { + close(transport->fd); + transport->fd = -1; + } + + spa_log_info(monitor->log, "Releasing transport %s", transport->path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + BLUEZ_MEDIA_TRANSPORT_INTERFACE, + "Release"); + if (m == NULL) + return -ENOMEM; + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err); + if (r == NULL) { + 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); + } + } else { + spa_log_info(monitor->log, "Transport %s released", transport->path); + } + + return 0; +} + +static int transport_release(void *data) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_transport *t; + + /* + * XXX: When as BAP Central, release CIS in a CIG when the last transport + * XXX: goes away. + */ + if (transport->bap_initiator) { + /* Check if another transport is alive */ + if (another_cig_transport_active(transport)) { + spa_log_debug(monitor->log, "Releasing %s: wait for CIG %d", + transport->path, transport->bap_cig); + return 0; + } + + /* Release remaining transports in CIG */ + spa_list_for_each(t, &monitor->transport_list, link) { + if (!transport_in_same_cig(transport, t) || t == transport) + continue; + + spa_log_debug(monitor->log, "Release CIG %d: transport %s", + transport->bap_cig, t->path); + + if (t->fd >= 0) + do_transport_release(t); + } + + spa_log_debug(monitor->log, "Release CIG %d: transport %s", + transport->bap_cig, transport->path); + } + + return do_transport_release(data); +} + +static int transport_set_delay(void *data, int64_t delay_nsec) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_monitor *monitor = transport->monitor; + DBusMessageIter it[2]; + spa_autoptr(DBusMessage) m = NULL; + uint16_t value; + const char *property = "Delay", *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE; + + if (!(transport->profile & SPA_BT_PROFILE_A2DP_DUPLEX)) + return -ENOTSUP; + + value = SPA_CLAMP(delay_nsec / (100 * SPA_NSEC_PER_USEC), 0, 10 * UINT16_MAX); + + if (transport->delay_us == 100 * value) + return 0; + transport->delay_us = 100 * value; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, transport->path, DBUS_INTERFACE_PROPERTIES, "Set"); + if (m == NULL) + return -ENOMEM; + + dbus_message_iter_init_append(m, &it[0]); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &interface); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &property); + dbus_message_iter_open_container(&it[0], DBUS_TYPE_VARIANT, DBUS_TYPE_UINT16_AS_STRING, &it[1]); + dbus_message_iter_append_basic(&it[1], DBUS_TYPE_UINT16, &value); + dbus_message_iter_close_container(&it[0], &it[1]); + + if (!dbus_connection_send(monitor->conn, m, NULL)) + return -EIO; + + spa_log_debug(monitor->log, "transport %p: set delay %d us", transport, 100 * value); + return 0; +} + +static const struct spa_bt_transport_implementation transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = transport_acquire, + .release = transport_release, + .set_volume = transport_set_volume, + .set_delay = transport_set_delay, +}; + +static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, struct spa_bt_monitor *monitor) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const struct media_codec *codec = NULL; + struct spa_bt_transport *transport; + char *tpath; + + if (!remote_endpoint->transport_path) { + spa_log_error(monitor->log, "Missing ASHA transport path"); + return -EINVAL; + } + + transport = spa_bt_transport_find(monitor, remote_endpoint->transport_path); + if (transport != NULL) { + spa_log_debug(monitor->log, "transport %p: free %s", + transport, transport->path); + spa_bt_transport_free(transport); + } + + tpath = strdup(remote_endpoint->transport_path); + transport = spa_bt_transport_create(monitor, tpath, 0); + if (transport == NULL) { + spa_log_error(monitor->log, "Failed to create transport for %s", + remote_endpoint->transport_path); + free(tpath); + return -EINVAL; + } + + spa_bt_transport_set_implementation(transport, &transport_impl, transport); + + spa_log_debug(monitor->log, "Created ASHA transport for %s", remote_endpoint->transport_path); + + for (int i = 0; media_codecs[i]; i++) { + const struct media_codec *mcodec = media_codecs[i]; + if (!mcodec->asha) + continue; + if (!spa_streq(mcodec->name, "g722")) + continue; + codec = mcodec; + spa_log_debug(monitor->log, "Setting ASHA codec: %s", mcodec->name); + } + + free(transport->endpoint_path); + transport->endpoint_path = strdup(remote_endpoint->path); + transport->profile = SPA_BT_PROFILE_ASHA_SINK; + transport->media_codec = codec; + transport->device = remote_endpoint->device; + + spa_list_append(&remote_endpoint->device->transport_list, &transport->device_link); + + spa_bt_device_update_last_bluez_action_time(transport->device); + + transport->volumes[SPA_BT_VOLUME_ID_TX].active = true; + transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME; + transport->n_channels = 1; + transport->channels[0] = SPA_AUDIO_CHANNEL_MONO; + + spa_bt_device_add_profile(transport->device, transport->profile); + spa_bt_device_connect_profile(transport->device, transport->profile); + + transport_sync_volume(transport); + + spa_log_debug(monitor->log, "ASHA transport setup complete"); + + return 0; +} + +static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata); + +static int media_codec_switch_cmp(const void *a, const void *b); + +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); + + cancel_and_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; + spa_autofree char *local_endpoint = NULL; + int res, config_size; + spa_autoptr(DBusMessage) m = NULL; + 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); + return false; + } + + /* 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); + return false; + } + + 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); + return false; + } + + 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); + return false; + } + + 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); + return false; + } + + /* 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); + return false; + } + } + + 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); + return false; + } + 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); + return false; + } + + 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); + sw->pending = send_with_reply(sw->device->monitor->conn, m, media_codec_switch_reply, sw); + if (!sw->pending) { + spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw); + return false; + } + + return true; +} + +static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw) +{ + while (*sw->codec_iter != NULL && *sw->path_iter != NULL) { + uint64_t now, threshold; + + /* Rate limit BlueZ calls */ + now = get_time_now(sw->device->monitor); + 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; + + spa_assert(sw->pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&sw->pending); + + spa_bt_device_update_last_bluez_action_time(device); + + if (!media_codec_switch_goto_active(sw)) + 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)); + goto next; + } + + /* 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->a2dp_application_registered && + !device->adapter->bap_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], device->connected_profiles)) { + 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]; + spa_autoptr(DBusMessage) r = NULL; + 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; + if (profile & SPA_BT_PROFILE_BAP_AUDIO) + transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_BAP_MAX; + else + transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_A2DP_MAX; + } + + free(transport->endpoint_path); + 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 : ""); + + 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; + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + spa_autoptr(DBusMessage) r = NULL; + const char *transport_path; + struct spa_bt_transport *transport; + + 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); + 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; + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + if (!reply_with_error(conn, m, BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented", "Method not implemented")) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + 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; + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + 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_legacy_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_adapter *adapter = user_data; + struct spa_bt_monitor *monitor = adapter->monitor; + + spa_autoptr(DBusMessage) r = steal_reply_and_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"); + return; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "RegisterEndpoint() failed: %s", + dbus_message_get_error_name(r)); + return; + } + + adapter->legacy_endpoints_registered = 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 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_legacy(struct spa_bt_adapter *adapter, + enum spa_bt_media_direction direction, + const char *uuid, const struct media_codec *codec) +{ + struct spa_bt_monitor *monitor = adapter->monitor; + const char *path = adapter->path; + spa_autofree char *object_path = NULL; + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter object_it, dict_it; + 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) + return ret; + + ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, &monitor->global_settings, caps); + if (ret < 0) + return ret; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + path, + BLUEZ_MEDIA_INTERFACE, + "RegisterEndpoint"); + if (m == NULL) + return -EIO; + + 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); + + if (!send_with_reply(monitor->conn, m, bluez_register_endpoint_legacy_reply, adapter)) + return -EIO; + + return 0; +} + +static int adapter_register_endpoints_legacy(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; + bool registered = false; + + if (a->legacy_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. " + "Please upgrade bluez5."); + + 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_legacy(a, 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_legacy(a, SPA_BT_MEDIA_SINK, + SPA_BT_UUID_A2DP_SINK, + codec))) + goto out; + } + + registered = true; + break; + } + + if (!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_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_SINK | SPA_BT_PROFILE_A2DP_SOURCE)) { + dbus_bool_t delay_reporting = TRUE; + + append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting); + } + if (spa_bt_profile_from_uuid(uuid) & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE)) { + dbus_uint32_t locations; + dbus_uint16_t supported_context, context; + + locations = BAP_CHANNEL_ALL; + if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_BAP_SINK) { + supported_context = context = BAP_CONTEXT_ALL; + } else { + supported_context = context = (BAP_CONTEXT_UNSPECIFIED | BAP_CONTEXT_CONVERSATIONAL | + BAP_CONTEXT_MEDIA | BAP_CONTEXT_GAME); + } + + append_basic_variant_dict_entry(&dict, "Locations", DBUS_TYPE_UINT32, "u", &locations); + append_basic_variant_dict_entry(&dict, "Context", DBUS_TYPE_UINT16, "q", &context); + append_basic_variant_dict_entry(&dict, "SupportedContext", DBUS_TYPE_UINT16, "q", &supported_context); + } + + 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, bool is_bap) +{ + struct spa_bt_monitor *monitor = user_data; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const char *path, *interface, *member; + 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; + spa_autoptr(DBusMessage) r = NULL; + + 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; + + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects")) { + spa_autoptr(DBusMessage) r = NULL; + struct spa_bt_adapter *a; + bool register_bcast = false; + + 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); + + /* + * Verify if an adapter exists that supports bap broadcast. + * If this adapter exists will register the broadcast endpoint. + */ + spa_list_for_each(a, &monitor->adapter_list, link) { + if (a->le_audio_bcast_supported) { + register_bcast = true; + break; + } + } + + 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 (codec->bap != is_bap) + continue; + if (codec->asha) + continue; + + if (!is_media_codec_enabled(monitor, codec)) + continue; + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) { + caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps); + if (caps_size < 0) + continue; + + spa_autofree char *endpoint = NULL; + 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); + } + } + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) { + caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps); + if (caps_size < 0) + continue; + + spa_autofree char *endpoint = NULL; + 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); + } + } + + if (codec->bap && register_bcast) { + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST)) { + caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps); + if (caps_size < 0) + continue; + + spa_autofree char *endpoint = NULL; + ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE_BROADCAST, &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, + SPA_BT_UUID_BAP_BROADCAST_SOURCE, + codec_id, caps, caps_size); + } + } + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST)) { + caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, &monitor->global_settings, caps); + if (caps_size < 0) + continue; + + spa_autofree char *endpoint = NULL; + ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK_BROADCAST, &endpoint); + if (ret == 0) { + spa_log_info(monitor->log, "register broadcast media sink codec %s: %s", media_codecs[i]->name, endpoint); + append_media_object(&array, endpoint, + SPA_BT_UUID_BAP_BROADCAST_SINK, + codec_id, caps, caps_size); + } + } + } + } + + 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 DBusHandlerResult object_manager_handler_a2dp(DBusConnection *c, DBusMessage *m, void *user_data) +{ + return object_manager_handler(c, m, user_data, false); +} + +static DBusHandlerResult object_manager_handler_bap(DBusConnection *c, DBusMessage *m, void *user_data) +{ + return object_manager_handler(c, m, user_data, true); +} + +static void bluez_register_application_a2dp_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_adapter *adapter = user_data; + struct spa_bt_monitor *monitor = adapter->monitor; + bool fallback = true; + + spa_autoptr(DBusMessage) r = steal_reply_and_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->a2dp_application_registered = true; + +finish: + if (fallback) + adapter_register_endpoints_legacy(adapter); +} + +static void bluez_register_application_bap_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_adapter *adapter = user_data; + struct spa_bt_monitor *monitor = adapter->monitor; + + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "RegisterApplication() failed: %s", + dbus_message_get_error_name(r)); + return; + } + + adapter->bap_application_registered = true; +} + +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; + + spa_autofree 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 DBus media endpoint: %s", object_path); + + if (!dbus_connection_register_object_path(monitor->conn, + object_path, + &vtable_endpoint, monitor)) + return -EIO; + + return 0; +} + +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_a2dp = { + .message_function = object_manager_handler_a2dp, + }; + const DBusObjectPathVTable vtable_object_manager_bap = { + .message_function = object_manager_handler_bap, + }; + + spa_log_info(monitor->log, "Registering DBus media object manager: %s", + A2DP_OBJECT_MANAGER_PATH); + + if (!dbus_connection_register_object_path(monitor->conn, + A2DP_OBJECT_MANAGER_PATH, + &vtable_object_manager_a2dp, monitor)) + return -EIO; + + spa_log_info(monitor->log, "Registering DBus media object manager: %s", + BAP_OBJECT_MANAGER_PATH); + + if (!dbus_connection_register_object_path(monitor->conn, + BAP_OBJECT_MANAGER_PATH, + &vtable_object_manager_bap, 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); + if (codec->bap) { + register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST); + register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST); + } + } + + 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; + + spa_autofree 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); +} + +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); + if (codec->bap) { + unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST); + unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST); + } + } + + dbus_connection_unregister_object_path(monitor->conn, BAP_OBJECT_MANAGER_PATH); + dbus_connection_unregister_object_path(monitor->conn, A2DP_OBJECT_MANAGER_PATH); +} + +static bool have_codec_endpoints(struct spa_bt_monitor *monitor, bool bap) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + int i; + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + + if (codec->bap != bap) + continue; + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK) || + endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE) || + endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST) || + endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST)) + return true; + } + return false; +} + +static int adapter_register_application(struct spa_bt_adapter *a, bool bap) +{ + const char *object_manager_path = bap ? BAP_OBJECT_MANAGER_PATH : A2DP_OBJECT_MANAGER_PATH; + struct spa_bt_monitor *monitor = a->monitor; + const char *ep_type_name = (bap ? "LE Audio" : "A2DP"); + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter i, d; + + if (bap && a->bap_application_registered) + return 0; + if (!bap && a->a2dp_application_registered) + return 0; + + if ((bap && !a->le_audio_supported) && (bap && !a->le_audio_bcast_supported)) { + spa_log_info(monitor->log, "Adapter %s indicates LE Audio unsupported: not registering application", + a->path); + return -ENOTSUP; + } + + if (!have_codec_endpoints(monitor, bap)) { + spa_log_warn(monitor->log, "No available %s codecs to register on adapter %s", + ep_type_name, a->path); + return -ENOENT; + } + + spa_log_debug(monitor->log, "Registering bluez5 %s media application on adapter %s", + ep_type_name, 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); + + if (!send_with_reply(monitor->conn, m, bap ? bluez_register_application_bap_reply : bluez_register_application_a2dp_reply, a)) + return -EIO; + + 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 void configure_bis(struct spa_bt_monitor *monitor, + const struct media_codec *codec, + DBusConnection *conn, + const char *object_path, + const char *interface_name, + struct spa_bt_big *big, + struct spa_bt_bis *bis, + const char *local_endpoint) +{ + DBusMessageIter iter, entry, variant, qos_dict; + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter dict; + int bis_id = 0xFF; + uint8_t caps [CC_MAX_LEN]; + uint8_t metadata [METADATA_MAX_LEN]; + uint8_t caps_size, metadata_size = 0; + struct bap_codec_qos qos; + int presentation_delay; + struct spa_bt_metadata *metadata_entry; + struct spa_dict settings; + struct spa_dict_item setting_items[2]; + char channel_allocation[64] = {0}; + + int mse = 0; + int options = 0; + int skip = 0; + int sync_cte_type = 0; + int sync_timeout = 2000; + int timeout = 2000; + + /* Configure each BIS from a BIG */ + spa_list_for_each(metadata_entry, &bis->metadata_list, link) { + if ((metadata_size + metadata_entry->length + 1) > METADATA_MAX_LEN) { + spa_log_warn(monitor->log, "Metadata configured for the BIS exceeds the maximum metadata size"); + return; + } + + metadata[metadata_size] = (uint8_t)metadata_entry->length; + metadata_size++; + metadata[metadata_size] = (uint8_t)metadata_entry->type; + metadata_size++; + memcpy(&metadata[metadata_size], metadata_entry->value, metadata_entry->length - 1); + metadata_size += metadata_entry->length - 1; + } + + spa_log_debug(monitor->log, "bis->channel_allocation %d", bis->channel_allocation); + if (bis->channel_allocation) + spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, bis->channel_allocation); + setting_items[0] = SPA_DICT_ITEM_INIT("channel_allocation", channel_allocation); + setting_items[1] = SPA_DICT_ITEM_INIT("preset", bis->qos_preset); + settings = SPA_DICT_INIT(setting_items, 2); + + codec->get_bis_config(codec, caps, &caps_size, &settings, &qos); + + msg = dbus_message_new_method_call(BLUEZ_SERVICE, + object_path, + interface_name, + "SetConfiguration"); + + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size); + + append_basic_array_variant_dict_entry(&dict, "Metadata", "ay", "y", DBUS_TYPE_BYTE, metadata, metadata_size); + + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &(const char *) { "QoS" }); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "a{sv}", &variant); + + dbus_message_iter_open_container(&variant, 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, + &qos_dict); + + append_basic_variant_dict_entry(&qos_dict, "BIG", DBUS_TYPE_BYTE, "y", &big->big_id); + append_basic_variant_dict_entry(&qos_dict, "BIS", DBUS_TYPE_BYTE, "y", &bis_id); + + /* sync_factor should be >=2 to avoid invalid extended advertising interval value */ + if (big->sync_factor < 2) + big->sync_factor = 2; + + append_basic_variant_dict_entry(&qos_dict, "SyncFactor", DBUS_TYPE_BYTE, "y", &big->sync_factor); + append_basic_variant_dict_entry(&qos_dict, "Options", DBUS_TYPE_BYTE, "y", &options); + append_basic_variant_dict_entry(&qos_dict, "Skip", DBUS_TYPE_UINT16, "q", &skip); + append_basic_variant_dict_entry(&qos_dict, "SyncTimeout", DBUS_TYPE_UINT16, "q", &sync_timeout); + append_basic_variant_dict_entry(&qos_dict, "SyncCteType", DBUS_TYPE_BYTE, "y", &sync_cte_type); + append_basic_variant_dict_entry(&qos_dict, "MSE", DBUS_TYPE_BYTE, "y", &mse); + append_basic_variant_dict_entry(&qos_dict, "Timeout", DBUS_TYPE_UINT16, "q", &timeout); + append_basic_array_variant_dict_entry(&qos_dict, "BCode", "ay", "y", DBUS_TYPE_BYTE, big->broadcast_code, BROADCAST_CODE_LEN); + append_basic_variant_dict_entry(&qos_dict, "Encryption", DBUS_TYPE_BYTE, "y", &big->encryption); + append_basic_variant_dict_entry(&qos_dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval); + append_basic_variant_dict_entry(&qos_dict, "Framing", DBUS_TYPE_BYTE, "y", &qos.framing); + append_basic_variant_dict_entry(&qos_dict, "PHY", DBUS_TYPE_BYTE, "y", &qos.phy); + append_basic_variant_dict_entry(&qos_dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu); + append_basic_variant_dict_entry(&qos_dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission); + append_basic_variant_dict_entry(&qos_dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency); + append_basic_variant_dict_entry(&qos_dict, "PresentationDelay", DBUS_TYPE_UINT32, "u", &presentation_delay); + + dbus_message_iter_close_container(&variant, &qos_dict); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + + dbus_message_iter_close_container(&iter, &dict); + dbus_message_set_no_reply(msg, TRUE); + if (!dbus_connection_send(conn, msg, NULL)) { + spa_log_error(monitor->log, "sending SetConfiguration failed"); + } +} + +static void configure_bcast_source(struct spa_bt_monitor *monitor, + const struct media_codec *codec, + DBusConnection *conn, + const char *object_path, + const char *interface_name, + const char *local_endpoint) +{ + struct spa_bt_big *big; + struct spa_bt_bis *bis; + /* Configure each BIS from a BIG */ + spa_list_for_each(big, &monitor->bcast_source_config_list, link) { + spa_list_for_each(bis, &big->bis_list, link) { + configure_bis(monitor, codec, conn, object_path, interface_name, + big, bis, local_endpoint); + } + } +} + +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) || + spa_streq(interface_name, BLUEZ_MEDIA_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; + } + } + + if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) { + adapter_update_props(a, props_iter, NULL); + a->has_adapter1_interface = true; + } else { + adapter_media_update_props(a, props_iter, NULL); + a->has_media1_interface = true; + } + + if (a->has_adapter1_interface && a->has_media1_interface) { + adapter_register_application(a, false); + adapter_register_application(a, true); + 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; + } + spa_log_info(monitor->log, "Created Bluetooth device %s", + object_path); + } + + 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_DEVICE_SET_INTERFACE)) { + device_set_update_props(monitor, object_path, props_iter, 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, 0); + + if (spa_streq(ep->uuid, SPA_BT_UUID_BAP_BROADCAST_SINK)) { + int ret, i; + bool codec_found = false; + spa_autofree char *local_endpoint = NULL; + /* get local endpoint */ + + for (i = 0; monitor->media_codecs[i]; i++) { + if (!monitor->media_codecs[i]->bap) + continue; + if (!is_media_codec_enabled(monitor, monitor->media_codecs[i])) + continue; + if (monitor->media_codecs[i]->codec_id == ep->codec){ + ret = media_codec_to_endpoint(monitor->media_codecs[i], SPA_BT_MEDIA_SOURCE_BROADCAST, &local_endpoint); + if (ret == 0) { + codec_found = true; + break; + } + } + } + + if (!codec_found) { + spa_log_warn(monitor->log, "endpoint codec not found"); + return; + } + + if (local_endpoint != NULL) + configure_bcast_source(monitor, monitor->media_codecs[i], conn, object_path, interface_name, local_endpoint); + } + } +} + +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_DEVICE_SET_INTERFACE)) { + device_set_update_props(monitor, object_path, NULL, NULL); + } else if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE) || + spa_streq(interface_name, BLUEZ_MEDIA_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, 0); + } + } else if (spa_streq(interface_name, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) { + struct spa_bt_transport *transport; + transport = spa_bt_transport_find(monitor, object_path); + if (transport != NULL) { + if (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) { + struct spa_bt_device *d = transport->device; + if (d != NULL){ + device_free(d); + } + } else if (transport->profile == SPA_BT_PROFILE_BAP_BROADCAST_SOURCE) { + /* + * For each transport that has a broadcast source profile, + * we need to create a new node for each BIS. + * example of transport path = /org/bluez/hci0/dev_2D_9D_93_F9_D7_5E/bis1/fd0 + * Create new devices only for a case of a big with multiple BISes, + * for this case will have the scanned device to the transport + * "/fd0" and create new devices for the other transports from this device + * that appear only in case of multiple BISes per BIG. + * + * Here we delete the created devices. + */ + char *pos = strstr(transport->path, "/fd0"); + if (pos == NULL) { + struct spa_bt_device *d = transport->device; + if (d != NULL){ + device_free(d); + } + } + } + spa_bt_transport_free(transport); + } + } + + dbus_message_iter_next(&it); + } +} + +static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + DBusMessageIter it[6]; + + spa_assert(monitor->get_managed_objects_call == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&monitor->get_managed_objects_call); + 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"); + return; + } + + if (dbus_message_is_error(r, DBUS_ERROR_NAME_HAS_NO_OWNER)) { + spa_log_warn(monitor->log, "BlueZ system service is not available"); + return; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "GetManagedObjects() failed: %s", + dbus_message_get_error_name(r)); + return; + } + + 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()"); + return; + } + + 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; +} + +static void get_managed_objects(struct spa_bt_monitor *monitor) +{ + if (monitor->objects_listed || monitor->get_managed_objects_call) + return; + + spa_autoptr(DBusMessage) m = NULL; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + "/", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects"); + + dbus_message_set_auto_start(m, false); + + monitor->get_managed_objects_call = send_with_reply(monitor->conn, m, get_managed_objects_reply, monitor); +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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 finish; + } + + 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) || + spa_streq(iface, BLUEZ_MEDIA_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); + + if (spa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) + adapter_update_props(a, &it[1], NULL); + else + adapter_media_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_DEVICE_SET_INTERFACE)) { + device_set_update_props(monitor, path, &it[1], 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, 0); + } + 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'. " + "Multiple sound server instances (PipeWire/Pulseaudio/bluez-alsa) are " + "probably trying to use Bluetooth audio at the same time, which can " + "cause problems. The system configuration likely should be fixed " + "to have only one sound server that manages Bluetooth audio.", + path); + goto finish; + } + + spa_log_debug(monitor->log, "Properties changed in transport %s", path); + + transport_update_props(transport, &it[1], NULL); + } + } + +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void add_filters(struct spa_bt_monitor *this) +{ + if (this->filters_added) + return; + + if (!dbus_connection_add_filter(this->conn, filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + return; + } + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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_MEDIA_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_DEVICE_SET_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; +} + +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; + struct spa_bt_big *b; + 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; + } + + cancel_and_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); + spa_list_consume(b, &monitor->bcast_source_config_list, link) + big_entry_free(b); + + 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_array; + char role_name[256]; + enum spa_bt_profile profiles = SPA_BT_PROFILE_NULL; + + if (spa_json_begin_array(&it_array, str, strlen(str)) <= 0) + return -EINVAL; + + while (spa_json_get_string(&it_array, role_name, sizeof(role_name)) > 0) { + 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; + } else if (spa_streq(role_name, "bap_bcast_source")) { + profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + } else if (spa_streq(role_name, "bap_bcast_sink")) { + profiles |= SPA_BT_PROFILE_BAP_BROADCAST_SINK; + } else if (spa_streq(role_name, "asha_sink")) { + profiles |= SPA_BT_PROFILE_ASHA_SINK; + } + } + + return profiles; +} + +static int parse_roles(struct spa_bt_monitor *monitor, const struct spa_dict *info) +{ + const char *str; + int res = 0; + int profiles = SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE | SPA_BT_PROFILE_ASHA_SINK; + + /* HSP/HFP backends parse this property separately */ + if (info && (str = spa_dict_lookup(info, "bluez5.roles"))) { + res = spa_bt_profiles_from_json_array(str); + if (res < 0) { + spa_log_warn(monitor->log, "malformed bluez5.roles setting ignored"); + goto done; + } + + profiles &= res; + } + + res = 0; + +done: + monitor->enabled_profiles = profiles; + return res; +} + +static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const struct spa_dict *info) +{ + const char *str; + char key[256]; + char bis_key[256]; + char qos_key[256]; + char bcode[BROADCAST_CODE_LEN + 3]; + int cursor; + int big_id = 0; + struct spa_json it[3], it_array[4]; + struct spa_list big_list = SPA_LIST_INIT(&big_list); + struct spa_error_location loc; + struct spa_bt_big *big; + + /* Search for bluez5.bcast_source.config */ + if (!(info && (str = spa_dict_lookup(info, "bluez5.bcast_source.config")))) + return; + + /* Verify is an array of BIGS */ + if (spa_json_begin_array(&it_array[0], str, strlen(str)) <= 0) + goto parse_failed; + + /* Iterate on all BIG objects */ + while (spa_json_enter_object(&it_array[0], &it[0]) > 0) { + struct spa_bt_big *big_entry = calloc(1, sizeof(struct spa_bt_big)); + + if (!big_entry) + goto errno_failed; + + big_entry->big_id = big_id++; + spa_list_init(&big_entry->bis_list); + spa_list_append(&big_list, &big_entry->link); + + /* Iterate on all BIG values */ + while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) { + if (spa_streq(key, "broadcast_code")) { + if (spa_json_get_string(&it[0], bcode, sizeof(bcode)) <= 0) + goto parse_failed; + if (strlen(bcode) > BROADCAST_CODE_LEN) + goto parse_failed; + memcpy(big_entry->broadcast_code, bcode, strlen(bcode)); + spa_log_debug(monitor->log, "big_entry->broadcast_code %s", big_entry->broadcast_code); + } else if (spa_streq(key, "encryption")) { + if (spa_json_get_bool(&it[0], &big_entry->encryption) <= 0) + goto parse_failed; + spa_log_debug(monitor->log, "big_entry->encryption %d", big_entry->encryption); + } else if (spa_streq(key, "sync_factor")) { + if (spa_json_get_int(&it[0], &big_entry->sync_factor) <= 0) + goto parse_failed; + spa_log_debug(monitor->log, "big_entry->sync_factor %d", big_entry->sync_factor); + } else if (spa_streq(key, "bis")) { + if (spa_json_enter_array(&it[0], &it_array[1]) <= 0) + goto parse_failed; + while (spa_json_enter_object(&it_array[1], &it[1]) > 0) { + /* Iterate on all BIS values */ + struct spa_bt_bis *bis_entry = calloc(1, sizeof(struct spa_bt_bis)); + + if (!bis_entry) + goto errno_failed; + + spa_list_init(&bis_entry->metadata_list); + spa_list_append(&big_entry->bis_list, &bis_entry->link); + + while (spa_json_get_string(&it[1], bis_key, sizeof(bis_key)) > 0) { + if (spa_streq(bis_key, "qos_preset")) { + if (spa_json_get_string(&it[1], bis_entry->qos_preset, sizeof(bis_entry->qos_preset)) <= 0) + goto parse_failed; + spa_log_debug(monitor->log, "bis_entry->qos_preset %s", bis_entry->qos_preset); + } else if (spa_streq(bis_key, "audio_channel_allocation")) { + if (spa_json_get_int(&it[1], &bis_entry->channel_allocation) <= 0) + goto parse_failed; + spa_log_debug(monitor->log, "bis_entry->channel_allocation %d", bis_entry->channel_allocation); + } else if (spa_streq(bis_key, "metadata")) { + if (spa_json_enter_array(&it[1], &it_array[2]) <= 0) + goto parse_failed; + while (spa_json_enter_object(&it_array[2], &it[2]) > 0) { + struct spa_bt_metadata *metadata_entry = calloc(1, sizeof(struct spa_bt_metadata)); + + if (!metadata_entry) + goto errno_failed; + + spa_list_append(&bis_entry->metadata_list, &metadata_entry->link); + + while (spa_json_get_string(&it[2], qos_key, sizeof(qos_key)) > 0) { + if (spa_streq(qos_key, "type")) { + if (spa_json_get_int(&it[2], &metadata_entry->type) <= 0) + goto parse_failed; + spa_log_debug(monitor->log, "metadata_entry->type %d", metadata_entry->type); + } else if (spa_streq(qos_key, "value")) { + if (spa_json_enter_array(&it[2], &it_array[3]) <= 0) + goto parse_failed; + for (cursor = 0; cursor < METADATA_MAX_LEN - 1; cursor++) { + int temp_val = 0; + if (spa_json_get_int(&it_array[3], &temp_val) <= 0) + break; + metadata_entry->value[cursor] = (uint8_t)temp_val; + spa_log_debug(monitor->log, "metadata_entry->value[%d] %d", cursor, metadata_entry->value[cursor]); + } + /* length is size of value plus 1 octet for type */ + metadata_entry->length = cursor + 1; + spa_log_debug(monitor->log, "metadata_entry->length %d", metadata_entry->length); + spa_log_debug(monitor->log, "metadata_entry->value_size %d", cursor); + } + } + } + } + } + } + } + } + } + + spa_list_insert_list(&monitor->bcast_source_config_list, &big_list); + return; + +errno_failed: + spa_log_warn(monitor->log, "failed in bluez5.bcast_source.config: %m"); + goto cleanup; + +parse_failed: + str = spa_dict_lookup(info, "bluez5.bcast_source.config"); + if (spa_json_get_error(&it_array[0], str, &loc)) { + spa_debug_log_error_location(monitor->log, SPA_LOG_LEVEL_WARN, + &loc, "malformed bluez5.bcast_source.config: %s", loc.reason); + } else { + spa_log_warn(monitor->log, "malformed bluez5.bcast_source.config"); + } + goto cleanup; + +cleanup: + spa_list_consume(big, &big_list, link) + big_entry_free(big); +} + +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_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; + + if (spa_json_begin_array(&it_array, str, strlen(str)) <= 0) { + spa_log_error(this->log, "property bluez5.codecs '%s' is not an array", str); + goto fallback; + } + + 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->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + 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); + spa_list_init(&this->bcast_source_config_list); + + if ((res = parse_codec_array(this, info)) < 0) + goto fail; + + parse_roles(this, info); + parse_broadcast_source_config(this, info); + + 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..2095ba8 --- /dev/null +++ b/spa/plugins/bluez5/bluez5-device.c @@ -0,0 +1,3084 @@ +/* Spa Bluez5 Device */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" +#include "media-codecs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.device"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define MAX_NODES (2*SPA_AUDIO_MAX_CHANNELS) + +#define DEVICE_ID_SOURCE 0 +#define DEVICE_ID_SINK 1 +#define DEVICE_ID_SOURCE_SET (MAX_NODES + 0) +#define DEVICE_ID_SINK_SET (MAX_NODES + 1) + +#define SINK_ID_FLAG 0x1 +#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_ASHA = 5, + DEVICE_PROFILE_LAST, +}; + +enum { + ROUTE_INPUT = 0, + ROUTE_OUTPUT, + ROUTE_HF_OUTPUT, + ROUTE_SET_INPUT, + ROUTE_SET_OUTPUT, + ROUTE_LAST, +}; + +struct props { + enum spa_bluetooth_audio_codec codec; + bool offload_active; +}; + +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 device_set_member { + struct impl *impl; + struct spa_bt_transport *transport; + struct spa_hook listener; + uint32_t id; +}; + +struct device_set { + struct impl *impl; + char *path; + bool sink_enabled; + bool source_enabled; + bool leader; + uint32_t sinks; + uint32_t sources; + struct device_set_member sink[SPA_AUDIO_MAX_CHANNELS]; + struct device_set_member source[SPA_AUDIO_MAX_CHANNELS]; +}; + +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; + + struct device_set device_set; + + const struct media_codec **supported_codecs; + size_t supported_codec_count; + + struct dynamic_node dyn_nodes[MAX_NODES + 2]; + +#define MAX_SETTINGS 32 + struct spa_dict_item setting_items[MAX_SETTINGS]; + struct spa_dict setting_dict; + + struct node nodes[MAX_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, enum spa_bt_profile profile) +{ + 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; + } + } + + if (!media_codec) + return NULL; + + if (!spa_bt_device_supports_media_codec(this->bt_dev, media_codec, profile)) + return NULL; + + return media_codec; +} + +static bool is_bap_client(struct impl *this) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_bt_transport *t; + + spa_list_for_each(t, &device->transport_list, device_link) { + if (t->bap_initiator) + return true; + } + + return false; +} + +static bool can_bap_codec_switch(struct impl *this) +{ + if (!is_bap_client(this)) + return false; + + /* XXX: codec switching for source/duplex is not currently + * XXX: implemented properly. TODO: fix this + */ + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE) + return false; + + return true; +} + +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; + case SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB: + return HFP_AUDIO_CODEC_LC3_SWB; + 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_LC3_SWB: + return SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB; + 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_LC3_SWB: + return "LC3-SWB"; + 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_LC3_SWB: + return "lc3_swb"; + 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 & SINK_ID_FLAG) && + !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->active || !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 const char *get_channel_name(uint32_t channel) +{ + return spa_type_to_short_name(channel, spa_type_audio_channel, NULL); +} + +static int channel_position_cmp(const void *pa, const void *pb) +{ + uint32_t a = *(uint32_t *)pa, b = *(uint32_t *)pb; + return (int)a - (int)b; +} + +static void emit_device_set_node(struct impl *this, uint32_t id) +{ + struct spa_bt_device *device = this->bt_dev; + struct node *node = &this->nodes[id]; + struct spa_device_object_info info; + struct spa_dict_item items[8]; + char str_id[32], members_json[8192], channels_json[512]; + struct device_set_member *members; + uint32_t n_members; + uint32_t n_items = 0; + struct spa_strbuf json; + unsigned int i; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address); + items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set", this->device_set.path); + items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.leader", "true"); + snprintf(str_id, sizeof(str_id), "%d", id); + items[n_items++] = SPA_DICT_ITEM_INIT("card.profile.device", str_id); + items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", "1"); + + if (id == DEVICE_ID_SOURCE_SET) { + items[n_items++] = SPA_DICT_ITEM_INIT("media.class", "Audio/Source"); + members = this->device_set.source; + n_members = this->device_set.sources; + } else if (id == DEVICE_ID_SINK_SET) { + items[n_items++] = SPA_DICT_ITEM_INIT("media.class", "Audio/Sink"); + members = this->device_set.sink; + n_members = this->device_set.sinks; + } else { + spa_assert_not_reached(); + } + + node->impl = this; + node->active = true; + node->transport = NULL; + node->a2dp_duplex = false; + node->offload_acquired = false; + node->mute = false; + node->save = false; + node->latency_offset = 0; + + /* Form channel map from members */ + node->n_channels = 0; + for (i = 0; i < n_members; ++i) { + struct spa_bt_transport *t = members[i].transport; + unsigned int j; + + for (j = 0; j < t->n_channels; ++j) { + unsigned int k; + + if (!get_channel_name(t->channels[j])) + continue; + + for (k = 0; k < node->n_channels; ++k) { + if (node->channels[k] == t->channels[j]) + break; + } + if (k == node->n_channels && node->n_channels < SPA_AUDIO_MAX_CHANNELS) + node->channels[node->n_channels++] = t->channels[j]; + } + } + + qsort(node->channels, node->n_channels, sizeof(uint32_t), channel_position_cmp); + + for (i = 0; i < node->n_channels; ++i) { + /* Session manager will override this, so put in some safe number */ + node->volumes[i] = node->soft_volumes[i] = 0.064f; + } + + /* Produce member info json */ + spa_strbuf_init(&json, members_json, sizeof(members_json)); + spa_strbuf_append(&json, "["); + for (i = 0; i < n_members; ++i) { + struct spa_bt_transport *t = members[i].transport; + uint32_t member_id = members[i].id; + char object_path[512]; + unsigned int j; + + if (i > 0) + spa_strbuf_append(&json, ","); + spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%"PRIu32, + this->device_set.path, t->device->address, member_id); + spa_strbuf_append(&json, "{\"object.path\":\"%s\",\"channels\":[", object_path); + for (j = 0; j < t->n_channels; ++j) { + if (j > 0) + spa_strbuf_append(&json, ","); + spa_strbuf_append(&json, "\"%s\"", get_channel_name(t->channels[j])); + } + spa_strbuf_append(&json, "]}"); + } + spa_strbuf_append(&json, "]"); + json.buffer[SPA_MIN(json.pos, json.maxsize-1)] = 0; + items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.members", members_json); + + spa_strbuf_init(&json, channels_json, sizeof(channels_json)); + spa_strbuf_append(&json, "["); + for (i = 0; i < node->n_channels; ++i) { + if (i > 0) + spa_strbuf_append(&json, ","); + spa_strbuf_append(&json, "\"%s\"", get_channel_name(node->channels[i])); + } + spa_strbuf_append(&json, "]"); + json.buffer[SPA_MIN(json.pos, json.maxsize-1)] = 0; + items[n_items++] = SPA_DICT_ITEM_INIT("api.bluez5.set.channels", channels_json); + + /* Emit */ + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + info.factory_name = (id == DEVICE_ID_SOURCE_SET) ? "source" : "sink"; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.props = &SPA_DICT_INIT(items, n_items); + + spa_device_emit_object_info(&this->hooks, id, &info); + + emit_node_props(this, &this->nodes[id], true); +} + +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[11]; + uint32_t n_items = 0; + char transport[32], str_id[32], object_path[512]; + bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG); + bool in_device_set = false; + + spa_log_debug(this->log, "%p: node, transport:%p id:%08x factory:%s", this, t, id, factory_name); + + if (id & SINK_ID_FLAG) + in_device_set = this->device_set.sink_enabled; + else + in_device_set = this->device_set.source_enabled; + + snprintf(transport, sizeof(transport), "pointer:%p", t); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport); + 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 && !in_device_set) { + snprintf(str_id, sizeof(str_id), "%d", id); + items[n_items] = 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++; + } + if (in_device_set) { + items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.set", this->device_set.path); + n_items++; + items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.internal", "true"); + n_items++; + + /* object.path can be used in match rules with only basic node props */ + spa_scnprintf(object_path, sizeof(object_path), "%s/%s-%d", + this->device_set.path, device->address, id); + items[n_items] = SPA_DICT_ITEM_INIT("object.path", object_path); + 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_assert(id < SPA_N_ELEMENTS(this->nodes)); + + spa_device_emit_object_info(&this->hooks, id, &info); + + if (in_device_set) { + /* Device set member nodes don't have their own routes */ + this->nodes[id].impl = this; + this->nodes[id].active = false; + if (this->nodes[id].transport) + spa_hook_remove(&this->nodes[id].transport_listener); + this->nodes[id].transport = NULL; + return; + } + + 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. */ + 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 void init_dummy_input_node(struct impl *this, uint32_t id) +{ + uint32_t prev_channels = this->nodes[id].n_channels; + + /* Don't emit a device node, only initialize volume etc. for the route */ + + spa_log_debug(this->log, "%p: node, id:%08x", this, id); + + this->nodes[id].impl = this; + this->nodes[id].active = true; + this->nodes[id].offload_acquired = false; + this->nodes[id].a2dp_duplex = false; + this->nodes[id].n_channels = 1; + this->nodes[id].channels[0] = SPA_AUDIO_CHANNEL_MONO; + + if (prev_channels > 0) { + size_t i; + + /* Spread mono volume to all channels */ + for (i = prev_channels; i < this->nodes[id].n_channels; ++i) + this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels]; + } +} + +static bool transport_enabled(struct spa_bt_transport *t, int profile) +{ + return (t->profile & t->device->connected_profiles) && + (t->profile & profile) == t->profile; +} + +static struct spa_bt_transport *find_device_transport(struct spa_bt_device *device, int profile) +{ + struct spa_bt_transport *t; + + spa_list_for_each(t, &device->transport_list, device_link) { + if (transport_enabled(t, profile)) + return t; + } + + return NULL; +} + +static struct spa_bt_transport *find_transport(struct impl *this, int profile) +{ + return find_device_transport(this->bt_dev, profile); +} + +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 %d: volume %d changed %f, profile %d", + node->id, 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 impl *impl, + struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex) +{ + struct dynamic_node *this = &impl->dyn_nodes[id]; + + spa_assert(id < SPA_N_ELEMENTS(impl->dyn_nodes)); + + spa_log_debug(impl->log, "%p: dynamic node, transport: %p->%p id: %08x->%08x", + this, 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 void device_set_clear(struct impl *impl, struct device_set *set) +{ + unsigned int i; + + for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i) + if (set->sink[i].transport) + spa_hook_remove(&set->sink[i].listener); + + for (i = 0; i < SPA_N_ELEMENTS(set->source); ++i) + if (set->source[i].transport) + spa_hook_remove(&set->source[i].listener); + + free(set->path); + spa_zero(*set); + + set->impl = impl; + for (i = 0; i < SPA_N_ELEMENTS(set->sink); ++i) + set->sink[i].impl = impl; + for (i = 0; i < SPA_N_ELEMENTS(set->source); ++i) + set->source[i].impl = impl; +} + +static void device_set_transport_destroy(void *data) +{ + struct device_set_member *member = data; + + member->transport = NULL; + spa_hook_remove(&member->listener); +} + +static const struct spa_bt_transport_events device_set_transport_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .destroy = device_set_transport_destroy, +}; + +static void device_set_update(struct impl *this, struct device_set *dset) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_bt_set_membership *set; + struct spa_bt_set_membership tmp_set = { + .device = device, + .rank = 0, + .leader = true, + .path = device->path, + .others = SPA_LIST_INIT(&tmp_set.others), + }; + struct spa_list tmp_set_list = SPA_LIST_INIT(&tmp_set_list); + struct spa_list *membership_list = &device->set_membership_list; + + /* + * If no device set, use a dummy one, so that we can handle also those devices + * here (they may have multiple transports regardless). + */ + if (spa_list_is_empty(membership_list)) { + spa_list_append(&tmp_set_list, &tmp_set.link); + membership_list = &tmp_set_list; + } + + spa_list_for_each(set, membership_list, link) { + struct spa_bt_set_membership *s; + int num_devices = 0; + + device_set_clear(this, dset); + + spa_bt_for_each_set_member(s, set) { + struct spa_bt_transport *t; + bool active = false; + uint32_t source_id = DEVICE_ID_SOURCE; + uint32_t sink_id = DEVICE_ID_SINK; + + if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX)) + continue; + + spa_list_for_each(t, &s->device->transport_list, device_link) { + if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE)) + continue; + if (!transport_enabled(t, SPA_BT_PROFILE_BAP_SOURCE)) + continue; + if (dset->sources >= SPA_N_ELEMENTS(dset->source)) + break; + + active = true; + dset->source[dset->sources].impl = this; + dset->source[dset->sources].transport = t; + dset->source[dset->sources].id = source_id; + source_id += 2; + spa_bt_transport_add_listener(t, &dset->source[dset->sources].listener, + &device_set_transport_events, &dset->source[dset->sources]); + ++dset->sources; + } + + spa_list_for_each(t, &s->device->transport_list, device_link) { + if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_SINK)) + continue; + if (!transport_enabled(t, SPA_BT_PROFILE_BAP_SINK)) + continue; + if (dset->sinks >= SPA_N_ELEMENTS(dset->sink)) + break; + + active = true; + dset->sink[dset->sinks].impl = this; + dset->sink[dset->sinks].transport = t; + dset->sink[dset->sinks].id = sink_id; + sink_id += 2; + spa_bt_transport_add_listener(t, &dset->sink[dset->sinks].listener, + &device_set_transport_events, &dset->sink[dset->sinks]); + ++dset->sinks; + } + + if (active) + ++num_devices; + } + + spa_log_debug(this->log, "%p: %s belongs to set %s leader:%d", this, + device->path, set->path, set->leader); + + if (is_bap_client(this)) { + dset->path = strdup(set->path); + dset->leader = set->leader; + } else { + /* XXX: device set nodes for BAP server not supported, + * XXX: it'll appear as multiple streams + */ + dset->path = NULL; + dset->leader = false; + } + + if (num_devices > 1) + break; + } + + dset->sink_enabled = dset->path && (dset->sinks > 1); + dset->source_enabled = dset->path && (dset->sources > 1); +} + +static bool device_set_equal(struct device_set *a, struct device_set *b) +{ + unsigned int i; + + if (!spa_streq(a->path, b->path) || a->sink_enabled != b->sink_enabled || + a->source_enabled != b->source_enabled || a->leader != b->leader || + a->sinks != b->sinks || a->sources != b->sources) + return false; + for (i = 0; i < a->sinks; ++i) + if (a->sink[i].transport != b->sink[i].transport) + return false; + for (i = 0; i < a->sources; ++i) + if (a->source[i].transport != b->source[i].transport) + return false; + return true; +} + +static int emit_nodes(struct impl *this) +{ + struct spa_bt_transport *t; + + this->props.codec = 0; + + device_set_update(this, &this->device_set); + + 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); + if (!t) + t = find_transport(this, SPA_BT_PROFILE_HSP_AG); + if (t) { + this->props.codec = get_hfp_codec_id(t->codec); + emit_dynamic_node(this, t, 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false); + emit_dynamic_node(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); + if (t) { + this->props.codec = t->media_codec->id; + emit_dynamic_node(this, t, 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false); + + if (t->media_codec->duplex_codec) + emit_dynamic_node(this, t, 3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true); + } + } + break; + case DEVICE_PROFILE_ASHA: + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK) { + t = find_transport(this, SPA_BT_PROFILE_ASHA_SINK); + if (t) { + this->props.codec = t->media_codec->id; + emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + } else { + spa_log_warn(this->log, "Unable to find transport for ASHA"); + } + } + break; + case DEVICE_PROFILE_A2DP: + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) { + t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE); + if (t) { + this->props.codec = t->media_codec->id; + emit_dynamic_node(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); + 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); + } + } + } + + /* Setup route for HFP input, for tracking its volume even though there is + * no node emitted yet. */ + if ((this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) && + !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) && + !this->nodes[DEVICE_ID_SOURCE].active) + init_dummy_input_node(this, DEVICE_ID_SOURCE); + + if (!this->props.codec) + this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_SBC; + break; + case DEVICE_PROFILE_BAP: { + struct device_set *set = &this->device_set; + unsigned int i; + + for (i = 0; i < set->sources; ++i) { + struct spa_bt_transport *t = set->source[i].transport; + uint32_t id = set->source[i].id; + + if (id >= MAX_NODES) + continue; + if (t->device != this->bt_dev) + continue; + + this->props.codec = t->media_codec->id; + if (t->bap_initiator) + emit_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false); + else + emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false); + } + + if (set->source_enabled && set->leader) + emit_device_set_node(this, DEVICE_ID_SOURCE_SET); + + for (i = 0; i < set->sinks; ++i) { + struct spa_bt_transport *t = set->sink[i].transport; + uint32_t id = set->sink[i].id; + + if (id >= MAX_NODES) + continue; + if (t->device != this->bt_dev) + continue; + + this->props.codec = t->media_codec->id; + if (t->bap_initiator) + emit_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + else + emit_dynamic_node(this, t, id, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + } + + if (set->sink_enabled && set->leader) + emit_device_set_node(this, DEVICE_ID_SINK_SET); + + if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SINK)) { + t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SINK); + if (t) { + this->props.codec = t->media_codec->id; + emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + } + } + + if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + t = find_transport(this, SPA_BT_PROFILE_BAP_BROADCAST_SOURCE); + if (t) { + this->props.codec = t->media_codec->id; + emit_dynamic_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false); + } + } + + if (!this->props.codec) + this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_LC3; + 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); + if (!t) + t = find_transport(this, SPA_BT_PROFILE_HSP_HS); + if (t) { + 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 (!this->props.codec) + this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_CVSD; + 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) +{ + spa_log_debug(this->log, "%p: remove nodes", this); + + for (uint32_t i = 0; i < SPA_N_ELEMENTS(this->dyn_nodes); i++) + remove_dynamic_node (&this->dyn_nodes[i]); + + for (uint32_t i = 0; i < SPA_N_ELEMENTS(this->nodes); 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_ASHA || codec == this->props.codec) && + (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; + + /* + * 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. + * + * For BAP, only BAP client can configure the codec. + * + * XXX: codec switching also currently does not work in the duplex or + * XXX: source-only case, as it will only switch the sink, and we only + * XXX: list the sink codecs here. TODO: fix this + */ + if ((profile == DEVICE_PROFILE_A2DP || (profile == DEVICE_PROFILE_BAP && can_bap_codec_switch(this))) + && !(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)) { + 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; + 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) + spa_log_error(this->log, "failed to switch codec (%d)", status); + + 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 bool device_set_needs_update(struct impl *this) +{ + struct device_set dset = { .impl = this }; + bool changed; + + if (this->profile != DEVICE_PROFILE_BAP) + return false; + + device_set_update(this, &dset); + changed = !device_set_equal(&dset, &this->device_set); + device_set_clear(this, &dset); + return changed; +} + +static void profiles_changed(void *userdata, uint32_t connected_change) +{ + struct impl *this = userdata; + bool nodes_changed = false; + + /* Profiles changed. We have to re-emit device information. */ + spa_log_info(this->log, "profiles changed to %08x %08x (change %08x) switching_codec:%d", + this->bt_dev->profiles, this->bt_dev->connected_profiles, + connected_change, this->switching_codec); + + if (this->switching_codec) + return; + + free(this->supported_codecs); + this->supported_codecs = spa_bt_device_get_supported_media_codecs( + this->bt_dev, &this->supported_codec_count); + + 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_A2DP_SOURCE)); + spa_log_debug(this->log, "profiles changed: AG nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_ASHA: + nodes_changed = (connected_change & SPA_BT_PROFILE_ASHA_SINK); + spa_log_debug(this->log, "profiles changed: ASHA nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_A2DP: + nodes_changed = (connected_change & SPA_BT_PROFILE_A2DP_DUPLEX); + spa_log_debug(this->log, "profiles changed: A2DP nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_BAP: + nodes_changed = ((connected_change & SPA_BT_PROFILE_BAP_DUPLEX) + && device_set_needs_update(this)) + || (connected_change & (SPA_BT_PROFILE_BAP_BROADCAST_SINK | + SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)); + spa_log_debug(this->log, "profiles changed: BAP nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_HSP_HFP: + 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 device_set_changed(void *userdata) +{ + struct impl *this = userdata; + + if (this->profile != DEVICE_PROFILE_BAP) + return; + + if (!device_set_needs_update(this)) { + spa_log_debug(this->log, "%p: device set not changed", this); + return; + } + + spa_log_debug(this->log, "%p: device set changed", this); + + emit_remove_nodes(this); + 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; + this->params[IDX_EnumRoute].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, "%p: connected: %d", this, connected); + + if (connected ^ (this->profile != DEVICE_PROFILE_OFF)) { + emit_remove_nodes(this); + set_initial_profile(this); + } +} + +static void device_switch_profile(void *userdata) +{ + struct impl *this = userdata; + uint32_t profile; + + switch(this->profile) { + case DEVICE_PROFILE_OFF: + profile = DEVICE_PROFILE_HSP_HFP; + break; + case DEVICE_PROFILE_HSP_HFP: + profile = DEVICE_PROFILE_OFF; + break; + default: + return; + } + + spa_log_debug(this->log, "%p: device switch profile %d -> %d", this, this->profile, profile); + + set_profile(this, profile, 0, false); +} + +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, + .device_set_changed = device_set_changed, + .switch_profile = device_switch_profile, +}; + +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, + bool hfp_input_for_a2dp) +{ + 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: + if (device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) + have_output = true; + + media_codec = get_supported_media_codec(this, codec, NULL, device->connected_profiles); + if (media_codec && media_codec->duplex_codec) + have_input = true; + if (hfp_input_for_a2dp && this->nodes[DEVICE_ID_SOURCE].active) + have_input = true; + break; + case DEVICE_PROFILE_BAP: + if (device->connected_profiles & SPA_BT_PROFILE_BAP_SINK) + have_output = true; + if (device->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE) + 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; + case DEVICE_PROFILE_ASHA: + if (device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK) + 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) +{ + uint32_t profile = (index >> 16); + const struct spa_type_info *info; + + switch (profile) { + case DEVICE_PROFILE_OFF: + case DEVICE_PROFILE_AG: + *codec = 0; + *next = (profile + 1) << 16; + return profile; + case DEVICE_PROFILE_ASHA: + *codec = SPA_BLUETOOTH_AUDIO_CODEC_G722; + *next = (profile + 1) << 16; + return profile; + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_HSP_HFP: + case DEVICE_PROFILE_BAP: + *codec = (index & 0xffff); + *next = (profile + 1) << 16; + + for (info = spa_type_bluetooth_audio_codec; info->type; ++info) + if (info->type > *codec) + *next = SPA_MIN(*next, (profile << 16) | (info->type & 0xffff)); + return profile; + default: + *codec = 0; + *next = SPA_ID_INVALID; + profile = SPA_ID_INVALID; + break; + } + + return profile; +} + +static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec) +{ + switch (profile) { + case DEVICE_PROFILE_OFF: + case DEVICE_PROFILE_AG: + return (profile << 16); + + case DEVICE_PROFILE_ASHA: + return (profile << 16) | (SPA_BLUETOOTH_AUDIO_CODEC_G722 & 0xffff); + + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_HSP_HFP: + if (!codec) + return SPA_ID_INVALID; + return (profile << 16) | (codec & 0xffff); + } + + return SPA_ID_INVALID; +} + +static bool set_initial_asha_profile(struct impl *this) +{ + struct spa_bt_transport *t; + if (!(this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK)) + return false; + + t = find_transport(this, SPA_BT_PROFILE_ASHA_SINK); + if (t) { + this->profile = DEVICE_PROFILE_ASHA; + this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_G722; + + spa_log_debug(this->log, "initial ASHA profile:%d codec:%d", + this->profile, this->props.codec); + return true; + } + + return false; +} + +static bool set_initial_hsp_hfp_profile(struct impl *this) +{ + struct spa_bt_transport *t; + 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); + 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); + + /* 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, "asha-sink") && set_initial_asha_profile(this)) + return; + if (spa_streq(str, "off")) + goto off; + if (spa_streq(str, "headset-head-unit") && set_initial_hsp_hfp_profile(this)) + return; + } + + for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_ASHA_SINK; i <<= 1) { + if (!(this->bt_dev->connected_profiles & i)) + continue; + + t = find_transport(this, i); + 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 if (i == SPA_BT_PROFILE_ASHA_SINK) + this->profile = DEVICE_PROFILE_ASHA; + else + this->profile = DEVICE_PROFILE_A2DP; + this->props.codec = t->media_codec->id; + 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)"); + } + + /* + * If the remote is A2DP sink and HF, we likely should prioritize being + * A2DP sender, not gateway. This can occur in PW<->PW if RFCOMM gets + * connected both as AG and HF. + */ + if ((device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) && + (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) + priority = 15; + else + priority = 256; + break; + } + case DEVICE_PROFILE_ASHA: + { + uint32_t profile = device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK; + + if (codec == 0) + return NULL; + if (profile == 0) + return NULL; + if (!(profile & SPA_BT_PROFILE_ASHA_SINK)) { + return NULL; + } + + name = spa_bt_profile_name(profile); + desc = _("Audio Streaming for Hearing Aids (ASHA Sink)"); + + n_sink++; + priority = 1; + + break; + } + case DEVICE_PROFILE_A2DP: + { + /* make this device profile visible only if there is an A2DP sink */ + 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; + } + + /* A2DP will only enlist codec profiles */ + if (!codec) + 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, profile); + if (media_codec == NULL) { + errno = EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, media_codec->name); + + /* + * Give base name to highest priority profile, so that best codec can be + * selected at command line with out knowing which codecs are actually + * supported + */ + if (idx != 0) + 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 + | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE + | SPA_BT_PROFILE_BAP_BROADCAST_SINK); + size_t idx; + const struct media_codec *media_codec; + + /* BAP will only enlist codec profiles */ + if (codec == 0) + return NULL; + + if (profile == 0) + return NULL; + + if ((profile & (SPA_BT_PROFILE_BAP_SINK)) || + (profile & (SPA_BT_PROFILE_BAP_BROADCAST_SINK))) + n_sink++; + if ((profile & (SPA_BT_PROFILE_BAP_SOURCE)) || + (profile & (SPA_BT_PROFILE_BAP_BROADCAST_SOURCE))) + n_source++; + + name = spa_bt_profile_name(profile); + + if (codec) { + media_codec = get_supported_media_codec(this, codec, &idx, profile); + if (media_codec == NULL) { + errno = EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, media_codec->name); + + /* + * Give base name to highest priority profile, so that best codec can be + * selected at command line with out knowing which codecs are actually + * supported + */ + if (idx != 0) + name = name_and_codec; + + switch (profile) { + case SPA_BT_PROFILE_BAP_SINK: + case SPA_BT_PROFILE_BAP_BROADCAST_SINK: + desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"), + media_codec->description); + break; + case SPA_BT_PROFILE_BAP_SOURCE: + case SPA_BT_PROFILE_BAP_BROADCAST_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 */ + } else { + switch (profile) { + case SPA_BT_PROFILE_BAP_SINK: + case SPA_BT_PROFILE_BAP_BROADCAST_SINK: + desc = _("High Fidelity Playback (BAP Sink)"); + break; + case SPA_BT_PROFILE_BAP_SOURCE: + case SPA_BT_PROFILE_BAP_BROADCAST_SOURCE: + desc = _("High Fidelity Input (BAP Source)"); + break; + default: + desc = _("High Fidelity Duplex (BAP Source/Sink)"); + } + priority = 128; + } + + if (this->device_set.sink_enabled) + n_sink = this->device_set.leader ? 1 : 0; + if (this->device_set.source_enabled) + n_source = this->device_set.leader ? 1 : 0; + break; + } + case DEVICE_PROFILE_HSP_HFP: + { + /* make this device profile visible only if there is a head unit */ + uint32_t profile = device->connected_profiles & + SPA_BT_PROFILE_HEADSET_HEAD_UNIT; + unsigned int hfp_codec = get_hfp_codec(codec); + unsigned int idx; + + if (profile == 0) + return NULL; + + /* HFP will only enlist codec profiles */ + if (codec == 0) + return NULL; + if (codec != SPA_BLUETOOTH_AUDIO_CODEC_CVSD && + spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1) + return NULL; + + name = spa_bt_profile_name(profile); + n_source++; + n_sink++; + + name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec)); + + /* + * Give base name to highest priority profile, so that best codec can be + * selected at command line with out knowing which codecs are actually + * supported + */ + for (idx = HFP_AUDIO_CODEC_LC3_SWB; idx > 0; --idx) + if (spa_bt_device_supports_hfp_codec(this->bt_dev, idx) == 1) + break; + if (hfp_codec < idx) + 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 lc3_swb > msbc > cvsd */ + 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 bool profile_has_route(uint32_t profile, uint32_t route) +{ + switch (profile) { + case DEVICE_PROFILE_OFF: + case DEVICE_PROFILE_AG: + break; + case DEVICE_PROFILE_A2DP: + switch (route) { + case ROUTE_INPUT: + case ROUTE_OUTPUT: + return true; + } + break; + case DEVICE_PROFILE_HSP_HFP: + switch (route) { + case ROUTE_INPUT: + case ROUTE_HF_OUTPUT: + return true; + } + break; + case DEVICE_PROFILE_BAP: + switch (route) { + case ROUTE_INPUT: + case ROUTE_OUTPUT: + case ROUTE_SET_INPUT: + case ROUTE_SET_OUTPUT: + return true; + } + break; + case DEVICE_PROFILE_ASHA: + switch (route) { + case ROUTE_OUTPUT: + return true; + } + break; + } + return false; +} + +static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, + uint32_t id, uint32_t route, 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; + enum spa_param_availability available; + char name[128]; + uint32_t i, j, mask, next; + uint32_t 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 (route) { + case ROUTE_INPUT: + direction = SPA_DIRECTION_INPUT; + snprintf(name, sizeof(name), "%s-input", name_prefix); + dev = DEVICE_ID_SOURCE; + available = this->device_set.source_enabled ? + SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes; + + if ((this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) && + !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) && + !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_AUDIO) && + (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) + description = hfp_description; + break; + case ROUTE_OUTPUT: + direction = SPA_DIRECTION_OUTPUT; + snprintf(name, sizeof(name), "%s-output", name_prefix); + dev = DEVICE_ID_SINK; + available = this->device_set.sink_enabled ? + SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes; + break; + case ROUTE_HF_OUTPUT: + direction = SPA_DIRECTION_OUTPUT; + snprintf(name, sizeof(name), "%s-hf-output", name_prefix); + description = hfp_description; + dev = DEVICE_ID_SINK; + available = SPA_PARAM_AVAILABILITY_yes; + break; + case ROUTE_SET_INPUT: + if (!(this->device_set.source_enabled && this->device_set.leader)) + return NULL; + direction = SPA_DIRECTION_INPUT; + snprintf(name, sizeof(name), "%s-set-input", name_prefix); + dev = DEVICE_ID_SOURCE_SET; + available = SPA_PARAM_AVAILABILITY_yes; + break; + case ROUTE_SET_OUTPUT: + if (!(this->device_set.sink_enabled && this->device_set.leader)) + return NULL; + direction = SPA_DIRECTION_OUTPUT; + snprintf(name, sizeof(name), "%s-set-output", name_prefix); + dev = DEVICE_ID_SINK_SET; + available = SPA_PARAM_AVAILABILITY_yes; + break; + default: + return NULL; + } + + if (profile != SPA_ID_INVALID && !profile_has_route(profile, route)) + 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(route), + SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction), + SPA_PARAM_ROUTE_name, SPA_POD_String(name), + SPA_PARAM_ROUTE_description, SPA_POD_String(description), + SPA_PARAM_ROUTE_priority, SPA_POD_Int(0), + SPA_PARAM_ROUTE_available, SPA_POD_Id(available), + 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 = 0; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) { + uint32_t profile_mask; + + if (!profile_has_route(j, route)) + continue; + + profile_mask = profile_direction_mask(this, j, codec, false); + 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 (profile != 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, true); + 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 & SINK_ID_FLAG)) { + 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_profile, 0); + spa_pod_builder_int(b, profile); + } + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0); + spa_pod_builder_push_array(b, &f[1]); + spa_pod_builder_int(b, dev); + spa_pod_builder_pop(b, &f[1]); + + 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_LC3_SWB; 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 || this->profile == DEVICE_PROFILE_ASHA) { + FOR_EACH_MEDIA_CODEC(j, codec) { + spa_pod_builder_int(b, codec->id); + spa_pod_builder_string(b, codec->description); + } + } 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); + if (profile == SPA_ID_INVALID) + return 0; + + param = build_profile(this, &b, id, result.index, profile, codec, false); + if (param == NULL) + goto next; + break; + } + case SPA_PARAM_Profile: + { + 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: + { + if (result.index < ROUTE_LAST) { + param = build_route(this, &b, id, result.index, SPA_ID_INVALID); + if (param == NULL) + goto next; + } else { + return 0; + } + break; + } + case SPA_PARAM_Route: + { + if (result.index < ROUTE_LAST) { + param = build_route(this, &b, id, result.index, this->profile); + if (param == NULL) + goto next; + break; + } else { + 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 void device_set_update_volumes(struct node *node) +{ + struct impl *impl = node->impl; + struct device_set *dset = &impl->device_set; + float hw_volume = node_get_hw_volume(node); + bool sink = (node->id == DEVICE_ID_SINK_SET); + struct device_set_member *members = sink ? dset->sink : dset->source; + uint32_t n_members = sink ? dset->sinks : dset->sources; + uint32_t i; + + /* Check if all sub-devices have HW volume */ + if ((sink && !dset->sink_enabled) || (!sink && !dset->source_enabled)) + goto soft_volume; + + for (i = 0; i < n_members; ++i) { + struct spa_bt_transport *t = members[i].transport; + struct spa_bt_transport_volume *t_volume = t ? &t->volumes[members[i].id] : NULL; + + if (!t_volume || !t_volume->active) + goto soft_volume; + } + + node_update_soft_volumes(node, hw_volume); + for (i = 0; i < n_members; ++i) + spa_bt_transport_set_volume(members[i].transport, members[i].id, hw_volume); + return; + +soft_volume: + /* Soft volume fallback */ + for (i = 0; i < n_members; ++i) + spa_bt_transport_set_volume(members[i].transport, members[i].id, 1.0f); + node_update_soft_volumes(node, 1.0f); + return; +} + +static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes) +{ + uint32_t i; + int changed = 0; + struct spa_bt_transport_volume *t_volume; + + if (n_volumes == 0) + return -EINVAL; + + spa_log_info(this->log, "node %d volume %f", node->id, 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 %d hardware volume %f", node->id, hw_volume); + + node_update_soft_volumes(node, hw_volume); + spa_bt_transport_set_volume(node->transport, node->id, hw_volume); + } else if (node->id == DEVICE_ID_SOURCE_SET || node->id == DEVICE_ID_SINK_SET) { + device_set_update_volumes(node); + } else { + float boost = get_soft_volume_boost(node); + for (uint32_t i = 0; i < node->n_channels; ++i) + 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 %d mute %d", node->id, 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 %d latency offset %"PRIi64" nsec", node->id, 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; + unsigned int i; + + this->props.offload_active = active; + + for (i = 0; i < SPA_N_ELEMENTS(this->nodes); 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, "%p: setting profile %d codec:%d save:%d", this, + 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 >= SPA_N_ELEMENTS(this->nodes) || !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 || this->profile == DEVICE_PROFILE_ASHA) { + size_t j; + for (j = 0; j < this->supported_codec_count; ++j) { + if (this->supported_codecs[j]->id == codec_id) { + 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); + } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB && + spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_LC3_SWB) == 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); + } + + device_set_clear(this, &this->device_set); + 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; + unsigned int 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); + _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); + + for (i = 0; i < SPA_N_ELEMENTS(this->nodes); ++i) + init_node(this, &this->nodes[i], i); + + 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); + + this->device_set.impl = 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 " }, + { SPA_KEY_FACTORY_DESCRIPTION, "A bluetooth device" }, + { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_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/bt-latency.h b/spa/plugins/bluez5/bt-latency.h new file mode 100644 index 0000000..6fba869 --- /dev/null +++ b/spa/plugins/bluez5/bt-latency.h @@ -0,0 +1,175 @@ +/* Spa Bluez5 ISO I/O */ +/* SPDX-FileCopyrightText: Copyright © 2024 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_BT_LATENCY_H +#define SPA_BLUEZ5_BT_LATENCY_H + +#include +#include +#include +#include + +#include +#include + +#include "rate-control.h" + +/* New kernel API */ +#ifndef BT_SCM_ERROR +#define BT_SCM_ERROR 0x04 +#endif +#ifndef BT_POLL_ERRQUEUE +#define BT_POLL_ERRQUEUE 21 +#endif + +/** + * Bluetooth latency tracking. + */ +struct spa_bt_latency +{ + uint64_t value; + struct spa_bt_ptp ptp; + bool valid; + bool disabled; + + struct { + int64_t send[64]; + uint32_t pos; + int64_t prev_tx; + } impl; +}; + +static inline void spa_bt_latency_init(struct spa_bt_latency *lat, int fd, + uint32_t period, struct spa_log *log) +{ + int so_timestamping = (SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_SOFTWARE | + SOF_TIMESTAMPING_OPT_ID | SOF_TIMESTAMPING_OPT_TSONLY); + uint32_t flag; + int res; + + spa_zero(*lat); + + flag = 0; + res = setsockopt(fd, SOL_BLUETOOTH, BT_POLL_ERRQUEUE, &flag, sizeof(flag)); + if (res < 0) { + spa_log_warn(log, "setsockopt(BT_POLL_ERRQUEUE) failed (kernel feature not enabled?): %d (%m)", errno); + lat->disabled = true; + return; + } + + res = setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &so_timestamping, sizeof(so_timestamping)); + if (res < 0) { + spa_log_warn(log, "setsockopt(SO_TIMESTAMPING) failed (kernel feature not enabled?): %d (%m)", errno); + lat->disabled = true; + return; + } + + /* Flush errqueue on start */ + do { + res = recv(fd, NULL, 0, MSG_ERRQUEUE | MSG_DONTWAIT | MSG_TRUNC); + } while (res == 0); + + spa_bt_ptp_init(&lat->ptp, period, period / 2); +} + +static inline void spa_bt_latency_reset(struct spa_bt_latency *lat) +{ + lat->value = 0; + lat->valid = false; + spa_bt_ptp_init(&lat->ptp, lat->ptp.period, lat->ptp.period / 2); +} + +static inline void spa_bt_latency_sent(struct spa_bt_latency *lat, uint64_t now) +{ + const unsigned int n = SPA_N_ELEMENTS(lat->impl.send); + + if (lat->disabled) + return; + + lat->impl.send[lat->impl.pos++] = now; + if (lat->impl.pos >= n) + lat->impl.pos = 0; +} + +static inline int spa_bt_latency_recv_errqueue(struct spa_bt_latency *lat, int fd, struct spa_log *log) +{ + struct { + struct cmsghdr cm; + char control[512]; + } control; + + if (lat->disabled) + return -EOPNOTSUPP; + + do { + struct iovec data = { + .iov_base = NULL, + .iov_len = 0 + }; + struct msghdr msg = { + .msg_iov = &data, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct cmsghdr *cmsg; + struct scm_timestamping *tss = NULL; + struct sock_extended_err *serr = NULL; + int res; + + res = recvmsg(fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); + if (res < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + return -errno; + } + + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) + tss = (void *)CMSG_DATA(cmsg); + else if (cmsg->cmsg_level == SOL_BLUETOOTH && cmsg->cmsg_type == BT_SCM_ERROR) + serr = (void *)CMSG_DATA(cmsg); + else + continue; + } + + if (!tss || !serr || serr->ee_errno != ENOMSG || serr->ee_origin != SO_EE_ORIGIN_TIMESTAMPING) + return -EINVAL; + if (serr->ee_info != SCM_TSTAMP_SND) + continue; + + struct timespec *ts = &tss->ts[0]; + int64_t tx_time = SPA_TIMESPEC_TO_NSEC(ts); + uint32_t tx_pos = serr->ee_data % SPA_N_ELEMENTS(lat->impl.send); + + lat->value = tx_time - lat->impl.send[tx_pos]; + + if (lat->impl.prev_tx && tx_time > lat->impl.prev_tx) + spa_bt_ptp_update(&lat->ptp, lat->value, tx_time - lat->impl.prev_tx); + + lat->impl.prev_tx = tx_time; + + spa_log_trace(log, "fd:%d latency[%d] nsec:%"PRIu64" range:%d..%d ms", + fd, tx_pos, lat->value, + (int)(spa_bt_ptp_valid(&lat->ptp) ? lat->ptp.min / SPA_NSEC_PER_MSEC : -1), + (int)(spa_bt_ptp_valid(&lat->ptp) ? lat->ptp.max / SPA_NSEC_PER_MSEC : -1)); + } while (true); + + lat->valid = spa_bt_ptp_valid(&lat->ptp); + + return 0; +} + +static inline void spa_bt_latency_flush(struct spa_bt_latency *lat, int fd, struct spa_log *log) +{ + int so_timestamping = 0; + + /* Disable timestamping and flush errqueue */ + setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &so_timestamping, sizeof(so_timestamping)); + spa_bt_latency_recv_errqueue(lat, fd, log); + + lat->disabled = true; +} + +#endif diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c new file mode 100644 index 0000000..6fd1d04 --- /dev/null +++ b/spa/plugins/bluez5/codec-loader.c @@ -0,0 +1,219 @@ +/* Spa A2DP codec API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#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 + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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_OPUS_G, + 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, + SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD, + SPA_BLUETOOTH_AUDIO_CODEC_G722, + }; + 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("opus-g"), + MEDIA_CODEC_FACTORY_LIB("lc3"), + MEDIA_CODEC_FACTORY_LIB("g722") +#undef MEDIA_CODEC_FACTORY_LIB + }; + + impl = calloc(1, sizeof(struct impl)); + 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..3e6de3d --- /dev/null +++ b/spa/plugins/bluez5/codec-loader.h @@ -0,0 +1,19 @@ +/* Spa A2DP codec API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_CODEC_LOADER_H_ +#define SPA_BLUEZ5_CODEC_LOADER_H_ + +#include +#include + +#include + +#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..2199c9c --- /dev/null +++ b/spa/plugins/bluez5/dbus-monitor.c @@ -0,0 +1,246 @@ +/* Spa midi dbus */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include + +#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 : ""); + + 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 : ""); + + 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 : ""); + + if (g_object_get_data(G_OBJECT(iface), "dbus-monitor-signals-connected")) { + g_object_disconnect(G_OBJECT(iface), "any_signal", + 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..2d0c235 --- /dev/null +++ b/spa/plugins/bluez5/dbus-monitor.h @@ -0,0 +1,64 @@ +/* Spa midi dbus */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef DBUS_MONITOR_H_ +#define DBUS_MONITOR_H_ + +#include + +#include +#include +#include + +#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..0441f04 --- /dev/null +++ b/spa/plugins/bluez5/decode-buffer.h @@ -0,0 +1,311 @@ +/* Spa Bluez5 decode buffer */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +/** + * \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 position of last received sample, relative to the current + * playback position. If it is larger than duration, there is no underrun. + * + * The rate correction aims to maintain the average level at a safety margin. + */ + +#ifndef SPA_BLUEZ5_DECODE_BUFFER_H +#define SPA_BLUEZ5_DECODE_BUFFER_H + +#include +#include +#include + +#include "rate-control.h" + +#define BUFFERING_LONG_MSEC (2*60000) +#define BUFFERING_SHORT_MSEC 1000 +#define BUFFERING_RATE_DIFF_MAX 0.005 + + +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 duration; + uint32_t pos; + + int32_t target; /**< target buffer (0: automatic) */ + int32_t max_extra; + + int32_t level; + uint64_t next_nsec; + double rate_diff; + + uint8_t buffering:1; +}; + +static inline int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log, + uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve) +{ + spa_zero(*this); + 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->target = 0; + this->buffering = true; + this->max_extra = INT32_MAX; + + spa_bt_rate_control_init(&this->ctl, 0); + + spa_bt_ptp_init(&this->spike, (uint64_t)this->rate * BUFFERING_LONG_MSEC / 1000, 0); + spa_bt_ptp_init(&this->packet_size, (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000, 0); + + if ((this->buffer_decoded = malloc(this->buffer_size)) == NULL) { + this->buffer_size = 0; + return -ENOMEM; + } + return 0; +} + +static inline void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this) +{ + free(this->buffer_decoded); + spa_zero(*this); +} + +static inline 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 inline void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail) +{ + spa_assert(this->write_index >= this->read_index); + if (!this->buffering) + *avail = this->write_index - this->read_index; + else + *avail = 0; + return SPA_PTROFF(this->buffer_decoded, this->read_index, void); +} + +static inline void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size) +{ + spa_assert(size % this->frame_size == 0); + this->read_index += size; +} + +static inline void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail) +{ + spa_bt_decode_buffer_compact(this); + spa_assert(this->buffer_size >= this->write_index); + *avail = this->buffer_size - this->write_index; + return SPA_PTROFF(this->buffer_decoded, this->write_index, void); +} + +static inline void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size, uint64_t nsec) +{ + int32_t remain; + uint32_t avail; + + spa_assert(size % this->frame_size == 0); + this->write_index += size; + spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size); + + if (nsec && this->next_nsec && this->rate_diff != 0.0) { + int64_t dt = (this->next_nsec >= nsec) ? + (int64_t)(this->next_nsec - nsec) : -(int64_t)(nsec - this->next_nsec); + remain = (int32_t)SPA_CLAMP(dt * this->rate_diff * this->rate / SPA_NSEC_PER_SEC, + -(int32_t)this->duration, this->duration); + } else { + remain = 0; + } + + spa_bt_decode_buffer_get_read(this, &avail); + this->level = avail / this->frame_size + remain; +} + +static inline void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this) +{ + int32_t size = (this->write_index - this->read_index) / this->frame_size; + + this->level = size; + this->corr = 1.0; + spa_bt_rate_control_init(&this->ctl, size); +} + +static inline void spa_bt_decode_buffer_set_target_latency(struct spa_bt_decode_buffer *this, int32_t samples) +{ + this->target = samples; +} + +static inline void spa_bt_decode_buffer_set_max_extra_latency(struct spa_bt_decode_buffer *this, int32_t samples) +{ + this->max_extra = samples; +} + +static inline int32_t spa_bt_decode_buffer_get_target_latency(struct spa_bt_decode_buffer *this) +{ + const int32_t duration = this->duration; + const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8); + const int32_t max_buf = (this->buffer_size - this->buffer_reserve) / this->frame_size; + const int32_t spike = SPA_CLAMP(this->spike.max, 0, max_buf); + int32_t target; + + if (this->target) + target = this->target; + else + target = SPA_CLAMP(SPA_ROUND_UP(SPA_MAX(spike * 3/2, duration), + SPA_CLAMP((int)this->rate / 50, 1, INT32_MAX)), + duration, max_buf - 2*packet_size); + + return SPA_MIN(target, duration + SPA_CLAMP(this->max_extra, 0, INT32_MAX - duration)); +} + +static inline void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration, + double rate_diff, uint64_t next_nsec) +{ + const uint32_t data_size = samples * this->frame_size; + const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8); + const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration); + const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000; + int32_t target; + uint32_t avail; + + this->rate_diff = rate_diff; + this->next_nsec = next_nsec; + + if (SPA_UNLIKELY(duration != this->duration)) { + this->duration = duration; + spa_bt_decode_buffer_recover(this); + } + + target = spa_bt_decode_buffer_get_target_latency(this); + + if (SPA_UNLIKELY(this->buffering)) { + int32_t size = (this->write_index - this->read_index) / this->frame_size; + + this->corr = 1.0; + + spa_log_trace(this->log, "%p buffering size:%d", this, (int)size); + + if (size >= SPA_MAX((int)duration, target)) + this->buffering = false; + else + return; + + spa_bt_ptp_update(&this->spike, packet_size, duration); + spa_bt_decode_buffer_recover(this); + } + + spa_bt_decode_buffer_get_read(this, &avail); + + /* Track buffer level */ + this->level = SPA_MAX(this->level, -max_level); + + spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration); + + if (this->level > SPA_MAX(4 * target, 3*(int32_t)duration) && + avail > data_size) { + /* Lagging too much: drop data */ + uint32_t size = SPA_MIN(avail - data_size, + (this->level - target) * this->frame_size); + + spa_bt_decode_buffer_read(this, size); + spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d", + this, (int)size/this->frame_size, + (int)this->level, (int)target); + + spa_bt_decode_buffer_recover(this); + } + + this->pos += duration; + if (this->pos > this->rate) { + spa_log_debug(this->log, + "%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f", + this, + (int)this->ctl.avg, + (int)target, + (int)this->level, + (int)(avail / this->frame_size), + (int)this->spike.max, + (double)this->corr); + this->pos = 0; + } + + this->corr = spa_bt_rate_control_update(&this->ctl, + this->level, target, duration, avg_period, + BUFFERING_RATE_DIFF_MAX); + + this->level -= duration; + + spa_bt_decode_buffer_get_read(this, &avail); + if (avail < data_size) { + spa_log_trace(this->log, "%p underrun samples:%d", this, + (data_size - avail) / this->frame_size); + this->buffering = true; + spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration); + } +} + +#endif diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h new file mode 100644 index 0000000..2309458 --- /dev/null +++ b/spa/plugins/bluez5/defs.h @@ -0,0 +1,883 @@ +/* Spa Bluez5 Monitor */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_DEFS_H +#define SPA_BLUEZ5_DEFS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#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_DEVICE_SET_INTERFACE BLUEZ_SERVICE ".DeviceSet1" +#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 \ + "\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\n" + +#define ENDPOINT_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +#define PROFILE_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_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 SPA_BT_UUID_BAP_BROADCAST_SOURCE "00001852-0000-1000-8000-00805f9b34fb" +#define SPA_BT_UUID_BAP_BROADCAST_SINK "00001851-0000-1000-8000-00805f9b34fb" +#define SPA_BT_UUID_ASHA_SINK "0000FDF0-0000-1000-8000-00805f9b34fb" + +#define PROFILE_HSP_AG "/Profile/HSPAG" +#define PROFILE_HSP_HS "/Profile/HSPHS" +#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 HFP_AUDIO_CODEC_LC3_SWB 0x03 + +#define A2DP_OBJECT_MANAGER_PATH "/MediaEndpoint" +#define A2DP_SINK_ENDPOINT A2DP_OBJECT_MANAGER_PATH "/A2DPSink" +#define A2DP_SOURCE_ENDPOINT A2DP_OBJECT_MANAGER_PATH "/A2DPSource" + +#define BAP_OBJECT_MANAGER_PATH "/MediaEndpointLE" +#define BAP_SINK_ENDPOINT BAP_OBJECT_MANAGER_PATH "/BAPSink" +#define BAP_SOURCE_ENDPOINT BAP_OBJECT_MANAGER_PATH "/BAPSource" +#define BAP_BROADCAST_SOURCE_ENDPOINT BAP_OBJECT_MANAGER_PATH "/BAPBroadcastSource" +#define BAP_BROADCAST_SINK_ENDPOINT BAP_OBJECT_MANAGER_PATH "/BAPBroadcastSink" + +#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_PAYLOAD_SIZE 57 /* 1 byte padding follows payload */ +#define LC3_SWB_DECODED_SIZE 960 /* 32 kHz mono S24_32 @ 7.5 ms */ +#define LC3_SWB_PAYLOAD_SIZE 58 +#define HFP_CODEC_PACKET_SIZE 60 /* 2 bytes header + payload */ + +enum spa_bt_media_direction { + SPA_BT_MEDIA_SOURCE, + SPA_BT_MEDIA_SINK, + SPA_BT_MEDIA_SOURCE_BROADCAST, + SPA_BT_MEDIA_SINK_BROADCAST, + SPA_BT_MEDIA_DIRECTION_LAST, +}; + +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_ASHA_SINK = (1 << 4), + SPA_BT_PROFILE_HSP_HS = (1 << 5), + SPA_BT_PROFILE_HSP_AG = (1 << 6), + SPA_BT_PROFILE_HFP_HF = (1 << 7), + SPA_BT_PROFILE_HFP_AG = (1 << 8), + SPA_BT_PROFILE_BAP_BROADCAST_SOURCE = (1 << 9), + SPA_BT_PROFILE_BAP_BROADCAST_SINK = (1 << 10), + + SPA_BT_PROFILE_A2DP_DUPLEX = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE), + SPA_BT_PROFILE_BAP_DUPLEX = (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE), + 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_BAP_AUDIO = (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SINK | + SPA_BT_PROFILE_BAP_SOURCE | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE), + + SPA_BT_PROFILE_MEDIA_SINK = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK | + SPA_BT_PROFILE_BAP_BROADCAST_SINK), + SPA_BT_PROFILE_MEDIA_SOURCE = (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE | + SPA_BT_PROFILE_BAP_BROADCAST_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 if (strcasecmp(uuid, SPA_BT_UUID_BAP_BROADCAST_SOURCE) == 0) + return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + else if (strcasecmp(uuid, SPA_BT_UUID_BAP_BROADCAST_SINK) == 0) + return SPA_BT_PROFILE_BAP_BROADCAST_SINK; + else if (strcasecmp(uuid, SPA_BT_UUID_ASHA_SINK) == 0) + return SPA_BT_PROFILE_ASHA_SINK; + else + return 0; +} +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), + SPA_BT_HFP_SDP_AG_FEATURE_ENH_VOICE_RECOG_STATUS = (1 << 6), + SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG_TEXT = (1 << 7), + SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH = (1 << 8), +}; + +enum spa_bt_hfp_hf_feature { + 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), + SPA_BT_HFP_SDP_HF_FEATURE_ENH_VOICE_RECOG_STATUS = (1 << 6), + SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOG_TEXT = (1 << 7), + SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH = (1 << 8), +}; + +static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) { + switch (profile) { + case SPA_BT_PROFILE_ASHA_SINK: + return "asha-sink"; + case SPA_BT_PROFILE_A2DP_SOURCE: + return "a2dp-source"; + case SPA_BT_PROFILE_A2DP_SINK: + 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: + case SPA_BT_PROFILE_BAP_BROADCAST_SOURCE: + return "bap-source"; + case SPA_BT_PROFILE_BAP_SINK: + case SPA_BT_PROFILE_BAP_BROADCAST_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 legacy_endpoints_registered:1; + unsigned int a2dp_application_registered:1; + unsigned int bap_application_registered:1; + unsigned int player_registered:1; + unsigned int has_battery_provider:1; + unsigned int battery_provider_unavailable:1; + unsigned int le_audio_supported:1; + unsigned int has_adapter1_interface:1; + unsigned int has_media1_interface:1; + unsigned int le_audio_bcast_supported: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 connected_change); + + /** Device set configuration changed */ + void (*device_set_changed) (void *data); + + /** Switch profile between OFF and HSP_HFP */ + void (*switch_profile) (void *data); + + /** Device freed */ + void (*destroy) (void *data); +}; + +struct media_codec; + +struct spa_bt_set_membership { + struct spa_list link; + struct spa_list others; + struct spa_bt_device *device; + char *path; + uint8_t rank; + bool leader; +}; + +#define spa_bt_for_each_set_member(s, set) \ + for ((s) = (set); (s); (s) = spa_list_next((s), others), (s) = (s) != (set) ? (s) : NULL) + +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; + struct spa_list set_membership_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, enum spa_bt_profile profile); +const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count); +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_device_set_changed(d) spa_bt_device_emit(d, device_set_changed, 0) +#define spa_bt_device_emit_switch_profile(d) spa_bt_device_emit(d, switch_profile, 0) +#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_iso_io; + +struct spa_bt_sco_io; + +struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, struct spa_loop *data_loop, struct spa_log *log); +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 +#define SPA_BT_VOLUME_BAP_MAX 255 + +enum spa_bt_transport_state { + SPA_BT_TRANSPORT_STATE_ERROR = -1, + SPA_BT_TRANSPORT_STATE_IDLE = 0, + SPA_BT_TRANSPORT_STATE_PENDING = 1, + SPA_BT_TRANSPORT_STATE_ACTIVE = 2, +}; + +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 (*set_delay) (void *data, int64_t delay_nsec); + 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 error_count; + uint64_t last_error_time; + int fd; + uint16_t read_mtu; + uint16_t write_mtu; + unsigned int delay_us; + unsigned int latency_us; + uint8_t bap_cig; + uint8_t bap_cis; + uint8_t bap_big; + uint8_t bap_bis; + + struct spa_bt_iso_io *iso_io; + struct spa_bt_sco_io *sco_io; + + struct spa_source volume_timer; + struct spa_source release_timer; + DBusPendingCall *acquire_call; + DBusPendingCall *volume_call; + + 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__) +#define spa_bt_transport_set_delay(t,...) spa_bt_transport_impl(t, set_delay, 0, __VA_ARGS__) + +static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(const char *value) +{ + if (strcasecmp("idle", value) == 0) + return SPA_BT_TRANSPORT_STATE_IDLE; + else if ((strcasecmp("pending", value) == 0) || (strcasecmp("broadcasting", 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_log_features(const struct spa_bt_quirks *this, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device); +void spa_bt_quirks_destroy(struct spa_bt_quirks *quirks); + +int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter); + +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/g722/g722_enc_dec.h b/spa/plugins/bluez5/g722/g722_enc_dec.h new file mode 100644 index 0000000..0da6060 --- /dev/null +++ b/spa/plugins/bluez5/g722/g722_enc_dec.h @@ -0,0 +1,148 @@ +/* + * SpanDSP - a series of DSP components for telephony + * + * g722.h - The ITU G.722 codec. + * + * Written by Steve Underwood + * + * Copyright (C) 2005 Steve Underwood + * + * Despite my general liking of the GPL, I place my own contributions + * to this code in the public domain for the benefit of all mankind - + * even the slimy ones who might try to proprietize my work and use it + * to my detriment. + * + * Based on a single channel G.722 codec which is: + * + ***** Copyright (c) CMU 1993 ***** + * Computer Science, Speech Group + * Chengxiang Lu and Alex Hauptmann + * + * $Id: g722.h 48959 2006-12-25 06:42:15Z rizzo $ + */ + +/*! \file */ + +#if !defined(_G722_H_) +#define _G722_H_ + +#include + +/*! \page g722_page G.722 encoding and decoding +\section g722_page_sec_1 What does it do? +The G.722 module is a bit exact implementation of the ITU G.722 specification for all three +specified bit rates - 64000bps, 56000bps and 48000bps. It passes the ITU tests. + +To allow fast and flexible interworking with narrow band telephony, the encoder and decoder +support an option for the linear audio to be an 8k samples/second stream. In this mode the +codec is considerably faster, and still fully compatible with wideband terminals using G.722. + +\section g722_page_sec_2 How does it work? +???. +*/ + +/* Format DAC12 is added to decode directly into samples suitable for + a 12-bit DAC using offset binary representation. */ + +enum { + G722_SAMPLE_RATE_8000 = 0x0001, + G722_PACKED = 0x0002, + G722_FORMAT_DAC12 = 0x0004, +}; + +#ifdef BUILD_FEATURE_DAC +#define NLDECOMPRESS_APPLY_GAIN(s, g) (((s) * (int32_t)(g)) >> 16) +// Equivalent to shift 16, add 0x8000, shift 4 +#define NLDECOMPRESS_APPLY_GAIN_CONVERTED_DAC(s, g) \ + (uint16_t)((uint16_t)(((s) * (int32_t)(g)) >> 20) + 0x800) +#else +#define NLDECOMPRESS_APPLY_GAIN(s, g) (((int32_t)(s) * (int32_t)(g)) >> 16) +#endif + +#ifdef BUILD_FEATURE_DAC +#define NLDECOMPRESS_PREPROCESS_PCM_SAMPLE_WITH_GAIN(s, g) \ + NLDECOMPRESS_APPLY_GAIN_CONVERTED_DAC((s), (g)) +#define NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN(s, g) ((int16_t)NLDECOMPRESS_APPLY_GAIN((s), (g))) +#else +#define NLDECOMPRESS_PREPROCESS_PCM_SAMPLE_WITH_GAIN NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN +#define NLDECOMPRESS_PREPROCESS_SAMPLE_WITH_GAIN(s, g) \ + ((int16_t)(NLDECOMPRESS_APPLY_GAIN((s), (g)))) +#endif + +typedef struct { + int s; + int sp; + int sz; + int r[3]; + int a[3]; + int ap[3]; + int p[3]; + int d[7]; + int b[7]; + int bp[7]; + int nb; + int det; +} g722_band_t; + +typedef struct { + /*! TRUE if the operating in the special ITU test mode, with the band split filters disabled. */ + int itu_test_mode; + /*! TRUE if the G.722 data is packed */ + int packed; + /*! TRUE if encode from 8k samples/second */ + int eight_k; + /*! 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. */ + int bits_per_sample; + + /*! Signal history for the QMF */ + int x[24]; + + g722_band_t band[2]; + + unsigned int in_buffer; + int in_bits; + unsigned int out_buffer; + int out_bits; +} g722_encode_state_t; + +typedef struct { + /*! TRUE if the operating in the special ITU test mode, with the band split filters disabled. */ + int itu_test_mode; + /*! TRUE if the G.722 data is packed */ + int packed; + /*! TRUE if decode to 8k samples/second */ + int eight_k; + /*! 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. */ + int bits_per_sample; + /*! TRUE if offset binary for a 12-bit DAC */ + int dac_pcm; + + /*! Signal history for the QMF */ + int x[24]; + + g722_band_t band[2]; + + unsigned int in_buffer; + int in_bits; + unsigned int out_buffer; + int out_bits; +} g722_decode_state_t; + +#ifdef __cplusplus +extern "C" { +#endif + +g722_encode_state_t *g722_encode_init(g722_encode_state_t *s, unsigned int rate, int options); +int g722_encode_release(g722_encode_state_t *s); +int g722_encode(g722_encode_state_t *s, uint8_t g722_data[], const int16_t amp[], int len); + +g722_decode_state_t *g722_decode_init(g722_decode_state_t *s, unsigned int rate, int options); +int g722_decode_release(g722_decode_state_t *s); +uint32_t g722_decode(g722_decode_state_t *s, int16_t amp[], const uint8_t g722_data[], int len, + uint16_t aGain); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/spa/plugins/bluez5/g722/g722_encode.c b/spa/plugins/bluez5/g722/g722_encode.c new file mode 100644 index 0000000..a185e25 --- /dev/null +++ b/spa/plugins/bluez5/g722/g722_encode.c @@ -0,0 +1,387 @@ +/* + * SpanDSP - a series of DSP components for telephony + * + * g722_encode.c - The ITU G.722 codec, encode part. + * + * Written by Steve Underwood + * + * Copyright (C) 2005 Steve Underwood + * + * All rights reserved. + * + * Despite my general liking of the GPL, I place my own contributions + * to this code in the public domain for the benefit of all mankind - + * even the slimy ones who might try to proprietize my work and use it + * to my detriment. + * + * Based on a single channel 64kbps only G.722 codec which is: + * + ***** Copyright (c) CMU 1993 ***** + * Computer Science, Speech Group + * Chengxiang Lu and Alex Hauptmann + * + * $Id: g722_encode.c,v 1.14 2006/07/07 16:37:49 steveu Exp $ + */ + +/*! \file */ + +#include +#include + +#include "g722_enc_dec.h" + +#if !defined(FALSE) +#define FALSE 0 +#endif +#if !defined(TRUE) +#define TRUE (!FALSE) +#endif + +#define PACKED_OUTPUT (0) +#define BITS_PER_SAMPLE (8) + +#ifndef BUILD_FEATURE_G722_USE_INTRINSIC_SAT +static __inline int16_t saturate(int32_t amp) { + int16_t amp16; + + /* Hopefully this is optimised for the common case - not clipping */ + amp16 = (int16_t)amp; + if (amp == amp16) { + return amp16; + } + if (amp > 0x7FFF) { + return 0x7FFF; + } + return 0x8000; +} +#else +static __inline int16_t saturate(int32_t val) { + register int32_t res; + __asm volatile("SSAT %0, #16, %1\n\t" : "=r"(res) : "r"(val) :); + return (int16_t)res; +} +#endif +/*- End of function --------------------------------------------------------*/ + +static void block4(g722_band_t *band, int d) { + int wd1; + int wd2; + int wd3; + int i; + int sg[7]; + int ap1, ap2; + int sg0, sgi; + int sz; + + /* Block 4, RECONS */ + band->d[0] = d; + band->r[0] = saturate(band->s + d); + + /* Block 4, PARREC */ + band->p[0] = saturate(band->sz + d); + + /* Block 4, UPPOL2 */ + for (i = 0; i < 3; i++) { + sg[i] = band->p[i] >> 15; + } + wd1 = saturate(band->a[1] << 2); + + wd2 = (sg[0] == sg[1]) ? -wd1 : wd1; + if (wd2 > 32767) { + wd2 = 32767; + } + + ap2 = (wd2 >> 7) + ((sg[0] == sg[2]) ? 128 : -128); + ap2 += (band->a[2] * 32512) >> 15; + if (ap2 > 12288) { + ap2 = 12288; + } else if (ap2 < -12288) { + ap2 = -12288; + } + band->ap[2] = ap2; + + /* Block 4, UPPOL1 */ + sg[0] = band->p[0] >> 15; + sg[1] = band->p[1] >> 15; + wd1 = (sg[0] == sg[1]) ? 192 : -192; + wd2 = (band->a[1] * 32640) >> 15; + + ap1 = saturate(wd1 + wd2); + wd3 = saturate(15360 - band->ap[2]); + if (ap1 > wd3) { + ap1 = wd3; + } else if (ap1 < -wd3) { + ap1 = -wd3; + } + band->ap[1] = ap1; + + /* Block 4, UPZERO */ + /* Block 4, FILTEZ */ + wd1 = (d == 0) ? 0 : 128; + + sg0 = sg[0] = d >> 15; + for (i = 1; i < 7; i++) { + sgi = band->d[i] >> 15; + wd2 = (sgi == sg0) ? wd1 : -wd1; + wd3 = (band->b[i] * 32640) >> 15; + band->bp[i] = saturate(wd2 + wd3); + } + + /* Block 4, DELAYA */ + sz = 0; + for (i = 6; i > 0; i--) { + int bi; + + band->d[i] = band->d[i - 1]; + bi = band->b[i] = band->bp[i]; + wd1 = saturate(band->d[i] + band->d[i]); + sz += (bi * wd1) >> 15; + } + band->sz = sz; + + for (i = 2; i > 0; i--) { + band->r[i] = band->r[i - 1]; + band->p[i] = band->p[i - 1]; + band->a[i] = band->ap[i]; + } + + /* Block 4, FILTEP */ + wd1 = saturate(band->r[1] + band->r[1]); + wd1 = (band->a[1] * wd1) >> 15; + wd2 = saturate(band->r[2] + band->r[2]); + wd2 = (band->a[2] * wd2) >> 15; + band->sp = saturate(wd1 + wd2); + + /* Block 4, PREDIC */ + band->s = saturate(band->sp + band->sz); +} +/*- End of function --------------------------------------------------------*/ + +g722_encode_state_t *g722_encode_init(g722_encode_state_t *s, unsigned int rate, int options) { + if (s == NULL) { +#ifdef G722_SUPPORT_MALLOC + if ((s = (g722_encode_state_t *)malloc(sizeof(*s))) == NULL) +#endif + return NULL; + } + memset(s, 0, sizeof(*s)); + if (rate == 48000) { + s->bits_per_sample = 6; + } else if (rate == 56000) { + s->bits_per_sample = 7; + } else { + s->bits_per_sample = 8; + } + s->band[0].det = 32; + s->band[1].det = 8; + return s; +} +/*- End of function --------------------------------------------------------*/ + +int g722_encode_release(g722_encode_state_t *s) { + free(s); + return 0; +} +/*- End of function --------------------------------------------------------*/ + +/* WebRtc, tlegrand: + * Only define the following if bit-exactness with reference implementation + * is needed. Will only have any effect if input signal is saturated. + */ +// #define RUN_LIKE_REFERENCE_G722 +#ifdef RUN_LIKE_REFERENCE_G722 +int16_t limitValues(int16_t rl) { + int16_t yl; + + yl = (rl > 16383) ? 16383 : ((rl < -16384) ? -16384 : rl); + + return yl; +} +/*- End of function --------------------------------------------------------*/ +#endif + +static int16_t q6[32] = {0, 35, 72, 110, 150, 190, 233, 276, 323, 370, 422, + 473, 530, 587, 650, 714, 786, 858, 940, 1023, 1121, 1219, + 1339, 1458, 1612, 1765, 1980, 2195, 2557, 2919, 0, 0}; +static int16_t iln[32] = {0, 63, 62, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, + 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 0}; +static int16_t ilp[32] = {0, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, + 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 0}; +static int16_t wl[8] = {-60, -30, 58, 172, 334, 538, 1198, 3042}; +static int16_t rl42[16] = {0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0}; +static int16_t ilb[32] = {2048, 2093, 2139, 2186, 2233, 2282, 2332, 2383, 2435, 2489, 2543, + 2599, 2656, 2714, 2774, 2834, 2896, 2960, 3025, 3091, 3158, 3228, + 3298, 3371, 3444, 3520, 3597, 3676, 3756, 3838, 3922, 4008}; +static int16_t qm4[16] = {0, -20456, -12896, -8968, -6288, -4240, -2584, -1200, + 20456, 12896, 8968, 6288, 4240, 2584, 1200, 0}; +static int16_t qm2[4] = {-7408, -1616, 7408, 1616}; +static int16_t qmf_coeffs[12] = { + 3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11, +}; +static int16_t ihn[3] = {0, 1, 0}; +static int16_t ihp[3] = {0, 3, 2}; +static int16_t wh[3] = {0, -214, 798}; +static int16_t rh2[4] = {2, 1, 2, 1}; + +int g722_encode(g722_encode_state_t *s, uint8_t g722_data[], const int16_t amp[], int len) { + int dlow; + int dhigh; + int el; + int wd; + int wd1; + int ril; + int wd2; + int il4; + int ih2; + int wd3; + int eh; + int mih; + int i; + int j; + /* Low and high band PCM from the QMF */ + int xlow; + int xhigh; + int g722_bytes; + /* Even and odd tap accumulators */ + int sumeven; + int sumodd; + int ihigh; + int ilow; + int code; + + g722_bytes = 0; + xhigh = 0; + for (j = 0; j < len;) { + if (s->itu_test_mode) { + xlow = xhigh = amp[j++] >> 1; + } else { + { + /* Apply the transmit QMF */ + /* Shuffle the buffer down */ + for (i = 0; i < 22; i++) { + s->x[i] = s->x[i + 2]; + } + // TODO: if len is odd, then this can be a buffer overrun + s->x[22] = amp[j++]; + s->x[23] = amp[j++]; + + /* Discard every other QMF output */ + sumeven = 0; + sumodd = 0; + for (i = 0; i < 12; i++) { + sumodd += s->x[2 * i] * qmf_coeffs[i]; + sumeven += s->x[2 * i + 1] * qmf_coeffs[11 - i]; + } + /* We shift by 12 to allow for the QMF filters (DC gain = 4096), plus 1 + to allow for us summing two filters, plus 1 to allow for the 15 bit + input to the G.722 algorithm. */ + xlow = (sumeven + sumodd) >> 14; + xhigh = (sumeven - sumodd) >> 14; + +#ifdef RUN_LIKE_REFERENCE_G722 + /* The following lines are only used to verify bit-exactness + * with reference implementation of G.722. Higher precision + * is achieved without limiting the values. + */ + xlow = limitValues(xlow); + xhigh = limitValues(xhigh); +#endif + } + } + /* Block 1L, SUBTRA */ + el = saturate(xlow - s->band[0].s); + + /* Block 1L, QUANTL */ + wd = (el >= 0) ? el : -(el + 1); + + for (i = 1; i < 30; i++) { + wd1 = (q6[i] * s->band[0].det) >> 12; + if (wd < wd1) { + break; + } + } + ilow = (el < 0) ? iln[i] : ilp[i]; + + /* Block 2L, INVQAL */ + ril = ilow >> 2; + wd2 = qm4[ril]; + dlow = (s->band[0].det * wd2) >> 15; + + /* Block 3L, LOGSCL */ + il4 = rl42[ril]; + wd = (s->band[0].nb * 127) >> 7; + s->band[0].nb = wd + wl[il4]; + if (s->band[0].nb < 0) { + s->band[0].nb = 0; + } else if (s->band[0].nb > 18432) { + s->band[0].nb = 18432; + } + + /* Block 3L, SCALEL */ + wd1 = (s->band[0].nb >> 6) & 31; + wd2 = 8 - (s->band[0].nb >> 11); + wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); + s->band[0].det = wd3 << 2; + + block4(&s->band[0], dlow); + { + int nb; + + /* Block 1H, SUBTRA */ + eh = saturate(xhigh - s->band[1].s); + + /* Block 1H, QUANTH */ + wd = (eh >= 0) ? eh : -(eh + 1); + wd1 = (564 * s->band[1].det) >> 12; + mih = (wd >= wd1) ? 2 : 1; + ihigh = (eh < 0) ? ihn[mih] : ihp[mih]; + + /* Block 2H, INVQAH */ + wd2 = qm2[ihigh]; + dhigh = (s->band[1].det * wd2) >> 15; + + /* Block 3H, LOGSCH */ + ih2 = rh2[ihigh]; + wd = (s->band[1].nb * 127) >> 7; + + nb = wd + wh[ih2]; + if (nb < 0) { + nb = 0; + } else if (nb > 22528) { + nb = 22528; + } + s->band[1].nb = nb; + + /* Block 3H, SCALEH */ + wd1 = (s->band[1].nb >> 6) & 31; + wd2 = 10 - (s->band[1].nb >> 11); + wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); + s->band[1].det = wd3 << 2; + + block4(&s->band[1], dhigh); +#if BITS_PER_SAMPLE == 8 + code = ((ihigh << 6) | ilow); +#elif BITS_PER_SAMPLE == 7 + code = ((ihigh << 6) | ilow) >> 1; +#elif BITS_PER_SAMPLE == 6 + code = ((ihigh << 6) | ilow) >> 2; +#endif + } + +#if PACKED_OUTPUT == 1 + /* Pack the code bits */ + s->out_buffer |= (code << s->out_bits); + s->out_bits += s->bits_per_sample; + if (s->out_bits >= 8) { + g722_data[g722_bytes++] = (uint8_t)(s->out_buffer & 0xFF); + s->out_bits -= 8; + s->out_buffer >>= 8; + } +#else + g722_data[g722_bytes++] = (uint8_t)code; +#endif + } + return g722_bytes; +} +/*- End of function --------------------------------------------------------*/ +/*- End of file ------------------------------------------------------------*/ diff --git a/spa/plugins/bluez5/hci.c b/spa/plugins/bluez5/hci.c new file mode 100644 index 0000000..9395a30 --- /dev/null +++ b/spa/plugins/bluez5/hci.c @@ -0,0 +1,67 @@ +/* Spa HSP/HFP native backend HCI support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include + +#include + +int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter) +{ + int hci_id; + spa_autoclose 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) + return -errno; + + memset(&a, 0, sizeof(a)); + a.hci_family = AF_BLUETOOTH; + a.hci_dev = hci_id; + if (bind(sock, (struct sockaddr *) &a, sizeof(a)) < 0) + return -errno; + + if (hci_read_local_ext_features(sock, 0, &max_page, features, 1000) < 0) + return -errno; + + adapter->msbc_probed = true; + adapter->has_msbc = ((features[2] & LMP_TRSP_SCO) && (features[3] & LMP_ESCO)) ? 1 : 0; + return adapter->has_msbc; +} + +#endif diff --git a/spa/plugins/bluez5/iso-io.c b/spa/plugins/bluez5/iso-io.c new file mode 100644 index 0000000..802961b --- /dev/null +++ b/spa/plugins/bluez5/iso-io.c @@ -0,0 +1,558 @@ +/* Spa ISO I/O */ +/* SPDX-FileCopyrightText: Copyright © 2023 Pauli Virtanen. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "iso-io.h" + +#include "media-codecs.h" +#include "defs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.iso"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#include "bt-latency.h" + +#define IDLE_TIME (500 * SPA_NSEC_PER_MSEC) +#define EMPTY_BUF_SIZE 65536 + +#define LATENCY_PERIOD (200 * SPA_NSEC_PER_MSEC) +#define MAX_PACKET_QUEUE 3 + +struct group { + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + struct spa_source source; + struct spa_list streams; + int timerfd; + uint8_t id; + uint64_t next; + uint64_t duration; + bool started; +}; + +struct stream { + struct spa_bt_iso_io this; + struct spa_list link; + struct group *group; + int fd; + bool sink; + bool idle; + + spa_bt_iso_io_pull_t pull; + + const struct media_codec *codec; + uint32_t block_size; + + struct spa_bt_latency tx_latency; +}; + +struct modify_info +{ + struct stream *stream; + struct spa_list *streams; +}; + +static int do_modify(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct modify_info *info = user_data; + + if (info->streams) + spa_list_append(info->streams, &info->stream->link); + else + spa_list_remove(&info->stream->link); + + return 0; +} + +static void stream_link(struct group *group, struct stream *stream) +{ + struct modify_info info = { .stream = stream, .streams = &group->streams }; + int res; + + res = spa_loop_invoke(group->data_loop, do_modify, 0, NULL, 0, true, &info); + spa_assert_se(res == 0); +} + +static void stream_unlink(struct stream *stream) +{ + struct modify_info info = { .stream = stream, .streams = NULL }; + int res; + + res = spa_loop_invoke(stream->group->data_loop, do_modify, 0, NULL, 0, true, &info); + spa_assert_se(res == 0); +} + +static int stream_silence(struct stream *stream) +{ + static uint8_t empty[EMPTY_BUF_SIZE] = {0}; + const size_t max_size = sizeof(stream->this.buf); + int res, used, need_flush; + size_t encoded; + + stream->idle = true; + + res = used = stream->codec->start_encode(stream->this.codec_data, stream->this.buf, max_size, 0, 0); + if (res < 0) + return res; + + res = stream->codec->encode(stream->this.codec_data, empty, stream->block_size, + SPA_PTROFF(stream->this.buf, used, void), max_size - used, &encoded, &need_flush); + if (res < 0) + return res; + + used += encoded; + + if (!need_flush) + return -EINVAL; + + stream->this.size = used; + return 0; +} + +static int set_timeout(struct group *group, 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(group->data_system, + group->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static uint64_t get_time_ns(struct spa_system *system, clockid_t clockid) +{ + struct timespec now; + + spa_system_clock_gettime(system, clockid, &now); + return SPA_TIMESPEC_TO_NSEC(&now); +} + +static int set_timers(struct group *group) +{ + if (group->duration == 0) + return -EINVAL; + + group->next = SPA_ROUND_UP(get_time_ns(group->data_system, CLOCK_MONOTONIC) + group->duration, + group->duration); + + return set_timeout(group, group->next); +} + +static void drop_rx(int fd) +{ + ssize_t res; + + do { + res = recv(fd, NULL, 0, MSG_TRUNC | MSG_DONTWAIT); + } while (res >= 0); +} + +static void group_on_timeout(struct spa_source *source) +{ + struct group *group = source->data; + struct stream *stream; + bool resync = false; + bool fail = false; + uint64_t exp; + int res; + + if ((res = spa_system_timerfd_read(group->data_system, group->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(group->log, "%p: ISO group:%u error reading timerfd: %s", + group, group->id, spa_strerror(res)); + return; + } + + spa_list_for_each(stream, &group->streams, link) { + if (!stream->sink) { + if (!stream->pull) { + /* Source not running: drop any incoming data */ + drop_rx(stream->fd); + } + continue; + } + + spa_bt_latency_recv_errqueue(&stream->tx_latency, stream->fd, group->log); + + if (stream->this.need_resync) { + resync = true; + stream->this.need_resync = false; + } + + if (!group->started && !stream->idle && stream->this.size > 0) + group->started = true; + } + + /* Produce output */ + spa_list_for_each(stream, &group->streams, link) { + int res = 0; + uint64_t now; + int32_t min_latency = INT32_MAX, max_latency = INT32_MIN; + struct stream *other; + + if (!stream->sink) + continue; + if (!group->started) { + stream->this.resync = true; + stream->this.size = 0; + continue; + } + if (stream->this.size == 0) { + spa_log_debug(group->log, "%p: ISO group:%u miss fd:%d", + group, group->id, stream->fd); + if (stream_silence(stream) < 0) { + fail = true; + continue; + } + } + + spa_list_for_each(other, &group->streams, link) { + if (!other->sink || stream == other || !other->tx_latency.valid) + continue; + min_latency = SPA_MIN(min_latency, other->tx_latency.ptp.min); + max_latency = SPA_MAX(max_latency, other->tx_latency.ptp.max); + } + + if (stream->tx_latency.valid && min_latency <= max_latency && + stream->tx_latency.ptp.min > min_latency + (int64_t)group->duration/2 && + stream->tx_latency.ptp.max > max_latency + (int64_t)group->duration/2) { + spa_log_debug(group->log, "%p: ISO group:%u latency skip align fd:%d", group, group->id, stream->fd); + spa_bt_latency_reset(&stream->tx_latency); + goto stream_done; + } + + /* TODO: this should use rate match */ + if (stream->tx_latency.valid && + stream->tx_latency.ptp.min > MAX_PACKET_QUEUE * (int64_t)group->duration) { + spa_log_debug(group->log, "%p: ISO group:%u latency skip fd:%d", group, group->id, stream->fd); + spa_bt_latency_reset(&stream->tx_latency); + goto stream_done; + } + + now = get_time_ns(group->data_system, CLOCK_REALTIME); + res = send(stream->fd, stream->this.buf, stream->this.size, MSG_DONTWAIT | MSG_NOSIGNAL); + if (res < 0) { + res = -errno; + fail = true; + } else { + spa_bt_latency_sent(&stream->tx_latency, now); + } + + stream_done: + spa_log_trace(group->log, "%p: ISO group:%u sent fd:%d size:%u ts:%u idle:%d res:%d latency:%d..%d us", + group, group->id, stream->fd, (unsigned)stream->this.size, + (unsigned)stream->this.timestamp, stream->idle, res, + stream->tx_latency.valid ? stream->tx_latency.ptp.min/1000 : -1, + stream->tx_latency.valid ? stream->tx_latency.ptp.max/1000 : -1); + + stream->this.size = 0; + } + + if (fail) + spa_log_debug(group->log, "%p: ISO group:%d send failure", group, group->id); + + /* Pull data for the next interval */ + group->next += exp * group->duration; + + spa_list_for_each(stream, &group->streams, link) { + if (!stream->sink) + continue; + + if (resync) + stream->this.resync = true; + + if (stream->pull) { + stream->idle = false; + stream->this.now = group->next; + stream->pull(&stream->this); + } else { + stream_silence(stream); + } + } + + set_timeout(group, group->next); +} + +static struct group *group_create(struct spa_bt_transport *t, + struct spa_log *log, struct spa_loop *data_loop, struct spa_system *data_system) +{ + struct group *group; + uint8_t id; + + if (t->profile & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE)) { + id = t->bap_cig; + } else if (t->profile & (SPA_BT_PROFILE_BAP_BROADCAST_SINK | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE)) { + id = t->bap_big; + } else { + errno = EINVAL; + return NULL; + } + + group = calloc(1, sizeof(struct group)); + if (group == NULL) + return NULL; + + spa_log_topic_init(log, &log_topic); + + group->id = id; + group->log = log; + group->data_loop = data_loop; + group->data_system = data_system; + group->duration = 0; + + spa_list_init(&group->streams); + + group->timerfd = spa_system_timerfd_create(group->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + if (group->timerfd < 0) { + int err = errno; + free(group); + errno = err; + return NULL; + } + + group->source.data = group; + group->source.fd = group->timerfd; + group->source.func = group_on_timeout; + group->source.mask = SPA_IO_IN; + group->source.rmask = 0; + spa_loop_add_source(group->data_loop, &group->source); + + return group; +} + +static int do_remove_source(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct group *group = user_data; + + if (group->source.loop) + spa_loop_remove_source(group->data_loop, &group->source); + + set_timeout(group, 0); + + return 0; +} + +static void group_destroy(struct group *group) +{ + int res; + + spa_assert(spa_list_is_empty(&group->streams)); + + res = spa_loop_invoke(group->data_loop, do_remove_source, 0, NULL, 0, true, group); + spa_assert_se(res == 0); + + close(group->timerfd); + free(group); +} + +static struct stream *stream_create(struct spa_bt_transport *t, struct group *group) +{ + struct stream *stream; + void *codec_data = NULL; + int block_size = 0; + struct spa_audio_info format = { 0 }; + int res; + bool sink; + + if (t->profile == SPA_BT_PROFILE_BAP_SINK || + t->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) { + sink = true; + } else { + sink = false; + } + + if (!t->media_codec->bap || !t->media_codec->get_interval) { + res = -EINVAL; + goto fail; + } + + if (sink) { + uint64_t interval; + + res = t->media_codec->validate_config(t->media_codec, 0, t->configuration, t->configuration_len, &format); + if (res < 0) + goto fail; + + codec_data = t->media_codec->init(t->media_codec, 0, t->configuration, t->configuration_len, + &format, NULL, t->write_mtu); + if (!codec_data) { + res = -EINVAL; + goto fail; + } + + block_size = t->media_codec->get_block_size(codec_data); + if (block_size < 0 || block_size > EMPTY_BUF_SIZE) { + res = -EINVAL; + goto fail; + } + + interval = t->media_codec->get_interval(codec_data); + if (interval <= 5000) { + res = -EINVAL; + goto fail; + } + + if (group->duration == 0) { + group->duration = interval; + } else if (interval != group->duration) { + /* SDU_Interval in ISO group must be same for each direction */ + res = -EINVAL; + goto fail; + } + } + + stream = calloc(1, sizeof(struct stream)); + if (stream == NULL) + goto fail_errno; + + stream->fd = t->fd; + stream->sink = sink; + stream->group = group; + stream->this.duration = sink ? group->duration : 0; + + stream->codec = t->media_codec; + stream->this.codec_data = codec_data; + stream->this.format = format; + stream->block_size = block_size; + + spa_bt_latency_init(&stream->tx_latency, stream->fd, LATENCY_PERIOD, group->log); + + if (sink) + stream_silence(stream); + + stream_link(group, stream); + + return stream; + +fail_errno: + res = -errno; +fail: + if (codec_data) + t->media_codec->deinit(codec_data); + errno = -res; + return NULL; +} + +struct spa_bt_iso_io *spa_bt_iso_io_create(struct spa_bt_transport *t, + struct spa_log *log, struct spa_loop *data_loop, struct spa_system *data_system) +{ + struct stream *stream; + struct group *group; + + group = group_create(t, log, data_loop, data_system); + if (group == NULL) + return NULL; + + stream = stream_create(t, group); + if (stream == NULL) { + int err = errno; + group_destroy(group); + errno = err; + return NULL; + } + + return &stream->this; +} + +struct spa_bt_iso_io *spa_bt_iso_io_attach(struct spa_bt_iso_io *this, struct spa_bt_transport *t) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + + stream = stream_create(t, stream->group); + if (stream == NULL) + return NULL; + + return &stream->this; +} + +void spa_bt_iso_io_destroy(struct spa_bt_iso_io *this) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + + stream_unlink(stream); + + spa_bt_latency_flush(&stream->tx_latency, stream->fd, stream->group->log); + + if (spa_list_is_empty(&stream->group->streams)) + group_destroy(stream->group); + + if (stream->this.codec_data) + stream->codec->deinit(stream->this.codec_data); + stream->this.codec_data = NULL; + + free(stream); +} + +static bool group_is_enabled(struct group *group) +{ + struct stream *stream; + + spa_list_for_each(stream, &group->streams, link) { + if (!stream->sink) + continue; + if (stream->pull) + return true; + } + + return false; +} + +/** Must be called from data thread */ +void spa_bt_iso_io_set_cb(struct spa_bt_iso_io *this, spa_bt_iso_io_pull_t pull, void *user_data) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + bool was_enabled, enabled; + + if (!stream->sink) + return; + + was_enabled = group_is_enabled(stream->group); + + stream->pull = pull; + stream->this.user_data = user_data; + + enabled = group_is_enabled(stream->group); + + if (!enabled && was_enabled) { + stream->group->started = false; + set_timeout(stream->group, 0); + } else if (enabled && !was_enabled) { + set_timers(stream->group); + } + + stream->idle = true; + stream->this.resync = true; + + if (pull == NULL) { + stream->this.size = 0; + return; + } +} + +/** Must be called from data thread */ +int spa_bt_iso_io_recv_errqueue(struct spa_bt_iso_io *this) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + struct group *group = stream->group; + + return spa_bt_latency_recv_errqueue(&stream->tx_latency, stream->fd, group->log); +} diff --git a/spa/plugins/bluez5/iso-io.h b/spa/plugins/bluez5/iso-io.h new file mode 100644 index 0000000..ed49c77 --- /dev/null +++ b/spa/plugins/bluez5/iso-io.h @@ -0,0 +1,49 @@ +/* Spa Bluez5 ISO I/O */ +/* SPDX-FileCopyrightText: Copyright © 2023 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_ISO_IO_H +#define SPA_BLUEZ5_ISO_IO_H + +#include +#include +#include +#include +#include + +struct spa_bt_transport; + +/** + * ISO I/O. + * + * Synchronizes related writes from different streams in the same group + * to occur at same real time instant (or not at all). + */ +struct spa_bt_iso_io +{ + uint64_t now; /**< Reference time position of next packet (read-only) */ + uint64_t duration; /**< ISO interval duration in ns (read-only) */ + bool resync; /**< Resync position for next packet; (pull callback sets to + * false when done) */ + + uint32_t timestamp; /**< Packet timestamp (set by pull callback) */ + uint8_t buf[4096]; /**< Packet data (set by pull callback) */ + size_t size; /**< Packet size (set by pull callback) */ + bool need_resync; /**< Resync requested (set by pull callback) */ + + struct spa_audio_info format; /**< Audio format */ + void *codec_data; /**< Codec data */ + + void *user_data; +}; + +typedef void (*spa_bt_iso_io_pull_t)(struct spa_bt_iso_io *io); + +struct spa_bt_iso_io *spa_bt_iso_io_create(struct spa_bt_transport *t, + struct spa_log *log, struct spa_loop *data_loop, struct spa_system *data_system); +struct spa_bt_iso_io *spa_bt_iso_io_attach(struct spa_bt_iso_io *io, struct spa_bt_transport *t); +void spa_bt_iso_io_destroy(struct spa_bt_iso_io *io); +void spa_bt_iso_io_set_cb(struct spa_bt_iso_io *io, spa_bt_iso_io_pull_t pull, void *user_data); +int spa_bt_iso_io_recv_errqueue(struct spa_bt_iso_io *io); + +#endif diff --git a/spa/plugins/bluez5/media-codecs.c b/spa/plugins/bluez5/media-codecs.c new file mode 100644 index 0000000..718e54e --- /dev/null +++ b/spa/plugins/bluez5/media-codecs.c @@ -0,0 +1,227 @@ +/* + * 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 +#include + +#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; + spa_autofree int *scores = NULL; + int 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) + return -EINVAL; + + return res; +} + +int media_codec_get_config(const struct media_codec_config configs[], size_t n, + uint32_t conf) +{ + size_t i; + + for (i = 0; i < n; ++i) + if (configs[i].config == conf) + return configs[i].value; + + return -EINVAL; +} + +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; +} + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..c9a00f7 --- /dev/null +++ b/spa/plugins/bluez5/media-codecs.h @@ -0,0 +1,232 @@ +/* Spa A2DP codec API */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_A2DP_CODECS_H_ +#define SPA_BLUEZ5_A2DP_CODECS_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 12 + +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; \ + SPA_LOG_TOPIC_DEFINE(codec_plugin_log_topic, "spa.bluez5.codecs." basename); + +extern const struct media_codec * const * const codec_plugin_media_codecs; +extern const char *codec_plugin_factory_name; +extern struct spa_log_topic codec_plugin_log_topic; +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &codec_plugin_log_topic +#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; + bool asha; + + 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 */ + + int (*get_bis_config)(const struct media_codec *codec, uint8_t *caps, + uint8_t *caps_size, struct spa_dict *settings, + struct bap_codec_qos *qos); + + /** 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, + const struct spa_dict *settings, uint8_t caps[A2DP_MAX_CAPS_SIZE]); + + int (*select_config) (const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + 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); + + /** Number of bytes needed for encoding */ + int (*get_block_size) (void *data); + + /** + * Duration of the next packet in nanoseconds. + * + * For BAP this shall be constant and equal to the SDU interval. + * + * \param data Codec data from init() + * \return Duration in nanoseconds. + */ + uint64_t (*get_interval) (void *data); + + int (*abr_process) (void *data, size_t unsent); + + /** + * Start encoding new packet. + * + * \param data Codec data from init() + * \param timestamp Packet time stamp (in samples played) + * \return Size of packet header written to dst in bytes, or < 0 for error + */ + int (*start_encode) (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp); + + /** + * Consume data from input buffer, encode to output buffer. + * + * \param data Codec data from init() + * \param src Source data. NULL if encoding packet fragment. + * \param dst Output buffer position. The memory region passed to the + * previous start_encode() is still valid, and this position is inside + * that region. The caller does not modify the contents of the buffer. + * \param dst_size Remaining buffer space after dst + * \param dst_out Bytes written to dst + * \param need_flush + * - NEED_FLUSH_NO: don't flush this packet, + * - NEED_FLUSH_ALL: flush this packet, + * - NEED_FLUSH_FRAGMENT: flush packet fragment. The next start_encode() + * and encode() are expected to produce more fragments or the final + * fragment with NEED_FLUSH_ALL, without consuming source data. + * The fragment start_encode() is called with the same output buffer + * as previous. The fragment encode() will be called with NULL src. + * No new source data will be fed in before NEED_FLUSH_ALL. + * \return Number of bytes consumed from src, or < 0 for error + */ + int (*encode) (void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush); + + /** + * Start decoding received packet. + * + * \return Number of bytes consumed from source data, or < 0 for error + */ + int (*start_decode) (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp); + + /** + * Decode received packet data. + * + * \param dst_out Number of bytes output to dst + * \return Number of bytes consumed from src, or < 0 for error + */ + 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); + + /** + * Get codec internal delays, in samples at input/output rates. + * + * The delay does not include the duration of the PCM input/output + * audio data, but is that internal to the codec. + * + * \param[out] encoder Encoder delay in samples, or NULL + * \param[out] decoder Decoder delay in samples, or NULL + */ + void (*get_delay) (void *data, uint32_t *encoder, uint32_t *decoder); +}; + +struct media_codec_config { + 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); + +int media_codec_get_config(const struct media_codec_config configs[], size_t n, + uint32_t conf); + +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..49dc5de --- /dev/null +++ b/spa/plugins/bluez5/media-sink.c @@ -0,0 +1,2309 @@ +/* Spa Media Sink */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "defs.h" +#include "rtp.h" +#include "media-codecs.h" +#include "rate-control.h" +#include "iso-io.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.sink.media"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#include "bt-latency.h" + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + int64_t latency_offset; + char clock_name[64]; +}; + +#define FILL_FRAMES 4 +#define MIN_BUFFERS 3 +#define MAX_BUFFERS 32 +#define BUFFER_SIZE (8192*8) +#define RATE_CTL_DIFF_MAX 0.005 +#define LATENCY_PERIOD (200 * SPA_NSEC_PER_MSEC) + +/* Wait for two cycles before trying to sync ISO. On start/driver reassign, + * first cycle may have strange number of samples. */ +#define RESYNC_CYCLES 2 + +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_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; + + size_t ready_offset; + + struct spa_bt_rate_control ratectl; +}; + +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_loop_utils *loop_utils; + + 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 start_ready:1; + unsigned int transport_started:1; + unsigned int following:1; + unsigned int is_output:1; + unsigned int flush_pending:1; + unsigned int iso_pending:1; + unsigned int own_codec_data:1; + + unsigned int is_duplex:1; + unsigned int is_internal: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 process_duration; + uint64_t process_rate; + + uint64_t prev_flush_time; + uint64_t next_flush_time; + + uint64_t packet_delay_ns; + struct spa_source *update_delay_event; + + uint32_t encoder_delay; + + const struct media_codec *codec; + bool codec_props_changed; + void *codec_props; + void *codec_data; + struct spa_audio_info codec_format; + + int need_flush; + bool fragment; + uint32_t resync; + 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, ¶m)) != 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 inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +struct reassign_io_info { + struct impl *this; + struct spa_io_position *position; + struct spa_io_clock *clock; +}; + +static int do_reassign_io(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct reassign_io_info *info = user_data; + struct impl *this = info->this; + bool following; + + if (this->position != info->position || this->clock != info->clock) + this->resync = RESYNC_CYCLES; + + this->position = info->position; + this->clock = info->clock; + + following = is_following(this); + + if (following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + 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; + struct reassign_io_info info = { .this = this, .position = this->position, .clock = this->clock }; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + info.clock = data; + if (info.clock != NULL) { + spa_scnprintf(info.clock->name, + sizeof(info.clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + info.position = data; + break; + default: + return -ENOENT; + } + + if (this->started) { + spa_loop_invoke(this->data_loop, do_reassign_io, 0, NULL, 0, true, &info); + } else { + this->clock = info.clock; + this->position = info.position; + } + + 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; + + /* in main loop */ + + if (this->transport == NULL || !port->have_format) + return; + + /* + * We start flushing data immediately, so the delay is: + * + * (packet delay) + (codec internal delay) + (transport delay) + (latency offset) + * + * and doesn't depend on the quantum. Kernel knows the latency due to + * socket/controller queue, but doesn't tell us, so not included but + * hopefully in < 10 ms range. + */ + + delay = __atomic_load_n(&this->packet_delay_ns, __ATOMIC_RELAXED); + delay += (int64_t)this->encoder_delay * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate; + delay += spa_bt_transport_get_delay_nsec(this->transport); + delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2); + delay = SPA_MAX(delay, 0); + + port->latency.min_ns = port->latency.max_ns = delay; + port->latency.min_rate = port->latency.max_rate = 0; + port->latency.min_quantum = port->latency.max_quantum = 0.0f; + + spa_log_info(this->log, "%p: total latency:%d ms", this, (int)(delay / SPA_NSEC_PER_MSEC)); + + 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 void update_delay_event(void *data, uint64_t count) +{ + struct impl *this = data; + + /* in main loop */ + set_latency(this, true); +} + +static void update_packet_delay(struct impl *this, uint64_t delay) +{ + uint64_t old_delay = this->packet_delay_ns; + + /* in data thread */ + + delay = SPA_MAX(delay, old_delay); + if (delay == old_delay) + return; + + __atomic_store_n(&this->packet_delay_ns, delay, __ATOMIC_RELAXED); + if (this->update_delay_event) + spa_loop_utils_signal_event(this->loop_utils, this->update_delay_event); +} + +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 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; + + /* Count (partially) encoded packet */ + bytes += this->tmp_buffer_used; + bytes += this->block_count * this->block_size; + + return bytes / port->frame_size; +} + +static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret) +{ + struct port *port = &this->port; + uint64_t t, duration_ns; + bool resampling; + + if (!this->process_rate || !this->process_duration) { + if (this->position) { + this->process_duration = this->position->clock.duration; + this->process_rate = this->position->clock.rate.denom; + } else { + this->process_duration = 1024; + this->process_rate = 48000; + } + } + + duration_ns = ((uint64_t)this->process_duration * SPA_NSEC_PER_SEC / this->process_rate); + if (duration_ns_ret) + *duration_ns_ret = duration_ns; + + /* Time at the first sample in the current packet. */ + t = this->process_time + duration_ns; + t -= ((uint64_t)get_queued_frames(this) * SPA_NSEC_PER_SEC + / port->current_format.info.raw.rate); + + /* Account for resampling delay */ + resampling = (port->current_format.info.raw.rate != this->process_rate) || this->following; + if (port->rate_match && this->position && resampling) { + t -= (port->rate_match->delay * SPA_NSEC_PER_SEC + port->rate_match->delay_frac) + / port->current_format.info.raw.rate; + } + + return t; +} + +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->timestamp = this->codec->bap ? (get_reference_time(this, NULL) / SPA_NSEC_PER_USEC) + : this->sample_count; + 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; + return 0; +} + +static int setup_matching(struct impl *this) +{ + struct port *port = &this->port; + + if (!this->transport_started) + port->ratectl.corr = 1.0; + + if (port->rate_match) { + port->rate_match->rate = 1 / port->ratectl.corr; + + SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->following); + } + + 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 need_flush:%d", this, + this->buffer_used, this->block_size, this->need_flush); + + 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 int flush_data(struct impl *this, uint64_t now_time) +{ + int written; + uint32_t total_frames; + struct port *port = &this->port; + int unused_buffer; + + spa_assert(this->transport_started); + + /* I/O in error state? */ + if (this->transport == NULL || !this->flush_source.loop) + return -EIO; + if (!this->flush_timer_source.loop && !this->transport->iso_io) + return -EIO; + + if (this->transport->iso_io && !this->iso_pending) + return 0; + + 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 (this->transport->iso_io) { + struct spa_bt_iso_io *iso_io = this->transport->iso_io; + + if (this->need_flush) { + size_t avail = SPA_MIN(this->buffer_used, sizeof(iso_io->buf)); + + spa_log_trace(this->log, "%p: ISO put fd:%d size:%u sn:%u ts:%u now:%"PRIu64, + this, this->transport->fd, (unsigned)avail, + (unsigned)this->seqnum, (unsigned)this->timestamp, + iso_io->now); + + memcpy(iso_io->buf, this->buffer, avail); + iso_io->size = avail; + iso_io->timestamp = this->timestamp; + this->iso_pending = false; + + reset_buffer(this); + + update_packet_delay(this, iso_io->duration * 3/2); + } + 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)) { + uint64_t duration_ns; + + /* + * Flush at the time position of the next buffered sample. + */ + this->next_flush_time = get_reference_time(this, &duration_ns) + + packet_time; + + /* + * We can 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. + * + * Although in principle this should not be needed, we + * do it regardless in case it helps. + */ +#if 1 + this->next_flush_time += SPA_MIN(packet_time, + duration_ns * (SPA_MAX(port->n_buffers, 2u) - 2)); +#endif + } else { + if (this->next_flush_time == 0) + this->next_flush_time = this->process_time; + this->next_flush_time += packet_time; + } + + update_packet_delay(this, 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); + + /* Encode next packet already now; it will be flushed later on timer */ + goto again; + } + 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 drop_frames(struct impl *this, uint32_t req) +{ + struct port *port = &this->port; + + while (req > 0 && !spa_list_is_empty(&port->ready)) { + struct buffer *b; + struct spa_data *d; + uint32_t avail; + + b = spa_list_first(&port->ready, struct buffer, link); + d = b->buf->datas; + + avail = d[0].chunk->size - port->ready_offset; + avail /= port->frame_size; + + avail = SPA_MIN(avail, req); + port->ready_offset += avail * port->frame_size; + req -= avail; + + 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; + } + + spa_log_trace(this->log, "%p: skipped %u frames", this, avail); + } +} + +static void media_iso_pull(struct spa_bt_iso_io *iso_io) +{ + struct impl *this = iso_io->user_data; + struct port *port = &this->port; + const double period = 0.1 * SPA_NSEC_PER_SEC; + uint64_t duration_ns; + double value, target, err, max_err; + + if (this->resync || !this->position) { + spa_bt_rate_control_init(&port->ratectl, 0); + goto done; + } + + /* + * Rate match sample position so that the graph is 3/2 ISO interval + * ahead of the time instant we have to send data. + * + * Being 1 ISO interval ahead is unavoidable otherwise we underrun, + * and the 1/2 is safety margin for the graph to deliver data + * in time. + * + * This is then the part of the TX latency on PipeWire side. There is + * another part of TX latency on kernel/controller side before the + * controller starts processing the packet. + */ + + value = (int64_t)iso_io->now - (int64_t)get_reference_time(this, &duration_ns); + target = iso_io->duration * 3/2; + err = value - target; + max_err = iso_io->duration; + + if (iso_io->resync && err >= 0) { + unsigned int req = (unsigned int)(err * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC); + + if (req > 0) { + spa_bt_rate_control_init(&port->ratectl, 0); + drop_frames(this, req); + } + spa_log_debug(this->log, "%p: ISO sync skip frames:%u", this, req); + } else if (iso_io->resync && -err >= 0) { + unsigned int req = (unsigned int)(-err * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC); + static const uint8_t empty[8192] = {0}; + + if (req > 0) { + spa_bt_rate_control_init(&port->ratectl, 0); + req = SPA_MIN(req, sizeof(empty) / port->frame_size); + add_data(this, empty, req * port->frame_size); + } + spa_log_debug(this->log, "%p: ISO sync pad frames:%u", this, req); + } else if (err > max_err || -err > max_err) { + iso_io->need_resync = true; + spa_log_debug(this->log, "%p: ISO sync need resync err:%+.3f", + this, err / SPA_NSEC_PER_MSEC); + } else { + spa_bt_rate_control_update(&port->ratectl, err, 0, + iso_io->duration, period, RATE_CTL_DIFF_MAX); + spa_log_trace(this->log, "%p: ISO sync err:%+.3g value:%.6f target:%.6f (ms) corr:%g", + this, + port->ratectl.avg / SPA_NSEC_PER_MSEC, + value / SPA_NSEC_PER_MSEC, + target / SPA_NSEC_PER_MSEC, + port->ratectl.corr); + } + + iso_io->resync = false; + +done: + this->iso_pending = true; + flush_data(this, this->current_time); +} + +static void media_on_flush_error(struct spa_source *source) +{ + struct impl *this = source->data; + + if (source->rmask & SPA_IO_ERR) { + /* TX timestamp info? */ + if (this->transport && this->transport->iso_io) + if (spa_bt_iso_io_recv_errqueue(this->transport->iso_io) == 0) + return; + + /* Otherwise: actual error */ + } + + spa_log_trace(this->log, "%p: flush event", this); + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + 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); + enable_flush_timer(this, false); + if (this->flush_timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + if (this->transport && this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + 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 status, res; + + 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.target_duration; + rate = this->position->clock.target_rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + setup_matching(this); + + this->next_time = (uint64_t)(now_time + duration * SPA_NSEC_PER_SEC / rate * port->ratectl.corr); + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->rate = this->clock->target_rate; + this->clock->position += this->clock->duration; + this->clock->duration = duration; + this->clock->rate_diff = 1 / port->ratectl.corr; + this->clock->next_nsec = this->next_time; + this->clock->delay = 0; + } + + status = this->transport_started ? SPA_STATUS_NEED_DATA : SPA_STATUS_HAVE_DATA; + + spa_log_trace(this->log, "%p: %d -> %d", this, io->status, status); + io->status = status; + io->buffer_id = SPA_ID_INVALID; + spa_node_call_ready(&this->callbacks, status); + + set_timeout(this, this->next_time); +} + +static int do_start_iso_io(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_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); + return 0; +} + +static int transport_start(struct impl *this) +{ + int val, size; + struct port *port; + socklen_t len; + uint8_t *conf; + uint32_t flags; + + if (this->transport_started) + return 0; + if (!this->start_ready) + return -EIO; + + spa_return_val_if_fail(this->transport, -EIO); + + spa_log_debug(this->log, "%p: start transport", this); + + 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; + + if (!this->transport->iso_io) { + this->own_codec_data = true; + 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) { + spa_log_error(this->log, "%p: codec %s initialization failed", this, + this->codec->description); + return -EIO; + } + } else { + this->own_codec_data = false; + this->codec_data = this->transport->iso_io->codec_data; + this->codec_props_changed = true; + } + + this->encoder_delay = 0; + if (this->codec->get_delay) + this->codec->get_delay(this->codec_data, &this->encoder_delay, NULL); + + const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP"); + spa_log_info(this->log, "%p: using %s codec %s, delay:%.2f ms, codec-delay:%.2f ms", this, + codec_profile, this->codec->description, + (double)spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC, + (double)this->encoder_delay * SPA_MSEC_PER_SEC / port->current_format.info.raw.rate); + + this->seqnum = UINT16_MAX; + + 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 = 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); + + spa_bt_rate_control_init(&port->ratectl, 0); + + this->update_delay_event = spa_loop_utils_add_event(this->loop_utils, update_delay_event, this); + + if (!this->transport->iso_io) { + 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->resync = RESYNC_CYCLES; + this->flush_pending = false; + this->iso_pending = false; + + this->transport_started = true; + + if (this->transport->iso_io) + spa_loop_invoke(this->data_loop, do_start_iso_io, 0, NULL, 0, true, this); + + return 0; +} + +static int do_start(struct impl *this) +{ + int res; + + 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); + + this->start_ready = true; + + if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) { + this->start_ready = false; + return res; + } + + this->packet_delay_ns = 0; + + 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); + + setup_matching(this); + + 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; + + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + set_timeout(this, 0); + + if (this->update_delay_event) { + spa_loop_utils_destroy_source(this->loop_utils, this->update_delay_event); + this->update_delay_event = NULL; + } + + return 0; +} + +static int do_remove_transport_source(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_started = false; + + 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); + enable_flush_timer(this, false); + + if (this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + + /* Drop queued data */ + drop_frames(this, UINT32_MAX); + + return 0; +} + +static void transport_stop(struct impl *this) +{ + if (!this->transport_started) + return; + + spa_log_trace(this->log, "%p: stop transport", this); + + spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + + if (this->codec_data && this->own_codec_data) + this->codec->deinit(this->codec_data); + this->codec_data = NULL; +} + +static int do_stop(struct impl *this) +{ + int res = 0; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "%p: stop", this); + + this->start_ready = false; + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + transport_stop(this); + + if (this->transport) + res = spa_bt_transport_release(this->transport); + + 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) +{ + char node_group_buf[256]; + char *node_group = NULL; + + if (this->transport && (this->transport->profile & SPA_BT_PROFILE_BAP_SINK)) { + spa_scnprintf(node_group_buf, sizeof(node_group_buf), "[\"bluez-iso-%s-cig-%d\"]", + this->transport->device->adapter->address, + this->transport->bap_cig); + node_group = node_group_buf; + } else if (this->transport && (this->transport->profile & SPA_BT_PROFILE_BAP_BROADCAST_SINK)) { + spa_scnprintf(node_group_buf, sizeof(node_group_buf), "[\"bluez-iso-%s-big-%d\"]", + this->transport->device->adapter->address, + this->transport->bap_big); + node_group = node_group_buf; + } + + const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP"); + struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" : + this->is_output ? "Audio/Sink" : "Stream/Input/Audio" }, + { "media.name", ((this->transport && this->transport->device->name) ? + this->transport->device->name : codec_profile ) }, + { SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" }, + { "node.group", node_group }, + }; + 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, ¶m)) != 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; + case 1: + if (!this->codec->bap) + return 0; + 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.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + if (this->transport && this->transport->iso_io) { + if (memcmp(&info.info.raw, &this->transport->iso_io->format.info.raw, + sizeof(info.info.raw))) { + spa_log_error(this->log, "unexpected incompatible " + "BAP audio format"); + 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; + } + + set_latency(this, false); + + 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[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, "%p: use buffers %d", this, 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; + case SPA_IO_RateMatch: + if (!this->codec->bap) + return -ENOENT; + 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; + int res; + + 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 (!this->started || !this->transport_started) { + if (io->status != SPA_STATUS_HAVE_DATA) { + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = SPA_ID_INVALID; + } + 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]; + struct spa_data *d = b->buf->datas; + unsigned int frames; + + 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; + } + + frames = d ? d[0].chunk->size / port->frame_size : 0; + spa_log_trace(this->log, "%p: queue buffer %u frames:%u", this, io->buffer_id, frames); + + 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); + } + } + + if (this->position) { + this->process_duration = this->position->clock.duration; + this->process_rate = this->position->clock.rate.denom; + } else { + this->process_duration = 1024; + this->process_rate = 48000; + } + + this->process_time = this->current_time; + if (this->resync) + --this->resync; + + setup_matching(this); + + spa_log_trace(this->log, "%p: on process time:%"PRIu64, this, this->process_time); + if ((res = flush_data(this, this->current_time)) < 0) { + io->status = res; + return SPA_STATUS_STOPPED; + } + + 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; + bool was_started = this->transport_started; + + spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state); + + if (state == SPA_BT_TRANSPORT_STATE_ACTIVE) + transport_start(this); + else + transport_stop(this); + + if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && was_started && !this->is_duplex && this->is_output) { + /* + * 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. + * + * Treat this as a transport error, so that upper levels don't try to + * retry too often. + */ + + spa_log_debug(this->log, "%p: transport %p becomes inactive: stop and indicate error", + this, this->transport); + + spa_bt_transport_set_state(this->transport, SPA_BT_TRANSPORT_STATE_ERROR); + return; + } + + if (state == SPA_BT_TRANSPORT_STATE_ERROR) { + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + + 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); + this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + + 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; + } + if (this->loop_utils == NULL) { + spa_log_error(this->log, "loop utils are needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + 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 = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + 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_INPUT); + + 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, "api.bluez5.internal")) != NULL) + this->is_internal = 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 " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the media" }, + { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_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..0055974 --- /dev/null +++ b/spa/plugins/bluez5/media-source.c @@ -0,0 +1,1978 @@ +/* Spa Media Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" +#include "rtp.h" +#include "media-codecs.h" +#include "iso-io.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 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[2]; +#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 delay_info { + union { + struct { + int32_t buffer; + uint32_t duration; + }; + uint64_t v; + }; +}; + +SPA_STATIC_ASSERT(sizeof(struct delay_info) == sizeof(uint64_t)); + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + struct spa_loop_utils *loop_utils; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + 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 start_ready:1; + unsigned int transport_started: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 is_internal:1; + + unsigned int node_latency; + + 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; + + uint32_t errqueue_count; + + struct delay_info delay; + int64_t delay_sink; + struct spa_source *update_delay_event; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) + +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, ¶m)) != 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); + if (this->transport_started) + 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 void set_latency(struct impl *this, bool emit_latency) +{ + if (this->codec->bap && !this->is_input && this->transport && + this->transport->delay_us != SPA_BT_UNKNOWN_DELAY) { + struct port *port = &this->port; + unsigned int node_latency = 2048; + uint64_t rate = port->current_format.info.raw.rate; + unsigned int target = this->transport->delay_us*rate/SPA_USEC_PER_SEC * 1/2; + + /* Adjust requested node latency to be somewhat (~1/2) smaller + * than presentation delay. The difference functions as room + * for buffering rate control. + */ + while (node_latency > 64 && node_latency > target) + node_latency /= 2; + + if (this->node_latency != node_latency) { + this->node_latency = node_latency; + if (emit_latency) + emit_node_info(this, false); + } + + spa_log_info(this->log, "BAP presentation delay %d us, node latency %u/%u", + (int)this->transport->delay_us, node_latency, + (unsigned int)rate); + } +} + +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 handle_errqueue(struct impl *this) +{ + int res; + + /* iso-io/media-sink use these for TX latency. + * Someone else should be reading them, so drop + * only after yielding. + */ + if (this->errqueue_count < 4) { + this->errqueue_count++; + return; + } + + this->errqueue_count = 0; + res = recv(this->fd, NULL, 0, MSG_ERRQUEUE | MSG_TRUNC); + spa_log_trace(this->log, "%p: ignoring errqueue data (%d)", this, res); +} + +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) { + if (source->rmask & SPA_IO_ERR) { + handle_errqueue(this); + return; + } + + 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; + } + + this->errqueue_count = 0; + + spa_log_trace(this->log, "socket poll"); + + /* update the current pts */ + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + + /* read */ + size_read = read_data (this); + if (size_read == 0) + return; + if (size_read < 0) { + spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read)); + goto stop; + } + + 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, SPA_TIMESPEC_TO_NSEC(&now)); + + 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); + if (this->transport && this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); +} + +static int setup_matching(struct impl *this) +{ + struct port *port = &this->port; + + if (!this->transport_started) + port->buffer.corr = 1.0; + + 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.target_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.target_duration; + rate = this->position->clock.target_rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + setup_matching(this); + + this->next_time = (uint64_t)(now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate); + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->rate = this->clock->target_rate; + this->clock->position += this->clock->duration; + this->clock->duration = duration; + this->clock->rate_diff = port->buffer.corr; + this->clock->next_nsec = this->next_time; + } + + if (port->io) { + int io_status = port->io->status; + int status = produce_buffer(this); + spa_log_trace(this->log, "%p: io:%d->%d status:%d", this, io_status, port->io->status, status); + } + + spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); + + set_timeout(this, this->next_time); +} + +static void media_iso_pull(struct spa_bt_iso_io *iso_io) +{ + /* TODO: eventually use iso-io here, currently this is used just to indicate to + * iso-io whether this source is running or not. */ +} + +static void emit_port_info(struct impl *this, struct port *port, bool full); + +static void update_transport_delay(struct impl *this) +{ + struct port *port = &this->port; + struct delay_info info; + float latency; + int64_t latency_nsec; + int64_t delay_sink; + + if (!this->transport || !port->have_format) + return; + + info.v = __atomic_load_n(&this->delay.v, __ATOMIC_RELAXED); + + /* Latency to sink */ + latency = info.buffer + + port->latency[SPA_DIRECTION_INPUT].min_rate + + port->latency[SPA_DIRECTION_INPUT].min_quantum * info.duration; + + latency_nsec = port->latency[SPA_DIRECTION_INPUT].min_ns + + (int64_t)(latency * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate); + + spa_bt_transport_set_delay(this->transport, latency_nsec); + + delay_sink = + port->latency[SPA_DIRECTION_INPUT].min_ns + + (int64_t)((port->latency[SPA_DIRECTION_INPUT].min_rate + + port->latency[SPA_DIRECTION_INPUT].min_quantum * info.duration) + * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate); + __atomic_store_n(&this->delay_sink, delay_sink, __ATOMIC_RELAXED); + + /* Latency from source */ + port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT, + .min_rate = info.buffer, .max_rate = info.buffer); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].user++; + emit_port_info(this, port, false); +} + +static void update_delay_event(void *data, uint64_t count) +{ + /* in main loop */ + update_transport_delay(data); +} + +static int do_start_iso_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + + spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); + return 0; +} + +static int transport_start(struct impl *this) +{ + int res, val; + struct port *port = &this->port; + uint32_t flags; + + if (this->transport_started) + return 0; + if (!this->start_ready) + return -EIO; + + spa_return_val_if_fail(this->transport != NULL, -EIO); + + spa_log_debug(this->log, "%p: start transport state:%d", + this, this->transport->state); + + 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); + + /* + * If the link is bidirectional, media-sink may also be polling the same FD, + * and this won't work properly with epoll. Always dup to avoid problems. + */ + this->fd = dup(this->transport->fd); + if (this->fd < 0) + return -errno; + + val = 6; + if (setsockopt(this->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; + + if (this->is_duplex) { + /* 80 ms max extra buffer */ + spa_bt_decode_buffer_set_max_extra_latency(&port->buffer, + port->current_format.info.raw.rate * 80 / 1000); + } + + this->delay.buffer = -1; + this->delay.duration = 0; + this->update_delay_event = spa_loop_utils_add_event(this->loop_utils, update_delay_event, this); + + this->sample_count = 0; + this->errqueue_count = 0; + + this->source.data = this; + + this->source.fd = this->fd; + this->source.func = media_on_ready_read; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + if ((res = spa_loop_add_source(this->data_loop, &this->source)) < 0) + spa_log_error(this->log, "%p: failed to add poll source: %s", this, + spa_strerror(res)); + + if (this->transport->iso_io) + spa_loop_invoke(this->data_loop, do_start_iso_io, 0, NULL, 0, true, this); + + this->transport_started = true; + + return 0; +} + +static int do_start(struct impl *this) +{ + int res; + + if (this->started) + return 0; + + spa_return_val_if_fail(this->transport != NULL, -EIO); + + this->following = is_following(this); + + this->start_ready = true; + + spa_log_debug(this->log, "%p: start following:%d", this, this->following); + + spa_log_debug(this->log, "%p: transport %p acquire", this, + this->transport); + if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) { + this->start_ready = false; + return res; + } + + 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); + + setup_matching(this); + + 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; + + spa_log_debug(this->log, "%p: remove source", this); + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + if (this->transport && this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + set_timeout(this, 0); + + if (this->update_delay_event) { + spa_loop_utils_destroy_source(this->loop_utils, this->update_delay_event); + this->update_delay_event = NULL; + } + + return 0; +} + +static int do_remove_transport_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_log_debug(this->log, "%p: remove transport source", this); + + this->transport_started = false; + + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + if (this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + + return 0; +} + +static void transport_stop(struct impl *this) +{ + struct port *port = &this->port; + + if (!this->transport_started) + return; + + spa_log_debug(this->log, "%p: transport stop", this); + + spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + + if (this->fd >= 0) { + close(this->fd); + this->fd = -1; + } + + if (this->codec_data) + this->codec->deinit(this->codec_data); + this->codec_data = NULL; + + spa_bt_decode_buffer_clear(&port->buffer); +} + +static int do_stop(struct impl *this) +{ + int res; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "%p: stop", this); + + this->start_ready = false; + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + transport_stop(this); + + if (this->transport) + res = spa_bt_transport_release(this->transport); + else + res = 0; + + 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; + char latency[64]; + char rate[64]; + char media_name[256]; + struct port *port = &this->port; + + spa_scnprintf( + media_name, + sizeof(media_name), + "%s (codec %s)", + ((this->transport && this->transport->device->name) ? + this->transport->device->name : this->codec->bap ? "BAP" : "A2DP"), + this->codec->description + ); + + struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Source/Internal" : + this->is_input ? "Audio/Source" : "Stream/Output/Audio" }, + { SPA_KEY_NODE_LATENCY, this->is_input ? "" : latency }, + { "media.name", media_name }, + { "node.rate", this->is_input ? "" : rate }, + { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" }, + }; + + spa_scnprintf(latency, sizeof(latency), "%u/%u", this->node_latency, port->current_format.info.raw.rate); + spa_scnprintf(rate, sizeof(rate), "1/%u", port->current_format.info.raw.rate); + + if (full) + this->info.change_mask = this->info_all; + 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, ¶m)) != 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: 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 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; + + set_latency(this, false); + } + + 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[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); + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + 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) +{ + 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: + { + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct spa_latency_info info; + + if (param == NULL) + info = SPA_LATENCY_INFO(other); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (info.direction != other) + return -EINVAL; + if (memcmp(&port->latency[info.direction], &info, sizeof(info)) == 0) + return 0; + + port->latency[info.direction] = info; + this->port.info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port.params[IDX_Latency].user++; + + update_transport_delay(this); + emit_port_info(this, port, false); + res = 0; + break; + } + default: + res = -ENOENT; + break; + } + 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 *result_duration) +{ + struct port *port = &this->port; + uint32_t samples, rate_denom; + uint64_t duration; + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate_denom = this->position->clock.rate.denom; + } else { + duration = 1024; + rate_denom = port->current_format.info.raw.rate; + } + + *result_duration = duration * port->current_format.info.raw.rate / rate_denom; + + if (SPA_LIKELY(port->rate_match) && this->resampling) { + samples = port->rate_match->size; + } else { + samples = *result_duration; + } + return samples; +} + +static void update_target_latency(struct impl *this) +{ + struct port *port = &this->port; + uint32_t samples, duration, latency; + int64_t delay_sink; + + if (this->transport == NULL || !port->have_format) + return; + + if (!this->codec->bap || this->is_input || + this->transport->delay_us == SPA_BT_UNKNOWN_DELAY) + return; + + get_samples(this, &duration); + + /* Presentation delay for BAP server + * + * This assumes the time when we receive the packet is (on average) + * the SDU synchronization reference (see Core v5.3 Vol 6/G Sec 3.2.2 Fig. 3.2, + * BAP v1.0 Sec 7.1.1). + * + * XXX: This is not exactly true, there might be some latency in between, + * XXX: but currently kernel does not provide us any better information. + * XXX: Some controllers (e.g. Intel AX210) also do not seem to set timestamps + * XXX: to the HCI ISO data packets, so it's not clear what we can do here + * XXX: better. + */ + samples = (uint64_t)this->transport->delay_us * + port->current_format.info.raw.rate / SPA_USEC_PER_SEC; + + delay_sink = __atomic_load_n(&this->delay_sink, __ATOMIC_RELAXED); + latency = delay_sink * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC; + + if (samples > latency) + samples -= latency; + else + samples = 1; + + /* Too small target latency might not produce working audio. + * The minimum (Presentation_Delay_Min) is configured in endpoint + * DBus properties, with some default value on BlueZ side if unspecified. + */ + + spa_bt_decode_buffer_set_target_latency(&port->buffer, samples); +} + +#define WARN_ONCE(cond, ...) \ + if (SPA_UNLIKELY(cond)) { static bool __once; if (!__once) { __once = true; spa_log_warn(__VA_ARGS__); } } + +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; + + update_target_latency(this); + + spa_bt_decode_buffer_process(&port->buffer, samples, duration, + this->position ? this->position->clock.rate_diff : 1.0, + this->position ? this->position->clock.next_nsec : 0); + + setup_matching(this); + + buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); + + /* copy data to buffers */ + if (!spa_list_is_empty(&port->free)) { + struct buffer *buffer; + struct spa_data *datas; + uint32_t data_size; + + buffer = spa_list_first(&port->free, struct buffer, link); + datas = buffer->buf->datas; + + data_size = samples * port->frame_size; + + WARN_ONCE(datas[0].maxsize < data_size && !this->following, + this->log, "source buffer too small (%u < %u)", + datas[0].maxsize, data_size); + + data_size = SPA_MIN(data_size, SPA_ROUND_DOWN(datas[0].maxsize, port->frame_size)); + + avail = SPA_MIN(avail, data_size); + + spa_bt_decode_buffer_read(&port->buffer, avail); + + 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[0].chunk->offset = 0; + datas[0].chunk->size = data_size; + datas[0].chunk->stride = port->frame_size; + + memcpy(datas[0].data, buf, avail); + + /* pad with silence */ + if (avail < data_size) + memset(SPA_PTROFF(datas[0].data, avail, void), 0, data_size - avail); + + this->sample_count += samples; + + /* ready buffer if full */ + spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)samples); + spa_list_append(&port->ready, &buffer->link); + } + + if (this->update_delay_event) { + int32_t target = spa_bt_decode_buffer_get_target_latency(&port->buffer); + uint32_t decoder_delay = 0; + + if (this->codec->get_delay) + this->codec->get_delay(this->codec_data, NULL, &decoder_delay); + + target += decoder_delay; + + if (target != this->delay.buffer || duration != this->delay.duration) { + struct delay_info info = { .buffer = target, .duration = duration }; + + __atomic_store_n(&this->delay.v, info.v, __ATOMIC_RELAXED); + spa_loop_utils_signal_event(this->loop_utils, this->update_delay_event); + } + } +} + +static int produce_buffer(struct impl *this) +{ + 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 && + (this->following || port->rate_match == NULL)) + 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; + } + + if (this->transport_started && !this->source.loop) { + io->status = -EIO; + return SPA_STATUS_STOPPED; + } + + /* Handle buffering */ + if (this->transport_started) + 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; + + if (!this->started || !this->transport_started) + return SPA_STATUS_OK; + + 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 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) + transport_start(this); + else + transport_stop(this); + + if (state == SPA_BT_TRANSPORT_STATE_ERROR) { + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + + 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 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 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; + 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); + 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); + this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + + 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; + } + if (this->loop_utils == NULL) { + spa_log_error(this->log, "loop utils are needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + 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_PHYSICAL | + 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_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + /* Init the buffer lists */ + spa_list_init(&port->ready); + 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 ((str = spa_dict_lookup(info, "api.bluez5.internal")) != NULL) + this->is_internal = 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; + } + + 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); + + this->node_latency = 512; + + set_latency(this, false); + + this->fd = -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. " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with media" }, + { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_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..b0990ea --- /dev/null +++ b/spa/plugins/bluez5/meson.build @@ -0,0 +1,219 @@ +gnome = import('gnome') + +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', + 'iso-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', 'telephony.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 + +if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found() + bluez5_deps += lc3_dep +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 + if ldac_abr_dep.found() + ldac_args += [ '-DENABLE_LDAC_ABR' ] + 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, ldac_abr_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 + 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') + + bluez_codec_opus_g = shared_library('spa-codec-bluez5-opus-g', + [ 'a2dp-codec-opus-g.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 + +if get_option('bluez5-codec-g722').allowed() + bluez_codec_g722 = shared_library('spa-codec-bluez5-g722', + [ 'g722/g722_encode.c', 'asha-codec-g722.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +test_apps = [ + 'test-midi', +] +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..9ef7d2b --- /dev/null +++ b/spa/plugins/bluez5/midi-enum.c @@ -0,0 +1,869 @@ +/* Spa midi dbus */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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; +} + +static 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; + } + + g_dbus_connection_set_exit_on_close(this->conn, FALSE); + + 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 " }, + { 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..b5fd179 --- /dev/null +++ b/spa/plugins/bluez5/midi-node.c @@ -0,0 +1,2150 @@ +/* Spa MIDI node */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "midi.h" + +#include "bluez5-interface-gen.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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, state = 0; + 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); + + while (size > 0) { + uint32_t ump[4]; + int ump_size = spa_ump_from_midi(&data, &size, + ump, sizeof(ump), 0, &state); + if (ump_size <= 0) + break; + + res = midi_event_ringbuffer_push(&this->event_rbuf, time, (uint8_t*)ump, ump_size); + if (res < 0) { + midi_event_ringbuffer_init(&this->event_rbuf); + spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s", + this, spa_strerror(res)); + } + } +} + +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 += (uint64_t)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_UMP); + 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) { + int size; + uint8_t event[32]; + + if (c->type != SPA_CONTROL_UMP) + continue; + + time = SPA_MAX(time, this->current_time + c->offset * SPA_NSEC_PER_SEC / this->rate); + + size = spa_ump_to_midi(SPA_POD_BODY(&c->value), + SPA_POD_BODY_SIZE(&c->value), event, sizeof(event)); + if (size <= 0) + continue; + + spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this, + (size > 0) ? event[0] : 0, time); + + 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); + + if (SPA_LIKELY(this->position)) { + this->duration = this->position->clock.target_duration; + this->rate = this->position->clock.target_rate.denom; + } else { + this->duration = 1024; + this->rate = 48000; + } + + 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->rate = this->clock->target_rate; + this->clock->position += this->clock->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; + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + set_timeout(this, 0); + 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), + SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(1u<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), + SPA_FORMAT_CONTROL_types, SPA_POD_Int(1u<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; + } + + g_dbus_connection_set_exit_on_close(this->conn, FALSE); + + 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, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), + }; + static const struct spa_dict_item out_port_items[] = { + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), + }; + 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 | + SPA_PORT_FLAG_PHYSICAL | + 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->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 " }, + { 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..39ed779 --- /dev/null +++ b/spa/plugins/bluez5/midi-parser.c @@ -0,0 +1,275 @@ +/* BLE MIDI parser */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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..a3a5e92 --- /dev/null +++ b/spa/plugins/bluez5/midi-server.c @@ -0,0 +1,560 @@ +/* Spa Bluez5 midi */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include + +#include "midi.h" + +#include "bluez5-interface-gen.h" +#include "dbus-monitor.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.midi.server"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &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 *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->conn = conn; + spa_log_topic_init(impl->log, &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..bfe1e2a --- /dev/null +++ b/spa/plugins/bluez5/midi.h @@ -0,0 +1,124 @@ +/* Spa V4l2 dbus */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BT_MIDI_H_ +#define SPA_BT_MIDI_H_ + +#include +#include +#include + +#include +#include + +#include +#include + +#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..0ed3d46 --- /dev/null +++ b/spa/plugins/bluez5/modemmanager.c @@ -0,0 +1,1120 @@ +/* Spa Bluez5 ModemManager proxy */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#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_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 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; + DBusMessageIter arg_i, element_i; + MMCallDirection direction; + MMCallState state; + + spa_assert(call->pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&call->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"); + return; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r)); + return; + } + + 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"); + return; + } + + 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); + } +} + +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; + DBusMessageIter i, array_i; + + spa_assert(this->pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&this->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)); + return; + } + + 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"); + return; + } + + 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); + } +} + +static void call_free(struct call *call) +{ + spa_list_remove(&call->link); + + cancel_and_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); + + cancel_and_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; + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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; + + 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; + spa_autoptr(DBusMessage) m2 = NULL; + + 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); + + m2 = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll"); + if (m2 == NULL) + goto finish; + dbus_message_append_args(m2, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID); + + call_object->pending = send_with_reply(this->conn, m2, mm_get_call_properties_reply, call_object); + if (!call_object->pending) { + spa_log_error(this->log, "dbus call failure"); + 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) +{ + if (this->filters_added) + return 0; + + if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + return -EIO; + } + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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; +} + +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(void) +{ + 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; + + free(data); + + spa_assert(call->pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&call->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; + + free(data); + + spa_assert(this->voice_pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&this->voice_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; + spa_autofree struct dbus_cmd_data *data = NULL; + spa_autoptr(DBusMessage) m = NULL; + + 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; + } + + call_object->pending = send_with_reply(this->conn, m, mm_get_call_simple_reply, data); + if (!call_object->pending) { + spa_log_error(this->log, "dbus call failure"); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return spa_steal_ptr(data), true; +} + +bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + spa_autofree struct dbus_cmd_data *data= NULL; + spa_autoptr(DBusMessage) m = NULL; + + 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; + } + + call_object->pending = send_with_reply(this->conn, m, mm_get_call_simple_reply, data); + if (!call_object->pending) { + spa_log_error(this->log, "dbus call failure"); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return spa_steal_ptr(data), 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; + spa_autofree struct dbus_cmd_data *data = NULL; + spa_autoptr(DBusMessage) m = NULL; + 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); + + this->voice_pending = send_with_reply(this->conn, m, mm_get_call_create_reply, data); + if (!this->voice_pending) { + spa_log_error(this->log, "dbus call failure"); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return spa_steal_ptr(data), 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; + spa_autofree struct dbus_cmd_data *data = NULL; + spa_autoptr(DBusMessage) m = NULL; + + 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); + + call_object->pending = send_with_reply(this->conn, m, mm_get_call_simple_reply, data); + if (!call_object->pending) { + spa_log_error(this->log, "dbus call failure"); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return spa_steal_ptr(data), 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) +{ + 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; + } + + spa_autofree struct impl *this = calloc(1, sizeof(*this)); + 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) + return NULL; + + spa_autoptr(DBusMessage) m = dbus_message_new_method_call(MM_DBUS_SERVICE, + "/org/freedesktop/ModemManager1", + DBUS_INTERFACE_OBJECTMANAGER, + "GetManagedObjects"); + if (m == NULL) + return NULL; + + dbus_message_set_auto_start(m, false); + + this->pending = send_with_reply(this->conn, m, mm_get_managed_objects_reply, this); + if (!this->pending) { + spa_log_error(this->log, "dbus call failure"); + return NULL; + } + + return spa_steal_ptr(this); +} + +void mm_unregister(void *data) +{ + struct impl *this = data; + + cancel_and_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..09bd655 --- /dev/null +++ b/spa/plugins/bluez5/modemmanager.h @@ -0,0 +1,141 @@ +/* Spa Bluez5 ModemManager proxy */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_MODEMMANAGER_H_ +#define SPA_BLUEZ5_MODEMMANAGER_H_ + +#include + +#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(void); +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 +static inline 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; +} + +static inline void mm_unregister(void *data) +{ +} + +static inline bool mm_is_available(void *modemmanager) +{ + return false; +} + +static inline unsigned int mm_supported_features(void) +{ + return 0; +} + +static inline bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +static inline bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +static inline 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; +} + +static inline 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; +} + +static inline const char *mm_get_incoming_call_number(void *modemmanager) +{ + return NULL; +} + +static inline 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spa/plugins/bluez5/player.c b/spa/plugins/bluez5/player.c new file mode 100644 index 0000000..7b267e7 --- /dev/null +++ b/spa/plugins/bluez5/player.c @@ -0,0 +1,389 @@ +/* Spa Bluez5 AVRCP Player */ +/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include + +#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 \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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; + spa_autoptr(DBusMessage) r = NULL; + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return NULL; + return spa_steal_ptr(r); +} + +static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *impl = userdata; + spa_autoptr(DBusMessage) r = NULL; + + 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)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + return DBUS_HANDLER_RESULT_HANDLED; +} + +static int send_update_signal(struct impl *impl) +{ + spa_autoptr(DBusMessage) m = NULL; + const char *iface = PLAYER_INTERFACE; + DBusMessageIter i, a; + + 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)) + return -EIO; + + return 0; +} + +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); + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + DBusMessageIter i; + spa_autoptr(DBusMessage) m = NULL, r = NULL; + + 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); + + r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err); + if (r == NULL) { + spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(impl->log, "RegisterPlayer() failed"); + return -EIO; + } + + return 0; +} + +int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path) +{ + struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this); + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + DBusMessageIter i; + spa_autoptr(DBusMessage) m = NULL, r = NULL; + + 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); + + r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err); + if (r == NULL) { + spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(impl->log, "UnregisterPlayer() failed"); + return -EIO; + } + + return 0; +} diff --git a/spa/plugins/bluez5/player.h b/spa/plugins/bluez5/player.h new file mode 100644 index 0000000..d5c2ee3 --- /dev/null +++ b/spa/plugins/bluez5/player.h @@ -0,0 +1,31 @@ +/* Spa Bluez5 AVRCP Player */ +/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#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..f6701e0 --- /dev/null +++ b/spa/plugins/bluez5/plugin.c @@ -0,0 +1,66 @@ +/* Spa Volume plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include + +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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..c4b293e --- /dev/null +++ b/spa/plugins/bluez5/quirks.c @@ -0,0 +1,407 @@ +/* Device/adapter/kernel quirk table */ +/* SPDX-FileCopyrightText: Copyright © 2021 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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, len; + uint32_t no_features_cur = 0; + const char *value; + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) { + char val[4096]; + const char *str; + bool success = false; + + if (spa_streq(key, "no-features")) { + if (spa_json_is_array(value, len) > 0) { + spa_json_enter(&it[0], &it[1]); + while (spa_json_get_string(&it[1], val, sizeof(val)) > 0) + no_features_cur |= parse_feature(val); + } + continue; + } + + if (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]; + struct spa_error_location loc; + int sz; + const char *value; + + if (spa_json_enter_object(&data, &rules) <= 0) + spa_json_init(&rules, str, len); + + while ((sz = spa_json_object_next(&rules, key, sizeof(key), &value)) > 0) { + 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); + } + + if (spa_json_get_error(&rules, str, &loc)) + spa_debug_log_error_location(this->log, SPA_LOG_LEVEL_ERROR, &loc, + "spa.bluez5 quirks syntax error: %s", loc.reason); +} + +static int load_conf(struct spa_bt_quirks *this, const char *path) +{ + char *data; + struct stat sbuf; + spa_autoclose int fd = -1; + + spa_log_debug(this->log, "loading %s", path); + + if ((fd = open(path, O_CLOEXEC | O_RDONLY)) < 0) + return -errno; + if (fstat(fd, &sbuf) < 0) + return -errno; + if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) + return -errno; + + load_quirks(this, data, sbuf.st_size); + munmap(data, sbuf.st_size); + + return 0; +} + +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'; +} + +static int get_features(const struct spa_bt_quirks *this, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device, + uint32_t *features, + bool debug) +{ + 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); + if (debug) + log_props(this->log, &props); + do_match(this->kernel_rules, &props, &no_features); + if (debug) + 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); + if (debug) + log_props(this->log, &props); + do_match(this->adapter_rules, &props, &no_features); + if (debug) + 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); + if (debug) + log_props(this->log, &props); + do_match(this->device_rules, &props, &no_features); + if (debug) + 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; +} + +int spa_bt_quirks_get_features(const struct spa_bt_quirks *this, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device, + uint32_t *features) +{ + return get_features(this, adapter, device, features, false); +} + +void spa_bt_quirks_log_features(const struct spa_bt_quirks *this, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device) +{ + uint32_t features = 0; + + get_features(this, adapter, device, &features, true); + spa_log_debug(this->log, "features:%08x", features); +} diff --git a/spa/plugins/bluez5/rate-control.h b/spa/plugins/bluez5/rate-control.h new file mode 100644 index 0000000..7ea8822 --- /dev/null +++ b/spa/plugins/bluez5/rate-control.h @@ -0,0 +1,194 @@ +/* Spa Bluez5 rate control */ +/* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_RATE_CONTROL_H +#define SPA_BLUEZ5_RATE_CONTROL_H + +#include + +/** 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 left; + uint32_t period; +}; + +static inline void spa_bt_ptp_init(struct spa_bt_ptp *p, int32_t period, uint32_t min_duration) +{ + 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->left = min_duration; + p->period = period; +} + +static inline 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; + } + + if (p->left < duration) + p->left = 0; + else + p->left -= duration; +} + +static inline bool spa_bt_ptp_valid(struct spa_bt_ptp *p) +{ + return p->left == 0; +} + +/** + * 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 inline void spa_bt_rate_control_init(struct spa_bt_rate_control *this, double level) +{ + this->avg = level; + this->corr = 1.0; +} + +static inline double spa_bt_rate_control_update(struct spa_bt_rate_control *this, double level, + double target, double duration, double period, double rate_diff_max) +{ + /* + * 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 - rate_diff_max, 1 + rate_diff_max); + + return this->corr; +} + +#endif 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 + * + * + * 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 . + */ + +#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..4b4b8f9 --- /dev/null +++ b/spa/plugins/bluez5/sco-io.c @@ -0,0 +1,296 @@ +/* Spa SCO I/O */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defs.h" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.sco-io"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + + +/* 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_log *log; + 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 = recv(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU), MSG_DONTWAIT); + 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; + } + + if (res != (int)io->read_size) + spa_log_trace(io->log, "%p: packet size:%d", io, res); + + 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 = send(io->fd, buf, packet_size, MSG_DONTWAIT | MSG_NOSIGNAL); + 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_bt_transport *transport, struct spa_loop *data_loop, struct spa_log *log) +{ + struct spa_bt_sco_io *io; + + spa_log_topic_init(log, &log_topic); + + io = calloc(1, sizeof(struct spa_bt_sco_io)); + if (io == NULL) + return io; + + io->fd = transport->fd; + io->read_mtu = transport->read_mtu; + io->write_mtu = transport->write_mtu; + io->data_loop = data_loop; + io->log = log; + + if (transport->device->adapter->bus_type == BUS_TYPE_USB) { + /* For USB we need to wait for RX to know it. Using wrong size doesn't + * work anyway, and may result to errors printed to dmesg if too big. + */ + io->read_size = 0; + } else { + /* Set some sensible initial packet size */ + switch (transport->codec) { + case HFP_AUDIO_CODEC_CVSD: + io->read_size = 48; /* 3ms S16_LE 8000 Hz */ + break; + case HFP_AUDIO_CODEC_MSBC: + case HFP_AUDIO_CODEC_LC3_SWB: + default: + io->read_size = HFP_CODEC_PACKET_SIZE; + break; + } + } + + spa_log_debug(io->log, "%p: initial packet size:%d", io, io->read_size); + + /* 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..fc377ae --- /dev/null +++ b/spa/plugins/bluez5/sco-sink.c @@ -0,0 +1,1763 @@ +/* Spa SCO Sink */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defs.h" + +#ifdef HAVE_LC3 +#include +#endif + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 { + int64_t latency_offset; + char clock_name[64]; +}; + +#define MAX_BUFFERS 32 + +#define ALT1_PACKET_SIZE 24 +#define ALT6_PACKET_SIZE 60 + +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 start_ready:1; + unsigned int transport_started:1; + unsigned int following:1; + unsigned int flush_pending:1; + + unsigned int is_internal: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; + + /* Codecs */ + uint8_t *buffer; + uint8_t *buffer_head; + uint8_t *buffer_next; + int buffer_size; + int h2_seq; + + /* mSBC */ + sbc_t msbc; + + /* LC3 */ +#ifdef HAVE_LC3 + lc3_encoder_t lc3; +#else + void *lc3; +#endif +}; + +#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) +{ + 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; + + 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: + 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)); + 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 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; + + /* + * We start flushing data immediately, so the delay is: + * + * (transport delay) + (packet delay) + (codec internal delay) + (latency offset) + * + * and doesn't depend on the quantum. The codec internal delay is neglected. + * Kernel knows the latency due to socket/controller queue, but doesn't + * tell us, so not included but hopefully in < 20 ms range. + */ + + switch (this->transport->codec) { + case HFP_AUDIO_CODEC_MSBC: + case HFP_AUDIO_CODEC_LC3_SWB: + delay = 7500 * SPA_NSEC_PER_USEC; + break; + default: + delay = this->transport->write_mtu / (2 * 8000); + break; + } + + delay += spa_bt_transport_get_delay_nsec(this->transport); + delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2); + delay = SPA_MAX(delay, 0); + + spa_log_info(this->log, "%p: total latency:%d ms", this, (int)(delay / SPA_NSEC_PER_MSEC)); + + port->latency.min_ns = port->latency.max_ns = delay; + port->latency.min_rate = port->latency.max_rate = 0; + port->latency.min_quantum = port->latency.max_quantum = 0.0f; + + 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)); + } + + 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 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 lc3_encode_frame(struct impl *this, const void *src, size_t src_size, void *dst, size_t dst_size, + ssize_t *dst_out) +{ +#ifdef HAVE_LC3 + int res; + + if (src_size < LC3_SWB_DECODED_SIZE) + return -EINVAL; + if (dst_size < LC3_SWB_PAYLOAD_SIZE) + return -EINVAL; + + res = lc3_encode(this->lc3, LC3_PCM_FORMAT_S24, src, 1, LC3_SWB_PAYLOAD_SIZE, dst); + if (res != 0) + return -EINVAL; + + *dst_out = LC3_SWB_PAYLOAD_SIZE; + return LC3_SWB_DECODED_SIZE; +#else + return -EOPNOTSUPP; +#endif +} + +static int flush_data(struct impl *this) +{ + struct port *port = &this->port; + int processed = 0; + int written; + + spa_assert(this->transport_started); + + if (this->transport == NULL || this->transport->sco_io == NULL || !this->flush_timer_source.loop) + return -EIO; + + const uint32_t min_in_size = (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? MSBC_DECODED_SIZE : + (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) ? LC3_SWB_DECODED_SIZE : + this->transport->write_mtu; + 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; + + 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_val_if_fail(!spa_list_is_empty(&port->ready), -EIO); + 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 0; + } + + 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 0; + } + + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC || + this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { + ssize_t out_encoded; + /* Encode */ + if (this->buffer_next + HFP_CODEC_PACKET_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/LC3 buffer overrun, dropping data"); + } + + /* H2 sync header */ + this->buffer_next[0] = 0x01; + this->buffer_next[1] = sntable[this->h2_seq % 4]; + this->buffer_next[59] = 0x00; + this->h2_seq = (this->h2_seq + 1) % 4; + + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size, + this->buffer_next + 2, HFP_CODEC_PACKET_SIZE - 3, &out_encoded); + out_encoded += 1; /* pad */ + } else { + processed = lc3_encode_frame(this, port->write_buffer, port->write_buffer_size, + this->buffer_next + 2, HFP_CODEC_PACKET_SIZE - 2, &out_encoded); + } + + if (processed < 0) { + spa_log_warn(this->log, "encode failed: %d", processed); + return -EINVAL; + } + this->buffer_next += out_encoded + 2; + port->write_buffer_size = 0; + + /* Write */ + written = spa_bt_sco_io_write(this->transport->sco_io, this->buffer_head, + 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 + HFP_CODEC_PACKET_SIZE > this->buffer + this->buffer_size) { + /* Written bytes is not necessarily commensurate + * with HFP_CODEC_PACKET_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, port->write_buffer, + 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 0; + +stop: + enable_flush_timer(this, false); + if (this->flush_timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + return -EIO; +} + +static void sco_on_flush_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t exp = 0; + 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 status, res; + + 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.target_duration; + rate = this->position->clock.target_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->rate = this->clock->target_rate; + this->clock->position += this->clock->duration; + this->clock->duration = duration; + this->clock->rate_diff = 1.0f; + this->clock->next_nsec = this->next_time; + this->clock->delay = 0; + } + + status = this->transport_started ? SPA_STATUS_NEED_DATA : SPA_STATUS_HAVE_DATA; + + spa_log_trace(this->log, "%p: %d -> %d", this, io->status, status); + io->status = status; + io->buffer_id = SPA_ID_INVALID; + spa_node_call_ready(&this->callbacks, status); + + 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 transport_start(struct impl *this) +{ + int res; + + /* Don't do anything if the node has already started */ + if (this->transport_started) + return 0; + if (!this->start_ready) + return -EIO; + + /* 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 transport", this); + + /* Init mSBC/LC3 if needed */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + res = sbc_init_msbc(&this->msbc, 0); + if (res < 0) + return res; + /* 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(ALT1_PACKET_SIZE, lcm(ALT6_PACKET_SIZE, lcm(this->transport->write_mtu, 2 * HFP_CODEC_PACKET_SIZE))); + } else if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { +#ifdef HAVE_LC3 + this->lc3 = lc3_setup_encoder(7500, 32000, 0, + calloc(1, lc3_encoder_size(7500, 32000))); + if (!this->lc3) + return -EINVAL; + + spa_assert(lc3_frame_samples(7500, 32000) * this->port.frame_size == LC3_SWB_DECODED_SIZE); + + this->buffer_size = lcm(ALT1_PACKET_SIZE, lcm(ALT6_PACKET_SIZE, lcm(this->transport->write_mtu, 2 * HFP_CODEC_PACKET_SIZE))); +#else + res = -EOPNOTSUPP; + goto fail; +#endif + } else { + this->buffer_size = 0; + } + + if (this->buffer_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); + + spa_log_info(this->log, "%p: using codec %d, delay:%"PRIi64" ms", this, this->transport->codec, + (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC)); + + /* start socket i/o */ + if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0) + goto fail; + + 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 the started flag */ + this->transport_started = true; + + return 0; + +fail: + free(this->buffer); + this->buffer = NULL; + sbc_finish(&this->msbc); + free(this->lc3); + this->lc3 = NULL; + return res; +} + +static int do_start(struct impl *this) +{ + bool do_accept; + int res; + + if (this->started) + return 0; + + spa_return_val_if_fail(this->transport, -EIO); + + this->following = is_following(this); + + this->start_ready = true; + + 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) { + this->start_ready = false; + return res; + } + + /* 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); + + set_timers(this); + + this->started = true; + + return 0; +} + +/* 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; + + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + set_timeout(this, 0); + + return 0; +} + +static int do_remove_transport_source(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_started = false; + + if (this->flush_timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + enable_flush_timer(this, false); + + /* Drop queued data */ + drop_port_output(this); + + return 0; +} + +static void transport_stop(struct impl *this) +{ + if (!this->transport_started) + return; + + spa_log_trace(this->log, "sco-sink %p: transport stop", this); + + spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + + if (this->buffer) { + free(this->buffer); + this->buffer = NULL; + this->buffer_head = this->buffer_next = this->buffer; + } + + sbc_finish(&this->msbc); + free(this->lc3); + this->lc3 = NULL; +} + +static int do_stop(struct impl *this) +{ + int res; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "%p: stop", this); + + this->start_ready = false; + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + transport_stop(this); + + if (this->transport) + res = spa_bt_transport_release(this->transport); + else + res = 0; + + 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_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) +{ + const struct spa_dict_item hu_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" : "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, }; + + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + info.format = SPA_AUDIO_FORMAT_S24_32_LE; + else + 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 + * LC3-SWB format has a rate of 32kHz + */ + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + info.rate = 32000; + else 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 (!this->transport) + return -EIO; + + 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 != 1) + return -EINVAL; + + switch (info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16_LE: + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + return -EINVAL; + port->frame_size = info.info.raw.channels * 2; + break; + case SPA_AUDIO_FORMAT_S24_32_LE: + if (this->transport->codec != HFP_AUDIO_CODEC_LC3_SWB) + return -EINVAL; + port->frame_size = info.info.raw.channels * 4; + break; + default: + return -EINVAL; + } + + port->current_format = info; + port->have_format = true; + } + + set_latency(this, false); + + 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[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 (!this->started || !this->transport_started) { + if (io->status != SPA_STATUS_HAVE_DATA) { + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = SPA_ID_INVALID; + } + 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)) { + int res; + spa_log_trace(this->log, "%p: flush on process", this); + if ((res = flush_data(this)) < 0) { + io->status = res; + return SPA_STATUS_STOPPED; + } + } + + 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_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) + transport_start(this); + else if (state < SPA_BT_TRANSPORT_STATE_ACTIVE) + transport_stop(this); + + if (state == SPA_BT_TRANSPORT_STATE_ERROR) { + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + + 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 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, + .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->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 = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + 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_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.internal")) != NULL) + this->is_internal = 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; + } + + 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, "Collabora Ltd. " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play bluetooth audio with hsp/hfp" }, + { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_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..dc2e1f0 --- /dev/null +++ b/spa/plugins/bluez5/sco-source.c @@ -0,0 +1,1779 @@ +/* Spa SCO Source */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defs.h" + +#ifdef HAVE_LC3 +#include +#endif + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 start_ready:1; + unsigned int transport_started:1; + unsigned int following:1; + unsigned int matching:1; + unsigned int resampling:1; + unsigned int io_error:1; + + unsigned int is_internal: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; + + /* Codecs */ + bool h2_seq_initialized; + uint8_t h2_seq; + + /* mSBC/LC3 frame parsing */ + uint8_t recv_buffer[HFP_CODEC_PACKET_SIZE]; + uint8_t recv_buffer_pos; + + /* mSBC */ + sbc_t msbc; + + /* LC3 */ +#ifdef HAVE_LC3 + lc3_decoder_t lc3; +#else + void *lc3; +#endif + + 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); + if (this->transport_started) + 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 recv buffer, syncing buffer start to headers */ +static void recv_buffer_append_byte(struct impl *this, uint8_t byte) +{ + /* Parse H2 sync header */ + if (this->recv_buffer_pos == 0) { + if (byte != 0x01) { + this->recv_buffer_pos = 0; + return; + } + } else if (this->recv_buffer_pos == 1) { + if (!((byte & 0x0F) == 0x08 && + ((byte >> 4) & 1) == ((byte >> 5) & 1) && + ((byte >> 6) & 1) == ((byte >> 7) & 1))) { + this->recv_buffer_pos = 0; + return; + } + } else if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + /* Beginning of MSBC frame: SYNCWORD + 2 nul bytes */ + if (this->recv_buffer_pos == 2) { + if (byte != 0xAD) { + this->recv_buffer_pos = 0; + return; + } + } + else if (this->recv_buffer_pos == 3) { + if (byte != 0x00) { + this->recv_buffer_pos = 0; + return; + } + } + else if (this->recv_buffer_pos == 4) { + if (byte != 0x00) { + this->recv_buffer_pos = 0; + return; + } + } + } + + if (this->recv_buffer_pos >= HFP_CODEC_PACKET_SIZE) { + /* Packet completed. Reset. */ + this->recv_buffer_pos = 0; + recv_buffer_append_byte(this, byte); + return; + } + + this->recv_buffer[this->recv_buffer_pos] = byte; + ++this->recv_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 int lc3_decode_frame(struct impl *this, const void *src, size_t src_size, void *dst, + size_t dst_size, size_t *dst_out) +{ +#ifdef HAVE_LC3 + int res; + + if (src_size != LC3_SWB_PAYLOAD_SIZE) + return -EINVAL; + if (dst_size < LC3_SWB_DECODED_SIZE) + return -EINVAL; + + res = lc3_decode(this->lc3, src, src_size, LC3_PCM_FORMAT_S24, dst, 1); + if (res != 0) + return -EINVAL; + + *dst_out = LC3_SWB_DECODED_SIZE; + return LC3_SWB_DECODED_SIZE; +#else + return -EOPNOTSUPP; +#endif +} + +static uint32_t preprocess_and_decode_codec_data(void *userdata, uint8_t *read_data, int size_read, uint64_t now) +{ + struct impl *this = userdata; + struct port *port = &this->port; + uint32_t decoded = 0; + int i; + uint32_t decoded_size = (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? MSBC_DECODED_SIZE : + LC3_SWB_DECODED_SIZE; + + spa_log_trace(this->log, "handling mSBC/LC3 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; + + recv_buffer_append_byte(this, read_data[i]); + + if (this->recv_buffer_pos != HFP_CODEC_PACKET_SIZE) + continue; + + /* + * Handle found mSBC/LC3 packet + */ + + buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + + /* Check sequence number */ + seq = ((this->recv_buffer[1] >> 4) & 1) | + ((this->recv_buffer[1] >> 6) & 2); + + spa_log_trace(this->log, "mSBC/LC3 packet seq=%u", seq); + if (!this->h2_seq_initialized) { + this->h2_seq_initialized = true; + this->h2_seq = seq; + } else if (seq != this->h2_seq) { + /* TODO: PLC (too late to insert data now) */ + spa_log_info(this->log, + "missing mSBC/LC3 packet: %u != %u", seq, this->h2_seq); + this->h2_seq = seq; + } + + this->h2_seq = (this->h2_seq + 1) % 4; + + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + if (avail < decoded_size) + spa_log_warn(this->log, "Output buffer full, dropping msbc data"); + + /* decode frame */ + processed = sbc_decode( + &this->msbc, this->recv_buffer + 2, HFP_CODEC_PACKET_SIZE - 3, + buf, avail, &written); + } else { + processed = lc3_decode_frame(this, this->recv_buffer + 2, HFP_CODEC_PACKET_SIZE - 2, + buf, avail, &written); + } + + if (processed < 0) { + spa_log_warn(this->log, "decode failed: %d", processed); + /* TODO: manage errors */ + continue; + } + + spa_bt_decode_buffer_write_packet(&port->buffer, written, now); + 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; + + /* Drop data when not started */ + if (!this->started) + return 0; + + 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 || + this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { + decoded = preprocess_and_decode_codec_data(userdata, read_data, size_read, SPA_TIMESPEC_TO_NSEC(&this->now)); + } 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, SPA_TIMESPEC_TO_NSEC(&this->now)); + + 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: + this->io_error = true; + return 1; +} + +static int setup_matching(struct impl *this) +{ + struct port *port = &this->port; + + if (!this->transport_started) + port->buffer.corr = 1.0; + + 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.target_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->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.target_duration; + rate = this->position->clock.target_rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + setup_matching(this); + + this->next_time = (uint64_t)(now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate); + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->rate = this->clock->target_rate; + this->clock->position += this->clock->duration; + this->clock->duration = duration; + this->clock->rate_diff = port->buffer.corr; + this->clock->next_nsec = this->next_time; + } + + if (port->io) { + int io_status = port->io->status; + int status = produce_buffer(this); + spa_log_trace(this->log, "%p: io:%d->%d status:%d", this, io_status, 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 transport_start(struct impl *this) +{ + struct port *port = &this->port; + int res; + + /* Don't do anything if the node has already started */ + if (this->transport_started) + return 0; + if (!this->start_ready) + return -EIO; + + spa_log_debug(this->log, "%p: start transport", this); + + /* Make sure the transport is valid */ + spa_return_val_if_fail (this->transport != NULL, -EIO); + + /* 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; + + /* 40 ms max buffer (on top of duration) */ + spa_bt_decode_buffer_set_max_extra_latency(&port->buffer, + port->current_format.info.raw.rate * 40 / 1000); + + /* Init mSBC/LC3 if needed */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + res = sbc_init_msbc(&this->msbc, 0); + if (res < 0) + return res; + + /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ + this->msbc.endian = SBC_LE; + this->h2_seq_initialized = false; + + this->recv_buffer_pos = 0; + } else if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { +#ifdef HAVE_LC3 + this->lc3 = lc3_setup_decoder(7500, 32000, 0, + calloc(1, lc3_decoder_size(7500, 32000))); + if (!this->lc3) + return -EINVAL; + + spa_assert(lc3_frame_samples(7500, 32000) * port->frame_size == LC3_SWB_DECODED_SIZE); + + this->h2_seq_initialized = false; + this->recv_buffer_pos = 0; +#else + res = -EINVAL; + goto fail; +#endif + } + + this->io_error = false; + + /* 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); + + /* Set the started flag */ + this->transport_started = true; + + return 0; + +fail: + sbc_finish(&this->msbc); + free(this->lc3); + this->lc3 = NULL; + return res; +} + +static int do_start(struct impl *this) +{ + bool do_accept; + int res; + + if (this->started) + return 0; + + spa_return_val_if_fail(this->transport, -EIO); + + this->following = is_following(this); + + this->start_ready = true; + + 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) { + this->start_ready = false; + return res; + } + + /* 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); + + 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; + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + set_timeout(this, 0); + + return 0; +} + +static int do_remove_transport_source(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_started = false; + + if (this->transport && this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); + + return 0; +} + +static void transport_stop(struct impl *this) +{ + struct port *port = &this->port; + + if (!this->transport_started) + return; + + spa_log_debug(this->log, "sco-source %p: transport stop", this); + + spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + + spa_bt_decode_buffer_clear(&port->buffer); + + sbc_finish(&this->msbc); + free(this->lc3); + this->lc3 = NULL; +} + +static int do_stop(struct impl *this) +{ + int res; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "%p: stop", this); + + this->start_ready = false; + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + transport_stop(this); + + if (this->transport) + res = spa_bt_transport_release(this->transport); + else + res = 0; + + 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_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) +{ + const struct spa_dict_item hu_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Source/Internal" : "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, }; + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + info.format = SPA_AUDIO_FORMAT_S24_32_LE; + else + 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 + * LC3-SWB format has a rate of 32kHz + */ + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + info.rate = 32000; + else 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 (!this->transport) + return -EIO; + + 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 != 1) + return -EINVAL; + + switch (info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16_LE: + if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) + return -EINVAL; + port->frame_size = info.info.raw.channels * 2; + break; + case SPA_AUDIO_FORMAT_S24_32_LE: + if (this->transport->codec != HFP_AUDIO_CODEC_LC3_SWB) + return -EINVAL; + port->frame_size = info.info.raw.channels * 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_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 *result_duration) +{ + struct port *port = &this->port; + uint32_t samples, rate_denom; + uint64_t duration; + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate_denom = this->position->clock.rate.denom; + } else { + duration = 1024; + rate_denom = port->current_format.info.raw.rate; + } + + *result_duration = duration * port->current_format.info.raw.rate / rate_denom; + + if (SPA_LIKELY(port->rate_match) && this->resampling) + samples = port->rate_match->size; + else + samples = *result_duration; + + return samples; +} + +#define WARN_ONCE(cond, ...) \ + if (SPA_UNLIKELY(cond)) { static bool __once; if (!__once) { __once = true; spa_log_warn(__VA_ARGS__); } } + +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, + this->position ? this->position->clock.rate_diff : 1.0, + this->position ? this->position->clock.next_nsec : 0); + + setup_matching(this); + + buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); + + /* copy data to buffers */ + if (!spa_list_is_empty(&port->free)) { + struct buffer *buffer; + struct spa_data *datas; + uint32_t data_size; + + buffer = spa_list_first(&port->free, struct buffer, link); + datas = buffer->buf->datas; + + data_size = samples * port->frame_size; + + WARN_ONCE(datas[0].maxsize < data_size && !this->following, + this->log, "source buffer too small (%u < %u)", + datas[0].maxsize, data_size); + + data_size = SPA_MIN(data_size, SPA_ROUND_DOWN(datas[0].maxsize, port->frame_size)); + + avail = SPA_MIN(avail, data_size); + + spa_bt_decode_buffer_read(&port->buffer, avail); + + spa_list_remove(&buffer->link); + + spa_log_trace(this->log, "dequeue %d", buffer->id); + + datas[0].chunk->offset = 0; + datas[0].chunk->size = data_size; + datas[0].chunk->stride = port->frame_size; + + memcpy(datas[0].data, buf, avail); + + /* pad with silence */ + if (avail < data_size) + memset(SPA_PTROFF(datas[0].data, avail, void), 0, data_size - avail); + + /* ready buffer if full */ + spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)samples); + 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 && + (this->following || port->rate_match == NULL)) + 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; + } + + if (this->io_error) { + io->status = -EIO; + return SPA_STATUS_STOPPED; + } + + /* Handle buffering */ + if (this->transport_started) + 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; + + if (!this->started || !this->transport_started) + return SPA_STATUS_OK; + + 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 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) + transport_start(this); + else if (state < SPA_BT_TRANSPORT_STATE_ACTIVE) + transport_stop(this); + + if (state == SPA_BT_TRANSPORT_STATE_ERROR) { + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + + 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 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, + .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->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_PHYSICAL | + 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, "api.bluez5.internal")) != NULL) + this->is_internal = 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; + } + 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. " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with hsp/hfp" }, + { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_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/telephony.c b/spa/plugins/bluez5/telephony.c new file mode 100644 index 0000000..c2f9d67 --- /dev/null +++ b/spa/plugins/bluez5/telephony.c @@ -0,0 +1,1870 @@ +/* Spa Bluez5 Telephony D-Bus service */ +/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include "telephony.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define PW_TELEPHONY_SERVICE "org.pipewire.Telephony" + +#define PW_TELEPHONY_OBJECT_PATH "/org/pipewire/Telephony" + +#define PW_TELEPHONY_AG_IFACE "org.pipewire.Telephony.AudioGateway1" +#define PW_TELEPHONY_AG_TRANSPORT_IFACE "org.pipewire.Telephony.AudioGatewayTransport1" +#define PW_TELEPHONY_CALL_IFACE "org.pipewire.Telephony.Call1" + +#define OFONO_MANAGER_IFACE "org.ofono.Manager" +#define OFONO_VOICE_CALL_MANAGER_IFACE "org.ofono.VoiceCallManager" +#define OFONO_VOICE_CALL_IFACE "org.ofono.VoiceCall" + +#define DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " + +#define DBUS_PROPERTIES_IFACE_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " + +#define DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " + +#define PW_TELEPHONY_MANAGER_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML \ + DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML \ + "" + +#define PW_TELEPHONY_AG_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " + +#define PW_TELEPHONY_AG_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + PW_TELEPHONY_AG_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + PW_TELEPHONY_AG_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + DBUS_OBJECT_MANAGER_IFACE_INTROSPECT_XML \ + DBUS_PROPERTIES_IFACE_INTROSPECT_XML \ + DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML \ + "" + +#define PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " + +#define PW_TELEPHONY_CALL_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + PW_TELEPHONY_CALL_COMMON_INTROSPECT_XML \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + DBUS_PROPERTIES_IFACE_INTROSPECT_XML \ + DBUS_INTROSPECTABLE_IFACE_INTROSPECT_XML \ + "" + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.telephony"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct callimpl; + +struct impl { + struct spa_bt_telephony this; + + struct spa_log *log; + struct spa_dbus *dbus; + + struct spa_dbus_connection *dbus_connection; + DBusConnection *conn; + + const char *path; + struct spa_list ag_list; + + bool default_reject_sco; +}; + +struct agimpl { + struct spa_bt_telephony_ag this; + struct spa_list link; + char *path; + struct spa_callbacks callbacks; + void *user_data; + + bool dial_in_progress; + struct callimpl *dial_return; + + struct { + struct spa_bt_telephony_ag_transport transport; + } prev; +}; + +struct callimpl { + struct spa_bt_telephony_call this; + char *path; + struct spa_callbacks callbacks; + void *user_data; + + /* previous values of properties */ + struct { + char *line_identification; + char *incoming_line; + char *name; + bool multiparty; + enum spa_bt_telephony_call_state state; + } prev; +}; + +#define ag_emit(ag,m,v,...) spa_callbacks_call(&ag->callbacks, struct spa_bt_telephony_ag_callbacks, m, v, ##__VA_ARGS__) +#define ag_emit_dial(s,n,e,cme) ag_emit(s,dial,0,n,e,cme) +#define ag_emit_swap_calls(s,e,cme) ag_emit(s,swap_calls,0,e,cme) +#define ag_emit_release_and_answer(s,e,cme) ag_emit(s,release_and_answer,0,e,cme) +#define ag_emit_release_and_swap(s,e,cme) ag_emit(s,release_and_swap,0,e,cme) +#define ag_emit_hold_and_answer(s,e,cme) ag_emit(s,hold_and_answer,0,e,cme) +#define ag_emit_hangup_all(s,e,cme) ag_emit(s,hangup_all,0,e,cme) +#define ag_emit_create_multiparty(s,e,cme) ag_emit(s,create_multiparty,0,e,cme) +#define ag_emit_send_tones(s,t,e,cme) ag_emit(s,send_tones,0,t,e,cme) +#define ag_emit_transport_activate(s,e,cme) ag_emit(s,transport_activate,0,e,cme) + +#define call_emit(c,m,v,...) spa_callbacks_call(&c->callbacks, struct spa_bt_telephony_call_callbacks, m, v, ##__VA_ARGS__) +#define call_emit_answer(s,e,cme) call_emit(s,answer,0,e,cme) +#define call_emit_hangup(s,e,cme) call_emit(s,hangup,0,e,cme) + +static void dbus_iter_append_ag_interfaces(DBusMessageIter *i, struct spa_bt_telephony_ag *ag); +static void dbus_iter_append_call_properties(DBusMessageIter *i, struct spa_bt_telephony_call *call, bool all); + +#define PW_TELEPHONY_ERROR_FAILED "org.pipewire.Telephony.Error.Failed" +#define PW_TELEPHONY_ERROR_NOT_SUPPORTED "org.pipewire.Telephony.Error.NotSupported" +#define PW_TELEPHONY_ERROR_INVALID_FORMAT "org.pipewire.Telephony.Error.InvalidFormat" +#define PW_TELEPHONY_ERROR_INVALID_STATE "org.pipewire.Telephony.Error.InvalidState" +#define PW_TELEPHONY_ERROR_IN_PROGRESS "org.pipewire.Telephony.Error.InProgress" +#define PW_TELEPHONY_ERROR_CME "org.pipewire.Telephony.Error.CME" + +static const char *telephony_error_to_dbus (enum spa_bt_telephony_error err) +{ + switch (err) { + case BT_TELEPHONY_ERROR_FAILED: + return PW_TELEPHONY_ERROR_FAILED; + case BT_TELEPHONY_ERROR_NOT_SUPPORTED: + return PW_TELEPHONY_ERROR_NOT_SUPPORTED; + case BT_TELEPHONY_ERROR_INVALID_FORMAT: + return PW_TELEPHONY_ERROR_INVALID_FORMAT; + case BT_TELEPHONY_ERROR_INVALID_STATE: + return PW_TELEPHONY_ERROR_INVALID_STATE; + case BT_TELEPHONY_ERROR_IN_PROGRESS: + return PW_TELEPHONY_ERROR_IN_PROGRESS; + case BT_TELEPHONY_ERROR_CME: + return PW_TELEPHONY_ERROR_CME; + default: + return ""; + } +} + +static const char *telephony_error_to_description (enum spa_bt_telephony_error err, uint8_t cme_error) +{ + switch (err) { + case BT_TELEPHONY_ERROR_FAILED: + return "Method call failed"; + case BT_TELEPHONY_ERROR_NOT_SUPPORTED: + return "Method is not supported on this Audio Gateway"; + case BT_TELEPHONY_ERROR_INVALID_FORMAT: + return "Invalid phone number or tones"; + case BT_TELEPHONY_ERROR_INVALID_STATE: + return "The current state does not allow this method call"; + case BT_TELEPHONY_ERROR_IN_PROGRESS: + return "Command already in progress"; + case BT_TELEPHONY_ERROR_CME: + switch (cme_error) { + case 0: return "AG failure"; + case 1: return "no connection to phone"; + case 3: return "operation not allowed"; + case 4: return "operation not supported"; + case 5: return "PH-SIM PIN required"; + case 10: return "SIM not inserted"; + case 11: return "SIM PIN required"; + case 12: return "SIM PUK required"; + case 13: return "SIM failure"; + case 14: return "SIM busy"; + case 16: return "incorrect password"; + case 17: return "SIM PIN2 required"; + case 18: return "SIM PUK2 required"; + case 20: return "memory full"; + case 21: return "invalid index"; + case 23: return "memory failure"; + case 24: return "text string too long"; + case 25: return "invalid characters in text string"; + case 26: return "dial string too long"; + case 27: return "invalid characters in dial string"; + case 30: return "no network service"; + case 31: return "network Timeout"; + case 32: return "network not allowed - Emergency calls only"; + default: return "Unknown CME error"; + } + default: + return ""; + } +} + +#define find_free_object_id(list, obj_type, link) \ +({ \ + int id = 1; \ + obj_type *object; \ + spa_list_for_each(object, list, link) { \ + if (object->this.id <= id) \ + id = object->this.id + 1; \ + } \ + id; \ +}) + +static DBusMessage *manager_introspect(struct impl *impl, DBusMessage *m) +{ + const char *xml = PW_TELEPHONY_MANAGER_INTROSPECT_XML; + spa_autoptr(DBusMessage) r = NULL; + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return NULL; + return spa_steal_ptr(r); +} + +static DBusMessage *manager_get_managed_objects(struct impl *impl, DBusMessage *m, bool ofono_compat) +{ + struct agimpl *agimpl; + spa_autoptr(DBusMessage) r = NULL; + DBusMessageIter iter, array1, entry1, props_dict; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + + dbus_message_iter_init_append(r, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + ofono_compat ? "{oa{sv}}" : "{oa{sa{sv}}}", &array1); + + spa_list_for_each (agimpl, &impl->ag_list, link) { + if (agimpl->path) { + dbus_message_iter_open_container(&array1, DBUS_TYPE_DICT_ENTRY, NULL, &entry1); + if (ofono_compat) { + dbus_message_iter_append_basic(&entry1, DBUS_TYPE_OBJECT_PATH, &agimpl->path); + dbus_message_iter_open_container(&entry1, DBUS_TYPE_ARRAY, "{sv}", &props_dict); + dbus_message_iter_close_container(&entry1, &props_dict); + } else { + dbus_iter_append_ag_interfaces(&entry1, &agimpl->this); + } + dbus_message_iter_close_container(&array1, &entry1); + } + } + dbus_message_iter_close_container(&iter, &array1); + + return spa_steal_ptr(r); +} + +static DBusHandlerResult manager_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *impl = userdata; + + spa_autoptr(DBusMessage) r = NULL; + const char *path, *interface, *member; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { + r = manager_introspect(impl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECT_MANAGER, "GetManagedObjects")) { + r = manager_get_managed_objects(impl, m, false); + } else if (dbus_message_is_method_call(m, OFONO_MANAGER_IFACE, "GetModems")) { + r = manager_get_managed_objects(impl, m, true); + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(impl->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + return DBUS_HANDLER_RESULT_HANDLED; +} + +struct spa_bt_telephony * +telephony_new(struct spa_log *log, struct spa_dbus *dbus, const struct spa_dict *info) +{ + struct impl *impl = NULL; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + bool service_enabled = true; + bool ofono_service_compat = false; + enum spa_dbus_type bus_type = SPA_DBUS_TYPE_SESSION; + int res; + + static const DBusObjectPathVTable vtable_manager = { + .message_function = manager_handler, + }; + + spa_assert(log); + spa_assert(dbus); + + spa_log_topic_init(log, &log_topic); + + if (info) { + const char *str; + if ((str = spa_dict_lookup(info, "bluez5.telephony-dbus-service")) != NULL) { + service_enabled = spa_atob(str); + } + if ((str = spa_dict_lookup(info, "bluez5.telephony.use-system-bus")) != NULL) { + bus_type = spa_atob(str) ? SPA_DBUS_TYPE_SYSTEM : SPA_DBUS_TYPE_SESSION; + } + if ((str = spa_dict_lookup(info, "bluez5.telephony.provide-ofono")) != NULL) { + ofono_service_compat = spa_atob(str); + bus_type = SPA_DBUS_TYPE_SYSTEM; + } + } + + if (!service_enabled) { + spa_log_info(log, "Bluetooth Telephony service disabled by configuration"); + return NULL; + } + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->log = log; + impl->dbus = dbus; + impl->ag_list = SPA_LIST_INIT(&impl->ag_list); + + impl->dbus_connection = spa_dbus_get_connection(impl->dbus, bus_type); + if (impl->dbus_connection == NULL) { + spa_log_warn(impl->log, "no session dbus connection"); + goto fail; + } + impl->conn = spa_dbus_connection_get(impl->dbus_connection); + if (impl->conn == NULL) { + spa_log_warn(impl->log, "failed to get session dbus connection"); + goto fail; + } + + impl->default_reject_sco = false; + if (info) { + const char *str; + if ((str = spa_dict_lookup(info, "bluez5.telephony.default-reject-sco")) != NULL) { + impl->default_reject_sco = spa_atob(str); + } + } + + /* XXX: We should handle spa_dbus reconnecting, but we don't, so ref + * XXX: the handle so that we can keep it if spa_dbus unrefs it. + */ + dbus_connection_ref(impl->conn); + + res = dbus_bus_request_name(impl->conn, + ofono_service_compat ? OFONO_SERVICE : PW_TELEPHONY_SERVICE, + DBUS_NAME_FLAG_DO_NOT_QUEUE, &err); + if (res < 0) { + spa_log_warn(impl->log, "D-Bus RequestName() error: %s", err.message); + goto fail; + } + if (res == DBUS_REQUEST_NAME_REPLY_EXISTS) { + spa_log_warn(impl->log, "Bluetooth Telephony service is already registered by another connection"); + goto fail; + } + + impl->path = ofono_service_compat ? "/" : PW_TELEPHONY_OBJECT_PATH; + + if (!dbus_connection_register_object_path(impl->conn, impl->path, + &vtable_manager, impl)) { + goto fail; + } + + return &impl->this; + +fail: + spa_log_info(impl->log, "Bluetooth Telephony service disabled due to failure"); + if (impl->conn) + dbus_connection_unref(impl->conn); + if (impl->dbus_connection) + spa_dbus_connection_destroy(impl->dbus_connection); + free(impl); + return NULL; +} + +void telephony_free(struct spa_bt_telephony *telephony) +{ + struct impl *impl = SPA_CONTAINER_OF(telephony, struct impl, this); + struct agimpl *agimpl; + + spa_list_consume (agimpl, &impl->ag_list, link) { + telephony_ag_destroy(&agimpl->this); + } + + dbus_connection_unref(impl->conn); + spa_dbus_connection_destroy(impl->dbus_connection); + impl->dbus_connection = NULL; + impl->conn = NULL; + + free(impl); +} + +static void telephony_ag_transport_commit_properties(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + agimpl->prev.transport = ag->transport; +} + +static const char * const * transport_state_to_string(int state) +{ + static const char * const state_str[] = { + "error", + "idle", + "pending", + "active", + }; + if (state < -1 || state > 2) + state = -1; + return &state_str[state + 1]; +} + +static bool +dbus_iter_append_ag_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *ag, bool all) +{ + DBusMessageIter dict, entry, variant; + bool changed = false; + + dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict); + + /* Address must be set before registering and never changes, + so there is no need to check for changes here */ + if (all) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "Address"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &ag->address); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + + dbus_message_iter_close_container(i, &dict); + return changed; +} + +static bool +dbus_iter_append_ag_transport_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *ag, bool all) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + DBusMessageIter dict, entry, variant; + bool changed = false; + + dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict); + + if (all || ag->transport.codec != agimpl->prev.transport.codec) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "Codec"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &ag->transport.codec); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + + if (all || ag->transport.state != agimpl->prev.transport.state) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "State"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + transport_state_to_string(ag->transport.state)); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + + if (all || ag->transport.rejectSCO != agimpl->prev.transport.rejectSCO) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "RejectSCO"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, + &ag->transport.rejectSCO); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + + dbus_message_iter_close_container(i, &dict); + return changed; +} + +static void +dbus_iter_append_ag_interfaces(DBusMessageIter *i, struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + DBusMessageIter entry, dict; + + dbus_message_iter_append_basic(i, DBUS_TYPE_OBJECT_PATH, &agimpl->path); + dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict); + + const char *interface = PW_TELEPHONY_AG_IFACE; + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface); + dbus_iter_append_ag_properties(&entry, ag, true); + dbus_message_iter_close_container(&dict, &entry); + + const char *interface2 = PW_TELEPHONY_AG_TRANSPORT_IFACE; + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface2); + dbus_iter_append_ag_transport_properties(&entry, ag, true); + dbus_message_iter_close_container(&dict, &entry); + + dbus_message_iter_close_container(i, &dict); +} + +static DBusMessage *ag_introspect(struct agimpl *agimpl, DBusMessage *m) +{ + const char *xml = PW_TELEPHONY_AG_INTROSPECT_XML; + spa_autoptr(DBusMessage) r = NULL; + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return NULL; + return spa_steal_ptr(r); +} + +static DBusMessage *ag_get_managed_objects(struct agimpl *agimpl, DBusMessage *m, bool ofono_compat) +{ + struct callimpl *callimpl; + spa_autoptr(DBusMessage) r = NULL; + DBusMessageIter iter, array1, entry1, array2, entry2; + const char *interface = PW_TELEPHONY_CALL_IFACE; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + + dbus_message_iter_init_append(r, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + ofono_compat ? "{oa{sv}}" : "{oa{sa{sv}}}", &array1); + + spa_list_for_each (callimpl, &agimpl->this.call_list, this.link) { + dbus_message_iter_open_container(&array1, DBUS_TYPE_DICT_ENTRY, NULL, &entry1); + dbus_message_iter_append_basic(&entry1, DBUS_TYPE_OBJECT_PATH, &callimpl->path); + if (ofono_compat) { + dbus_iter_append_call_properties(&entry1, &callimpl->this, true); + } else { + dbus_message_iter_open_container(&entry1, DBUS_TYPE_ARRAY, "{sa{sv}}", &array2); + dbus_message_iter_open_container(&array2, DBUS_TYPE_DICT_ENTRY, NULL, &entry2); + dbus_message_iter_append_basic(&entry2, DBUS_TYPE_STRING, &interface); + dbus_iter_append_call_properties(&entry2, &callimpl->this, true); + dbus_message_iter_close_container(&array2, &entry2); + dbus_message_iter_close_container(&entry1, &array2); + } + dbus_message_iter_close_container(&array1, &entry1); + } + dbus_message_iter_close_container(&iter, &array1); + + return spa_steal_ptr(r); +} + +static DBusMessage *ag_properties_get(struct agimpl *agimpl, DBusMessage *m) +{ + const char *iface, *name; + DBusMessage *r; + DBusMessageIter i, v; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) + return NULL; + + if (spa_streq(iface, PW_TELEPHONY_AG_IFACE)) { + if (spa_streq(name, "Address")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, + &agimpl->this.address); + dbus_message_iter_close_container(&i, &v); + return r; + } + } else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { + if (spa_streq(name, "Codec")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_BYTE, + &agimpl->this.transport.codec); + dbus_message_iter_close_container(&i, &v); + return r; + } else if (spa_streq(name, "State")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, + transport_state_to_string(agimpl->this.transport.state)); + dbus_message_iter_close_container(&i, &v); + return r; + } else if (spa_streq(name, "RejectSCO")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_BOOLEAN, + &agimpl->this.transport.rejectSCO); + dbus_message_iter_close_container(&i, &v); + return r; + } + } else { + return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE, + "No such interface"); + } + + return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_PROPERTY, + "No such property"); +} + +static DBusMessage *ag_properties_get_all(struct agimpl *agimpl, DBusMessage *m) +{ + DBusMessage *r; + DBusMessageIter i; + const char *iface; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_INVALID)) + return NULL; + + if (spa_streq(iface, PW_TELEPHONY_AG_IFACE)) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_iter_append_ag_properties(&i, &agimpl->this, true); + return r; + } else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_iter_append_ag_transport_properties(&i, &agimpl->this, true); + return r; + } else { + return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE, + "No such interface"); + } +} + +static DBusMessage *ag_properties_set(struct agimpl *agimpl, DBusMessage *m) +{ + const char *iface, *name; + DBusMessageIter i, variant; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) + return NULL; + + if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { + if (spa_streq(name, "RejectSCO")) { + dbus_message_iter_init(m, &i); + dbus_message_iter_next(&i); /* skip iface */ + dbus_message_iter_next(&i); /* skip name */ + dbus_message_iter_recurse(&i, &variant); /* value */ + dbus_message_iter_get_basic(&variant, &agimpl->this.transport.rejectSCO); + return dbus_message_new_method_return(m); + } + } + + return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY, + "Property not writable"); +} + +static bool validate_phone_number(const char *number) +{ + const char *c; + int count = 0; + + if (!number) + return false; + for (c = number; *c != '\0'; c++) { + if (!(*c >= '0' && *c <= '9') && !(*c >= 'A' && *c <= 'D') && + *c != '#' && *c != '*' && *c != '+' && *c != ',' ) + return false; + count++; + } + if (count < 1 || count > 80) + return false; + return true; +} + +static bool validate_tones(const char *tones) +{ + const char *c; + if (!tones) + return false; + for (c = tones; *c != '\0'; c++) { + if (!(*c >= '0' && *c <= '9') && !(*c >= 'A' && *c <= 'D') && + *c != '#' && *c != '*') + return false; + } + return true; +} + +static DBusMessage *ag_dial(struct agimpl *agimpl, DBusMessage *m) +{ + const char *number = NULL; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + spa_autoptr(DBusMessage) r = NULL; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return NULL; + + if (!validate_phone_number(number)) { + err = BT_TELEPHONY_ERROR_INVALID_FORMAT; + goto failed; + } + + agimpl->dial_in_progress = true; + if (!ag_emit_dial(agimpl, number, &err, &cme_error)) { + agimpl->dial_in_progress = false; + goto failed; + } + agimpl->dial_in_progress = false; + + if (!agimpl->dial_return || !agimpl->dial_return->path) + err = BT_TELEPHONY_ERROR_FAILED; + + if (err != BT_TELEPHONY_ERROR_NONE) + goto failed; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_OBJECT_PATH, + &agimpl->dial_return->path, DBUS_TYPE_INVALID)) + return NULL; + + agimpl->dial_return = NULL; + + return spa_steal_ptr(r); + +failed: + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_swap_calls(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_swap_calls(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_release_and_answer(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_release_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_release_and_swap(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_release_and_swap(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_hold_and_answer(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_hold_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_hangup_all(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_hangup_all(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_create_multiparty(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_create_multiparty(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_send_tones(struct agimpl *agimpl, DBusMessage *m) +{ + const char *tones = NULL; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &tones, + DBUS_TYPE_INVALID)) + return NULL; + + if (!validate_tones(tones)) { + err = BT_TELEPHONY_ERROR_INVALID_FORMAT; + goto failed; + } + + if (ag_emit_send_tones(agimpl, tones, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + +failed: + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *ag_transport_activate(struct agimpl *agimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (ag_emit_transport_activate(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusHandlerResult ag_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct agimpl *agimpl = userdata; + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + spa_autoptr(DBusMessage) r = NULL; + const char *path, *interface, *member; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { + r = ag_introspect(agimpl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECT_MANAGER, "GetManagedObjects")) { + r = ag_get_managed_objects(agimpl, m, false); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) { + r = ag_properties_get(agimpl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) { + r = ag_properties_get_all(agimpl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) { + r = ag_properties_set(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "Dial") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "Dial")) { + r = ag_dial(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "SwapCalls") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "SwapCalls")) { + r = ag_swap_calls(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "ReleaseAndAnswer") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "ReleaseAndAnswer")) { + r = ag_release_and_answer(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "ReleaseAndSwap") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "ReleaseAndSwap")) { + r = ag_release_and_swap(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "HoldAndAnswer") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "HoldAndAnswer")) { + r = ag_hold_and_answer(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "HangupAll") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "HangupAll")) { + r = ag_hangup_all(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "CreateMultiparty") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "CreateMultiparty")) { + r = ag_create_multiparty(agimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_IFACE, "SendTones") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "SendTones")) { + r = ag_send_tones(agimpl, m); + } else if (dbus_message_is_method_call(m, OFONO_VOICE_CALL_MANAGER_IFACE, "GetCalls")) { + r = ag_get_managed_objects(agimpl, m, true); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_AG_TRANSPORT_IFACE, "Activate")) { + r = ag_transport_activate(agimpl, m); + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(impl->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + return DBUS_HANDLER_RESULT_HANDLED; +} + +struct spa_bt_telephony_ag * +telephony_ag_new(struct spa_bt_telephony *telephony, size_t user_data_size) +{ + struct impl *impl = SPA_CONTAINER_OF(telephony, struct impl, this); + struct agimpl *agimpl; + + spa_assert(user_data_size < SIZE_MAX - sizeof(*agimpl)); + + agimpl = calloc(1, sizeof(*agimpl) + user_data_size); + if (agimpl == NULL) + return NULL; + + agimpl->this.telephony = telephony; + agimpl->this.id = find_free_object_id(&impl->ag_list, struct agimpl, link); + spa_list_init(&agimpl->this.call_list); + + spa_list_append(&impl->ag_list, &agimpl->link); + + if (user_data_size > 0) + agimpl->user_data = SPA_PTROFF(agimpl, sizeof(struct agimpl), void); + + agimpl->this.transport.rejectSCO = impl->default_reject_sco; + + return &agimpl->this; +} + +void telephony_ag_destroy(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct callimpl *callimpl; + + spa_list_consume (callimpl, &agimpl->this.call_list, this.link) { + telephony_call_destroy(&callimpl->this); + } + + telephony_ag_unregister(ag); + spa_list_remove(&agimpl->link); + + free(ag->address); + + free(agimpl); +} + +void *telephony_ag_get_user_data(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + return agimpl->user_data; +} + +int telephony_ag_register(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + char *path; + + const DBusObjectPathVTable vtable = { + .message_function = ag_handler, + }; + + path = spa_aprintf (PW_TELEPHONY_OBJECT_PATH "/ag%d", agimpl->this.id); + + /* register object */ + if (!dbus_connection_register_object_path(impl->conn, path, &vtable, agimpl)) { + spa_log_error(impl->log, "failed to register %s", path); + return -EIO; + } + agimpl->path = strdup(path); + + /* notify on ObjectManager of the Manager object */ + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter; + + msg = dbus_message_new_signal(impl->path, DBUS_INTERFACE_OBJECT_MANAGER, + "InterfacesAdded"); + dbus_message_iter_init_append(msg, &iter); + dbus_iter_append_ag_interfaces(&iter, ag); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_error(impl->log, "failed to send InterfacesAdded for %s", path); + telephony_ag_unregister(ag); + return -EIO; + } + } + + /* emit ModemAdded on the Manager object */ + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter, props_dict; + + msg = dbus_message_new_signal(impl->path, OFONO_MANAGER_IFACE, + "ModemAdded"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &props_dict); + dbus_message_iter_close_container(&iter, &props_dict); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_error(impl->log, "failed to send ModemAdded for %s", path); + telephony_ag_unregister(ag); + return -EIO; + } + } + + spa_log_debug(impl->log, "registered AudioGateway: %s", path); + + return 0; +} + +void telephony_ag_unregister(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + if (!agimpl->path) + return; + + spa_log_debug(impl->log, "removing AudioGateway: %s", agimpl->path); + + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter, entry; + const char *interface = PW_TELEPHONY_AG_IFACE; + const char *interface2 = PW_TELEPHONY_AG_TRANSPORT_IFACE; + + msg = dbus_message_new_signal(impl->path, DBUS_INTERFACE_OBJECT_MANAGER, + "InterfacesRemoved"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &agimpl->path); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface2); + dbus_message_iter_close_container(&iter, &entry); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_warn(impl->log, "sending InterfacesRemoved failed"); + } + } + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter; + + msg = dbus_message_new_signal(impl->path, OFONO_MANAGER_IFACE, + "ModemRemoved"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &agimpl->path); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_warn(impl->log, "sending ModemRemoved failed"); + } + } + + if (!dbus_connection_unregister_object_path(impl->conn, agimpl->path)) { + spa_log_warn(impl->log, "failed to unregister %s", agimpl->path); + } + + free(agimpl->path); + agimpl->path = NULL; +} + +/* send message to notify about property changes */ +void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + spa_autoptr(DBusMessage) msg = NULL; + const char *interface = PW_TELEPHONY_AG_TRANSPORT_IFACE; + DBusMessageIter i, a; + + msg = dbus_message_new_signal(agimpl->path, + DBUS_INTERFACE_PROPERTIES, + "PropertiesChanged"); + + dbus_message_iter_init_append(msg, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &interface); + + if (!dbus_iter_append_ag_transport_properties(&i, ag, false)) + return; + + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &a); + dbus_message_iter_close_container(&i, &a); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertiesChanged failed"); + } + + telephony_ag_transport_commit_properties(ag); +} + +void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag, + const struct spa_bt_telephony_ag_callbacks *cbs, + void *data) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + agimpl->callbacks.funcs = cbs; + agimpl->callbacks.data = data; +} + +struct spa_bt_telephony_call * +telephony_call_new(struct spa_bt_telephony_ag *ag, size_t user_data_size) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct callimpl *callimpl; + + spa_assert(user_data_size < SIZE_MAX - sizeof(*callimpl)); + + callimpl = calloc(1, sizeof(*callimpl) + user_data_size); + if (callimpl == NULL) + return NULL; + + callimpl->this.ag = ag; + callimpl->this.id = find_free_object_id(&ag->call_list, struct callimpl, this.link); + + spa_list_append(&ag->call_list, &callimpl->this.link); + + if (user_data_size > 0) + callimpl->user_data = SPA_PTROFF(callimpl, sizeof(struct callimpl), void); + + /* mark this object as the return value of the Dial method */ + if (agimpl->dial_in_progress) + agimpl->dial_return = callimpl; + + return &callimpl->this; +} + +void telephony_call_destroy(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + + telephony_call_unregister(call); + spa_list_remove(&call->link); + + free(callimpl->prev.line_identification); + free(callimpl->prev.incoming_line); + free(callimpl->prev.name); + + free(call->line_identification); + free(call->incoming_line); + free(call->name); + + free(callimpl); +} + +void *telephony_call_get_user_data(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + return callimpl->user_data; +} + +static void telephony_call_commit_properties(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + + if (!spa_streq (call->line_identification, callimpl->prev.line_identification)) { + free(callimpl->prev.line_identification); + callimpl->prev.line_identification = call->line_identification ? + strdup (call->line_identification) : NULL; + } + if (!spa_streq (call->incoming_line, callimpl->prev.incoming_line)) { + free(callimpl->prev.incoming_line); + callimpl->prev.incoming_line = call->incoming_line ? + strdup (call->incoming_line) : NULL; + } + if (!spa_streq (call->name, callimpl->prev.name)) { + free(callimpl->prev.name); + callimpl->prev.name = call->name ? strdup (call->name) : NULL; + } + callimpl->prev.multiparty = call->multiparty; + callimpl->prev.state = call->state; +} + +static const char * const call_state_to_string[] = { + "active", + "held", + "dialing", + "alerting", + "incoming", + "waiting", + "disconnected", +}; + +static inline const void *safe_string(char **str) +{ + static const char *empty_string = ""; + return *str ? (const char **) str : &empty_string; +} + +static void +dbus_iter_append_call_properties(DBusMessageIter *i, struct spa_bt_telephony_call *call, bool all) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + DBusMessageIter dict, entry, variant; + + dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, "{sv}", &dict); + + if (all || !spa_streq (call->line_identification, callimpl->prev.line_identification)) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, + &entry); + const char *line_identification = "LineIdentification"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &line_identification); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->line_identification)); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + if (all || !spa_streq (call->incoming_line, callimpl->prev.incoming_line)) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *incoming_line = "IncomingLine"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &incoming_line); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->incoming_line)); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + if (all || !spa_streq (call->name, callimpl->prev.name)) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "Name"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->name)); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + if (all || call->multiparty != callimpl->prev.multiparty) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *multiparty = "Multiparty"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &multiparty); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &call->multiparty); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + if (all || call->state != callimpl->prev.state) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *state = "State"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &state); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + &call_state_to_string[call->state]); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + } + + dbus_message_iter_close_container(i, &dict); +} + +static DBusMessage *call_introspect(struct callimpl *callimpl, DBusMessage *m) +{ + const char *xml = PW_TELEPHONY_CALL_INTROSPECT_XML; + spa_autoptr(DBusMessage) r = NULL; + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return NULL; + return spa_steal_ptr(r); +} + +static DBusMessage *call_properties_get(struct callimpl *callimpl, DBusMessage *m) +{ + const char *iface, *name; + DBusMessage *r; + DBusMessageIter i, v; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) + return NULL; + + if (spa_streq(iface, PW_TELEPHONY_CALL_IFACE)) + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "No such interface"); + + if (spa_streq(name, "Multiparty")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_BOOLEAN, + &callimpl->this.multiparty); + dbus_message_iter_close_container(&i, &v); + return r; + } else { + const char * const *property = NULL; + if (spa_streq(name, "LineIdentification")) { + property = (const char * const *) &callimpl->this.line_identification; + } else if (spa_streq(name, "IncomingLine")) { + property = (const char * const *) &callimpl->this.incoming_line; + } else if (spa_streq(name, "Name")) { + property = (const char * const *) &callimpl->this.name; + } else if (spa_streq(name, "State")) { + property = &call_state_to_string[callimpl->this.state]; + } + + if (property) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, + property); + dbus_message_iter_close_container(&i, &v); + return r; + } + } + + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "No such property"); +} + +static DBusMessage *call_properties_get_all(struct callimpl *callimpl, DBusMessage *m, bool ofono_compat) +{ + DBusMessage *r; + DBusMessageIter i; + + if (!ofono_compat) { + const char *iface; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_INVALID)) + return NULL; + + if (!spa_streq(iface, PW_TELEPHONY_CALL_IFACE)) + return dbus_message_new_error(m, DBUS_ERROR_UNKNOWN_INTERFACE, + "No such interface"); + } + + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + + dbus_message_iter_init_append(r, &i); + dbus_iter_append_call_properties(&i, &callimpl->this, true); + return r; +} + +static DBusMessage *call_properties_set(struct callimpl *callimpl, DBusMessage *m) +{ + return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY, + "Property not writable"); +} + +static DBusMessage *call_answer(struct callimpl *callimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (call_emit_answer(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusMessage *call_hangup(struct callimpl *callimpl, DBusMessage *m) +{ + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; + uint8_t cme_error; + + if (call_emit_hangup(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) + return dbus_message_new_method_return(m); + + return dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); +} + +static DBusHandlerResult call_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct callimpl *callimpl = userdata; + struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + spa_autoptr(DBusMessage) r = NULL; + const char *path, *interface, *member; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(impl->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { + r = call_introspect(callimpl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) { + r = call_properties_get(callimpl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) { + r = call_properties_get_all(callimpl, m, false); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) { + r = call_properties_set(callimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_CALL_IFACE, "Answer") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "Answer")) { + r = call_answer(callimpl, m); + } else if (dbus_message_is_method_call(m, PW_TELEPHONY_CALL_IFACE, "Hangup") || + dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "Hangup")) { + r = call_hangup(callimpl, m); + } else if (dbus_message_is_method_call(m, OFONO_VOICE_CALL_IFACE, "GetProperties")) { + r = call_properties_get_all(callimpl, m, true); + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(impl->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + return DBUS_HANDLER_RESULT_HANDLED; +} + +int telephony_call_register(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + char *path; + + const DBusObjectPathVTable vtable = { + .message_function = call_handler, + }; + + path = spa_aprintf ("%s/call%d", agimpl->path, callimpl->this.id); + + /* register object */ + if (!dbus_connection_register_object_path(impl->conn, path, &vtable, callimpl)) { + spa_log_error(impl->log, "failed to register %s", path); + return -EIO; + } + callimpl->path = strdup(path); + + /* notify on ObjectManager of the AudioGateway object */ + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter, entry, dict; + const char *interface = PW_TELEPHONY_CALL_IFACE; + + msg = dbus_message_new_signal(agimpl->path, + DBUS_INTERFACE_OBJECT_MANAGER, + "InterfacesAdded"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict); + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface); + dbus_iter_append_call_properties(&entry, call, true); + dbus_message_iter_close_container(&dict, &entry); + dbus_message_iter_close_container(&iter, &dict); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_error(impl->log, "failed to send InterfacesAdded for %s", path); + telephony_call_unregister(call); + return -EIO; + } + } + + /* emit CallAdded on the AudioGateway object */ + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter; + + msg = dbus_message_new_signal(agimpl->path, + OFONO_VOICE_CALL_MANAGER_IFACE, + "CallAdded"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + dbus_iter_append_call_properties(&iter, call, true); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_error(impl->log, "failed to send CallAdded for %s", path); + telephony_call_unregister(call); + return -EIO; + } + } + + telephony_call_commit_properties(call); + + spa_log_debug(impl->log, "registered Call: %s", path); + + return 0; +} + +void telephony_call_unregister(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + if (!callimpl->path) + return; + + spa_log_debug(impl->log, "removing Call: %s", callimpl->path); + + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter, entry; + const char *interface = PW_TELEPHONY_CALL_IFACE; + + msg = dbus_message_new_signal(agimpl->path, + DBUS_INTERFACE_OBJECT_MANAGER, + "InterfacesRemoved"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &callimpl->path); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface); + dbus_message_iter_close_container(&iter, &entry); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_warn(impl->log, "sending InterfacesRemoved failed"); + } + } + { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter iter; + + msg = dbus_message_new_signal(agimpl->path, + OFONO_VOICE_CALL_MANAGER_IFACE, + "CallRemoved"); + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &callimpl->path); + + if (!dbus_connection_send(impl->conn, msg, NULL)) { + spa_log_warn(impl->log, "sending CallRemoved failed"); + } + } + + if (!dbus_connection_unregister_object_path(impl->conn, callimpl->path)) { + spa_log_warn(impl->log, "failed to unregister %s", callimpl->path); + } + + free(callimpl->path); + callimpl->path = NULL; +} + +/* send message to notify about property changes */ +void telephony_call_notify_updated_props(struct spa_bt_telephony_call *call) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + struct agimpl *agimpl = SPA_CONTAINER_OF(callimpl->this.ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + { + spa_autoptr(DBusMessage) msg = NULL; + const char *interface = PW_TELEPHONY_CALL_IFACE; + DBusMessageIter i, a; + + msg = dbus_message_new_signal(callimpl->path, + DBUS_INTERFACE_PROPERTIES, + "PropertiesChanged"); + + dbus_message_iter_init_append(msg, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &interface); + + dbus_iter_append_call_properties(&i, call, false); + + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &a); + dbus_message_iter_close_container(&i, &a); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertiesChanged failed"); + } + } + + if (!spa_streq (call->line_identification, callimpl->prev.line_identification)) { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter entry, variant; + + msg = dbus_message_new_signal(callimpl->path, + OFONO_VOICE_CALL_IFACE, + "PropertyChanged"); + + const char *line_identification = "LineIdentification"; + dbus_message_iter_init_append(msg, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &line_identification); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->line_identification)); + dbus_message_iter_close_container(&entry, &variant); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertyChanged failed"); + } + } + + if (!spa_streq (call->incoming_line, callimpl->prev.incoming_line)) { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter entry, variant; + + msg = dbus_message_new_signal(callimpl->path, + OFONO_VOICE_CALL_IFACE, + "PropertyChanged"); + + const char *incoming_line = "IncomingLine"; + dbus_message_iter_init_append(msg, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &incoming_line); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->incoming_line)); + dbus_message_iter_close_container(&entry, &variant); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertyChanged failed"); + } + } + + if (!spa_streq (call->name, callimpl->prev.name)) { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter entry, variant; + + msg = dbus_message_new_signal(callimpl->path, + OFONO_VOICE_CALL_IFACE, + "PropertyChanged"); + + const char *name = "Name"; + dbus_message_iter_init_append(msg, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + safe_string (&call->name)); + dbus_message_iter_close_container(&entry, &variant); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertyChanged failed"); + } + } + + if (call->multiparty != callimpl->prev.multiparty) { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter entry, variant; + + msg = dbus_message_new_signal(callimpl->path, + OFONO_VOICE_CALL_IFACE, + "PropertyChanged"); + + const char *multiparty = "Multiparty"; + dbus_message_iter_init_append(msg, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &multiparty); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &call->multiparty); + dbus_message_iter_close_container(&entry, &variant); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertyChanged failed"); + } + } + + if (call->state != callimpl->prev.state) { + spa_autoptr(DBusMessage) msg = NULL; + DBusMessageIter entry, variant; + + msg = dbus_message_new_signal(callimpl->path, + OFONO_VOICE_CALL_IFACE, + "PropertyChanged"); + + const char *state = "State"; + dbus_message_iter_init_append(msg, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &state); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, + &call_state_to_string[call->state]); + dbus_message_iter_close_container(&entry, &variant); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertyChanged failed"); + } + } + + telephony_call_commit_properties(call); +} + +void telephony_call_set_callbacks(struct spa_bt_telephony_call *call, + const struct spa_bt_telephony_call_callbacks *cbs, + void *data) +{ + struct callimpl *callimpl = SPA_CONTAINER_OF(call, struct callimpl, this); + callimpl->callbacks.funcs = cbs; + callimpl->callbacks.data = data; +} diff --git a/spa/plugins/bluez5/telephony.h b/spa/plugins/bluez5/telephony.h new file mode 100644 index 0000000..8ba85ec --- /dev/null +++ b/spa/plugins/bluez5/telephony.h @@ -0,0 +1,132 @@ +/* Spa Bluez5 Telephony D-Bus service */ +/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_TELEPHONY_H +#define SPA_BLUEZ5_TELEPHONY_H + +#include "defs.h" + +enum spa_bt_telephony_error { + BT_TELEPHONY_ERROR_NONE = 0, + BT_TELEPHONY_ERROR_FAILED, + BT_TELEPHONY_ERROR_NOT_SUPPORTED, + BT_TELEPHONY_ERROR_INVALID_FORMAT, + BT_TELEPHONY_ERROR_INVALID_STATE, + BT_TELEPHONY_ERROR_IN_PROGRESS, + BT_TELEPHONY_ERROR_CME, +}; + +enum spa_bt_telephony_call_state { + CALL_STATE_ACTIVE, + CALL_STATE_HELD, + CALL_STATE_DIALING, + CALL_STATE_ALERTING, + CALL_STATE_INCOMING, + CALL_STATE_WAITING, + CALL_STATE_DISCONNECTED, +}; + +struct spa_bt_telephony { + +}; + +struct spa_bt_telephony_ag_transport { + int8_t codec; + enum spa_bt_transport_state state; + dbus_bool_t rejectSCO; +}; + +struct spa_bt_telephony_ag { + struct spa_bt_telephony *telephony; + struct spa_list call_list; + + int id; + + /* D-Bus properties */ + char *address; + struct spa_bt_telephony_ag_transport transport; +}; + +struct spa_bt_telephony_call { + struct spa_bt_telephony_ag *ag; + struct spa_list link; /* link in ag->call_list */ + + int id; + + /* D-Bus properties */ + char *line_identification; + char *incoming_line; + char *name; + bool multiparty; + enum spa_bt_telephony_call_state state; +}; + +struct spa_bt_telephony_ag_callbacks { +#define SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS 0 + uint32_t version; + + void (*dial)(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*swap_calls)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*release_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*release_and_swap)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*hold_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*hangup_all)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*create_multiparty)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*send_tones)(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error); + + void (*transport_activate)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); +}; + +struct spa_bt_telephony_call_callbacks { +#define SPA_VERSION_BT_TELEPHONY_CALL_CALLBACKS 0 + uint32_t version; + + void (*answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*hangup)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); +}; + +struct spa_bt_telephony *telephony_new(struct spa_log *log, struct spa_dbus *dbus, + const struct spa_dict *info); +void telephony_free(struct spa_bt_telephony *telephony); + + +/* create/destroy the ag object */ +struct spa_bt_telephony_ag * telephony_ag_new(struct spa_bt_telephony *telephony, + size_t user_data_size); +void telephony_ag_destroy(struct spa_bt_telephony_ag *ag); + +/* get the user data structure; struct size is set when creating the AG */ +void *telephony_ag_get_user_data(struct spa_bt_telephony_ag *ag); + +void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag, + const struct spa_bt_telephony_ag_callbacks *cbs, + void *data); + +void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag); + +/* register/unregister AudioGateway object on the bus */ +int telephony_ag_register(struct spa_bt_telephony_ag *ag); +void telephony_ag_unregister(struct spa_bt_telephony_ag *ag); + + +/* create/destroy the call object */ +struct spa_bt_telephony_call * telephony_call_new(struct spa_bt_telephony_ag *ag, + size_t user_data_size); +void telephony_call_destroy(struct spa_bt_telephony_call *call); + +/* get the user data structure; struct size is set when creating the Call */ +void *telephony_call_get_user_data(struct spa_bt_telephony_call *call); + +/* register/unregister Call object on the bus */ +int telephony_call_register(struct spa_bt_telephony_call *call); +void telephony_call_unregister(struct spa_bt_telephony_call *call); + +/* send message to notify about property changes */ +void telephony_call_notify_updated_props(struct spa_bt_telephony_call *call); + +void telephony_call_set_callbacks(struct spa_bt_telephony_call *call, + const struct spa_bt_telephony_call_callbacks *cbs, + void *data); + +#endif diff --git a/spa/plugins/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 + +#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..17c324c --- /dev/null +++ b/spa/plugins/bluez5/upower.c @@ -0,0 +1,240 @@ +/* Spa Bluez5 UPower proxy */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#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; + + DBusPendingCall *pending_get_call; + + 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; + DBusMessageIter i, variant_i; + + spa_assert(backend->pending_get_call == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&backend->pending_get_call); + 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)); + return; + } + + 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"); + return; + } + + dbus_message_iter_recurse(&i, &variant_i); + upower_parse_percentage(backend, &variant_i); +} + +static int update_battery_percentage(struct impl *this) +{ + cancel_and_unref(&this->pending_get_call); + + spa_autoptr(DBusMessage) m = dbus_message_new_method_call(UPOWER_SERVICE, + UPOWER_DISPLAY_DEVICE_OBJECT, + DBUS_INTERFACE_PROPERTIES, + "Get"); + if (!m) + return -ENOMEM; + + dbus_message_append_args(m, + DBUS_TYPE_STRING, &(const char *){ UPOWER_DEVICE_INTERFACE }, + DBUS_TYPE_STRING, &(const char *){ "Percentage" }, + DBUS_TYPE_INVALID); + dbus_message_set_auto_start(m, false); + + this->pending_get_call = send_with_reply(this->conn, m, upower_get_percentage_properties_reply, this); + if (!this->pending_get_call) + return -EIO; + + return 0; +} + +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; + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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) { + spa_log_debug(this->log, "UPower daemon appeared (%s)", new_owner); + update_battery_percentage(this); + } + } + } 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) +{ + if (this->filters_added) + return 0; + + if (!dbus_connection_add_filter(this->conn, upower_filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + return -EIO; + } + + spa_auto(DBusError) err = DBUS_ERROR_INIT; + + 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; +} + +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 (update_battery_percentage(this) < 0) + goto fail4; + + return this; + +fail4: + free(this); + return NULL; +} + +void upower_unregister(void *data) +{ + struct impl *this = data; + + cancel_and_unref(&this->pending_get_call); + + 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..8988ca3 --- /dev/null +++ b/spa/plugins/bluez5/upower.h @@ -0,0 +1,16 @@ +/* Spa Bluez5 UPower proxy */ +/* SPDX-FileCopyrightText: Copyright © 2022 Collabora */ +/* SPDX-License-Identifier: MIT */ + +#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 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..439bea7 --- /dev/null +++ b/spa/plugins/control/mixer.c @@ -0,0 +1,997 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.control-mixer"); + +#define MAX_BUFFERS 64 +#define MAX_PORTS 512 + +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[2]; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + unsigned int valid:1; + unsigned int have_format:1; + + uint32_t types; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list queue; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + uint32_t quantum_limit; + + struct spa_log *log; + + struct spa_loop *data_loop; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[8]; + + struct spa_io_position *position; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + struct spa_pod_control *mix_ctrl[MAX_PORTS]; + struct spa_pod_sequence *mix_seq[MAX_PORTS]; + + int n_formats; + + unsigned int have_format:1; + unsigned int started:1; +}; + +#define PORT_VALID(p) ((p) != NULL && (p)->valid) +#define CHECK_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) +#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 CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_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 GET_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) ? NULL : GET_PORT(this,d,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) +{ + struct impl *this = object; + + switch (id) { + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOTSUP; + } + 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; + 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, "%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, "%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, struct port *port, + 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_ANY(this, direction, port_id), -EINVAL); + + port = GET_PORT_ANY(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, port, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (port == NULL || !port->have_format) + return -EIO; + if ((res = port_enum_formats(this, port, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Buffers: + if (port == NULL || !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->quantum_limit, 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; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_AsyncBuffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_async_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); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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, types = 0; + 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; + + if ((res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_CONTROL_types, SPA_POD_OPT_Int(&types))) < 0) + return res; + + this->have_format = true; + + if (!port->have_format) { + this->n_formats++; + port->have_format = true; + port->types = types; + spa_log_debug(this->log, "%p: set format on port %d:%d", + this, direction, port_id); + } + } + 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, "%p: use buffers %d on port %d:%d", + this, n_buffers, direction, port_id); + + spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); + + 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, "%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; +} + +struct io_info { + struct port *port; + void *data; + size_t size; +}; + +static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct io_info *info = user_data; + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + info->port->io[0] = &ab->buffers[0]; + info->port->io[1] = &ab->buffers[1]; + } else if (info->size >= sizeof(struct spa_io_buffers)) { + info->port->io[0] = info->data; + info->port->io[1] = info->data; + } else { + info->port->io[0] = NULL; + info->port->io[1] = NULL; + } + 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; + struct io_info info; + + 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); + info.port = port; + info.data = data; + info.size = size; + + switch (id) { + case SPA_IO_Buffers: + case SPA_IO_AsyncBuffers: + spa_loop_invoke(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + 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_compare(uint8_t s1, uint8_t s2) +{ + /* 11 (controller) > 12 (program change) > + * 8 (note off) > 9 (note on) > 10 (aftertouch) > + * 13 (channel pressure) > 14 (pitch bend) */ + static int priotab[] = { 5,4,3,7,6,2,1,0 }; + if ((s1 & 0xf) != (s2 & 0xf)) + return 0; + return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7]; +} + +static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b) +{ + if (a->offset < b->offset) + return -1; + if (a->offset > b->offset) + return 1; + if (a->type != b->type) + return 0; + switch(a->type) { + case SPA_CONTROL_Midi: + { + uint8_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value); + if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1) + return 0; + return event_compare(da[0], db[0]); + } + case SPA_CONTROL_UMP: + { + uint32_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value); + if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4) + return 0; + if ((da[0] >> 28) != 2 || (da[0] >> 28) != 4 || + (db[0] >> 28) != 2 || (db[0] >> 28) != 4) + return 0; + return event_compare(da[0] >> 16, db[0] >> 16); + } + 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; + uint32_t cycle = this->position->clock.cycle & 1; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io[cycle]) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%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) { + if (outport->n_buffers > 0) + spa_log_warn(this->log, "%p: out of buffers (%d)", + this, outport->n_buffers); + return -EPIPE; + } + + ctrl = this->mix_ctrl; + seq = this->mix_seq; + 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 (SPA_UNLIKELY(!PORT_VALID(inport) || (inio = inport->io[cycle]) == NULL)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", + this, i, PORT_VALID(inport), + inport->io[0], inport->io[1], cycle); + continue; + } + if (inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, inio, inio->status, inio->buffer_id, inport->n_buffers); + continue; + } + + spa_log_trace_fp(this->log, "%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) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d max:%u " + "offset:%u size:%u", this, i, + d->maxsize, d->chunk->offset, d->chunk->size); + continue; + } + if (!spa_pod_is_sequence(pod)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d", this, i); + 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; + + if (outport->types && (outport->types & (1u << next->type)) == 0) { + uint8_t *data = SPA_POD_BODY(&next->value); + size_t size = SPA_POD_BODY_SIZE(&next->value); + + switch (next->type) { + case SPA_CONTROL_Midi: + { + uint32_t ump[4]; + uint64_t state = 0; + while (size > 0) { + int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state); + if (ump_size <= 0) + break; + spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&builder, ump, ump_size); + } + break; + } + case SPA_CONTROL_UMP: + { + uint8_t ev[8]; + int ev_size = spa_ump_to_midi((uint32_t*)data, size, ev, sizeof(ev)); + if (ev_size <= 0) + break; + spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&builder, ev, ev_size); + break; + } + } + } else { + spa_pod_builder_control(&builder, next->offset, next->type); + spa_pod_builder_primitive(&builder, &next->value); + } + + ctrl[next_index] = spa_pod_control_next(ctrl[next_index]); + } + spa_pod_builder_pop(&builder, &f); + + if (builder.state.offset > d->maxsize) { + spa_log_warn(this->log, "%p: control overflow %d > %d", + this, builder.state.offset, d->maxsize); + builder.state.offset = 0; + } + + 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; + 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); + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + + this->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, &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_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..2b19acf --- /dev/null +++ b/spa/plugins/control/plugin.c @@ -0,0 +1,29 @@ +/* Spa Control plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_control_mixer_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..61b9a1a --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg-dec.c @@ -0,0 +1,513 @@ +/* Spa FFmpeg decoder */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffmpeg.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.ffmpeg.dec"); + +#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, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if ((res = port_get_format(this, direction, port_id, + result.index, filter, ¶m, &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..b9a5e42 --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg-enc.c @@ -0,0 +1,491 @@ +/* Spa FFmpeg encoder */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffmpeg.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.ffmpeg.enc"); + +#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, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if ((res = port_get_format(this, direction, port_id, + result.index, filter, ¶m, &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..af3c51b --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg.c @@ -0,0 +1,143 @@ +/* Spa FFmpeg support */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include +#include + +#include + +#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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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 +#include + +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/filter-graph/audio-dsp-avx.c b/spa/plugins/filter-graph/audio-dsp-avx.c new file mode 100644 index 0000000..1509284 --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp-avx.c @@ -0,0 +1,326 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#include "config.h" +#ifndef HAVE_FFTW +#include "pffft.h" +#endif +#include "audio-dsp-impl.h" + +#include + +static void dsp_add_avx(void *obj, float *dst, const float * SPA_RESTRICT src[], + uint32_t n_src, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m256 in[4]; + const float **s = (const float **)src; + float *d = dst; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) { + unrolled = n_samples & ~31; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + for (n = 0; n < unrolled; n += 32) { + in[0] = _mm256_load_ps(&s[0][n+ 0]); + in[1] = _mm256_load_ps(&s[0][n+ 8]); + in[2] = _mm256_load_ps(&s[0][n+16]); + in[3] = _mm256_load_ps(&s[0][n+24]); + + for (i = 1; i < n_src; i++) { + in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n+ 0])); + in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n+ 8])); + in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n+16])); + in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n+24])); + } + _mm256_store_ps(&d[n+ 0], in[0]); + _mm256_store_ps(&d[n+ 8], in[1]); + _mm256_store_ps(&d[n+16], in[2]); + _mm256_store_ps(&d[n+24], in[3]); + } + for (; n < n_samples; n++) { + __m128 in[1]; + in[0] = _mm_load_ss(&s[0][n]); + for (i = 1; i < n_src; i++) + in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n])); + _mm_store_ss(&d[n], in[0]); + } +} + +static void dsp_add_1_gain_avx(void *obj, float *dst, const float * SPA_RESTRICT src[], + uint32_t n_src, float gain, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m256 in[4], g; + const float **s = (const float **)src; + float *d = dst; + __m128 g1; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) { + unrolled = n_samples & ~31; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + g = _mm256_set1_ps(gain); + g1 = _mm_set_ss(gain); + + for (n = 0; n < unrolled; n += 32) { + in[0] = _mm256_load_ps(&s[0][n+ 0]); + in[1] = _mm256_load_ps(&s[0][n+ 8]); + in[2] = _mm256_load_ps(&s[0][n+16]); + in[3] = _mm256_load_ps(&s[0][n+24]); + + for (i = 1; i < n_src; i++) { + in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&s[i][n+ 0])); + in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&s[i][n+ 8])); + in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&s[i][n+16])); + in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&s[i][n+24])); + } + _mm256_store_ps(&d[n+ 0], _mm256_mul_ps(g, in[0])); + _mm256_store_ps(&d[n+ 8], _mm256_mul_ps(g, in[1])); + _mm256_store_ps(&d[n+16], _mm256_mul_ps(g, in[2])); + _mm256_store_ps(&d[n+24], _mm256_mul_ps(g, in[3])); + } + for (; n < n_samples; n++) { + __m128 in[1]; + in[0] = _mm_load_ss(&s[0][n]); + for (i = 1; i < n_src; i++) + in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n])); + _mm_store_ss(&d[n], _mm_mul_ss(g1, in[0])); + } +} + +static void dsp_add_n_gain_avx(void *obj, float *dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m256 in[4], g; + const float **s = (const float **)src; + float *d = dst; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 32))) { + unrolled = n_samples & ~31; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 32))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + for (n = 0; n < unrolled; n += 32) { + g = _mm256_set1_ps(gain[0]); + in[0] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+ 0])); + in[1] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+ 8])); + in[2] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+16])); + in[3] = _mm256_mul_ps(g, _mm256_load_ps(&s[0][n+24])); + + for (i = 1; i < n_src; i++) { + g = _mm256_set1_ps(gain[i]); + in[0] = _mm256_add_ps(in[0], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 0]))); + in[1] = _mm256_add_ps(in[1], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 8]))); + in[2] = _mm256_add_ps(in[2], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+16]))); + in[3] = _mm256_add_ps(in[3], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+24]))); + } + _mm256_store_ps(&d[n+ 0], in[0]); + _mm256_store_ps(&d[n+ 8], in[1]); + _mm256_store_ps(&d[n+16], in[2]); + _mm256_store_ps(&d[n+24], in[3]); + } + for (; n < n_samples; n++) { + __m128 in[1], g; + g = _mm_set_ss(gain[0]); + in[0] = _mm_mul_ss(g, _mm_load_ss(&s[0][n])); + for (i = 1; i < n_src; i++) { + g = _mm_set_ss(gain[i]); + in[0] = _mm_add_ss(in[0], _mm_mul_ss(g, _mm_load_ss(&s[i][n]))); + } + _mm_store_ss(&d[n], in[0]); + } +} + + +void dsp_mix_gain_avx(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + if (n_src == 0) { + memset(dst, 0, n_samples * sizeof(float)); + } else if (n_src == 1 && gain[0] == 1.0f) { + if (dst != src[0]) + spa_memcpy(dst, src[0], n_samples * sizeof(float)); + } else { + if (n_gain == 0) + dsp_add_avx(obj, dst, src, n_src, n_samples); + else if (n_gain < n_src) + dsp_add_1_gain_avx(obj, dst, src, n_src, gain[0], n_samples); + else + dsp_add_n_gain_avx(obj, dst, src, n_src, gain, n_gain, n_samples); + } +} + +void dsp_sum_avx(void *obj, float *r, const float *a, const float *b, uint32_t n_samples) +{ + uint32_t n, unrolled; + __m256 in[4]; + + unrolled = n_samples & ~31; + + if (SPA_LIKELY(SPA_IS_ALIGNED(r, 32)) && + SPA_LIKELY(SPA_IS_ALIGNED(a, 32)) && + SPA_LIKELY(SPA_IS_ALIGNED(b, 32))) { + for (n = 0; n < unrolled; n += 32) { + in[0] = _mm256_load_ps(&a[n+ 0]); + in[1] = _mm256_load_ps(&a[n+ 8]); + in[2] = _mm256_load_ps(&a[n+16]); + in[3] = _mm256_load_ps(&a[n+24]); + + in[0] = _mm256_add_ps(in[0], _mm256_load_ps(&b[n+ 0])); + in[1] = _mm256_add_ps(in[1], _mm256_load_ps(&b[n+ 8])); + in[2] = _mm256_add_ps(in[2], _mm256_load_ps(&b[n+16])); + in[3] = _mm256_add_ps(in[3], _mm256_load_ps(&b[n+24])); + + _mm256_store_ps(&r[n+ 0], in[0]); + _mm256_store_ps(&r[n+ 8], in[1]); + _mm256_store_ps(&r[n+16], in[2]); + _mm256_store_ps(&r[n+24], in[3]); + } + } else { + for (n = 0; n < unrolled; n += 32) { + in[0] = _mm256_loadu_ps(&a[n+ 0]); + in[1] = _mm256_loadu_ps(&a[n+ 8]); + in[2] = _mm256_loadu_ps(&a[n+16]); + in[3] = _mm256_loadu_ps(&a[n+24]); + + in[0] = _mm256_add_ps(in[0], _mm256_loadu_ps(&b[n+ 0])); + in[1] = _mm256_add_ps(in[1], _mm256_loadu_ps(&b[n+ 8])); + in[2] = _mm256_add_ps(in[2], _mm256_loadu_ps(&b[n+16])); + in[3] = _mm256_add_ps(in[3], _mm256_loadu_ps(&b[n+24])); + + _mm256_storeu_ps(&r[n+ 0], in[0]); + _mm256_storeu_ps(&r[n+ 8], in[1]); + _mm256_storeu_ps(&r[n+16], in[2]); + _mm256_storeu_ps(&r[n+24], in[3]); + } + } + for (; n < n_samples; n++) { + __m128 in[1]; + in[0] = _mm_load_ss(&a[n]); + in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n])); + _mm_store_ss(&r[n], in[0]); + } +} + +inline static __m256 _mm256_mul_pz(__m256 ab, __m256 cd) +{ + __m256 aa, bb, dc, x0, x1; + aa = _mm256_moveldup_ps(ab); + bb = _mm256_movehdup_ps(ab); + x0 = _mm256_mul_ps(aa, cd); + dc = _mm256_shuffle_ps(cd, cd, _MM_SHUFFLE(2,3,0,1)); + x1 = _mm256_mul_ps(bb, dc); + return _mm256_addsub_ps(x0, x1); +} + +void dsp_fft_cmul_avx(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + __m256 s = _mm256_set1_ps(scale); + __m256 aa[2], bb[2], dd[2]; + uint32_t i, unrolled; + + if (SPA_IS_ALIGNED(a, 32) && + SPA_IS_ALIGNED(b, 32) && + SPA_IS_ALIGNED(dst, 32)) + unrolled = len & ~7; + else + unrolled = 0; + + for (i = 0; i < unrolled; i+=8) { + aa[0] = _mm256_load_ps(&a[2*i]); /* ar0 ai0 ar1 ai1 */ + aa[1] = _mm256_load_ps(&a[2*i+8]); /* ar1 ai1 ar2 ai2 */ + bb[0] = _mm256_load_ps(&b[2*i]); /* br0 bi0 br1 bi1 */ + bb[1] = _mm256_load_ps(&b[2*i+8]); /* br2 bi2 br3 bi3 */ + dd[0] = _mm256_mul_pz(aa[0], bb[0]); + dd[1] = _mm256_mul_pz(aa[1], bb[1]); + dd[0] = _mm256_mul_ps(dd[0], s); + dd[1] = _mm256_mul_ps(dd[1], s); + _mm256_store_ps(&dst[2*i], dd[0]); + _mm256_store_ps(&dst[2*i+8], dd[1]); + } + for (; i < len; i++) { + dst[2*i ] = (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve(fft, a, b, dst, scale); +#endif +} + +void dsp_fft_cmuladd_avx(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT src, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, + uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + __m256 s = _mm256_set1_ps(scale); + __m256 aa[2], bb[2], dd[2], t[2]; + uint32_t i, unrolled; + + if (SPA_IS_ALIGNED(a, 32) && + SPA_IS_ALIGNED(b, 32) && + SPA_IS_ALIGNED(src, 32) && + SPA_IS_ALIGNED(dst, 32)) + unrolled = len & ~7; + else + unrolled = 0; + + for (i = 0; i < unrolled; i+=8) { + aa[0] = _mm256_load_ps(&a[2*i]); /* ar0 ai0 ar1 ai1 */ + aa[1] = _mm256_load_ps(&a[2*i+8]); /* ar1 ai1 ar2 ai2 */ + bb[0] = _mm256_load_ps(&b[2*i]); /* br0 bi0 br1 bi1 */ + bb[1] = _mm256_load_ps(&b[2*i+8]); /* br2 bi2 br3 bi3 */ + dd[0] = _mm256_mul_pz(aa[0], bb[0]); + dd[1] = _mm256_mul_pz(aa[1], bb[1]); + dd[0] = _mm256_mul_ps(dd[0], s); + dd[1] = _mm256_mul_ps(dd[1], s); + t[0] = _mm256_load_ps(&src[2*i]); + t[1] = _mm256_load_ps(&src[2*i+8]); + t[0] = _mm256_add_ps(t[0], dd[0]); + t[1] = _mm256_add_ps(t[1], dd[1]); + _mm256_store_ps(&dst[2*i], t[0]); + _mm256_store_ps(&dst[2*i+8], t[1]); + } + for (; i < len; i++) { + dst[2*i ] = src[2*i ] + (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve_accumulate(fft, a, b, src, dst, scale); +#endif +} diff --git a/spa/plugins/filter-graph/audio-dsp-c.c b/spa/plugins/filter-graph/audio-dsp-c.c new file mode 100644 index 0000000..f2a509a --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp-c.c @@ -0,0 +1,345 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include "config.h" +#ifdef HAVE_FFTW +#include +#else +#include "pffft.h" +#endif +#include "audio-dsp-impl.h" + +void dsp_clear_c(void *obj, float * SPA_RESTRICT dst, uint32_t n_samples) +{ + memset(dst, 0, sizeof(float) * n_samples); +} + +void dsp_copy_c(void *obj, float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, uint32_t n_samples) +{ + if (dst != src) + spa_memcpy(dst, src, sizeof(float) * n_samples); +} + +static inline void dsp_add_c(void *obj, float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, uint32_t n_samples) +{ + uint32_t i; + const float *s = src; + float *d = dst; + for (i = 0; i < n_samples; i++) + d[i] += s[i]; +} + +static inline void dsp_gain_c(void *obj, float * dst, + const float * src, float gain, uint32_t n_samples) +{ + uint32_t i; + const float *s = src; + float *d = dst; + if (gain == 0.0f) + dsp_clear_c(obj, dst, n_samples); + else if (gain == 1.0f) + dsp_copy_c(obj, dst, src, n_samples); + else { + for (i = 0; i < n_samples; i++) + d[i] = s[i] * gain; + } +} + +static inline void dsp_gain_add_c(void *obj, float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, float gain, uint32_t n_samples) +{ + uint32_t i; + const float *s = src; + float *d = dst; + + if (gain == 0.0f) + return; + else if (gain == 1.0f) + dsp_add_c(obj, dst, src, n_samples); + else { + for (i = 0; i < n_samples; i++) + d[i] += s[i] * gain; + } +} + + +void dsp_mix_gain_c(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + uint32_t i; + if (n_src == 0) { + dsp_clear_c(obj, dst, n_samples); + } else { + if (n_gain < n_src) { + dsp_copy_c(obj, dst, src[0], n_samples); + for (i = 1; i < n_src; i++) + dsp_add_c(obj, dst, src[i], n_samples); + if (n_gain > 0) + dsp_gain_c(obj, dst, dst, gain[0], n_samples); + } else { + dsp_gain_c(obj, dst, src[0], gain[0], n_samples); + for (i = 1; i < n_src; i++) + dsp_gain_add_c(obj, dst, src[i], gain[i], n_samples); + } + } +} + +static inline void dsp_mult1_c(void *obj, float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, uint32_t n_samples) +{ + uint32_t i; + const float *s = src; + float *d = dst; + for (i = 0; i < n_samples; i++) + d[i] *= s[i]; +} + +void dsp_mult_c(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], + uint32_t n_src, uint32_t n_samples) +{ + uint32_t i; + if (n_src == 0) { + dsp_clear_c(obj, dst, n_samples); + } else { + dsp_copy_c(obj, dst, src[0], n_samples); + for (i = 1; i < n_src; i++) + dsp_mult1_c(obj, dst, src[i], n_samples); + } +} + +static void biquad_run_c(void *obj, struct biquad *bq, + float *out, const float *in, uint32_t n_samples) +{ + float x, y, x1, x2; + float b0, b1, b2, a1, a2; + uint32_t i; + + if (bq->type == BQ_NONE) { + dsp_copy_c(obj, out, in, n_samples); + return; + } + + x1 = bq->x1; + x2 = bq->x2; + b0 = bq->b0; + b1 = bq->b1; + b2 = bq->b2; + a1 = bq->a1; + a2 = bq->a2; + for (i = 0; i < n_samples; i++) { + x = in[i]; + y = b0 * x + x1; + x1 = b1 * x - a1 * y + x2; + x2 = b2 * x - a2 * y; + out[i] = y; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq->x1 = F(x1); + bq->x2 = F(x2); +#undef F +} + +void dsp_biquad_run_c(void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, + float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], + uint32_t n_src, uint32_t n_samples) +{ + uint32_t i, j; + const float *s; + float *d; + for (i = 0; i < n_src; i++, bq+=bq_stride) { + s = in[i]; + d = out[i]; + if (s == NULL || d == NULL) + continue; + if (n_bq > 0) + biquad_run_c(obj, &bq[0], d, s, n_samples); + for (j = 1; j < n_bq; j++) + biquad_run_c(obj, &bq[j], d, d, n_samples); + } +} + +void dsp_sum_c(void *obj, float * dst, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples) +{ + uint32_t i; + for (i = 0; i < n_samples; i++) + dst[i] = a[i] + b[i]; +} + +void dsp_linear_c(void *obj, float * dst, + const float * SPA_RESTRICT src, const float mult, + const float add, uint32_t n_samples) +{ + uint32_t i; + if (add == 0.0f) { + dsp_gain_c(obj, dst, src, mult, n_samples); + } else { + if (mult == 0.0f) { + for (i = 0; i < n_samples; i++) + dst[i] = add; + } else if (mult == 1.0f) { + for (i = 0; i < n_samples; i++) + dst[i] = src[i] + add; + } else { + for (i = 0; i < n_samples; i++) + dst[i] = mult * src[i] + add; + } + } +} + + +void dsp_delay_c(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, + uint32_t delay, float *dst, const float *src, uint32_t n_samples) +{ + if (delay == 0) { + dsp_copy_c(obj, dst, src, n_samples); + } else { + uint32_t w, o, i; + + w = *pos; + o = n_buffer - delay; + + for (i = 0; i < n_samples; i++) { + buffer[w] = buffer[w + n_buffer] = src[i]; + dst[i] = buffer[w + o]; + w = w + 1 > n_buffer ? 0 : w + 1; + } + *pos = w; + } +} + +#ifdef HAVE_FFTW +struct fft_info { + fftwf_plan plan_r2c; + fftwf_plan plan_c2r; +}; +#endif + +void *dsp_fft_new_c(void *obj, uint32_t size, bool real) +{ +#ifdef HAVE_FFTW + struct fft_info *info = calloc(1, sizeof(struct fft_info)); + float *rdata; + fftwf_complex *cdata; + + if (info == NULL) + return NULL; + + rdata = fftwf_alloc_real(size * 2); + cdata = fftwf_alloc_complex(size + 1); + + info->plan_r2c = fftwf_plan_dft_r2c_1d(size, rdata, cdata, FFTW_ESTIMATE); + info->plan_c2r = fftwf_plan_dft_c2r_1d(size, cdata, rdata, FFTW_ESTIMATE); + + fftwf_free(rdata); + fftwf_free(cdata); + + return info; +#else + return pffft_new_setup(size, real ? PFFFT_REAL : PFFFT_COMPLEX); +#endif +} + +void dsp_fft_free_c(void *obj, void *fft) +{ +#ifdef HAVE_FFTW + struct fft_info *info = fft; + fftwf_destroy_plan(info->plan_r2c); + fftwf_destroy_plan(info->plan_c2r); + free(info); +#else + pffft_destroy_setup(fft); +#endif +} + +void *dsp_fft_memalloc_c(void *obj, uint32_t size, bool real) +{ +#ifdef HAVE_FFTW + if (real) + return fftwf_alloc_real(size); + else + return fftwf_alloc_complex(size); +#else + if (real) + return pffft_aligned_malloc(size * sizeof(float)); + else + return pffft_aligned_malloc(size * 2 * sizeof(float)); +#endif +} + +void dsp_fft_memfree_c(void *obj, void *data) +{ +#ifdef HAVE_FFTW + fftwf_free(data); +#else + pffft_aligned_free(data); +#endif +} + +void dsp_fft_memclear_c(void *obj, void *data, uint32_t size, bool real) +{ +#ifdef HAVE_FFTW + spa_fga_dsp_clear(obj, data, real ? size : size * 2); +#else + spa_fga_dsp_clear(obj, data, real ? size : size * 2); +#endif +} + +void dsp_fft_run_c(void *obj, void *fft, int direction, + const float * SPA_RESTRICT src, float * SPA_RESTRICT dst) +{ +#ifdef HAVE_FFTW + struct fft_info *info = fft; + if (direction > 0) + fftwf_execute_dft_r2c (info->plan_r2c, (float*)src, (fftwf_complex*)dst); + else + fftwf_execute_dft_c2r (info->plan_c2r, (fftwf_complex*)src, dst); +#else + pffft_transform(fft, src, dst, NULL, direction < 0 ? PFFFT_BACKWARD : PFFFT_FORWARD); +#endif +} + +void dsp_fft_cmul_c(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + for (uint32_t i = 0; i < len; i++) { + dst[2*i ] = (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve(fft, a, b, dst, scale); +#endif +} + +void dsp_fft_cmuladd_c(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT src, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, + uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + for (uint32_t i = 0; i < len; i++) { + dst[2*i ] = src[2*i ] + (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve_accumulate(fft, a, b, src, dst, scale); +#endif +} + diff --git a/spa/plugins/filter-graph/audio-dsp-impl.h b/spa/plugins/filter-graph/audio-dsp-impl.h new file mode 100644 index 0000000..388a745 --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp-impl.h @@ -0,0 +1,94 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef DSP_IMPL_H +#define DSP_IMPL_H + +#include "audio-dsp.h" + +struct spa_fga_dsp * spa_fga_dsp_new(uint32_t cpu_flags); +void spa_fga_dsp_free(struct spa_fga_dsp *dsp); + +#define MAKE_CLEAR_FUNC(arch) \ +void dsp_clear_##arch(void *obj, float * SPA_RESTRICT dst, uint32_t n_samples) +#define MAKE_COPY_FUNC(arch) \ +void dsp_copy_##arch(void *obj, float * SPA_RESTRICT dst, \ + const float * SPA_RESTRICT src, uint32_t n_samples) +#define MAKE_MIX_GAIN_FUNC(arch) \ +void dsp_mix_gain_##arch(void *obj, float * SPA_RESTRICT dst, \ + const float * SPA_RESTRICT src[], uint32_t n_src, float gain[], uint32_t n_gain, uint32_t n_samples) +#define MAKE_SUM_FUNC(arch) \ +void dsp_sum_##arch (void *obj, float * SPA_RESTRICT dst, \ + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, uint32_t n_samples) +#define MAKE_LINEAR_FUNC(arch) \ +void dsp_linear_##arch (void *obj, float * SPA_RESTRICT dst, \ + const float * SPA_RESTRICT src, const float mult, const float add, uint32_t n_samples) +#define MAKE_MULT_FUNC(arch) \ +void dsp_mult_##arch(void *obj, float * SPA_RESTRICT dst, \ + const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples) +#define MAKE_BIQUAD_RUN_FUNC(arch) \ +void dsp_biquad_run_##arch (void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, \ + float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], uint32_t n_src, uint32_t n_samples) +#define MAKE_DELAY_FUNC(arch) \ +void dsp_delay_##arch (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, \ + uint32_t delay, float *dst, const float *src, uint32_t n_samples) + +#define MAKE_FFT_NEW_FUNC(arch) \ +void *dsp_fft_new_##arch(void *obj, uint32_t size, bool real) +#define MAKE_FFT_FREE_FUNC(arch) \ +void dsp_fft_free_##arch(void *obj, void *fft) +#define MAKE_FFT_MEMALLOC_FUNC(arch) \ +void *dsp_fft_memalloc_##arch(void *obj, uint32_t size, bool real) +#define MAKE_FFT_MEMFREE_FUNC(arch) \ +void dsp_fft_memfree_##arch(void *obj, void *mem) +#define MAKE_FFT_MEMCLEAR_FUNC(arch) \ +void dsp_fft_memclear_##arch(void *obj, void *mem, uint32_t size, bool real) +#define MAKE_FFT_RUN_FUNC(arch) \ +void dsp_fft_run_##arch(void *obj, void *fft, int direction, \ + const float * SPA_RESTRICT src, float * SPA_RESTRICT dst) +#define MAKE_FFT_CMUL_FUNC(arch) \ +void dsp_fft_cmul_##arch(void *obj, void *fft, \ + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, \ + const float * SPA_RESTRICT b, uint32_t len, const float scale) +#define MAKE_FFT_CMULADD_FUNC(arch) \ +void dsp_fft_cmuladd_##arch(void *obj, void *fft, \ + float * dst, const float * src, \ + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, \ + uint32_t len, const float scale) + + +MAKE_CLEAR_FUNC(c); +MAKE_COPY_FUNC(c); +MAKE_MIX_GAIN_FUNC(c); +MAKE_SUM_FUNC(c); +MAKE_LINEAR_FUNC(c); +MAKE_MULT_FUNC(c); +MAKE_BIQUAD_RUN_FUNC(c); +MAKE_DELAY_FUNC(c); + +MAKE_FFT_NEW_FUNC(c); +MAKE_FFT_FREE_FUNC(c); +MAKE_FFT_MEMALLOC_FUNC(c); +MAKE_FFT_MEMFREE_FUNC(c); +MAKE_FFT_MEMCLEAR_FUNC(c); +MAKE_FFT_RUN_FUNC(c); +MAKE_FFT_CMUL_FUNC(c); +MAKE_FFT_CMULADD_FUNC(c); + +#if defined (HAVE_SSE) +MAKE_MIX_GAIN_FUNC(sse); +MAKE_SUM_FUNC(sse); +MAKE_BIQUAD_RUN_FUNC(sse); +MAKE_DELAY_FUNC(sse); +MAKE_FFT_CMUL_FUNC(sse); +MAKE_FFT_CMULADD_FUNC(sse); +#endif +#if defined (HAVE_AVX) +MAKE_MIX_GAIN_FUNC(avx); +MAKE_SUM_FUNC(avx); +MAKE_FFT_CMUL_FUNC(avx); +MAKE_FFT_CMULADD_FUNC(avx); +#endif + +#endif /* DSP_OPS_IMPL_H */ diff --git a/spa/plugins/filter-graph/audio-dsp-sse.c b/spa/plugins/filter-graph/audio-dsp-sse.c new file mode 100644 index 0000000..8c2ffa8 --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp-sse.c @@ -0,0 +1,744 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include "config.h" +#ifndef HAVE_FFTW +#include "pffft.h" +#endif + +#include "audio-dsp-impl.h" + +#include + +static void dsp_add_sse(void *obj, float *dst, const float * SPA_RESTRICT src[], + uint32_t n_src, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m128 in[4]; + const float **s = (const float **)src; + float *d = dst; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) { + unrolled = n_samples & ~15; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + for (n = 0; n < unrolled; n += 16) { + in[0] = _mm_load_ps(&s[0][n+ 0]); + in[1] = _mm_load_ps(&s[0][n+ 4]); + in[2] = _mm_load_ps(&s[0][n+ 8]); + in[3] = _mm_load_ps(&s[0][n+12]); + + for (i = 1; i < n_src; i++) { + in[0] = _mm_add_ps(in[0], _mm_load_ps(&s[i][n+ 0])); + in[1] = _mm_add_ps(in[1], _mm_load_ps(&s[i][n+ 4])); + in[2] = _mm_add_ps(in[2], _mm_load_ps(&s[i][n+ 8])); + in[3] = _mm_add_ps(in[3], _mm_load_ps(&s[i][n+12])); + } + _mm_store_ps(&d[n+ 0], in[0]); + _mm_store_ps(&d[n+ 4], in[1]); + _mm_store_ps(&d[n+ 8], in[2]); + _mm_store_ps(&d[n+12], in[3]); + } + for (; n < n_samples; n++) { + in[0] = _mm_load_ss(&s[0][n]); + for (i = 1; i < n_src; i++) + in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n])); + _mm_store_ss(&d[n], in[0]); + } +} + +static void dsp_add_1_gain_sse(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m128 in[4], g; + const float **s = (const float **)src; + float *d = dst; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) { + unrolled = n_samples & ~15; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + g = _mm_set1_ps(gain); + + for (n = 0; n < unrolled; n += 16) { + in[0] = _mm_load_ps(&s[0][n+ 0]); + in[1] = _mm_load_ps(&s[0][n+ 4]); + in[2] = _mm_load_ps(&s[0][n+ 8]); + in[3] = _mm_load_ps(&s[0][n+12]); + + for (i = 1; i < n_src; i++) { + in[0] = _mm_add_ps(in[0], _mm_load_ps(&s[i][n+ 0])); + in[1] = _mm_add_ps(in[1], _mm_load_ps(&s[i][n+ 4])); + in[2] = _mm_add_ps(in[2], _mm_load_ps(&s[i][n+ 8])); + in[3] = _mm_add_ps(in[3], _mm_load_ps(&s[i][n+12])); + } + _mm_store_ps(&d[n+ 0], _mm_mul_ps(in[0], g)); + _mm_store_ps(&d[n+ 4], _mm_mul_ps(in[1], g)); + _mm_store_ps(&d[n+ 8], _mm_mul_ps(in[2], g)); + _mm_store_ps(&d[n+12], _mm_mul_ps(in[3], g)); + } + for (; n < n_samples; n++) { + in[0] = _mm_load_ss(&s[0][n]); + for (i = 1; i < n_src; i++) + in[0] = _mm_add_ss(in[0], _mm_load_ss(&s[i][n])); + _mm_store_ss(&d[n], _mm_mul_ss(in[0], g)); + } +} + +static void dsp_add_n_gain_sse(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + uint32_t n, i, unrolled; + __m128 in[4], g; + const float **s = (const float **)src; + float *d = dst; + + if (SPA_LIKELY(SPA_IS_ALIGNED(dst, 16))) { + unrolled = n_samples & ~15; + for (i = 0; i < n_src; i++) { + if (SPA_UNLIKELY(!SPA_IS_ALIGNED(src[i], 16))) { + unrolled = 0; + break; + } + } + } else + unrolled = 0; + + for (n = 0; n < unrolled; n += 16) { + g = _mm_set1_ps(gain[0]); + in[0] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 0])); + in[1] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 4])); + in[2] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+ 8])); + in[3] = _mm_mul_ps(g, _mm_load_ps(&s[0][n+12])); + + for (i = 1; i < n_src; i++) { + g = _mm_set1_ps(gain[i]); + in[0] = _mm_add_ps(in[0], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 0]))); + in[1] = _mm_add_ps(in[1], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 4]))); + in[2] = _mm_add_ps(in[2], _mm_mul_ps(g, _mm_load_ps(&s[i][n+ 8]))); + in[3] = _mm_add_ps(in[3], _mm_mul_ps(g, _mm_load_ps(&s[i][n+12]))); + } + _mm_store_ps(&d[n+ 0], in[0]); + _mm_store_ps(&d[n+ 4], in[1]); + _mm_store_ps(&d[n+ 8], in[2]); + _mm_store_ps(&d[n+12], in[3]); + } + for (; n < n_samples; n++) { + g = _mm_set_ss(gain[0]); + in[0] = _mm_mul_ss(g, _mm_load_ss(&s[0][n])); + for (i = 1; i < n_src; i++) { + g = _mm_set_ss(gain[i]); + in[0] = _mm_add_ss(in[0], _mm_mul_ss(g, _mm_load_ss(&s[i][n]))); + } + _mm_store_ss(&d[n], in[0]); + } +} + +void dsp_mix_gain_sse(void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + if (n_src == 0) { + memset(dst, 0, n_samples * sizeof(float)); + } else if (n_src == 1 && gain[0] == 1.0f) { + if (dst != src[0]) + spa_memcpy(dst, src[0], n_samples * sizeof(float)); + } else { + if (n_gain == 0) + dsp_add_sse(obj, dst, src, n_src, n_samples); + else if (n_gain < n_src) + dsp_add_1_gain_sse(obj, dst, src, n_src, gain[0], n_samples); + else + dsp_add_n_gain_sse(obj, dst, src, n_src, gain, n_gain, n_samples); + } +} + +void dsp_sum_sse(void *obj, float *r, const float *a, const float *b, uint32_t n_samples) +{ + uint32_t n, unrolled; + __m128 in[4]; + + unrolled = n_samples & ~15; + + if (SPA_LIKELY(SPA_IS_ALIGNED(r, 16)) && + SPA_LIKELY(SPA_IS_ALIGNED(a, 16)) && + SPA_LIKELY(SPA_IS_ALIGNED(b, 16))) { + for (n = 0; n < unrolled; n += 16) { + in[0] = _mm_load_ps(&a[n+ 0]); + in[1] = _mm_load_ps(&a[n+ 4]); + in[2] = _mm_load_ps(&a[n+ 8]); + in[3] = _mm_load_ps(&a[n+12]); + + in[0] = _mm_add_ps(in[0], _mm_load_ps(&b[n+ 0])); + in[1] = _mm_add_ps(in[1], _mm_load_ps(&b[n+ 4])); + in[2] = _mm_add_ps(in[2], _mm_load_ps(&b[n+ 8])); + in[3] = _mm_add_ps(in[3], _mm_load_ps(&b[n+12])); + + _mm_store_ps(&r[n+ 0], in[0]); + _mm_store_ps(&r[n+ 4], in[1]); + _mm_store_ps(&r[n+ 8], in[2]); + _mm_store_ps(&r[n+12], in[3]); + } + } else { + for (n = 0; n < unrolled; n += 16) { + in[0] = _mm_loadu_ps(&a[n+ 0]); + in[1] = _mm_loadu_ps(&a[n+ 4]); + in[2] = _mm_loadu_ps(&a[n+ 8]); + in[3] = _mm_loadu_ps(&a[n+12]); + + in[0] = _mm_add_ps(in[0], _mm_loadu_ps(&b[n+ 0])); + in[1] = _mm_add_ps(in[1], _mm_loadu_ps(&b[n+ 4])); + in[2] = _mm_add_ps(in[2], _mm_loadu_ps(&b[n+ 8])); + in[3] = _mm_add_ps(in[3], _mm_loadu_ps(&b[n+12])); + + _mm_storeu_ps(&r[n+ 0], in[0]); + _mm_storeu_ps(&r[n+ 4], in[1]); + _mm_storeu_ps(&r[n+ 8], in[2]); + _mm_storeu_ps(&r[n+12], in[3]); + } + } + for (; n < n_samples; n++) { + in[0] = _mm_load_ss(&a[n]); + in[0] = _mm_add_ss(in[0], _mm_load_ss(&b[n])); + _mm_store_ss(&r[n], in[0]); + } +} + +static void dsp_biquad_run1_sse(void *obj, struct biquad *bq, + float *out, const float *in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b012; + __m128 a12; + __m128 x12; + uint32_t i; + + b012 = _mm_setr_ps(bq->b0, bq->b1, bq->b2, 0.0f); /* b0 b1 b2 0 */ + a12 = _mm_setr_ps(0.0f, bq->a1, bq->a2, 0.0f); /* 0 a1 a2 0 */ + x12 = _mm_setr_ps(bq->x1, bq->x2, 0.0f, 0.0f); /* x1 x2 0 0 */ + + for (i = 0; i < n_samples; i++) { + x = _mm_load1_ps(&in[i]); /* x x x x */ + z = _mm_mul_ps(x, b012); /* b0*x b1*x b2*x 0 */ + z = _mm_add_ps(z, x12); /* b0*x+x1 b1*x+x2 b2*x 0 */ + _mm_store_ss(&out[i], z); /* out[i] = b0*x+x1 */ + y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0)); /* b0*x+x1 b0*x+x1 b0*x+x1 b0*x+x1 = y*/ + y = _mm_mul_ps(y, a12); /* 0 a1*y a2*y 0 */ + y = _mm_sub_ps(z, y); /* y x1 x2 0 */ + x12 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1)); /* x1 x2 0 0*/ + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq->x1 = F(x12[0]); + bq->x2 = F(x12[1]); +#undef F +} + +static void dsp_biquad2_run_sse(void *obj, struct biquad *bq, + float *out, const float *in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b0, b1; + __m128 a0, a1; + __m128 x0, x1; + uint32_t i; + + b0 = _mm_setr_ps(bq[0].b0, bq[0].b1, bq[0].b2, 0.0f); /* b0 b1 b2 0 */ + a0 = _mm_setr_ps(0.0f, bq[0].a1, bq[0].a2, 0.0f); /* 0 a1 a2 0 */ + x0 = _mm_setr_ps(bq[0].x1, bq[0].x2, 0.0f, 0.0f); /* x1 x2 0 0 */ + + b1 = _mm_setr_ps(bq[1].b0, bq[1].b1, bq[1].b2, 0.0f); /* b0 b1 b2 0 */ + a1 = _mm_setr_ps(0.0f, bq[1].a1, bq[1].a2, 0.0f); /* 0 a1 a2 0 */ + x1 = _mm_setr_ps(bq[1].x1, bq[1].x2, 0.0f, 0.0f); /* x1 x2 0 0 */ + + for (i = 0; i < n_samples; i++) { + x = _mm_load1_ps(&in[i]); /* x x x x */ + + z = _mm_mul_ps(x, b0); /* b0*x b1*x b2*x 0 */ + z = _mm_add_ps(z, x0); /* b0*x+x1 b1*x+x2 b2*x 0 */ + y = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0)); /* b0*x+x1 b0*x+x1 b0*x+x1 b0*x+x1 = y*/ + x = _mm_mul_ps(y, a0); /* 0 a1*y a2*y 0 */ + x = _mm_sub_ps(z, x); /* y x1 x2 0 */ + x0 = _mm_shuffle_ps(x, x, _MM_SHUFFLE(3,3,2,1)); /* x1 x2 0 0*/ + + z = _mm_mul_ps(y, b1); /* b0*x b1*x b2*x 0 */ + z = _mm_add_ps(z, x1); /* b0*x+x1 b1*x+x2 b2*x 0 */ + x = _mm_shuffle_ps(z, z, _MM_SHUFFLE(0,0,0,0)); /* b0*x+x1 b0*x+x1 b0*x+x1 b0*x+x1 = y*/ + y = _mm_mul_ps(x, a1); /* 0 a1*y a2*y 0 */ + y = _mm_sub_ps(z, y); /* y x1 x2 0 */ + x1 = _mm_shuffle_ps(y, y, _MM_SHUFFLE(3,3,2,1)); /* x1 x2 0 0*/ + + _mm_store_ss(&out[i], x); /* out[i] = b0*x+x1 */ + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq[0].x1 = F(x0[0]); + bq[0].x2 = F(x0[1]); + bq[1].x1 = F(x1[0]); + bq[1].x2 = F(x1[1]); +#undef F +} + +static void dsp_biquad_run2_sse(void *obj, struct biquad *bq, uint32_t bq_stride, + float **out, const float **in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b0, b1, b2; + __m128 a1, a2; + __m128 x1, x2; + uint32_t i; + + b0 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, 0.0f, 0.0f); /* b00 b10 0 0 */ + b1 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, 0.0f, 0.0f); /* b01 b11 0 0 */ + b2 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, 0.0f, 0.0f); /* b02 b12 0 0 */ + a1 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, 0.0f, 0.0f); /* b00 b10 0 0 */ + a2 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, 0.0f, 0.0f); /* b01 b11 0 0 */ + x1 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, 0.0f, 0.0f); /* b00 b10 0 0 */ + x2 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, 0.0f, 0.0f); /* b01 b11 0 0 */ + + for (i = 0; i < n_samples; i++) { + x = _mm_setr_ps(in[0][i], in[1][i], 0.0f, 0.0f); + + y = _mm_mul_ps(x, b0); /* y = x * b0 */ + y = _mm_add_ps(y, x1); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a1); /* z = a1 * y */ + x1 = _mm_mul_ps(x, b1); /* x1 = x * b1 */ + x1 = _mm_add_ps(x1, x2); /* x1 = x * b1 + x2*/ + x1 = _mm_sub_ps(x1, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a2); /* z = a2 * y */ + x2 = _mm_mul_ps(x, b2); /* x2 = x * b2 */ + x2 = _mm_sub_ps(x2, z); /* x2 = x * b2 - a2 * y*/ + + out[0][i] = y[0]; + out[1][i] = y[1]; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq[0*bq_stride].x1 = F(x1[0]); + bq[0*bq_stride].x2 = F(x2[0]); + bq[1*bq_stride].x1 = F(x1[1]); + bq[1*bq_stride].x2 = F(x2[1]); +#undef F +} + + +static void dsp_biquad2_run2_sse(void *obj, struct biquad *bq, uint32_t bq_stride, + float **out, const float **in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b00, b01, b02, b10, b11, b12; + __m128 a01, a02, a11, a12; + __m128 x01, x02, x11, x12; + uint32_t i; + + b00 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, 0.0f, 0.0f); /* b00 b10 0 0 */ + b01 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, 0.0f, 0.0f); /* b01 b11 0 0 */ + b02 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, 0.0f, 0.0f); /* b02 b12 0 0 */ + a01 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, 0.0f, 0.0f); /* b00 b10 0 0 */ + a02 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, 0.0f, 0.0f); /* b01 b11 0 0 */ + x01 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, 0.0f, 0.0f); /* b00 b10 0 0 */ + x02 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, 0.0f, 0.0f); /* b01 b11 0 0 */ + + b10 = _mm_setr_ps(bq[1].b0, bq[bq_stride+1].b0, 0.0f, 0.0f); /* b00 b10 0 0 */ + b11 = _mm_setr_ps(bq[1].b1, bq[bq_stride+1].b1, 0.0f, 0.0f); /* b01 b11 0 0 */ + b12 = _mm_setr_ps(bq[1].b2, bq[bq_stride+1].b2, 0.0f, 0.0f); /* b02 b12 0 0 */ + a11 = _mm_setr_ps(bq[1].a1, bq[bq_stride+1].a1, 0.0f, 0.0f); /* b00 b10 0 0 */ + a12 = _mm_setr_ps(bq[1].a2, bq[bq_stride+1].a2, 0.0f, 0.0f); /* b01 b11 0 0 */ + x11 = _mm_setr_ps(bq[1].x1, bq[bq_stride+1].x1, 0.0f, 0.0f); /* b00 b10 0 0 */ + x12 = _mm_setr_ps(bq[1].x2, bq[bq_stride+1].x2, 0.0f, 0.0f); /* b01 b11 0 0 */ + + for (i = 0; i < n_samples; i++) { + x = _mm_setr_ps(in[0][i], in[1][i], 0.0f, 0.0f); + + y = _mm_mul_ps(x, b00); /* y = x * b0 */ + y = _mm_add_ps(y, x01); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a01); /* z = a1 * y */ + x01 = _mm_mul_ps(x, b01); /* x1 = x * b1 */ + x01 = _mm_add_ps(x01, x02); /* x1 = x * b1 + x2*/ + x01 = _mm_sub_ps(x01, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a02); /* z = a2 * y */ + x02 = _mm_mul_ps(x, b02); /* x2 = x * b2 */ + x02 = _mm_sub_ps(x02, z); /* x2 = x * b2 - a2 * y*/ + + x = y; + + y = _mm_mul_ps(x, b10); /* y = x * b0 */ + y = _mm_add_ps(y, x11); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a11); /* z = a1 * y */ + x11 = _mm_mul_ps(x, b11); /* x1 = x * b1 */ + x11 = _mm_add_ps(x11, x12); /* x1 = x * b1 + x2*/ + x11 = _mm_sub_ps(x11, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a12); /* z = a2 * y*/ + x12 = _mm_mul_ps(x, b12); /* x2 = x * b2 */ + x12 = _mm_sub_ps(x12, z); /* x2 = x * b2 - a2 * y*/ + + out[0][i] = y[0]; + out[1][i] = y[1]; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq[0*bq_stride+0].x1 = F(x01[0]); + bq[0*bq_stride+0].x2 = F(x02[0]); + bq[1*bq_stride+0].x1 = F(x01[1]); + bq[1*bq_stride+0].x2 = F(x02[1]); + + bq[0*bq_stride+1].x1 = F(x11[0]); + bq[0*bq_stride+1].x2 = F(x12[0]); + bq[1*bq_stride+1].x1 = F(x11[1]); + bq[1*bq_stride+1].x2 = F(x12[1]); +#undef F +} + +static void dsp_biquad_run4_sse(void *obj, struct biquad *bq, uint32_t bq_stride, + float **out, const float **in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b0, b1, b2; + __m128 a1, a2; + __m128 x1, x2; + uint32_t i; + + b0 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, bq[2*bq_stride].b0, bq[3*bq_stride].b0); + b1 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, bq[2*bq_stride].b1, bq[3*bq_stride].b1); + b2 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, bq[2*bq_stride].b2, bq[3*bq_stride].b2); + a1 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, bq[2*bq_stride].a1, bq[3*bq_stride].a1); + a2 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, bq[2*bq_stride].a2, bq[3*bq_stride].a2); + x1 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, bq[2*bq_stride].x1, bq[3*bq_stride].x1); + x2 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, bq[2*bq_stride].x2, bq[3*bq_stride].x2); + + for (i = 0; i < n_samples; i++) { + x = _mm_setr_ps(in[0][i], in[1][i], in[2][i], in[3][i]); + + y = _mm_mul_ps(x, b0); /* y = x * b0 */ + y = _mm_add_ps(y, x1); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a1); /* z = a1 * y */ + x1 = _mm_mul_ps(x, b1); /* x1 = x * b1 */ + x1 = _mm_add_ps(x1, x2); /* x1 = x * b1 + x2*/ + x1 = _mm_sub_ps(x1, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a2); /* z = a2 * y */ + x2 = _mm_mul_ps(x, b2); /* x2 = x * b2 */ + x2 = _mm_sub_ps(x2, z); /* x2 = x * b2 - a2 * y*/ + + out[0][i] = y[0]; + out[1][i] = y[1]; + out[2][i] = y[2]; + out[3][i] = y[3]; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq[0*bq_stride].x1 = F(x1[0]); + bq[0*bq_stride].x2 = F(x2[0]); + bq[1*bq_stride].x1 = F(x1[1]); + bq[1*bq_stride].x2 = F(x2[1]); + bq[2*bq_stride].x1 = F(x1[2]); + bq[2*bq_stride].x2 = F(x2[2]); + bq[3*bq_stride].x1 = F(x1[3]); + bq[3*bq_stride].x2 = F(x2[3]); +#undef F +} + +static void dsp_biquad2_run4_sse(void *obj, struct biquad *bq, uint32_t bq_stride, + float **out, const float **in, uint32_t n_samples) +{ + __m128 x, y, z; + __m128 b00, b01, b02, b10, b11, b12; + __m128 a01, a02, a11, a12; + __m128 x01, x02, x11, x12; + uint32_t i; + + b00 = _mm_setr_ps(bq[0].b0, bq[bq_stride].b0, bq[2*bq_stride].b0, bq[3*bq_stride].b0); + b01 = _mm_setr_ps(bq[0].b1, bq[bq_stride].b1, bq[2*bq_stride].b1, bq[3*bq_stride].b1); + b02 = _mm_setr_ps(bq[0].b2, bq[bq_stride].b2, bq[2*bq_stride].b2, bq[3*bq_stride].b2); + a01 = _mm_setr_ps(bq[0].a1, bq[bq_stride].a1, bq[2*bq_stride].a1, bq[3*bq_stride].a1); + a02 = _mm_setr_ps(bq[0].a2, bq[bq_stride].a2, bq[2*bq_stride].a2, bq[3*bq_stride].a2); + x01 = _mm_setr_ps(bq[0].x1, bq[bq_stride].x1, bq[2*bq_stride].x1, bq[3*bq_stride].x1); + x02 = _mm_setr_ps(bq[0].x2, bq[bq_stride].x2, bq[2*bq_stride].x2, bq[3*bq_stride].x2); + + b10 = _mm_setr_ps(bq[1].b0, bq[bq_stride+1].b0, bq[2*bq_stride+1].b0, bq[3*bq_stride+1].b0); + b11 = _mm_setr_ps(bq[1].b1, bq[bq_stride+1].b1, bq[2*bq_stride+1].b1, bq[3*bq_stride+1].b1); + b12 = _mm_setr_ps(bq[1].b2, bq[bq_stride+1].b2, bq[2*bq_stride+1].b2, bq[3*bq_stride+1].b2); + a11 = _mm_setr_ps(bq[1].a1, bq[bq_stride+1].a1, bq[2*bq_stride+1].a1, bq[3*bq_stride+1].a1); + a12 = _mm_setr_ps(bq[1].a2, bq[bq_stride+1].a2, bq[2*bq_stride+1].a2, bq[3*bq_stride+1].a2); + x11 = _mm_setr_ps(bq[1].x1, bq[bq_stride+1].x1, bq[2*bq_stride+1].x1, bq[3*bq_stride+1].x1); + x12 = _mm_setr_ps(bq[1].x2, bq[bq_stride+1].x2, bq[2*bq_stride+1].x2, bq[3*bq_stride+1].x2); + + for (i = 0; i < n_samples; i++) { + x = _mm_setr_ps(in[0][i], in[1][i], in[2][i], in[3][i]); + + y = _mm_mul_ps(x, b00); /* y = x * b0 */ + y = _mm_add_ps(y, x01); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a01); /* z = a1 * y */ + x01 = _mm_mul_ps(x, b01); /* x1 = x * b1 */ + x01 = _mm_add_ps(x01, x02); /* x1 = x * b1 + x2*/ + x01 = _mm_sub_ps(x01, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a02); /* z = a2 * y */ + x02 = _mm_mul_ps(x, b02); /* x2 = x * b2 */ + x02 = _mm_sub_ps(x02, z); /* x2 = x * b2 - a2 * y*/ + + x = y; + + y = _mm_mul_ps(x, b10); /* y = x * b0 */ + y = _mm_add_ps(y, x11); /* y = x * b0 + x1*/ + z = _mm_mul_ps(y, a11); /* z = a1 * y */ + x11 = _mm_mul_ps(x, b11); /* x1 = x * b1 */ + x11 = _mm_add_ps(x11, x12); /* x1 = x * b1 + x2*/ + x11 = _mm_sub_ps(x11, z); /* x1 = x * b1 + x2 - a1 * y*/ + z = _mm_mul_ps(y, a12); /* z = a2 * y*/ + x12 = _mm_mul_ps(x, b12); /* x2 = x * b2 */ + x12 = _mm_sub_ps(x12, z); /* x2 = x * b2 - a2 * y*/ + + out[0][i] = y[0]; + out[1][i] = y[1]; + out[2][i] = y[2]; + out[3][i] = y[3]; + } +#define F(x) (isnormal(x) ? (x) : 0.0f) + bq[0*bq_stride+0].x1 = F(x01[0]); + bq[0*bq_stride+0].x2 = F(x02[0]); + bq[1*bq_stride+0].x1 = F(x01[1]); + bq[1*bq_stride+0].x2 = F(x02[1]); + bq[2*bq_stride+0].x1 = F(x01[2]); + bq[2*bq_stride+0].x2 = F(x02[2]); + bq[3*bq_stride+0].x1 = F(x01[3]); + bq[3*bq_stride+0].x2 = F(x02[3]); + + bq[0*bq_stride+1].x1 = F(x11[0]); + bq[0*bq_stride+1].x2 = F(x12[0]); + bq[1*bq_stride+1].x1 = F(x11[1]); + bq[1*bq_stride+1].x2 = F(x12[1]); + bq[2*bq_stride+1].x1 = F(x11[2]); + bq[2*bq_stride+1].x2 = F(x12[2]); + bq[3*bq_stride+1].x1 = F(x11[3]); + bq[3*bq_stride+1].x2 = F(x12[3]); +#undef F +} + +void dsp_biquad_run_sse(void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, + float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], + uint32_t n_src, uint32_t n_samples) +{ + uint32_t i, j, bqs2 = bq_stride*2, bqs4 = bqs2*2; + uint32_t iunrolled4 = n_src & ~3; + uint32_t iunrolled2 = n_src & ~1; + uint32_t junrolled2 = n_bq & ~1; + + for (i = 0; i < iunrolled4; i+=4, bq+=bqs4) { + const float *s[4] = { in[i], in[i+1], in[i+2], in[i+3] }; + float *d[4] = { out[i], out[i+1], out[i+2], out[i+3] }; + + if (s[0] == NULL || s[1] == NULL || s[2] == NULL || s[3] == NULL || + d[0] == NULL || d[1] == NULL || d[2] == NULL || d[3] == NULL) + break; + + j = 0; + if (j < junrolled2) { + dsp_biquad2_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples); + s[0] = d[0]; + s[1] = d[1]; + s[2] = d[2]; + s[3] = d[3]; + j+=2; + } + for (; j < junrolled2; j+=2) { + dsp_biquad2_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples); + } + if (j < n_bq) { + dsp_biquad_run4_sse(obj, &bq[j], bq_stride, d, s, n_samples); + } + } + for (; i < iunrolled2; i+=2, bq+=bqs2) { + const float *s[2] = { in[i], in[i+1] }; + float *d[2] = { out[i], out[i+1] }; + + if (s[0] == NULL || s[1] == NULL || d[0] == NULL || d[1] == NULL) + break; + + j = 0; + if (j < junrolled2) { + dsp_biquad2_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples); + s[0] = d[0]; + s[1] = d[1]; + j+=2; + } + for (; j < junrolled2; j+=2) { + dsp_biquad2_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples); + } + if (j < n_bq) { + dsp_biquad_run2_sse(obj, &bq[j], bq_stride, d, s, n_samples); + } + } + for (; i < n_src; i++, bq+=bq_stride) { + const float *s = in[i]; + float *d = out[i]; + if (s == NULL || d == NULL) + continue; + + j = 0; + if (j < junrolled2) { + dsp_biquad2_run_sse(obj, &bq[j], d, s, n_samples); + s = d; + j+=2; + } + for (; j < junrolled2; j+=2) { + dsp_biquad2_run_sse(obj, &bq[j], d, s, n_samples); + } + if (j < n_bq) { + dsp_biquad_run1_sse(obj, &bq[j], d, s, n_samples); + } + } +} + +void dsp_delay_sse(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay, + float *dst, const float *src, uint32_t n_samples) +{ + __m128 t[1]; + uint32_t w = *pos; + uint32_t o = n_buffer - delay; + uint32_t n, unrolled; + + if (SPA_IS_ALIGNED(src, 16) && + SPA_IS_ALIGNED(dst, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + t[0] = _mm_load_ps(&src[n]); + _mm_storeu_ps(&buffer[w], t[0]); + _mm_storeu_ps(&buffer[w+n_buffer], t[0]); + t[0] = _mm_loadu_ps(&buffer[w+o]); + _mm_store_ps(&dst[n], t[0]); + w = w + 4 >= n_buffer ? 0 : w + 4; + } + for(; n < n_samples; n++) { + t[0] = _mm_load_ss(&src[n]); + _mm_store_ss(&buffer[w], t[0]); + _mm_store_ss(&buffer[w+n_buffer], t[0]); + t[0] = _mm_load_ss(&buffer[w+o]); + _mm_store_ss(&dst[n], t[0]); + w = w + 1 >= n_buffer ? 0 : w + 1; + } + *pos = w; +} + +inline static void _mm_mul_pz(__m128 *a, __m128 *b, __m128 *d) +{ + __m128 ar, ai, br, bi, arbr, arbi, aibi, aibr, dr, di; + ar = _mm_shuffle_ps(a[0], a[1], _MM_SHUFFLE(2,0,2,0)); /* ar0 ar1 ar2 ar3 */ + ai = _mm_shuffle_ps(a[0], a[1], _MM_SHUFFLE(3,1,3,1)); /* ai0 ai1 ai2 ai3 */ + br = _mm_shuffle_ps(b[0], b[1], _MM_SHUFFLE(2,0,2,0)); /* br0 br1 br2 br3 */ + bi = _mm_shuffle_ps(b[0], b[1], _MM_SHUFFLE(3,1,3,1)) /* bi0 bi1 bi2 bi3 */; + + arbr = _mm_mul_ps(ar, br); /* ar * br */ + arbi = _mm_mul_ps(ar, bi); /* ar * bi */ + + aibi = _mm_mul_ps(ai, bi); /* ai * bi */ + aibr = _mm_mul_ps(ai, br); /* ai * br */ + + dr = _mm_sub_ps(arbr, aibi); /* ar * br - ai * bi */ + di = _mm_add_ps(arbi, aibr); /* ar * bi + ai * br */ + d[0] = _mm_unpacklo_ps(dr, di); + d[1] = _mm_unpackhi_ps(dr, di); +} + +void dsp_fft_cmul_sse(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + __m128 s = _mm_set1_ps(scale); + __m128 aa[2], bb[2], dd[2]; + uint32_t i, unrolled; + + if (SPA_IS_ALIGNED(a, 16) && + SPA_IS_ALIGNED(b, 16) && + SPA_IS_ALIGNED(dst, 16)) + unrolled = len & ~3; + else + unrolled = 0; + + for (i = 0; i < unrolled; i+=4) { + aa[0] = _mm_load_ps(&a[2*i]); /* ar0 ai0 ar1 ai1 */ + aa[1] = _mm_load_ps(&a[2*i+4]); /* ar1 ai1 ar2 ai2 */ + bb[0] = _mm_load_ps(&b[2*i]); /* br0 bi0 br1 bi1 */ + bb[1] = _mm_load_ps(&b[2*i+4]); /* br2 bi2 br3 bi3 */ + _mm_mul_pz(aa, bb, dd); + dd[0] = _mm_mul_ps(dd[0], s); + dd[1] = _mm_mul_ps(dd[1], s); + _mm_store_ps(&dst[2*i], dd[0]); + _mm_store_ps(&dst[2*i+4], dd[1]); + } + for (; i < len; i++) { + dst[2*i ] = (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve(fft, a, b, dst, scale); +#endif +} + +void dsp_fft_cmuladd_sse(void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT src, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, + uint32_t len, const float scale) +{ +#ifdef HAVE_FFTW + __m128 s = _mm_set1_ps(scale); + __m128 aa[2], bb[2], dd[2], t[2]; + uint32_t i, unrolled; + + if (SPA_IS_ALIGNED(a, 16) && + SPA_IS_ALIGNED(b, 16) && + SPA_IS_ALIGNED(src, 16) && + SPA_IS_ALIGNED(dst, 16)) + unrolled = len & ~3; + else + unrolled = 0; + + for (i = 0; i < unrolled; i+=4) { + aa[0] = _mm_load_ps(&a[2*i]); /* ar0 ai0 ar1 ai1 */ + aa[1] = _mm_load_ps(&a[2*i+4]); /* ar1 ai1 ar2 ai2 */ + bb[0] = _mm_load_ps(&b[2*i]); /* br0 bi0 br1 bi1 */ + bb[1] = _mm_load_ps(&b[2*i+4]); /* br2 bi2 br3 bi3 */ + _mm_mul_pz(aa, bb, dd); + dd[0] = _mm_mul_ps(dd[0], s); + dd[1] = _mm_mul_ps(dd[1], s); + t[0] = _mm_load_ps(&src[2*i]); + t[1] = _mm_load_ps(&src[2*i+4]); + t[0] = _mm_add_ps(t[0], dd[0]); + t[1] = _mm_add_ps(t[1], dd[1]); + _mm_store_ps(&dst[2*i], t[0]); + _mm_store_ps(&dst[2*i+4], t[1]); + } + for (; i < len; i++) { + dst[2*i ] = src[2*i ] + (a[2*i] * b[2*i ] - a[2*i+1] * b[2*i+1]) * scale; + dst[2*i+1] = src[2*i+1] + (a[2*i] * b[2*i+1] + a[2*i+1] * b[2*i ]) * scale; + } +#else + pffft_zconvolve_accumulate(fft, a, b, src, dst, scale); +#endif +} diff --git a/spa/plugins/filter-graph/audio-dsp.c b/spa/plugins/filter-graph/audio-dsp.c new file mode 100644 index 0000000..fadd853 --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp.c @@ -0,0 +1,124 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "pffft.h" + +#include "audio-dsp-impl.h" + +struct dsp_info { + uint32_t cpu_flags; + + struct spa_fga_dsp_methods funcs; +}; + +static const struct dsp_info dsp_table[] = +{ +#if defined (HAVE_AVX) + { SPA_CPU_FLAG_AVX, + .funcs.clear = dsp_clear_c, + .funcs.copy = dsp_copy_c, + .funcs.mix_gain = dsp_mix_gain_avx, + .funcs.biquad_run = dsp_biquad_run_sse, + .funcs.sum = dsp_sum_avx, + .funcs.linear = dsp_linear_c, + .funcs.mult = dsp_mult_c, + .funcs.fft_new = dsp_fft_new_c, + .funcs.fft_free = dsp_fft_free_c, + .funcs.fft_memalloc = dsp_fft_memalloc_c, + .funcs.fft_memfree = dsp_fft_memfree_c, + .funcs.fft_memclear = dsp_fft_memclear_c, + .funcs.fft_run = dsp_fft_run_c, + .funcs.fft_cmul = dsp_fft_cmul_avx, + .funcs.fft_cmuladd = dsp_fft_cmuladd_avx, + .funcs.delay = dsp_delay_sse, + }, +#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_sse, + .funcs.sum = dsp_sum_sse, + .funcs.linear = dsp_linear_c, + .funcs.mult = dsp_mult_c, + .funcs.fft_new = dsp_fft_new_c, + .funcs.fft_free = dsp_fft_free_c, + .funcs.fft_memalloc = dsp_fft_memalloc_c, + .funcs.fft_memfree = dsp_fft_memfree_c, + .funcs.fft_memclear = dsp_fft_memclear_c, + .funcs.fft_run = dsp_fft_run_c, + .funcs.fft_cmul = dsp_fft_cmul_sse, + .funcs.fft_cmuladd = dsp_fft_cmuladd_sse, + .funcs.delay = dsp_delay_sse, + }, +#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.linear = dsp_linear_c, + .funcs.mult = dsp_mult_c, + .funcs.fft_new = dsp_fft_new_c, + .funcs.fft_free = dsp_fft_free_c, + .funcs.fft_memalloc = dsp_fft_memalloc_c, + .funcs.fft_memfree = dsp_fft_memfree_c, + .funcs.fft_memclear = dsp_fft_memclear_c, + .funcs.fft_run = dsp_fft_run_c, + .funcs.fft_cmul = dsp_fft_cmul_c, + .funcs.fft_cmuladd = dsp_fft_cmuladd_c, + .funcs.delay = dsp_delay_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; +} + +void spa_fga_dsp_free(struct spa_fga_dsp *dsp) +{ + free(dsp); +} + +struct spa_fga_dsp * spa_fga_dsp_new(uint32_t cpu_flags) +{ + const struct dsp_info *info; + struct spa_fga_dsp *dsp; + + info = find_dsp_info(cpu_flags); + if (info == NULL) { + errno = ENOTSUP; + return NULL; + } + dsp = calloc(1, sizeof(*dsp)); + if (dsp == NULL) + return NULL; + + pffft_select_cpu(cpu_flags); + dsp->cpu_flags = cpu_flags; + dsp->iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP, + SPA_VERSION_FGA_DSP, + &info->funcs, dsp); + + return dsp; +} diff --git a/spa/plugins/filter-graph/audio-dsp.h b/spa/plugins/filter-graph/audio-dsp.h new file mode 100644 index 0000000..4fc06eb --- /dev/null +++ b/spa/plugins/filter-graph/audio-dsp.h @@ -0,0 +1,168 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_FGA_DSP_H +#define SPA_FGA_DSP_H + +#include +#include + +#include "biquad.h" + +#define SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph:AudioDSP" + +#define SPA_VERSION_FGA_DSP 0 +struct spa_fga_dsp { + struct spa_interface iface; + uint32_t cpu_flags; +}; + +struct spa_fga_dsp_methods { +#define SPA_VERSION_FGA_DSP_METHODS 0 + uint32_t version; + + void (*clear) (void *obj, float * SPA_RESTRICT dst, uint32_t n_samples); + void (*copy) (void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, uint32_t n_samples); + void (*mix_gain) (void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples); + void (*sum) (void *obj, + float * dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t n_samples); + + void *(*fft_new) (void *obj, uint32_t size, bool real); + void (*fft_free) (void *obj, void *fft); + void *(*fft_memalloc) (void *obj, uint32_t size, bool real); + void (*fft_memfree) (void *obj, void *mem); + void (*fft_memclear) (void *obj, void *mem, uint32_t size, bool real); + void (*fft_run) (void *obj, void *fft, int direction, + const float * SPA_RESTRICT src, float * SPA_RESTRICT dst); + void (*fft_cmul) (void *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t len, const float scale); + void (*fft_cmuladd) (void *obj, void *fft, + float * dst, const float * src, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, + uint32_t len, const float scale); + void (*linear) (void *obj, + float * dst, const float * SPA_RESTRICT src, + const float mult, const float add, uint32_t n_samples); + void (*mult) (void *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples); + void (*biquad_run) (void *obj, struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, + float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], + uint32_t n_src, uint32_t n_samples); + void (*delay) (void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay, + float *dst, const float *src, uint32_t n_samples); +}; + +static inline void spa_fga_dsp_clear(struct spa_fga_dsp *obj, float * SPA_RESTRICT dst, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, clear, 0, + dst, n_samples); +} +static inline void spa_fga_dsp_copy(struct spa_fga_dsp *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, copy, 0, + dst, src, n_samples); +} +static inline void spa_fga_dsp_mix_gain(struct spa_fga_dsp *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, + float gain[], uint32_t n_gain, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, mix_gain, 0, + dst, src, n_src, gain, n_gain, n_samples); +} +static inline void spa_fga_dsp_sum(struct spa_fga_dsp *obj, + float * dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, sum, 0, + dst, a, b, n_samples); +} + +static inline void *spa_fga_dsp_fft_new(struct spa_fga_dsp *obj, uint32_t size, bool real) +{ + return spa_api_method_r(void *, NULL, spa_fga_dsp, &obj->iface, fft_new, 0, + size, real); +} +static inline void spa_fga_dsp_fft_free(struct spa_fga_dsp *obj, void *fft) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_free, 0, + fft); +} +static inline void *spa_fga_dsp_fft_memalloc(struct spa_fga_dsp *obj, uint32_t size, bool real) +{ + return spa_api_method_r(void *, NULL, spa_fga_dsp, &obj->iface, fft_memalloc, 0, + size, real); +} +static inline void spa_fga_dsp_fft_memfree(struct spa_fga_dsp *obj, void *mem) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_memfree, 0, + mem); +} +static inline void spa_fga_dsp_fft_memclear(struct spa_fga_dsp *obj, void *mem, uint32_t size, bool real) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_memclear, 0, + mem, size, real); +} +static inline void spa_fga_dsp_fft_run(struct spa_fga_dsp *obj, void *fft, int direction, + const float * SPA_RESTRICT src, float * SPA_RESTRICT dst) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_run, 0, + fft, direction, src, dst); +} +static inline void spa_fga_dsp_fft_cmul(struct spa_fga_dsp *obj, void *fft, + float * SPA_RESTRICT dst, const float * SPA_RESTRICT a, + const float * SPA_RESTRICT b, uint32_t len, const float scale) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_cmul, 0, + fft, dst, a, b, len, scale); +} +static inline void spa_fga_dsp_fft_cmuladd(struct spa_fga_dsp *obj, void *fft, + float * dst, const float * src, + const float * SPA_RESTRICT a, const float * SPA_RESTRICT b, + uint32_t len, const float scale) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, fft_cmuladd, 0, + fft, dst, src, a, b, len, scale); +} +static inline void spa_fga_dsp_linear(struct spa_fga_dsp *obj, + float * dst, const float * SPA_RESTRICT src, + const float mult, const float add, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, linear, 0, + dst, src, mult, add, n_samples); +} +static inline void spa_fga_dsp_mult(struct spa_fga_dsp *obj, + float * SPA_RESTRICT dst, + const float * SPA_RESTRICT src[], uint32_t n_src, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, mult, 0, + dst, src, n_src, n_samples); +} +static inline void spa_fga_dsp_biquad_run(struct spa_fga_dsp *obj, + struct biquad *bq, uint32_t n_bq, uint32_t bq_stride, + float * SPA_RESTRICT out[], const float * SPA_RESTRICT in[], + uint32_t n_src, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, biquad_run, 0, + bq, n_bq, bq_stride, out, in, n_src, n_samples); +} +static inline void spa_fga_dsp_delay(struct spa_fga_dsp *obj, + float *buffer, uint32_t *pos, uint32_t n_buffer, uint32_t delay, + float *dst, const float *src, uint32_t n_samples) +{ + spa_api_method_v(spa_fga_dsp, &obj->iface, delay, 0, + buffer, pos, n_buffer, delay, dst, src, n_samples); +} + +#endif /* SPA_FGA_DSP_H */ diff --git a/spa/plugins/filter-graph/audio-plugin.h b/spa/plugins/filter-graph/audio-plugin.h new file mode 100644 index 0000000..e05d7a8 --- /dev/null +++ b/spa/plugins/filter-graph/audio-plugin.h @@ -0,0 +1,95 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_FGA_PLUGIN_H +#define SPA_FGA_PLUGIN_H + +#include +#include + +#include +#include +#include + +#define SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph:AudioPlugin" + +#define SPA_VERSION_FGA_PLUGIN 0 +struct spa_fga_plugin { struct spa_interface iface; }; + +struct spa_fga_plugin_methods { +#define SPA_VERSION_FGA_PLUGIN_METHODS 0 + uint32_t version; + + const struct spa_fga_descriptor *(*make_desc) (void *plugin, const char *name); +}; + +struct spa_fga_port { + uint32_t index; + const char *name; +#define SPA_FGA_PORT_INPUT (1ULL << 0) +#define SPA_FGA_PORT_OUTPUT (1ULL << 1) +#define SPA_FGA_PORT_CONTROL (1ULL << 2) +#define SPA_FGA_PORT_AUDIO (1ULL << 3) + uint64_t flags; + +#define SPA_FGA_HINT_BOOLEAN (1ULL << 2) +#define SPA_FGA_HINT_SAMPLE_RATE (1ULL << 3) +#define SPA_FGA_HINT_INTEGER (1ULL << 5) + uint64_t hint; + float def; + float min; + float max; +}; + +#define SPA_FGA_IS_PORT_INPUT(x) ((x) & SPA_FGA_PORT_INPUT) +#define SPA_FGA_IS_PORT_OUTPUT(x) ((x) & SPA_FGA_PORT_OUTPUT) +#define SPA_FGA_IS_PORT_CONTROL(x) ((x) & SPA_FGA_PORT_CONTROL) +#define SPA_FGA_IS_PORT_AUDIO(x) ((x) & SPA_FGA_PORT_AUDIO) + +struct spa_fga_descriptor { + const char *name; +#define SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA (1ULL << 0) +#define SPA_FGA_DESCRIPTOR_COPY (1ULL << 1) + uint64_t flags; + + void (*free) (const struct spa_fga_descriptor *desc); + + uint32_t n_ports; + struct spa_fga_port *ports; + + void *(*instantiate) (const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, + unsigned long SampleRate, int index, const char *config); + + void (*cleanup) (void *instance); + + void (*connect_port) (void *instance, unsigned long port, float *data); + void (*control_changed) (void *instance); + + void (*activate) (void *instance); + void (*deactivate) (void *instance); + + void (*run) (void *instance, unsigned long SampleCount); +}; + +static inline void spa_fga_descriptor_free(const struct spa_fga_descriptor *desc) +{ + if (desc->free) + desc->free(desc); +} + +static inline const struct spa_fga_descriptor * +spa_fga_plugin_make_desc(struct spa_fga_plugin *plugin, const char *name) +{ + return spa_api_method_r(const struct spa_fga_descriptor *, NULL, + spa_fga_plugin, &plugin->iface, make_desc, 0, name); +} + +typedef struct spa_fga_plugin *(spa_filter_graph_audio_plugin_load_func_t)(const struct spa_support *support, + uint32_t n_support, const char *path, const struct spa_dict *info); + +#define SPA_FILTER_GRAPH_AUDIO_PLUGIN_LOAD_FUNC_NAME "spa_filter_graph_audio_plugin_load" + + + +#endif /* PLUGIN_H */ diff --git a/spa/plugins/filter-graph/biquad.h b/spa/plugins/filter-graph/biquad.h new file mode 100644 index 0000000..3344598 --- /dev/null +++ b/spa/plugins/filter-graph/biquad.h @@ -0,0 +1,58 @@ +/* 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 type of the biquad filters */ +enum biquad_type { + BQ_NONE, + BQ_LOWPASS, + BQ_HIGHPASS, + BQ_BANDPASS, + BQ_LOWSHELF, + BQ_HIGHSHELF, + BQ_PEAKING, + BQ_NOTCH, + BQ_ALLPASS, + BQ_RAW, +}; + +/* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1) + * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)). The previous two inputs + * are stored in x1 and x2, and the previous two outputs are stored in y1 and + * y2. + * + * We use double during the coefficients calculation for better accuracy, but + * float is used during the actual filtering for faster computation. + */ +struct biquad { + enum biquad_type type; + float b0, b1, b2; + float a1, a2; + float x1, x2; +}; + +/* 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/spa/plugins/filter-graph/builtin_plugin.c b/spa/plugins/filter-graph/builtin_plugin.c new file mode 100644 index 0000000..f50ffd7 --- /dev/null +++ b/spa/plugins/filter-graph/builtin_plugin.c @@ -0,0 +1,2647 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#ifdef HAVE_SNDFILE +#include +#endif +#include +#include + +#include +#include +#include +#include +#include + +#include "audio-plugin.h" + +#include "biquad.h" +#include "convolver.h" +#include "audio-dsp.h" + +#define MAX_RATES 32u + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; +}; + +struct builtin { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[64]; + + int type; + struct biquad bq; + float freq; + float Q; + float gain; + float b0, b1, b2; + float a0, a1, a2; + float accum; +}; + +static void *builtin_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct builtin *impl; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->rate = SampleRate; + impl->dsp = impl->plugin->dsp; + impl->log = impl->plugin->log; + + return impl; +} + +static void builtin_connect_port(void *Instance, unsigned long Port, float * DataLocation) +{ + struct builtin *impl = Instance; + impl->port[Port] = DataLocation; +} + +static void builtin_cleanup(void * Instance) +{ + struct builtin *impl = Instance; + free(impl); +} + +/** copy */ +static void copy_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + spa_fga_dsp_copy(impl->dsp, out, in, SampleCount); +} + +static struct spa_fga_port copy_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + } +}; + +static const struct spa_fga_descriptor copy_desc = { + .name = "copy", + .flags = SPA_FGA_DESCRIPTOR_COPY, + + .n_ports = 2, + .ports = copy_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = copy_run, + .cleanup = builtin_cleanup, +}; + +/** mixer */ +static void mixer_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + int i, n_src = 0; + float *out = impl->port[0]; + const float *src[8]; + float gains[8]; + bool eq_gain = true; + + if (out == NULL) + return; + + for (i = 0; i < 8; i++) { + float *in = impl->port[1+i]; + float gain = impl->port[9+i][0]; + + if (in == NULL || gain == 0.0f) + continue; + + src[n_src] = in; + gains[n_src++] = gain; + if (gain != gains[0]) + eq_gain = false; + } + if (eq_gain) + spa_fga_dsp_mix_gain(impl->dsp, out, src, n_src, gains, 1, SampleCount); + else + spa_fga_dsp_mix_gain(impl->dsp, out, src, n_src, gains, n_src, SampleCount); +} + +static struct spa_fga_port mixer_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 1, + .name = "In 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 3, + .name = "In 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 4, + .name = "In 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 5, + .name = "In 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 6, + .name = "In 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 7, + .name = "In 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 8, + .name = "In 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 9, + .name = "Gain 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 10, + .name = "Gain 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 11, + .name = "Gain 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 12, + .name = "Gain 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 13, + .name = "Gain 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 14, + .name = "Gain 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 15, + .name = "Gain 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, + { .index = 16, + .name = "Gain 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 10.0f + }, +}; + +static const struct spa_fga_descriptor mixer_desc = { + .name = "mixer", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = 17, + .ports = mixer_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = mixer_run, + .cleanup = builtin_cleanup, +}; + +/** biquads */ +static int bq_type_from_name(const char *name) +{ + if (spa_streq(name, "bq_lowpass")) + return BQ_LOWPASS; + if (spa_streq(name, "bq_highpass")) + return BQ_HIGHPASS; + if (spa_streq(name, "bq_bandpass")) + return BQ_BANDPASS; + if (spa_streq(name, "bq_lowshelf")) + return BQ_LOWSHELF; + if (spa_streq(name, "bq_highshelf")) + return BQ_HIGHSHELF; + if (spa_streq(name, "bq_peaking")) + return BQ_PEAKING; + if (spa_streq(name, "bq_notch")) + return BQ_NOTCH; + if (spa_streq(name, "bq_allpass")) + return BQ_ALLPASS; + if (spa_streq(name, "bq_raw")) + return BQ_NONE; + return BQ_NONE; +} + +static const char *bq_name_from_type(int type) +{ + switch (type) { + case BQ_LOWPASS: + return "lowpass"; + case BQ_HIGHPASS: + return "highpass"; + case BQ_BANDPASS: + return "bandpass"; + case BQ_LOWSHELF: + return "lowshelf"; + case BQ_HIGHSHELF: + return "highshelf"; + case BQ_PEAKING: + return "peaking"; + case BQ_NOTCH: + return "notch"; + case BQ_ALLPASS: + return "allpass"; + case BQ_NONE: + return "raw"; + } + return "unknown"; +} + +static void bq_raw_update(struct builtin *impl, float b0, float b1, float b2, + float a0, float a1, float a2) +{ + struct biquad *bq = &impl->bq; + impl->b0 = b0; + impl->b1 = b1; + impl->b2 = b2; + impl->a0 = a0; + impl->a1 = a1; + impl->a2 = a2; + if (a0 != 0.0f) + a0 = 1.0f / a0; + bq->b0 = impl->b0 * a0; + bq->b1 = impl->b1 * a0; + bq->b2 = impl->b2 * a0; + bq->a1 = impl->a1 * a0; + bq->a2 = impl->a2 * a0; + bq->x1 = bq->x2 = 0.0f; + bq->type = BQ_RAW; +} + +/* + * config = { + * coefficients = [ + * { rate = 44100, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. }, + * { rate = 48000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. }, + * { rate = 192000, b0=.., b1=.., b2=.., a0=.., a1=.., a2=.. } + * ] + * } + */ +static void *bq_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct builtin *impl; + struct spa_json it[3]; + const char *val; + char key[256]; + uint32_t best_rate = 0; + int len; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->log = impl->plugin->log; + impl->dsp = impl->plugin->dsp; + impl->rate = SampleRate; + impl->b0 = impl->a0 = 1.0f; + impl->type = bq_type_from_name(Descriptor->name); + if (impl->type != BQ_NONE) + return impl; + + if (config == NULL) { + spa_log_error(impl->log, "biquads:bq_raw requires a config section"); + goto error; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(impl->log, "biquads:config section must be an object"); + goto error; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "coefficients")) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "biquads:coefficients require an array"); + goto error; + } + spa_json_enter(&it[0], &it[1]); + while (spa_json_enter_object(&it[1], &it[2]) > 0) { + int32_t rate = 0; + float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f; + float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f; + + while ((len = spa_json_object_next(&it[2], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "rate")) { + if (spa_json_parse_int(val, len, &rate) <= 0) { + spa_log_error(impl->log, "biquads:rate requires a number"); + goto error; + } + } + else if (spa_streq(key, "b0")) { + if (spa_json_parse_float(val, len, &b0) <= 0) { + spa_log_error(impl->log, "biquads:b0 requires a float"); + goto error; + } + } + else if (spa_streq(key, "b1")) { + if (spa_json_parse_float(val, len, &b1) <= 0) { + spa_log_error(impl->log, "biquads:b1 requires a float"); + goto error; + } + } + else if (spa_streq(key, "b2")) { + if (spa_json_parse_float(val, len, &b2) <= 0) { + spa_log_error(impl->log, "biquads:b2 requires a float"); + goto error; + } + } + else if (spa_streq(key, "a0")) { + if (spa_json_parse_float(val, len, &a0) <= 0) { + spa_log_error(impl->log, "biquads:a0 requires a float"); + goto error; + } + } + else if (spa_streq(key, "a1")) { + if (spa_json_parse_float(val, len, &a1) <= 0) { + spa_log_error(impl->log, "biquads:a1 requires a float"); + goto error; + } + } + else if (spa_streq(key, "a2")) { + if (spa_json_parse_float(val, len, &a2) <= 0) { + spa_log_error(impl->log, "biquads:a0 requires a float"); + goto error; + } + } + else { + spa_log_warn(impl->log, "biquads: ignoring coefficients key: '%s'", key); + } + } + if (labs((long)rate - (long)SampleRate) < + labs((long)best_rate - (long)SampleRate)) { + best_rate = rate; + bq_raw_update(impl, b0, b1, b2, a0, a1, a2); + } + } + } + else { + spa_log_warn(impl->log, "biquads: ignoring config key: '%s'", key); + } + } + + return impl; +error: + free(impl); + errno = EINVAL; + return NULL; +} + +#define BQ_NUM_PORTS 11 +static struct spa_fga_port bq_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Freq", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .hint = SPA_FGA_HINT_SAMPLE_RATE, + .def = 0.0f, .min = 0.0f, .max = 1.0f, + }, + { .index = 3, + .name = "Q", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = 0.0f, .max = 10.0f, + }, + { .index = 4, + .name = "Gain", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -120.0f, .max = 20.0f, + }, + { .index = 5, + .name = "b0", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -10.0f, .max = 10.0f, + }, + { .index = 6, + .name = "b1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f, + }, + { .index = 7, + .name = "b2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f, + }, + { .index = 8, + .name = "a0", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -10.0f, .max = 10.0f, + }, + { .index = 9, + .name = "a1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f, + }, + { .index = 10, + .name = "a2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f, + }, + +}; + +static void bq_freq_update(struct builtin *impl, int type, float freq, float Q, float gain) +{ + struct biquad *bq = &impl->bq; + impl->freq = freq; + impl->Q = Q; + impl->gain = gain; + biquad_set(bq, type, freq * 2 / impl->rate, Q, gain); + impl->port[5][0] = impl->b0 = bq->b0; + impl->port[6][0] = impl->b1 = bq->b1; + impl->port[7][0] = impl->b2 = bq->b2; + impl->port[8][0] = impl->a0 = 1.0f; + impl->port[9][0] = impl->a1 = bq->a1; + impl->port[10][0] = impl->a2 = bq->a2; +} + +static void bq_activate(void * Instance) +{ + struct builtin *impl = Instance; + if (impl->type == BQ_NONE) { + impl->port[5][0] = impl->b0; + impl->port[6][0] = impl->b1; + impl->port[7][0] = impl->b2; + impl->port[8][0] = impl->a0; + impl->port[9][0] = impl->a1; + impl->port[10][0] = impl->a2; + } else { + float freq = impl->port[2][0]; + float Q = impl->port[3][0]; + float gain = impl->port[4][0]; + bq_freq_update(impl, impl->type, freq, Q, gain); + } +} + +static void bq_run(void *Instance, unsigned long samples) +{ + struct builtin *impl = Instance; + struct biquad *bq = &impl->bq; + float *out = impl->port[0]; + float *in = impl->port[1]; + + if (impl->type == BQ_NONE) { + float b0, b1, b2, a0, a1, a2; + b0 = impl->port[5][0]; + b1 = impl->port[6][0]; + b2 = impl->port[7][0]; + a0 = impl->port[8][0]; + a1 = impl->port[9][0]; + a2 = impl->port[10][0]; + if (impl->b0 != b0 || impl->b1 != b1 || impl->b2 != b2 || + impl->a0 != a0 || impl->a1 != a1 || impl->a2 != a2) { + bq_raw_update(impl, b0, b1, b2, a0, a1, a2); + } + } else { + float freq = impl->port[2][0]; + float Q = impl->port[3][0]; + float gain = impl->port[4][0]; + if (impl->freq != freq || impl->Q != Q || impl->gain != gain) + bq_freq_update(impl, impl->type, freq, Q, gain); + } + spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); +} + +/** bq_lowpass */ +static const struct spa_fga_descriptor bq_lowpass_desc = { + .name = "bq_lowpass", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_highpass */ +static const struct spa_fga_descriptor bq_highpass_desc = { + .name = "bq_highpass", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_bandpass */ +static const struct spa_fga_descriptor bq_bandpass_desc = { + .name = "bq_bandpass", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_lowshelf */ +static const struct spa_fga_descriptor bq_lowshelf_desc = { + .name = "bq_lowshelf", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_highshelf */ +static const struct spa_fga_descriptor bq_highshelf_desc = { + .name = "bq_highshelf", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_peaking */ +static const struct spa_fga_descriptor bq_peaking_desc = { + .name = "bq_peaking", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** bq_notch */ +static const struct spa_fga_descriptor bq_notch_desc = { + .name = "bq_notch", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + + +/** bq_allpass */ +static const struct spa_fga_descriptor bq_allpass_desc = { + .name = "bq_allpass", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/* bq_raw */ +static const struct spa_fga_descriptor bq_raw_desc = { + .name = "bq_raw", + + .n_ports = BQ_NUM_PORTS, + .ports = bq_ports, + + .instantiate = bq_instantiate, + .connect_port = builtin_connect_port, + .activate = bq_activate, + .run = bq_run, + .cleanup = builtin_cleanup, +}; + +/** convolve */ +struct convolver_impl { + struct plugin *plugin; + + struct spa_log *log; + struct spa_fga_dsp *dsp; + unsigned long rate; + float *port[2]; + + struct convolver *conv; +}; + +#ifdef HAVE_SNDFILE +static float *read_samples_from_sf(SNDFILE *f, const SF_INFO *info, float gain, int delay, + int offset, int length, int channel, long unsigned *rate, int *n_samples) { + float *samples; + int i, n; + + if (length <= 0) + length = info->frames; + else + length = SPA_MIN(length, info->frames); + + length -= SPA_MIN(offset, length); + + n = delay + length; + if (n == 0) + return NULL; + + samples = calloc(n * info->channels, sizeof(float)); + if (samples == NULL) + return NULL; + + if (offset > 0) + sf_seek(f, offset, SEEK_SET); + sf_readf_float(f, samples + (delay * info->channels), length); + + channel = channel % info->channels; + + for (i = 0; i < n; i++) + samples[i] = samples[info->channels * i + channel] * gain; + + *n_samples = n; + *rate = info->samplerate; + return samples; +} +#endif + +static float *read_closest(struct plugin *pl, char **filenames, float gain, float delay_sec, int offset, + int length, int channel, long unsigned *rate, int *n_samples) +{ +#ifdef HAVE_SNDFILE + SF_INFO infos[MAX_RATES]; + SNDFILE *fs[MAX_RATES]; + + spa_zero(infos); + spa_zero(fs); + + int diff = INT_MAX; + uint32_t best = 0, i; + float *samples = NULL; + + for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) { + fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]); + if (fs[i] == NULL) + continue; + + if (labs((long)infos[i].samplerate - (long)*rate) < diff) { + best = i; + diff = labs((long)infos[i].samplerate - (long)*rate); + spa_log_debug(pl->log, "new closest match: %d", infos[i].samplerate); + } + } + if (fs[best] != NULL) { + spa_log_info(pl->log, "loading best rate:%u %s", infos[best].samplerate, filenames[best]); + samples = read_samples_from_sf(fs[best], &infos[best], gain, + (int) (delay_sec * infos[best].samplerate), offset, length, + channel, rate, n_samples); + } else { + char buf[PATH_MAX]; + spa_log_error(pl->log, "Can't open any sample file (CWD %s):", + getcwd(buf, sizeof(buf))); + for (i = 0; i < MAX_RATES && filenames[i] && filenames[i][0]; i++) { + fs[i] = sf_open(filenames[i], SFM_READ, &infos[i]); + if (fs[i] == NULL) + spa_log_error(pl->log, " failed file %s: %s", filenames[i], sf_strerror(fs[i])); + else + spa_log_warn(pl->log, " unexpectedly opened file %s", filenames[i]); + } + } + for (i = 0; i < MAX_RATES; i++) + if (fs[i] != NULL) + sf_close(fs[i]); + + return samples; +#else + spa_log_error(pl->log, "compiled without sndfile support, can't load samples: " + "using dirac impulse"); + float *samples = calloc(1, sizeof(float)); + samples[0] = gain; + *n_samples = 1; + return samples; +#endif +} + +static float *create_hilbert(struct plugin *pl, const char *filename, float gain, int rate, float delay_sec, int offset, + int length, int *n_samples) +{ + float *samples, v; + int i, n, h; + int delay = (int) (delay_sec * rate); + + if (length <= 0) + length = 1024; + + length -= SPA_MIN(offset, length); + + n = delay + length; + if (n == 0) + return NULL; + + samples = calloc(n, sizeof(float)); + if (samples == NULL) + return NULL; + + gain *= 2 / (float)M_PI; + h = length / 2; + for (i = 1; i < h; i += 2) { + v = (gain / i) * (0.43f + 0.57f * cosf(i * (float)M_PI / h)); + samples[delay + h + i] = -v; + samples[delay + h - i] = v; + } + *n_samples = n; + return samples; +} + +static float *create_dirac(struct plugin *pl, const char *filename, float gain, int rate, float delay_sec, int offset, + int length, int *n_samples) +{ + float *samples; + int delay = (int) (delay_sec * rate); + int n; + + n = delay + 1; + + samples = calloc(n, sizeof(float)); + if (samples == NULL) + return NULL; + + samples[delay] = gain; + + *n_samples = n; + return samples; +} + +static float *resample_buffer(struct plugin *pl, float *samples, int *n_samples, + unsigned long in_rate, unsigned long out_rate, uint32_t quality) +{ +#ifdef HAVE_SPA_PLUGINS + uint32_t in_len, out_len, total_out = 0; + int out_n_samples; + float *out_samples, *out_buf, *in_buf; + struct resample r; + int res; + + spa_zero(r); + r.channels = 1; + r.i_rate = in_rate; + r.o_rate = out_rate; + r.cpu_flags = pl->dsp->cpu_flags; + r.quality = quality; + if ((res = resample_native_init(&r)) < 0) { + spa_log_error(pl->log, "resampling failed: %s", spa_strerror(res)); + errno = -res; + return NULL; + } + + out_n_samples = SPA_ROUND_UP(*n_samples * out_rate, in_rate) / in_rate; + out_samples = calloc(out_n_samples, sizeof(float)); + if (out_samples == NULL) + goto error; + + in_len = *n_samples; + in_buf = samples; + out_len = out_n_samples; + out_buf = out_samples; + + spa_log_info(pl->log, "Resampling filter: rate: %lu => %lu, n_samples: %u => %u, q:%u", + in_rate, out_rate, in_len, out_len, quality); + + resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len); + spa_log_debug(pl->log, "resampled: %u -> %u samples", in_len, out_len); + total_out += out_len; + + in_len = resample_delay(&r); + in_buf = calloc(in_len, sizeof(float)); + if (in_buf == NULL) + goto error; + + out_buf = out_samples + total_out; + out_len = out_n_samples - total_out; + + spa_log_debug(pl->log, "flushing resampler: %u in %u out", in_len, out_len); + resample_process(&r, (void*)&in_buf, &in_len, (void*)&out_buf, &out_len); + spa_log_debug(pl->log, "flushed: %u -> %u samples", in_len, out_len); + total_out += out_len; + + free(in_buf); + free(samples); + resample_free(&r); + + *n_samples = total_out; + + float gain = (float)in_rate / (float)out_rate; + for (uint32_t i = 0; i < total_out; i++) + out_samples[i] = out_samples[i] * gain; + + return out_samples; + +error: + resample_free(&r); + free(samples); + free(out_samples); + return NULL; +#else + spa_log_error(impl->log, "compiled without spa-plugins support, can't resample"); + float *out_samples = calloc(*n_samples, sizeof(float)); + memcpy(out_samples, samples, *n_samples * sizeof(float)); + return out_samples; +#endif +} + +static void * convolver_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct convolver_impl *impl; + float *samples; + int offset = 0, length = 0, channel = index, n_samples = 0, len; + uint32_t i = 0; + struct spa_json it[2]; + const char *val; + char key[256], v[256]; + char *filenames[MAX_RATES] = { 0 }; + int blocksize = 0, tailsize = 0; + int resample_quality = RESAMPLE_DEFAULT_QUALITY; + float gain = 1.0f, delay = 0.0f; + unsigned long rate; + + errno = EINVAL; + if (config == NULL) { + spa_log_error(pl->log, "convolver: requires a config section"); + return NULL; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "convolver:config must be an object"); + return NULL; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "blocksize")) { + if (spa_json_parse_int(val, len, &blocksize) <= 0) { + spa_log_error(pl->log, "convolver:blocksize requires a number"); + return NULL; + } + } + else if (spa_streq(key, "tailsize")) { + if (spa_json_parse_int(val, len, &tailsize) <= 0) { + spa_log_error(pl->log, "convolver:tailsize requires a number"); + return NULL; + } + } + else if (spa_streq(key, "gain")) { + if (spa_json_parse_float(val, len, &gain) <= 0) { + spa_log_error(pl->log, "convolver:gain requires a number"); + return NULL; + } + } + else if (spa_streq(key, "delay")) { + int delay_i; + if (spa_json_parse_int(val, len, &delay_i) > 0) { + delay = delay_i / (float)SampleRate; + } else if (spa_json_parse_float(val, len, &delay) <= 0) { + spa_log_error(pl->log, "convolver:delay requires a number"); + return NULL; + } + } + else if (spa_streq(key, "filename")) { + if (spa_json_is_array(val, len)) { + spa_json_enter(&it[0], &it[1]); + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + i < SPA_N_ELEMENTS(filenames)) { + filenames[i] = strdup(v); + i++; + } + } + else if (spa_json_parse_stringn(val, len, v, sizeof(v)) <= 0) { + spa_log_error(pl->log, "convolver:filename requires a string or an array"); + return NULL; + } else { + filenames[0] = strdup(v); + } + } + else if (spa_streq(key, "offset")) { + if (spa_json_parse_int(val, len, &offset) <= 0) { + spa_log_error(pl->log, "convolver:offset requires a number"); + return NULL; + } + } + else if (spa_streq(key, "length")) { + if (spa_json_parse_int(val, len, &length) <= 0) { + spa_log_error(pl->log, "convolver:length requires a number"); + return NULL; + } + } + else if (spa_streq(key, "channel")) { + if (spa_json_parse_int(val, len, &channel) <= 0) { + spa_log_error(pl->log, "convolver:channel requires a number"); + return NULL; + } + } + else if (spa_streq(key, "resample_quality")) { + if (spa_json_parse_int(val, len, &resample_quality) <= 0) { + spa_log_error(pl->log, "convolver:resample_quality requires a number"); + return NULL; + } + } + else { + spa_log_warn(pl->log, "convolver: ignoring config key: '%s'", key); + } + } + if (filenames[0] == NULL) { + spa_log_error(pl->log, "convolver:filename was not given"); + return NULL; + } + + if (delay < 0.0f) + delay = 0.0f; + if (offset < 0) + offset = 0; + + if (spa_streq(filenames[0], "/hilbert")) { + samples = create_hilbert(pl, filenames[0], gain, SampleRate, delay, offset, + length, &n_samples); + } else if (spa_streq(filenames[0], "/dirac")) { + samples = create_dirac(pl, filenames[0], gain, SampleRate, delay, offset, + length, &n_samples); + } else { + rate = SampleRate; + samples = read_closest(pl, filenames, gain, delay, offset, + length, channel, &rate, &n_samples); + if (samples != NULL && rate != SampleRate) { + samples = resample_buffer(pl, samples, &n_samples, + rate, SampleRate, resample_quality); + } + } + + for (i = 0; i < MAX_RATES; i++) + if (filenames[i]) + free(filenames[i]); + + if (samples == NULL) { + errno = ENOENT; + return NULL; + } + + if (blocksize <= 0) + blocksize = SPA_CLAMP(n_samples, 64, 256); + if (tailsize <= 0) + tailsize = SPA_CLAMP(4096, blocksize, 32768); + + spa_log_info(pl->log, "using n_samples:%u %d:%d blocksize delay:%f", n_samples, + blocksize, tailsize, delay); + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + goto error; + + impl->plugin = pl; + impl->log = pl->log; + impl->dsp = pl->dsp; + impl->rate = SampleRate; + + impl->conv = convolver_new(impl->dsp, blocksize, tailsize, samples, n_samples); + if (impl->conv == NULL) + goto error; + + free(samples); + + return impl; +error: + free(samples); + free(impl); + return NULL; +} + +static void convolver_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct convolver_impl *impl = Instance; + impl->port[Port] = DataLocation; +} + +static void convolver_cleanup(void * Instance) +{ + struct convolver_impl *impl = Instance; + if (impl->conv) + convolver_free(impl->conv); + free(impl); +} + +static struct spa_fga_port convolve_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static void convolver_deactivate(void * Instance) +{ + struct convolver_impl *impl = Instance; + convolver_reset(impl->conv); +} + +static void convolve_run(void * Instance, unsigned long SampleCount) +{ + struct convolver_impl *impl = Instance; + if (impl->port[1] != NULL && impl->port[0] != NULL) + convolver_run(impl->conv, impl->port[1], impl->port[0], SampleCount); +} + +static const struct spa_fga_descriptor convolve_desc = { + .name = "convolver", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = 2, + .ports = convolve_ports, + + .instantiate = convolver_instantiate, + .connect_port = convolver_connect_port, + .deactivate = convolver_deactivate, + .run = convolve_run, + .cleanup = convolver_cleanup, +}; + +/** delay */ +struct delay_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[4]; + + float delay; + uint32_t delay_samples; + uint32_t buffer_samples; + float *buffer; + uint32_t ptr; +}; + +static void delay_cleanup(void * Instance) +{ + struct delay_impl *impl = Instance; + free(impl->buffer); + free(impl); +} + +static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct delay_impl *impl; + struct spa_json it[1]; + const char *val; + char key[256]; + float max_delay = 1.0f; + int len; + + if (config == NULL) { + spa_log_error(pl->log, "delay: requires a config section"); + errno = EINVAL; + return NULL; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "delay:config must be an object"); + return NULL; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "max-delay")) { + if (spa_json_parse_float(val, len, &max_delay) <= 0) { + spa_log_error(pl->log, "delay:max-delay requires a number"); + return NULL; + } + } else { + spa_log_warn(pl->log, "delay: ignoring config key: '%s'", key); + } + } + if (max_delay <= 0.0f) + max_delay = 1.0f; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->rate = SampleRate; + impl->buffer_samples = SPA_ROUND_UP_N((uint32_t)(max_delay * impl->rate), 64); + spa_log_info(impl->log, "max-delay:%f seconds rate:%lu samples:%d", max_delay, impl->rate, impl->buffer_samples); + + impl->buffer = calloc(impl->buffer_samples * 2 + 64, sizeof(float)); + if (impl->buffer == NULL) { + delay_cleanup(impl); + return NULL; + } + return impl; +} + +static void delay_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct delay_impl *impl = Instance; + if (Port > 2) + return; + impl->port[Port] = DataLocation; +} + +static void delay_run(void * Instance, unsigned long SampleCount) +{ + struct delay_impl *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + float delay = impl->port[2][0]; + + if (in == NULL || out == NULL) + return; + + if (delay != impl->delay) { + impl->delay_samples = SPA_CLAMP((uint32_t)(delay * impl->rate), 0u, impl->buffer_samples-1); + impl->delay = delay; + } + spa_fga_dsp_delay(impl->dsp, impl->buffer, &impl->ptr, impl->buffer_samples, + impl->delay_samples, out, in, SampleCount); +} + +static struct spa_fga_port delay_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Delay (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = 0.0f, .max = 100.0f + }, +}; + +static const struct spa_fga_descriptor delay_desc = { + .name = "delay", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = 3, + .ports = delay_ports, + + .instantiate = delay_instantiate, + .connect_port = delay_connect_port, + .run = delay_run, + .cleanup = delay_cleanup, +}; + +/* invert */ +static void invert_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + unsigned long n; + for (n = 0; n < SampleCount; n++) + out[n] = -in[n]; +} + +static struct spa_fga_port invert_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor invert_desc = { + .name = "invert", + + .n_ports = 2, + .ports = invert_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = invert_run, + .cleanup = builtin_cleanup, +}; + +/* clamp */ +static void clamp_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float min = impl->port[4][0], max = impl->port[5][0]; + float *in = impl->port[1], *out = impl->port[0]; + float *ctrl = impl->port[3], *notify = impl->port[2]; + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) + out[n] = SPA_CLAMPF(in[n], min, max); + } + if (ctrl != NULL && notify != NULL) + notify[0] = SPA_CLAMPF(ctrl[0], min, max); +} + +static struct spa_fga_port clamp_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 4, + .name = "Min", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -100.0f, .max = 100.0f + }, + { .index = 5, + .name = "Max", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -100.0f, .max = 100.0f + }, +}; + +static const struct spa_fga_descriptor clamp_desc = { + .name = "clamp", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(clamp_ports), + .ports = clamp_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = clamp_run, + .cleanup = builtin_cleanup, +}; + +/* linear */ +static void linear_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float mult = impl->port[4][0], add = impl->port[5][0]; + float *in = impl->port[1], *out = impl->port[0]; + float *ctrl = impl->port[3], *notify = impl->port[2]; + + if (in != NULL && out != NULL) + spa_fga_dsp_linear(impl->dsp, out, in, mult, add, SampleCount); + + if (ctrl != NULL && notify != NULL) + notify[0] = ctrl[0] * mult + add; +} + +static struct spa_fga_port linear_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 4, + .name = "Mult", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -10.0f, .max = 10.0f + }, + { .index = 5, + .name = "Add", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f + }, +}; + +static const struct spa_fga_descriptor linear_desc = { + .name = "linear", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(linear_ports), + .ports = linear_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = linear_run, + .cleanup = builtin_cleanup, +}; + + +/* reciprocal */ +static void recip_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + float *ctrl = impl->port[3], *notify = impl->port[2]; + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) { + if (in[0] == 0.0f) + out[n] = 0.0f; + else + out[n] = 1.0f / in[n]; + } + } + if (ctrl != NULL && notify != NULL) { + if (ctrl[0] == 0.0f) + notify[0] = 0.0f; + else + notify[0] = 1.0f / ctrl[0]; + } +} + +static struct spa_fga_port recip_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, +}; + +static const struct spa_fga_descriptor recip_desc = { + .name = "recip", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(recip_ports), + .ports = recip_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = recip_run, + .cleanup = builtin_cleanup, +}; + +/* exp */ +static void exp_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float base = impl->port[4][0]; + float *in = impl->port[1], *out = impl->port[0]; + float *ctrl = impl->port[3], *notify = impl->port[2]; + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) + out[n] = powf(base, in[n]); + } + if (ctrl != NULL && notify != NULL) + notify[0] = powf(base, ctrl[0]); +} + +static struct spa_fga_port exp_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 4, + .name = "Base", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = (float)M_E, .min = -10.0f, .max = 10.0f + }, +}; + +static const struct spa_fga_descriptor exp_desc = { + .name = "exp", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(exp_ports), + .ports = exp_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = exp_run, + .cleanup = builtin_cleanup, +}; + +/* log */ +static void log_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float base = impl->port[4][0]; + float m1 = impl->port[5][0]; + float m2 = impl->port[6][0]; + float *in = impl->port[1], *out = impl->port[0]; + float *ctrl = impl->port[3], *notify = impl->port[2]; + float lb = log2f(base); + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) + out[n] = m2 * log2f(fabsf(in[n] * m1)) / lb; + } + if (ctrl != NULL && notify != NULL) + notify[0] = m2 * log2f(fabsf(ctrl[0] * m1)) / lb; +} + +static struct spa_fga_port log_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 4, + .name = "Base", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = (float)M_E, .min = 2.0f, .max = 100.0f + }, + { .index = 5, + .name = "M1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -10.0f, .max = 10.0f + }, + { .index = 6, + .name = "M2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -10.0f, .max = 10.0f + }, +}; + +static const struct spa_fga_descriptor log_desc = { + .name = "log", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(log_ports), + .ports = log_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = log_run, + .cleanup = builtin_cleanup, +}; + +/* mult */ +static void mult_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + int i, n_src = 0; + float *out = impl->port[0]; + const float *src[8]; + + if (out == NULL) + return; + + for (i = 0; i < 8; i++) { + float *in = impl->port[1+i]; + + if (in == NULL) + continue; + + src[n_src++] = in; + } + spa_fga_dsp_mult(impl->dsp, out, src, n_src, SampleCount); +} + +static struct spa_fga_port mult_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 3, + .name = "In 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 4, + .name = "In 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 5, + .name = "In 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 6, + .name = "In 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 7, + .name = "In 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 8, + .name = "In 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor mult_desc = { + .name = "mult", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(mult_ports), + .ports = mult_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = mult_run, + .cleanup = builtin_cleanup, +}; + +#define M_PI_M2f (float)(M_PI+M_PI) + +/* sine */ +static void sine_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *out = impl->port[0]; + float *notify = impl->port[1]; + float freq = impl->port[2][0]; + float ampl = impl->port[3][0]; + float offs = impl->port[5][0]; + unsigned long n; + + for (n = 0; n < SampleCount; n++) { + if (out != NULL) + out[n] = sinf(impl->accum) * ampl + offs; + if (notify != NULL && n == 0) + notify[0] = sinf(impl->accum) * ampl + offs; + + impl->accum += M_PI_M2f * freq / impl->rate; + if (impl->accum >= M_PI_M2f) + impl->accum -= M_PI_M2f; + } +} + +static struct spa_fga_port sine_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 2, + .name = "Freq", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 440.0f, .min = 0.0f, .max = 1000000.0f + }, + { .index = 3, + .name = "Ampl", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0, .min = 0.0f, .max = 10.0f + }, + { .index = 4, + .name = "Phase", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = (float)-M_PI, .max = (float)M_PI + }, + { .index = 5, + .name = "Offset", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -10.0f, .max = 10.0f + }, +}; + +static const struct spa_fga_descriptor sine_desc = { + .name = "sine", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(sine_ports), + .ports = sine_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = sine_run, + .cleanup = builtin_cleanup, +}; + +#define PARAM_EQ_MAX 64 +struct param_eq_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[8*2]; + + uint32_t n_bq; + struct biquad bq[PARAM_EQ_MAX * 8]; +}; + +static int load_eq_bands(struct plugin *pl, const char *filename, int rate, + struct biquad *bq, uint32_t max_bq, uint32_t *n_bq) +{ + FILE *f = NULL; + char *line = NULL; + ssize_t nread; + size_t linelen; + uint32_t n = 0; + char filter_type[4]; + char filter[4]; + char freq[9], q[7], gain[7]; + float vf, vg, vq; + int res = 0; + + if ((f = fopen(filename, "r")) == NULL) { + res = -errno; + spa_log_error(pl->log, "failed to open param_eq file '%s': %m", filename); + goto exit; + } + /* + * Read the Preamp gain line. + * Example: Preamp: -6.8 dB + * + * When a pre-amp gain is required, which is usually the case when + * applying EQ, we need to modify the first EQ band to apply a + * bq_highshelf filter at frequency 0 Hz with the provided negative + * gain. + * + * Pre-amp gain is always negative to offset the effect of possible + * clipping introduced by the amplification resulting from EQ. + */ + nread = getline(&line, &linelen, f); + if (nread != -1 && sscanf(line, "%*s %6s %*s", gain) == 1) { + if (spa_json_parse_float(gain, strlen(gain), &vg)) { + spa_log_info(pl->log, "%d %s freq:0 q:1.0 gain:%f", n, + bq_name_from_type(BQ_HIGHSHELF), vg); + biquad_set(&bq[n++], BQ_HIGHSHELF, 0.0f, 1.0f, vg); + } + } + /* Read the filter bands */ + while ((nread = getline(&line, &linelen, f)) != -1) { + if (n == PARAM_EQ_MAX) { + res = -ENOSPC; + goto exit; + } + /* + * On field widths: + * - filter can be ON or OFF + * - filter type can be PK, LSC, HSC + * - freq can be at most 5 decimal digits + * - gain can be -xy.z + * - Q can be x.y00 + * + * Use a field width of 6 for gain and Q to account for any + * possible zeros. + */ + if (sscanf(line, "%*s %*d: %3s %3s %*s %8s %*s %*s %6s %*s %*c %6s", + filter, filter_type, freq, gain, q) == 5) { + if (strcmp(filter, "ON") == 0) { + int type; + + if (spa_streq(filter_type, "PK")) + type = BQ_PEAKING; + else if (spa_streq(filter_type, "LSC")) + type = BQ_LOWSHELF; + else if (spa_streq(filter_type, "HSC")) + type = BQ_HIGHSHELF; + else + continue; + + if (spa_json_parse_float(freq, strlen(freq), &vf) && + spa_json_parse_float(gain, strlen(gain), &vg) && + spa_json_parse_float(q, strlen(q), &vq)) { + spa_log_info(pl->log, "%d %s freq:%f q:%f gain:%f", n, + bq_name_from_type(type), vf, vq, vg); + biquad_set(&bq[n++], type, vf * 2.0f / rate, vq, vg); + } + } + } + } + *n_bq = n; +exit: + if (f) + fclose(f); + return res; +} + + + +/* + * [ + * { type=bq_peaking freq=21 gain=6.7 q=1.100 } + * { type=bq_peaking freq=85 gain=6.9 q=3.000 } + * { type=bq_peaking freq=110 gain=-2.6 q=2.700 } + * { type=bq_peaking freq=210 gain=5.9 q=2.100 } + * { type=bq_peaking freq=710 gain=-1.0 q=0.600 } + * { type=bq_peaking freq=1600 gain=2.3 q=2.700 } + * ] + */ +static int parse_filters(struct plugin *pl, struct spa_json *iter, int rate, + struct biquad *bq, uint32_t max_bq, uint32_t *n_bq) +{ + struct spa_json it[1]; + const char *val; + char key[256]; + char type_str[17]; + int len; + uint32_t n = 0; + + while (spa_json_enter_object(iter, &it[0]) > 0) { + float freq = 0.0f, gain = 0.0f, q = 1.0f; + int type = BQ_NONE; + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "type")) { + if (spa_json_parse_stringn(val, len, type_str, sizeof(type_str)) <= 0) { + spa_log_error(pl->log, "param_eq:type requires a string"); + return -EINVAL; + } + type = bq_type_from_name(type_str); + } + else if (spa_streq(key, "freq")) { + if (spa_json_parse_float(val, len, &freq) <= 0) { + spa_log_error(pl->log, "param_eq:rate requires a number"); + return -EINVAL; + } + } + else if (spa_streq(key, "q")) { + if (spa_json_parse_float(val, len, &q) <= 0) { + spa_log_error(pl->log, "param_eq:q requires a float"); + return -EINVAL; + } + } + else if (spa_streq(key, "gain")) { + if (spa_json_parse_float(val, len, &gain) <= 0) { + spa_log_error(pl->log, "param_eq:gain requires a float"); + return -EINVAL; + } + } + else { + spa_log_warn(pl->log, "param_eq: ignoring filter key: '%s'", key); + } + } + if (n == max_bq) + return -ENOSPC; + + spa_log_info(pl->log, "%d %s freq:%f q:%f gain:%f", n, + bq_name_from_type(type), freq, q, gain); + biquad_set(&bq[n++], type, freq * 2 / rate, q, gain); + } + *n_bq = n; + return 0; +} + +/* + * { + * filename = "...", + * filenameX = "...", # to load channel X + * filters = [ ... ] + * filtersX = [ ... ] # to load channel X + * } + */ +static void *param_eq_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct spa_json it[3]; + const char *val; + char key[256], filename[PATH_MAX]; + int len, res; + struct param_eq_impl *impl; + uint32_t i, n_bq = 0; + + if (config == NULL) { + spa_log_error(pl->log, "param_eq: requires a config section"); + errno = EINVAL; + return NULL; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "param_eq: config must be an object"); + return NULL; + } + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->rate = SampleRate; + for (i = 0; i < SPA_N_ELEMENTS(impl->bq); i++) + biquad_set(&impl->bq[i], BQ_NONE, 0.0f, 0.0f, 0.0f); + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + int32_t idx = 0; + struct biquad *bq = impl->bq; + + if (spa_strstartswith(key, "filename")) { + if (spa_json_parse_stringn(val, len, filename, sizeof(filename)) <= 0) { + spa_log_error(impl->log, "param_eq: filename requires a string"); + goto error; + } + if (spa_atoi32(key+8, &idx, 0)) + bq = &impl->bq[(SPA_CLAMP(idx, 1, 8) - 1) * PARAM_EQ_MAX]; + + res = load_eq_bands(pl, filename, impl->rate, bq, PARAM_EQ_MAX, &n_bq); + if (res < 0) { + spa_log_error(impl->log, "param_eq: failed to parse configuration from '%s'", filename); + goto error; + } + spa_log_info(impl->log, "loaded %d biquads for channel %d from %s", n_bq, idx, filename); + impl->n_bq = SPA_MAX(impl->n_bq, n_bq); + } + else if (spa_strstartswith(key, "filters")) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "param_eq:filters require an array"); + goto error; + } + spa_json_enter(&it[0], &it[1]); + + if (spa_atoi32(key+7, &idx, 0)) + bq = &impl->bq[(SPA_CLAMP(idx, 1, 8) - 1) * PARAM_EQ_MAX]; + + res = parse_filters(pl, &it[1], impl->rate, bq, PARAM_EQ_MAX, &n_bq); + if (res < 0) { + spa_log_error(impl->log, "param_eq: failed to parse configuration"); + goto error; + } + spa_log_info(impl->log, "parsed %d biquads for channel %d", n_bq, idx); + impl->n_bq = SPA_MAX(impl->n_bq, n_bq); + } else { + spa_log_warn(impl->log, "param_eq: ignoring config key: '%s'", key); + } + if (idx == 0) { + for (i = 1; i < 8; i++) + memcpy(&impl->bq[i*PARAM_EQ_MAX], impl->bq, + sizeof(struct biquad) * PARAM_EQ_MAX); + } + } + return impl; +error: + free(impl); + return NULL; +} + +static void param_eq_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct param_eq_impl *impl = Instance; + impl->port[Port] = DataLocation; +} + +static void param_eq_run(void * Instance, unsigned long SampleCount) +{ + struct param_eq_impl *impl = Instance; + spa_fga_dsp_biquad_run(impl->dsp, impl->bq, impl->n_bq, PARAM_EQ_MAX, + &impl->port[8], (const float**)impl->port, 8, SampleCount); +} + +static struct spa_fga_port param_eq_ports[] = { + { .index = 0, + .name = "In 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 3, + .name = "In 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 4, + .name = "In 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 5, + .name = "In 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 6, + .name = "In 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 7, + .name = "In 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 8, + .name = "Out 1", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 9, + .name = "Out 2", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 10, + .name = "Out 3", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 11, + .name = "Out 4", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 12, + .name = "Out 5", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 13, + .name = "Out 6", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 14, + .name = "Out 7", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 15, + .name = "Out 8", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor param_eq_desc = { + .name = "param_eq", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(param_eq_ports), + .ports = param_eq_ports, + + .instantiate = param_eq_instantiate, + .connect_port = param_eq_connect_port, + .run = param_eq_run, + .cleanup = free, +}; + +/** max */ +static void max_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *out = impl->port[0], *in1 = impl->port[1], *in2 = impl->port[2]; + unsigned long n; + + if (out == NULL) + return; + + if (in1 != NULL && in2 != NULL) { + for (n = 0; n < SampleCount; n++) + out[n] = SPA_MAX(in1[n], in2[n]); + } else if (in1 != NULL) { + for (n = 0; n < SampleCount; n++) + out[n] = in1[n]; + } else if (in2 != NULL) { + for (n = 0; n < SampleCount; n++) + out[n] = in2[n]; + } else { + for (n = 0; n < SampleCount; n++) + out[n] = 0.0f; + } +} + +static struct spa_fga_port max_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 1, + .name = "In 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + } +}; + +static const struct spa_fga_descriptor max_desc = { + .name = "max", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(max_ports), + .ports = max_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = max_run, + .cleanup = builtin_cleanup, +}; + +/* DC blocking */ +struct dcblock { + float xm1; + float ym1; +}; + +struct dcblock_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[17]; + + struct dcblock dc[8]; +}; + +static void *dcblock_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct dcblock_impl *impl; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->rate = SampleRate; + return impl; +} + +static void dcblock_run_n(struct dcblock dc[], float *dst[], const float *src[], + uint32_t n_src, float R, uint32_t n_samples) +{ + float x, y; + uint32_t i, n; + + for (i = 0; i < n_src; i++) { + const float *in = src[i]; + float *out = dst[i]; + float xm1 = dc[i].xm1; + float ym1 = dc[i].ym1; + + if (out == NULL || in == NULL) + continue; + + for (n = 0; n < n_samples; n++) { + x = in[n]; + y = x - xm1 + R * ym1; + xm1 = x; + ym1 = y; + out[n] = y; + } + dc[i].xm1 = xm1; + dc[i].ym1 = ym1; + } +} + +static void dcblock_run(void * Instance, unsigned long SampleCount) +{ + struct dcblock_impl *impl = Instance; + float R = impl->port[16][0]; + dcblock_run_n(impl->dc, &impl->port[8], (const float**)&impl->port[0], 8, + R, SampleCount); +} + +static void dcblock_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct dcblock_impl *impl = Instance; + impl->port[Port] = DataLocation; +} + +static struct spa_fga_port dcblock_ports[] = { + { .index = 0, + .name = "In 1", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In 2", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 3, + .name = "In 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 4, + .name = "In 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 5, + .name = "In 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 6, + .name = "In 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 7, + .name = "In 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 8, + .name = "Out 1", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 9, + .name = "Out 2", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 10, + .name = "Out 3", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 11, + .name = "Out 4", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 12, + .name = "Out 5", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 13, + .name = "Out 6", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 14, + .name = "Out 7", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 15, + .name = "Out 8", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 16, + .name = "R", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.995f, .min = 0.0f, .max = 1.0f + }, +}; + +static const struct spa_fga_descriptor dcblock_desc = { + .name = "dcblock", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(dcblock_ports), + .ports = dcblock_ports, + + .instantiate = dcblock_instantiate, + .connect_port = dcblock_connect_port, + .run = dcblock_run, + .cleanup = free, +}; + +/* ramp */ +static struct spa_fga_port ramp_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Start", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 2, + .name = "Stop", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Current", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 4, + .name = "Duration (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, +}; + +static void ramp_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *out = impl->port[0]; + float start = impl->port[1][0]; + float stop = impl->port[2][0], last; + float *current = impl->port[3]; + float duration = impl->port[4][0]; + float inc = (stop - start) / (duration * impl->rate); + uint32_t n; + + last = stop; + if (inc < 0.f) + SPA_SWAP(start, stop); + + if (out != NULL) { + if (impl->accum == last) { + for (n = 0; n < SampleCount; n++) + out[n] = last; + } else { + for (n = 0; n < SampleCount; n++) { + out[n] = impl->accum; + impl->accum = SPA_CLAMP(impl->accum + inc, start, stop); + } + } + } else { + impl->accum = SPA_CLAMP(impl->accum + SampleCount * inc, start, stop); + } + if (current) + current[0] = impl->accum; +} + +static const struct spa_fga_descriptor ramp_desc = { + .name = "ramp", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(ramp_ports), + .ports = ramp_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = ramp_run, + .cleanup = builtin_cleanup, +}; + +/* abs */ +static void abs_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) { + out[n] = SPA_ABS(in[n]); + } + } +} + +static struct spa_fga_port abs_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor abs_desc = { + .name = "abs", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(abs_ports), + .ports = abs_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = abs_run, + .cleanup = builtin_cleanup, +}; + +/* sqrt */ +static void sqrt_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[1], *out = impl->port[0]; + + if (in != NULL && out != NULL) { + unsigned long n; + for (n = 0; n < SampleCount; n++) { + if (in[n] <= 0.0f) + out[n] = 0.0f; + else + out[n] = sqrtf(in[n]); + } + } +} + +static struct spa_fga_port sqrt_ports[] = { + { .index = 0, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor sqrt_desc = { + .name = "sqrt", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(sqrt_ports), + .ports = sqrt_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = sqrt_run, + .cleanup = builtin_cleanup, +}; + +static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) +{ + switch(Index) { + case 0: + return &mixer_desc; + case 1: + return &bq_lowpass_desc; + case 2: + return &bq_highpass_desc; + case 3: + return &bq_bandpass_desc; + case 4: + return &bq_lowshelf_desc; + case 5: + return &bq_highshelf_desc; + case 6: + return &bq_peaking_desc; + case 7: + return &bq_notch_desc; + case 8: + return &bq_allpass_desc; + case 9: + return ©_desc; + case 10: + return &convolve_desc; + case 11: + return &delay_desc; + case 12: + return &invert_desc; + case 13: + return &bq_raw_desc; + case 14: + return &clamp_desc; + case 15: + return &linear_desc; + case 16: + return &recip_desc; + case 17: + return &exp_desc; + case 18: + return &log_desc; + case 19: + return &mult_desc; + case 20: + return &sine_desc; + case 21: + return ¶m_eq_desc; + case 22: + return &max_desc; + case 23: + return &dcblock_desc; + case 24: + return &ramp_desc; + case 25: + return &abs_desc; + case 26: + return &sqrt_desc; + } + return NULL; +} + +static const struct spa_fga_descriptor *builtin_plugin_make_desc(void *plugin, const char *name) +{ + unsigned long i; + for (i = 0; ;i++) { + const struct spa_fga_descriptor *d = builtin_descriptor(i); + if (d == NULL) + break; + if (spa_streq(d->name, name)) + return d; + } + return NULL; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = builtin_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP); + + for (uint32_t i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "filter.graph.audio.dsp")) + sscanf(s, "pointer:%p", &impl->dsp); + } + if (impl->dsp == NULL) { + spa_log_error(impl->log, "%p: could not find DSP functions", impl); + return -EINVAL; + } + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_plugin_builtin_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.builtin", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_plugin_builtin_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/convolver.c b/spa/plugins/filter-graph/convolver.c new file mode 100644 index 0000000..e5bd47b --- /dev/null +++ b/spa/plugins/filter-graph/convolver.c @@ -0,0 +1,396 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2017 HiFi-LoFi */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ +/* Adapted from https://github.com/HiFi-LoFi/FFTConvolver */ + +#include "convolver.h" + +#include + +#include + +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 int next_power_of_two(int val) +{ + int r = 1; + while (r < val) + r *= 2; + return r; +} + +static void convolver1_reset(struct spa_fga_dsp *dsp, struct convolver1 *conv) +{ + int i; + for (i = 0; i < conv->segCount; i++) + spa_fga_dsp_fft_memclear(dsp, conv->segments[i], conv->fftComplexSize, false); + spa_fga_dsp_fft_memclear(dsp, conv->overlap, conv->blockSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer, conv->segSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->pre_mult, conv->fftComplexSize, false); + spa_fga_dsp_fft_memclear(dsp, conv->conv, conv->fftComplexSize, false); + conv->inputBufferFill = 0; + conv->current = 0; +} + +static void convolver1_free(struct spa_fga_dsp *dsp, struct convolver1 *conv) +{ + int i; + for (i = 0; i < conv->segCount; i++) { + if (conv->segments) + spa_fga_dsp_fft_memfree(dsp, conv->segments[i]); + if (conv->segmentsIr) + spa_fga_dsp_fft_memfree(dsp, conv->segmentsIr[i]); + } + if (conv->fft) + spa_fga_dsp_fft_free(dsp, conv->fft); + if (conv->ifft) + spa_fga_dsp_fft_free(dsp, conv->ifft); + if (conv->fft_buffer) + spa_fga_dsp_fft_memfree(dsp, conv->fft_buffer); + free(conv->segments); + free(conv->segmentsIr); + spa_fga_dsp_fft_memfree(dsp, conv->pre_mult); + spa_fga_dsp_fft_memfree(dsp, conv->conv); + spa_fga_dsp_fft_memfree(dsp, conv->overlap); + spa_fga_dsp_fft_memfree(dsp, conv->inputBuffer); + free(conv); +} + +static struct convolver1 *convolver1_new(struct spa_fga_dsp *dsp, int block, const float *ir, int irlen) +{ + struct convolver1 *conv; + int i; + + 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 = spa_fga_dsp_fft_new(dsp, conv->segSize, true); + if (conv->fft == NULL) + goto error; + conv->ifft = spa_fga_dsp_fft_new(dsp, conv->segSize, true); + if (conv->ifft == NULL) + goto error; + + conv->fft_buffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); + if (conv->fft_buffer == NULL) + goto error; + + conv->segments = calloc(conv->segCount, sizeof(float*)); + conv->segmentsIr = calloc(conv->segCount, sizeof(float*)); + if (conv->segments == NULL || conv->segmentsIr == NULL) + goto error; + + for (i = 0; i < conv->segCount; i++) { + int left = irlen - (i * conv->blockSize); + int copy = SPA_MIN(conv->blockSize, left); + + conv->segments[i] = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); + conv->segmentsIr[i] = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); + if (conv->segments[i] == NULL || conv->segmentsIr[i] == NULL) + goto error; + + spa_fga_dsp_copy(dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy); + if (copy < conv->segSize) + spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer + copy, conv->segSize - copy, true); + + spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->fft_buffer, conv->segmentsIr[i]); + } + conv->pre_mult = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); + conv->conv = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); + conv->overlap = spa_fga_dsp_fft_memalloc(dsp, conv->blockSize, true); + conv->inputBuffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); + if (conv->pre_mult == NULL || conv->conv == NULL || conv->overlap == NULL || + conv->inputBuffer == NULL) + goto error; + conv->scale = 1.0f / conv->segSize; + convolver1_reset(dsp, conv); + + return conv; +error: + convolver1_free(dsp, conv); + return NULL; +} + +static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, const float *input, float *output, int len) +{ + int i, processed = 0; + + if (conv == NULL || conv->segCount == 0) { + spa_fga_dsp_fft_memclear(dsp, output, len, true); + return len; + } + + while (processed < len) { + const int processing = SPA_MIN(len - processed, conv->blockSize - conv->inputBufferFill); + const int inputBufferPos = conv->inputBufferFill; + + spa_fga_dsp_copy(dsp, conv->inputBuffer + inputBufferPos, input + processed, processing); + if (inputBufferPos == 0 && processing < conv->blockSize) + spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer + processing, conv->blockSize - processing, true); + + spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->inputBuffer, conv->segments[conv->current]); + + if (conv->segCount > 1) { + if (conv->inputBufferFill == 0) { + int indexAudio = (conv->current + 1) % conv->segCount; + + spa_fga_dsp_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; + + spa_fga_dsp_fft_cmuladd(dsp, conv->fft, + conv->pre_mult, + conv->pre_mult, + conv->segmentsIr[i], + conv->segments[indexAudio], + conv->fftComplexSize, conv->scale); + } + } + spa_fga_dsp_fft_cmuladd(dsp, conv->fft, + conv->conv, + conv->pre_mult, + conv->segments[conv->current], + conv->segmentsIr[0], + conv->fftComplexSize, conv->scale); + } else { + spa_fga_dsp_fft_cmul(dsp, conv->fft, + conv->conv, + conv->segments[conv->current], + conv->segmentsIr[0], + conv->fftComplexSize, conv->scale); + } + + spa_fga_dsp_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer); + + spa_fga_dsp_sum(dsp, output + processed, conv->fft_buffer + inputBufferPos, + conv->overlap + inputBufferPos, processing); + + conv->inputBufferFill += processing; + if (conv->inputBufferFill == conv->blockSize) { + conv->inputBufferFill = 0; + + spa_fga_dsp_copy(dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize); + + conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1); + } + + processed += processing; + } + return len; +} + +struct convolver +{ + struct spa_fga_dsp *dsp; + 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) +{ + struct spa_fga_dsp *dsp = conv->dsp; + + if (conv->headConvolver) + convolver1_reset(dsp, conv->headConvolver); + if (conv->tailConvolver0) { + convolver1_reset(dsp, conv->tailConvolver0); + spa_fga_dsp_fft_memclear(dsp, conv->tailOutput0, conv->tailBlockSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->tailPrecalculated0, conv->tailBlockSize, true); + } + if (conv->tailConvolver) { + convolver1_reset(dsp, conv->tailConvolver); + spa_fga_dsp_fft_memclear(dsp, conv->tailOutput, conv->tailBlockSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->tailPrecalculated, conv->tailBlockSize, true); + } + conv->tailInputFill = 0; + conv->precalculatedPos = 0; +} + +struct convolver *convolver_new(struct spa_fga_dsp *dsp, int head_block, int tail_block, const float *ir, int irlen) +{ + struct convolver *conv; + int head_ir_len; + + 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; + + conv->dsp = dsp; + + 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(dsp, conv->headBlockSize, ir, head_ir_len); + if (conv->headConvolver == NULL) + goto error; + + if (irlen > conv->tailBlockSize) { + int conv1IrLen = SPA_MIN(irlen - conv->tailBlockSize, conv->tailBlockSize); + conv->tailConvolver0 = convolver1_new(dsp, conv->headBlockSize, ir + conv->tailBlockSize, conv1IrLen); + conv->tailOutput0 = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true); + conv->tailPrecalculated0 = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true); + if (conv->tailConvolver0 == NULL || conv->tailOutput0 == NULL || + conv->tailPrecalculated0 == NULL) + goto error; + } + + if (irlen > 2 * conv->tailBlockSize) { + int tailIrLen = irlen - (2 * conv->tailBlockSize); + conv->tailConvolver = convolver1_new(dsp, conv->tailBlockSize, ir + (2 * conv->tailBlockSize), tailIrLen); + conv->tailOutput = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true); + conv->tailPrecalculated = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true); + if (conv->tailConvolver == NULL || conv->tailOutput == NULL || + conv->tailPrecalculated == NULL) + goto error; + } + + if (conv->tailConvolver0 || conv->tailConvolver) { + conv->tailInput = spa_fga_dsp_fft_memalloc(dsp, conv->tailBlockSize, true); + if (conv->tailInput == NULL) + goto error; + } + + convolver_reset(conv); + + return conv; +error: + convolver_free(conv); + return NULL; +} + +void convolver_free(struct convolver *conv) +{ + struct spa_fga_dsp *dsp = conv->dsp; + + if (conv->headConvolver) + convolver1_free(dsp, conv->headConvolver); + if (conv->tailConvolver0) + convolver1_free(dsp, conv->tailConvolver0); + if (conv->tailConvolver) + convolver1_free(dsp, conv->tailConvolver); + spa_fga_dsp_fft_memfree(dsp, conv->tailOutput0); + spa_fga_dsp_fft_memfree(dsp, conv->tailPrecalculated0); + spa_fga_dsp_fft_memfree(dsp, conv->tailOutput); + spa_fga_dsp_fft_memfree(dsp, conv->tailPrecalculated); + spa_fga_dsp_fft_memfree(dsp, conv->tailInput); + free(conv); +} + +int convolver_run(struct convolver *conv, const float *input, float *output, int length) +{ + struct spa_fga_dsp *dsp = conv->dsp; + + convolver1_run(dsp, 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) + spa_fga_dsp_sum(dsp, &output[processed], &output[processed], + &conv->tailPrecalculated0[conv->precalculatedPos], + processing); + if (conv->tailPrecalculated) + spa_fga_dsp_sum(dsp, &output[processed], &output[processed], + &conv->tailPrecalculated[conv->precalculatedPos], + processing); + conv->precalculatedPos += processing; + + spa_fga_dsp_copy(dsp, conv->tailInput + conv->tailInputFill, input + processed, processing); + conv->tailInputFill += processing; + + if (conv->tailPrecalculated0 && (conv->tailInputFill % conv->headBlockSize == 0)) { + int blockOffset = conv->tailInputFill - conv->headBlockSize; + convolver1_run(dsp, 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(dsp, 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/spa/plugins/filter-graph/convolver.h b/spa/plugins/filter-graph/convolver.h new file mode 100644 index 0000000..ad6139a --- /dev/null +++ b/spa/plugins/filter-graph/convolver.h @@ -0,0 +1,14 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "audio-dsp.h" + +struct convolver *convolver_new(struct spa_fga_dsp *dsp, int block, int tail, const float *ir, int irlen); +void convolver_free(struct convolver *conv); + +void convolver_reset(struct convolver *conv); +int convolver_run(struct convolver *conv, const float *input, float *output, int length); diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/ebur128_plugin.c new file mode 100644 index 0000000..92904e1 --- /dev/null +++ b/spa/plugins/filter-graph/ebur128_plugin.c @@ -0,0 +1,638 @@ +#include "config.h" + +#include + +#include +#include + +#include "audio-plugin.h" +#include "audio-dsp.h" + +#include + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + uint32_t quantum_limit; +}; + +enum { + PORT_IN_FL, + PORT_IN_FR, + PORT_IN_FC, + PORT_IN_UNUSED, + PORT_IN_SL, + PORT_IN_SR, + PORT_IN_DUAL_MONO, + + PORT_OUT_FL, + PORT_OUT_FR, + PORT_OUT_FC, + PORT_OUT_UNUSED, + PORT_OUT_SL, + PORT_OUT_SR, + PORT_OUT_DUAL_MONO, + + PORT_OUT_MOMENTARY, + PORT_OUT_SHORTTERM, + PORT_OUT_GLOBAL, + PORT_OUT_WINDOW, + PORT_OUT_RANGE, + PORT_OUT_PEAK, + PORT_OUT_TRUE_PEAK, + + PORT_MAX, + + PORT_IN_START = PORT_IN_FL, + PORT_OUT_START = PORT_OUT_FL, + PORT_NOTIFY_START = PORT_OUT_MOMENTARY, +}; + + +struct ebur128_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[PORT_MAX]; + + unsigned int max_history; + unsigned int max_window; + bool use_histogram; + + ebur128_state *st[7]; +}; + +static void * ebur128_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct ebur128_impl *impl; + struct spa_json it[1]; + const char *val; + char key[256]; + int len; + float f; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) { + errno = ENOMEM; + return NULL; + } + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->max_history = 10000; + impl->max_window = 0; + impl->rate = SampleRate; + + if (config == NULL) + return impl; + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "ebur128: expected object in config"); + errno = EINVAL; + goto error; + } + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "max-history")) { + if (spa_json_parse_float(val, len, &f) <= 0) { + spa_log_error(impl->log, "ebur128:max-history requires a number"); + errno = EINVAL; + goto error; + } + impl->max_history = (unsigned int) (f * 1000.0f); + } + else if (spa_streq(key, "max-window")) { + if (spa_json_parse_float(val, len, &f) <= 0) { + spa_log_error(impl->log, "ebur128:max-window requires a number"); + errno = EINVAL; + goto error; + } + impl->max_window = (unsigned int) (f * 1000.0f); + } + else if (spa_streq(key, "use-histogram")) { + if (spa_json_parse_bool(val, len, &impl->use_histogram) <= 0) { + spa_log_error(impl->log, "ebur128:use-histogram requires a boolean"); + errno = EINVAL; + goto error; + } + } else { + spa_log_warn(impl->log, "ebur128: unknown key %s", key); + } + } + return impl; +error: + free(impl); + return NULL; +} + +static void ebur128_run(void * Instance, unsigned long SampleCount) +{ + struct ebur128_impl *impl = Instance; + int i, c; + double value; + ebur128_state *st[7]; + + for (i = 0; i < 7; i++) { + float *in = impl->port[PORT_IN_START + i]; + float *out = impl->port[PORT_OUT_START + i]; + + st[i] = NULL; + if (in == NULL) + continue; + + st[i] = impl->st[i]; + if (st[i] != NULL) + ebur128_add_frames_float(st[i], in, SampleCount); + + if (out != NULL) + memcpy(out, in, SampleCount * sizeof(float)); + } + if (impl->port[PORT_OUT_MOMENTARY] != NULL) { + double sum = 0.0; + for (i = 0, c = 0; i < 7; i++) { + if (st[i] != NULL) { + ebur128_loudness_momentary(st[i], &value); + sum += value; + c++; + } + } + impl->port[PORT_OUT_MOMENTARY][0] = (float) (sum / c); + } + if (impl->port[PORT_OUT_SHORTTERM] != NULL) { + double sum = 0.0; + for (i = 0, c = 0; i < 7; i++) { + if (st[i] != NULL) { + ebur128_loudness_shortterm(st[i], &value); + sum += value; + c++; + } + } + impl->port[PORT_OUT_SHORTTERM][0] = (float) (sum / c); + } + if (impl->port[PORT_OUT_GLOBAL] != NULL) { + ebur128_loudness_global_multiple(st, 7, &value); + impl->port[PORT_OUT_GLOBAL][0] = (float)value; + } + if (impl->port[PORT_OUT_WINDOW] != NULL) { + double sum = 0.0; + for (i = 0, c = 0; i < 7; i++) { + if (st[i] != NULL) { + ebur128_loudness_window(st[i], impl->max_window, &value); + sum += value; + c++; + } + } + impl->port[PORT_OUT_WINDOW][0] = (float) (sum / c); + } + if (impl->port[PORT_OUT_RANGE] != NULL) { + ebur128_loudness_range_multiple(st, 7, &value); + impl->port[PORT_OUT_RANGE][0] = (float)value; + } + if (impl->port[PORT_OUT_PEAK] != NULL) { + double max = 0.0; + for (i = 0; i < 7; i++) { + if (st[i] != NULL) { + ebur128_sample_peak(st[i], i, &value); + max = SPA_MAX(max, value); + } + } + impl->port[PORT_OUT_PEAK][0] = (float) max; + } + if (impl->port[PORT_OUT_TRUE_PEAK] != NULL) { + double max = 0.0; + for (i = 0; i < 7; i++) { + if (st[i] != NULL) { + ebur128_true_peak(st[i], i, &value); + max = SPA_MAX(max, value); + } + } + impl->port[PORT_OUT_TRUE_PEAK][0] = (float) max; + } +} + +static void ebur128_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct ebur128_impl *impl = Instance; + if (Port < PORT_MAX) + impl->port[Port] = DataLocation; +} + +static void ebur128_cleanup(void * Instance) +{ + struct ebur128_impl *impl = Instance; + free(impl); +} + +static void ebur128_activate(void * Instance) +{ + struct ebur128_impl *impl = Instance; + unsigned long max_window; + int major, minor, patch; + int mode = 0, i; + int modes[] = { + EBUR128_MODE_M, + EBUR128_MODE_S, + EBUR128_MODE_I, + 0, + EBUR128_MODE_LRA, + EBUR128_MODE_SAMPLE_PEAK, + EBUR128_MODE_TRUE_PEAK, + }; + enum channel channels[] = { + EBUR128_LEFT, + EBUR128_RIGHT, + EBUR128_CENTER, + EBUR128_UNUSED, + EBUR128_LEFT_SURROUND, + EBUR128_RIGHT_SURROUND, + EBUR128_DUAL_MONO, + }; + + if (impl->use_histogram) + mode |= EBUR128_MODE_HISTOGRAM; + + /* check modes */ + for (i = 0; i < 7; i++) { + if (impl->port[PORT_NOTIFY_START + i] != NULL) + mode |= modes[i]; + } + + ebur128_get_version(&major, &minor, &patch); + max_window = impl->max_window; + if (major == 1 && minor == 2 && (patch == 5 || patch == 6)) + max_window = (max_window + 999) / 1000; + + for (i = 0; i < 7; i++) { + impl->st[i] = ebur128_init(1, impl->rate, mode); + if (impl->st[i]) { + ebur128_set_channel(impl->st[i], i, channels[i]); + ebur128_set_max_history(impl->st[i], impl->max_history); + ebur128_set_max_window(impl->st[i], max_window); + } + } +} + +static void ebur128_deactivate(void * Instance) +{ + struct ebur128_impl *impl = Instance; + int i; + + for (i = 0; i < 7; i++) { + if (impl->st[i] != NULL) + ebur128_destroy(&impl->st[i]); + } +} + +static struct spa_fga_port ebur128_ports[] = { + { .index = PORT_IN_FL, + .name = "In FL", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_FR, + .name = "In FR", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_FC, + .name = "In FC", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_UNUSED, + .name = "In UNUSED", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_SL, + .name = "In SL", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_SR, + .name = "In SR", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_IN_DUAL_MONO, + .name = "In DUAL MONO", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = PORT_OUT_FL, + .name = "Out FL", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_FR, + .name = "Out FR", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_FC, + .name = "Out FC", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_UNUSED, + .name = "Out UNUSED", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_SL, + .name = "Out SL", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_SR, + .name = "Out SR", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = PORT_OUT_DUAL_MONO, + .name = "Out DUAL MONO", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = PORT_OUT_MOMENTARY, + .name = "Momentary LUFS", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_SHORTTERM, + .name = "Shorttem LUFS", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_GLOBAL, + .name = "Global LUFS", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_WINDOW, + .name = "Window LUFS", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_RANGE, + .name = "Range LU", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_PEAK, + .name = "Peak", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = PORT_OUT_TRUE_PEAK, + .name = "True Peak", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, +}; + +static const struct spa_fga_descriptor ebur128_desc = { + .name = "ebur128", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .ports = ebur128_ports, + .n_ports = SPA_N_ELEMENTS(ebur128_ports), + + .instantiate = ebur128_instantiate, + .connect_port = ebur128_connect_port, + .activate = ebur128_activate, + .deactivate = ebur128_deactivate, + .run = ebur128_run, + .cleanup = ebur128_cleanup, +}; + +static struct spa_fga_port lufs2gain_ports[] = { + { .index = 0, + .name = "LUFS", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 1, + .name = "Gain", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 2, + .name = "Target LUFS", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = -23.0f, .min = -70.0f, .max = 0.0f + }, +}; + +struct lufs2gain_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[3]; +}; + +static void * lufs2gain_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct lufs2gain_impl *impl; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) { + errno = ENOMEM; + return NULL; + } + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + impl->rate = SampleRate; + + return impl; +} + +static void lufs2gain_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct lufs2gain_impl *impl = Instance; + if (Port < 3) + impl->port[Port] = DataLocation; +} + +static void lufs2gain_run(void * Instance, unsigned long SampleCount) +{ + struct lufs2gain_impl *impl = Instance; + float *in = impl->port[0]; + float *out = impl->port[1]; + float *target = impl->port[2]; + float gain; + + if (in == NULL || out == NULL || target == NULL) + return; + + if (isfinite(in[0])) { + float gaindB = target[0] - in[0]; + gain = powf(10.0f, gaindB / 20.0f); + } else { + gain = 1.0f; + } + out[0] = gain; +} + +static void lufs2gain_cleanup(void * Instance) +{ + struct lufs2gain_impl *impl = Instance; + free(impl); +} + +static const struct spa_fga_descriptor lufs2gain_desc = { + .name = "lufs2gain", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .ports = lufs2gain_ports, + .n_ports = SPA_N_ELEMENTS(lufs2gain_ports), + + .instantiate = lufs2gain_instantiate, + .connect_port = lufs2gain_connect_port, + .run = lufs2gain_run, + .cleanup = lufs2gain_cleanup, +}; + +static const struct spa_fga_descriptor * ebur128_descriptor(unsigned long Index) +{ + switch(Index) { + case 0: + return &ebur128_desc; + case 1: + return &lufs2gain_desc; + } + return NULL; +} + + +static const struct spa_fga_descriptor *ebur128_plugin_make_desc(void *plugin, const char *name) +{ + unsigned long i; + for (i = 0; ;i++) { + const struct spa_fga_descriptor *d = ebur128_descriptor(i); + if (d == NULL) + break; + if (spa_streq(d->name, name)) + return d; + } + return NULL; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = ebur128_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + impl->quantum_limit = 8192u; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP); + + for (uint32_t i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &impl->quantum_limit, 0); + if (spa_streq(k, "filter.graph.audio.dsp")) + sscanf(s, "pointer:%p", &impl->dsp); + } + if (impl->dsp == NULL) { + spa_log_error(impl->log, "%p: could not find DSP functions", impl); + return -EINVAL; + } + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_ebur128_plugin_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.ebur128", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_ebur128_plugin_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c new file mode 100644 index 0000000..bc9ff15 --- /dev/null +++ b/spa/plugins/filter-graph/filter-graph.c @@ -0,0 +1,2170 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audio-plugin.h" +#include "audio-dsp-impl.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.filter-graph"); + +#define MAX_HNDL 64 + +#define DEFAULT_RATE 48000 + +#define spa_filter_graph_emit(hooks,method,version,...) \ + spa_hook_list_call_simple(hooks, struct spa_filter_graph_events, \ + method, version, ##__VA_ARGS__) + +#define spa_filter_graph_emit_info(hooks,...) spa_filter_graph_emit(hooks,info, 0, __VA_ARGS__) +#define spa_filter_graph_emit_apply_props(hooks,...) spa_filter_graph_emit(hooks,apply_props, 0, __VA_ARGS__) +#define spa_filter_graph_emit_props_changed(hooks,...) spa_filter_graph_emit(hooks,props_changed, 0, __VA_ARGS__) + +struct plugin { + struct spa_list link; + struct impl *impl; + + int ref; + char type[256]; + char path[PATH_MAX]; + + struct spa_handle *hndl; + struct spa_fga_plugin *plugin; + struct spa_list descriptor_list; +}; + +struct descriptor { + struct spa_list link; + int ref; + struct plugin *plugin; + char label[256]; + + const struct spa_fga_descriptor *desc; + + uint32_t n_input; + uint32_t n_output; + uint32_t n_control; + uint32_t n_notify; + unsigned long *input; + unsigned long *output; + unsigned long *control; + unsigned long *notify; + float *default_control; +}; + +struct port { + struct spa_list link; + struct node *node; + + uint32_t idx; + unsigned long p; + + struct spa_list link_list; + uint32_t n_links; + uint32_t external; + + float control_data[MAX_HNDL]; + float *audio_data[MAX_HNDL]; + void *audio_mem[MAX_HNDL]; +}; + +struct node { + struct spa_list link; + struct graph *graph; + + struct descriptor *desc; + + char name[256]; + char *config; + + struct port *input_port; + struct port *output_port; + struct port *control_port; + struct port *notify_port; + + uint32_t n_hndl; + void *hndl[MAX_HNDL]; + + unsigned int n_deps; + unsigned int visited:1; + unsigned int disabled:1; + unsigned int control_changed:1; +}; + +struct link { + struct spa_list link; + + struct spa_list input_link; + struct spa_list output_link; + + struct port *output; + struct port *input; +}; + +struct graph_port { + const struct spa_fga_descriptor *desc; + void **hndl; + uint32_t port; + unsigned next:1; +}; + +struct graph_hndl { + const struct spa_fga_descriptor *desc; + void **hndl; +}; + +struct volume { + bool mute; + uint32_t n_volumes; + float volumes[SPA_AUDIO_MAX_CHANNELS]; + + uint32_t n_ports; + struct port *ports[SPA_AUDIO_MAX_CHANNELS]; + float min[SPA_AUDIO_MAX_CHANNELS]; + float max[SPA_AUDIO_MAX_CHANNELS]; +#define SCALE_LINEAR 0 +#define SCALE_CUBIC 1 + int scale[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct graph { + struct impl *impl; + + struct spa_list node_list; + struct spa_list link_list; + + uint32_t n_input; + struct graph_port *input; + + uint32_t n_output; + struct graph_port *output; + + uint32_t n_hndl; + struct graph_hndl *hndl; + + uint32_t n_control; + struct port **control_port; + + struct volume volume[2]; + + unsigned activated:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_filter_graph filter_graph; + struct spa_hook_list hooks; + + struct spa_log *log; + struct spa_cpu *cpu; + struct spa_fga_dsp *dsp; + struct spa_plugin_loader *loader; + + uint64_t info_all; + struct spa_filter_graph_info info; + + struct graph graph; + + uint32_t quantum_limit; + uint32_t max_align; + long unsigned rate; + + struct spa_list plugin_list; + + float *silence_data; + float *discard_data; +}; + +static void emit_filter_graph_info(struct impl *impl, bool full) +{ + uint64_t old = full ? impl->info.change_mask : 0; + + if (full) + impl->info.change_mask = impl->info_all; + if (impl->info.change_mask || full) { + spa_filter_graph_emit_info(&impl->hooks, &impl->info); + impl->info.change_mask = old; + } +} +static int +impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_filter_graph_events *events, + void *data) +{ + struct impl *impl = object; + struct spa_hook_list save; + + spa_log_trace(impl->log, "%p: add listener %p", impl, listener); + spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); + + emit_filter_graph_info(impl, true); + + spa_hook_list_join(&impl->hooks, &save); + + return 0; +} + +static int impl_process(void *object, + const void *in[], void *out[], uint32_t n_samples) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + uint32_t i, j, n_hndl = graph->n_hndl; + struct graph_port *port; + + for (i = 0, j = 0; i < impl->info.n_inputs; i++) { + while (j < graph->n_input) { + port = &graph->input[j++]; + if (port->desc && in[i]) + port->desc->connect_port(*port->hndl, port->port, (float*)in[i]); + if (!port->next) + break; + } + } + for (i = 0; i < impl->info.n_outputs; i++) { + if (out[i] == NULL) + continue; + + port = i < graph->n_output ? &graph->output[i] : NULL; + + if (port && port->desc) + port->desc->connect_port(*port->hndl, port->port, out[i]); + else + memset(out[i], 0, n_samples * sizeof(float)); + } + for (i = 0; i < n_hndl; i++) { + struct graph_hndl *hndl = &graph->hndl[i]; + hndl->desc->run(*hndl->hndl, n_samples); + } + return 0; +} + +static float get_default(struct impl *impl, struct descriptor *desc, uint32_t p) +{ + struct spa_fga_port *port = &desc->desc->ports[p]; + return port->def; +} + +static struct node *find_node(struct graph *graph, const char *name) +{ + struct node *node; + spa_list_for_each(node, &graph->node_list, link) { + if (spa_streq(node->name, name)) + return node; + } + return NULL; +} + +/* find a port by name. Valid syntax is: + * ":" + * ":" + * "" + * "" + * When no node_name is given, the port is assumed in the current node. */ +static struct port *find_port(struct node *node, const char *name, int descriptor) +{ + char *col, *node_name, *port_name, *str; + struct port *ports; + const struct spa_fga_descriptor *d; + uint32_t i, n_ports, port_id = SPA_ID_INVALID; + + str = strdupa(name); + col = strchr(str, ':'); + if (col != NULL) { + struct node *find; + node_name = str; + port_name = col + 1; + *col = '\0'; + find = find_node(node->graph, node_name); + if (find == NULL) { + /* it's possible that the : is part of the port name, + * try again without splitting things up. */ + *col = ':'; + col = NULL; + } else { + node = find; + } + } + if (col == NULL) { + node_name = node->name; + port_name = str; + } + if (node == NULL) + return NULL; + + if (!spa_atou32(port_name, &port_id, 0)) + port_id = SPA_ID_INVALID; + + if (SPA_FGA_IS_PORT_INPUT(descriptor)) { + if (SPA_FGA_IS_PORT_CONTROL(descriptor)) { + ports = node->control_port; + n_ports = node->desc->n_control; + } else { + ports = node->input_port; + n_ports = node->desc->n_input; + } + } else if (SPA_FGA_IS_PORT_OUTPUT(descriptor)) { + if (SPA_FGA_IS_PORT_CONTROL(descriptor)) { + ports = node->notify_port; + n_ports = node->desc->n_notify; + } else { + ports = node->output_port; + n_ports = node->desc->n_output; + } + } else + return NULL; + + d = node->desc->desc; + for (i = 0; i < n_ports; i++) { + struct port *port = &ports[i]; + if (i == port_id || + spa_streq(d->ports[port->p].name, port_name)) + return port; + } + return NULL; +} + +static int impl_enum_prop_info(void *object, uint32_t idx, struct spa_pod_builder *b, + struct spa_pod **param) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + struct spa_pod *pod; + struct spa_pod_frame f[2]; + struct port *port; + struct node *node; + struct descriptor *desc; + const struct spa_fga_descriptor *d; + struct spa_fga_port *p; + float def, min, max; + char name[512]; + uint32_t rate = impl->rate ? impl->rate : DEFAULT_RATE; + + if (idx >= graph->n_control) + return 0; + + port = graph->control_port[idx]; + node = port->node; + desc = node->desc; + d = desc->desc; + p = &d->ports[port->p]; + + if (p->hint & SPA_FGA_HINT_SAMPLE_RATE) { + def = p->def * rate; + min = p->min * rate; + max = p->max * rate; + } else { + def = p->def; + min = p->min; + max = p->max; + } + + if (node->name[0] != '\0') + snprintf(name, sizeof(name), "%s:%s", node->name, p->name); + else + snprintf(name, sizeof(name), "%s", p->name); + + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add (b, + SPA_PROP_INFO_name, SPA_POD_String(name), + 0); + spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0); + if (p->hint & SPA_FGA_HINT_BOOLEAN) { + if (min == max) { + spa_pod_builder_bool(b, def <= 0.0f ? false : true); + } else { + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + spa_pod_builder_bool(b, def <= 0.0f ? false : true); + spa_pod_builder_bool(b, false); + spa_pod_builder_bool(b, true); + spa_pod_builder_pop(b, &f[1]); + } + } else if (p->hint & SPA_FGA_HINT_INTEGER) { + if (min == max) { + spa_pod_builder_int(b, (int32_t)def); + } else { + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0); + spa_pod_builder_int(b, (int32_t)def); + spa_pod_builder_int(b, (int32_t)min); + spa_pod_builder_int(b, (int32_t)max); + spa_pod_builder_pop(b, &f[1]); + } + } else { + if (min == max) { + spa_pod_builder_float(b, def); + } else { + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Range, 0); + spa_pod_builder_float(b, def); + spa_pod_builder_float(b, min); + spa_pod_builder_float(b, max); + spa_pod_builder_pop(b, &f[1]); + } + } + spa_pod_builder_prop(b, SPA_PROP_INFO_params, 0); + spa_pod_builder_bool(b, true); + pod = spa_pod_builder_pop(b, &f[0]); + if (pod == NULL) + return -ENOSPC; + if (param) + *param = pod; + + return 1; +} + +static int impl_get_props(void *object, struct spa_pod_builder *b, struct spa_pod **props) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + struct spa_pod_frame f[2]; + uint32_t i; + char name[512]; + struct spa_pod *res; + + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); + spa_pod_builder_prop(b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(b, &f[1]); + + for (i = 0; i < graph->n_control; i++) { + struct port *port = graph->control_port[i]; + struct node *node = port->node; + struct descriptor *desc = node->desc; + const struct spa_fga_descriptor *d = desc->desc; + struct spa_fga_port *p = &d->ports[port->p]; + + if (node->name[0] != '\0') + snprintf(name, sizeof(name), "%s:%s", node->name, p->name); + else + snprintf(name, sizeof(name), "%s", p->name); + + spa_pod_builder_string(b, name); + if (p->hint & SPA_FGA_HINT_BOOLEAN) { + spa_pod_builder_bool(b, port->control_data[0] <= 0.0f ? false : true); + } else if (p->hint & SPA_FGA_HINT_INTEGER) { + spa_pod_builder_int(b, (int32_t)port->control_data[0]); + } else { + spa_pod_builder_float(b, port->control_data[0]); + } + } + spa_pod_builder_pop(b, &f[1]); + res = spa_pod_builder_pop(b, &f[0]); + if (res == NULL) + return -ENOSPC; + if (props) + *props = res; + return 1; +} + +static int port_set_control_value(struct port *port, float *value, uint32_t id) +{ + struct node *node = port->node; + struct impl *impl = node->graph->impl; + + struct descriptor *desc = node->desc; + float old; + bool changed; + + old = port->control_data[id]; + port->control_data[id] = value ? *value : desc->default_control[port->idx]; + spa_log_info(impl->log, "control %d %d ('%s') from %f to %f", port->idx, id, + desc->desc->ports[port->p].name, old, port->control_data[id]); + changed = old != port->control_data[id]; + node->control_changed |= changed; + return changed ? 1 : 0; +} + +static int set_control_value(struct node *node, const char *name, float *value) +{ + struct port *port; + int count = 0; + uint32_t i, n_hndl; + + port = find_port(node, name, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL); + if (port == NULL) + return -ENOENT; + + /* if we don't have any instances yet, set the first control value, we will + * copy to other instances later */ + n_hndl = SPA_MAX(1u, port->node->n_hndl); + for (i = 0; i < n_hndl; i++) + count += port_set_control_value(port, value, i); + + return count; +} + +static int parse_params(struct graph *graph, const struct spa_pod *pod) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + int res, changed = 0; + struct node *def_node; + + def_node = spa_list_first(&graph->node_list, struct node, link); + + spa_pod_parser_pod(&prs, pod); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + float value, *val = NULL; + double dbl_val; + bool bool_val; + int32_t int_val; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + if (spa_pod_parser_get_float(&prs, &value) >= 0) { + val = &value; + } else if (spa_pod_parser_get_double(&prs, &dbl_val) >= 0) { + value = (float)dbl_val; + val = &value; + } else if (spa_pod_parser_get_int(&prs, &int_val) >= 0) { + value = int_val; + val = &value; + } else if (spa_pod_parser_get_bool(&prs, &bool_val) >= 0) { + value = bool_val ? 1.0f : 0.0f; + val = &value; + } else { + struct spa_pod *pod; + spa_pod_parser_get_pod(&prs, &pod); + } + if ((res = set_control_value(def_node, name, val)) > 0) + changed += res; + } + return changed; +} + +static int impl_reset(void *object) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + uint32_t i; + for (i = 0; i < graph->n_hndl; i++) { + struct graph_hndl *hndl = &graph->hndl[i]; + const struct spa_fga_descriptor *d = hndl->desc; + if (hndl->hndl == NULL || *hndl->hndl == NULL) + continue; + if (d->deactivate) + d->deactivate(*hndl->hndl); + if (d->activate) + d->activate(*hndl->hndl); + } + return 0; +} + +static void node_control_changed(struct node *node) +{ + const struct spa_fga_descriptor *d = node->desc->desc; + uint32_t i; + + if (!node->control_changed) + return; + + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] == NULL) + continue; + if (d->control_changed) + d->control_changed(node->hndl[i]); + } + node->control_changed = false; +} + +static int sync_volume(struct graph *graph, struct volume *vol) +{ + uint32_t i; + int res = 0; + + if (vol->n_ports == 0) + return 0; + for (i = 0; i < vol->n_volumes; i++) { + uint32_t n_port = i % vol->n_ports, n_hndl; + struct port *p = vol->ports[n_port]; + float v = vol->mute ? 0.0f : vol->volumes[i]; + switch (vol->scale[n_port]) { + case SCALE_CUBIC: + v = cbrtf(v); + break; + } + v = v * (vol->max[n_port] - vol->min[n_port]) + vol->min[n_port]; + + n_hndl = SPA_MAX(1u, p->node->n_hndl); + res += port_set_control_value(p, &v, i % n_hndl); + } + return res; +} + +static int impl_set_props(void *object, enum spa_direction direction, const struct spa_pod *props) +{ + struct impl *impl = object; + struct spa_pod_object *obj = (struct spa_pod_object *) props; + struct spa_pod_frame f[1]; + const struct spa_pod_prop *prop; + struct graph *graph = &impl->graph; + int changed = 0; + char buf[1024]; + struct spa_pod_dynamic_builder b; + struct volume *vol = &graph->volume[direction]; + bool do_volume = false; + + spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 1024); + spa_pod_builder_push_object(&b.b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_params: + changed += parse_params(graph, &prop->value); + spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop)); + break; + case SPA_PROP_mute: + { + bool mute; + if (spa_pod_get_bool(&prop->value, &mute) == 0) { + if (vol->mute != mute) { + vol->mute = mute; + do_volume = true; + } + } + spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop)); + break; + } + case SPA_PROP_channelVolumes: + { + uint32_t i, n_vols; + float vols[SPA_AUDIO_MAX_CHANNELS]; + + if ((n_vols = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vols, + SPA_AUDIO_MAX_CHANNELS)) > 0) { + if (vol->n_volumes != n_vols) + do_volume = true; + vol->n_volumes = n_vols; + for (i = 0; i < n_vols; i++) { + float v = vols[i]; + if (v != vol->volumes[i]) { + vol->volumes[i] = v; + do_volume = true; + } + } + } + spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop)); + break; + } + case SPA_PROP_softVolumes: + case SPA_PROP_softMute: + break; + default: + spa_pod_builder_raw_padded(&b.b, prop, SPA_POD_PROP_SIZE(prop)); + break; + } + } + if (do_volume && vol->n_ports != 0) { + float soft_vols[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i; + + for (i = 0; i < vol->n_volumes; i++) + soft_vols[i] = (vol->mute || vol->volumes[i] == 0.0f) ? 0.0f : 1.0f; + + spa_pod_builder_prop(&b.b, SPA_PROP_softMute, 0); + spa_pod_builder_bool(&b.b, vol->mute); + spa_pod_builder_prop(&b.b, SPA_PROP_softVolumes, 0); + spa_pod_builder_array(&b.b, sizeof(float), SPA_TYPE_Float, + vol->n_volumes, soft_vols); + props = spa_pod_builder_pop(&b.b, &f[0]); + + sync_volume(graph, vol); + + } else { + props = spa_pod_builder_pop(&b.b, &f[0]); + } + spa_filter_graph_emit_apply_props(&impl->hooks, direction, props); + + spa_pod_dynamic_builder_clean(&b); + + if (changed > 0) { + struct node *node; + + spa_list_for_each(node, &graph->node_list, link) + node_control_changed(node); + + spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT); + } + return 0; +} + +static uint32_t count_array(struct spa_json *json) +{ + struct spa_json it = *json; + char v[256]; + uint32_t count = 0; + while (spa_json_get_string(&it, v, sizeof(v)) > 0) + count++; + return count; +} + +static void plugin_unref(struct plugin *hndl) +{ + struct impl *impl = hndl->impl; + + if (--hndl->ref > 0) + return; + + spa_list_remove(&hndl->link); + if (hndl->hndl) + spa_plugin_loader_unload(impl->loader, hndl->hndl); + free(hndl); +} + +static inline const char *split_walk(const char *str, const char *delimiter, size_t * len, const char **state) +{ + const char *s = *state ? *state : str; + + s += strspn(s, delimiter); + if (*s == '\0') + return NULL; + + *len = strcspn(s, delimiter); + *state = s + *len; + + return s; +} + +static struct plugin *plugin_load(struct impl *impl, const char *type, const char *path) +{ + struct spa_handle *hndl = NULL; + struct plugin *plugin; + char module[PATH_MAX]; + char factory_name[256], dsp_ptr[256]; + void *iface; + int res; + + spa_list_for_each(plugin, &impl->plugin_list, link) { + if (spa_streq(plugin->type, type) && + spa_streq(plugin->path, path)) { + plugin->ref++; + return plugin; + } + } + + spa_scnprintf(module, sizeof(module), + "filter-graph/libspa-filter-graph-plugin-%s", type); + spa_scnprintf(factory_name, sizeof(factory_name), + "filter.graph.plugin.%s", type); + spa_scnprintf(dsp_ptr, sizeof(dsp_ptr), + "pointer:%p", impl->dsp); + + hndl = spa_plugin_loader_load(impl->loader, factory_name, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_LIBRARY_NAME, module), + SPA_DICT_ITEM("filter.graph.path", path), + SPA_DICT_ITEM("filter.graph.audio.dsp", dsp_ptr))); + + if (hndl == NULL) { + res = -errno; + spa_log_error(impl->log, "can't load plugin type '%s': %m", type); + goto exit; + } + if ((res = spa_handle_get_interface(hndl, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, &iface)) < 0) { + spa_log_error(impl->log, "can't find iface '%s': %s", + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, spa_strerror(res)); + goto exit; + } + plugin = calloc(1, sizeof(*plugin)); + if (!plugin) { + res = -errno; + goto exit; + } + + plugin->ref = 1; + snprintf(plugin->type, sizeof(plugin->type), "%s", type); + snprintf(plugin->path, sizeof(plugin->path), "%s", path); + + spa_log_info(impl->log, "successfully opened '%s':'%s'", type, path); + + plugin->impl = impl; + plugin->hndl = hndl; + plugin->plugin = iface; + + spa_list_init(&plugin->descriptor_list); + spa_list_append(&impl->plugin_list, &plugin->link); + + return plugin; +exit: + if (hndl) + spa_plugin_loader_unload(impl->loader, hndl); + errno = -res; + return NULL; +} + +static void descriptor_unref(struct descriptor *desc) +{ + if (--desc->ref > 0) + return; + + spa_list_remove(&desc->link); + if (desc->desc) + spa_fga_descriptor_free(desc->desc); + plugin_unref(desc->plugin); + free(desc->input); + free(desc->output); + free(desc->control); + free(desc->default_control); + free(desc->notify); + free(desc); +} + +static struct descriptor *descriptor_load(struct impl *impl, const char *type, + const char *plugin, const char *label) +{ + struct plugin *pl; + struct descriptor *desc; + const struct spa_fga_descriptor *d; + uint32_t i, n_input, n_output, n_control, n_notify; + unsigned long p; + int res; + + if ((pl = plugin_load(impl, type, plugin)) == NULL) + return NULL; + + spa_list_for_each(desc, &pl->descriptor_list, link) { + if (spa_streq(desc->label, label)) { + desc->ref++; + + /* + * since ladspa_handle_load() increments the reference count of the handle, + * if the descriptor is found, then the handle's reference count + * has already been incremented to account for the descriptor, + * so we need to unref handle here since we're merely reusing + * thedescriptor, not creating a new one + */ + plugin_unref(pl); + return desc; + } + } + + desc = calloc(1, sizeof(*desc)); + desc->ref = 1; + desc->plugin = pl; + spa_list_init(&desc->link); + + if ((d = spa_fga_plugin_make_desc(pl->plugin, label)) == NULL) { + spa_log_error(impl->log, "cannot find label %s", label); + res = -ENOENT; + goto exit; + } + desc->desc = d; + snprintf(desc->label, sizeof(desc->label), "%s", label); + + n_input = n_output = n_control = n_notify = 0; + for (p = 0; p < d->n_ports; p++) { + struct spa_fga_port *fp = &d->ports[p]; + if (SPA_FGA_IS_PORT_AUDIO(fp->flags)) { + if (SPA_FGA_IS_PORT_INPUT(fp->flags)) + n_input++; + else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) + n_output++; + } else if (SPA_FGA_IS_PORT_CONTROL(fp->flags)) { + if (SPA_FGA_IS_PORT_INPUT(fp->flags)) + n_control++; + else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) + n_notify++; + } + } + desc->input = calloc(n_input, sizeof(unsigned long)); + desc->output = calloc(n_output, sizeof(unsigned long)); + desc->control = calloc(n_control, sizeof(unsigned long)); + desc->default_control = calloc(n_control, sizeof(float)); + desc->notify = calloc(n_notify, sizeof(unsigned long)); + + for (p = 0; p < d->n_ports; p++) { + struct spa_fga_port *fp = &d->ports[p]; + + if (SPA_FGA_IS_PORT_AUDIO(fp->flags)) { + if (SPA_FGA_IS_PORT_INPUT(fp->flags)) { + spa_log_info(impl->log, "using port %lu ('%s') as input %d", p, + fp->name, desc->n_input); + desc->input[desc->n_input++] = p; + } + else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) { + spa_log_info(impl->log, "using port %lu ('%s') as output %d", p, + fp->name, desc->n_output); + desc->output[desc->n_output++] = p; + } + } else if (SPA_FGA_IS_PORT_CONTROL(fp->flags)) { + if (SPA_FGA_IS_PORT_INPUT(fp->flags)) { + spa_log_info(impl->log, "using port %lu ('%s') as control %d", p, + fp->name, desc->n_control); + desc->control[desc->n_control++] = p; + } + else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) { + spa_log_info(impl->log, "using port %lu ('%s') as notify %d", p, + fp->name, desc->n_notify); + desc->notify[desc->n_notify++] = p; + } + } + } + if (desc->n_input == 0 && desc->n_output == 0 && desc->n_control == 0 && desc->n_notify == 0) { + spa_log_error(impl->log, "plugin has no input and no output ports"); + res = -ENOTSUP; + goto exit; + } + for (i = 0; i < desc->n_control; i++) { + p = desc->control[i]; + desc->default_control[i] = get_default(impl, desc, p); + spa_log_info(impl->log, "control %d ('%s') default to %f", i, + d->ports[p].name, desc->default_control[i]); + } + spa_list_append(&pl->descriptor_list, &desc->link); + + return desc; + +exit: + descriptor_unref(desc); + errno = -res; + return NULL; +} + +/** + * { + * ... + * } + */ +static int parse_config(struct node *node, struct spa_json *config) +{ + const char *val, *s = config->cur; + struct impl *impl = node->graph->impl; + int res = 0, len; + struct spa_error_location loc; + + if ((len = spa_json_next(config, &val)) <= 0) { + res = -EINVAL; + goto done; + } + if (spa_json_is_null(val, len)) + goto done; + + if (spa_json_is_container(val, len)) { + len = spa_json_container_len(config, val, len); + if (len == 0) { + res = -EINVAL; + goto done; + } + } + if ((node->config = malloc(len+1)) == NULL) { + res = -errno; + goto done; + } + + spa_json_parse_stringn(val, len, node->config, len+1); +done: + if (spa_json_get_error(config, s, &loc)) + spa_debug_log_error_location(impl->log, SPA_LOG_LEVEL_WARN, + &loc, "error: %s", loc.reason); + return res; +} + +/** + * { + * "Reverb tail" = 2.0 + * ... + * } + */ +static int parse_control(struct node *node, struct spa_json *control) +{ + struct impl *impl = node->graph->impl; + char key[256]; + const char *val; + int len; + + while ((len = spa_json_object_next(control, key, sizeof(key), &val)) > 0) { + float fl; + int res; + + if (spa_json_parse_float(val, len, &fl) <= 0) { + spa_log_warn(impl->log, "control '%s' expects a number, ignoring", key); + } + else if ((res = set_control_value(node, key, &fl)) < 0) { + spa_log_warn(impl->log, "control '%s' can not be set: %s", key, spa_strerror(res)); + } + } + return 0; +} + +/** + * output = [name:][portname] + * input = [name:][portname] + * ... + */ +static int parse_link(struct graph *graph, struct spa_json *json) +{ + struct impl *impl = graph->impl; + char key[256]; + char output[256] = ""; + char input[256] = ""; + const char *val; + struct node *def_in_node, *def_out_node; + struct port *in_port, *out_port; + struct link *link; + int len; + + if (spa_list_is_empty(&graph->node_list)) { + spa_log_error(impl->log, "can't make links in graph without nodes"); + return -EINVAL; + } + + while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "output")) { + if (spa_json_parse_stringn(val, len, output, sizeof(output)) <= 0) { + spa_log_error(impl->log, "output expects a string"); + return -EINVAL; + } + } + else if (spa_streq(key, "input")) { + if (spa_json_parse_stringn(val, len, input, sizeof(input)) <= 0) { + spa_log_error(impl->log, "input expects a string"); + return -EINVAL; + } + } + else { + spa_log_error(impl->log, "unexpected link key '%s'", key); + } + } + def_out_node = spa_list_first(&graph->node_list, struct node, link); + def_in_node = spa_list_last(&graph->node_list, struct node, link); + + out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT); + in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT); + + if (out_port == NULL && out_port == NULL) { + /* try control ports */ + out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL); + in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL); + } + if (in_port == NULL || out_port == NULL) { + if (out_port == NULL) + spa_log_error(impl->log, "unknown output port %s", output); + if (in_port == NULL) + spa_log_error(impl->log, "unknown input port %s", input); + return -ENOENT; + } + + if (in_port->n_links > 0) { + spa_log_info(impl->log, "Can't have more than 1 link to %s, use a mixer", input); + return -ENOTSUP; + } + + if ((link = calloc(1, sizeof(*link))) == NULL) + return -errno; + + link->output = out_port; + link->input = in_port; + + spa_log_info(impl->log, "linking %s:%s -> %s:%s", + out_port->node->name, + out_port->node->desc->desc->ports[out_port->p].name, + in_port->node->name, + in_port->node->desc->desc->ports[in_port->p].name); + + spa_list_append(&out_port->link_list, &link->output_link); + out_port->n_links++; + spa_list_append(&in_port->link_list, &link->input_link); + in_port->n_links++; + + in_port->node->n_deps++; + + spa_list_append(&graph->link_list, &link->link); + + return 0; +} + +static void link_free(struct link *link) +{ + spa_list_remove(&link->input_link); + link->input->n_links--; + link->input->node->n_deps--; + spa_list_remove(&link->output_link); + link->output->n_links--; + spa_list_remove(&link->link); + free(link); +} + +/** + * { + * control = [name:][portname] + * min = + * max = + * scale = + * } + */ +static int parse_volume(struct graph *graph, struct spa_json *json, enum spa_direction direction) +{ + struct impl *impl = graph->impl; + char key[256]; + char control[256] = ""; + char scale[64] = "linear"; + float min = 0.0f, max = 1.0f; + const char *val; + struct node *def_control; + struct port *port; + struct volume *vol = &graph->volume[direction]; + int len; + + if (spa_list_is_empty(&graph->node_list)) { + spa_log_error(impl->log, "can't set volume in graph without nodes"); + return -EINVAL; + } + while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "control")) { + if (spa_json_parse_stringn(val, len, control, sizeof(control)) <= 0) { + spa_log_error(impl->log, "control expects a string"); + return -EINVAL; + } + } + else if (spa_streq(key, "min")) { + if (spa_json_parse_float(val, len, &min) <= 0) { + spa_log_error(impl->log, "min expects a float"); + return -EINVAL; + } + } + else if (spa_streq(key, "max")) { + if (spa_json_parse_float(val, len, &max) <= 0) { + spa_log_error(impl->log, "max expects a float"); + return -EINVAL; + } + } + else if (spa_streq(key, "scale")) { + if (spa_json_parse_stringn(val, len, scale, sizeof(scale)) <= 0) { + spa_log_error(impl->log, "scale expects a string"); + return -EINVAL; + } + } + else { + spa_log_error(impl->log, "unexpected volume key '%s'", key); + } + } + if (direction == SPA_DIRECTION_INPUT) + def_control = spa_list_first(&graph->node_list, struct node, link); + else + def_control = spa_list_last(&graph->node_list, struct node, link); + + port = find_port(def_control, control, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL); + if (port == NULL) { + spa_log_error(impl->log, "unknown control port %s", control); + return -ENOENT; + } + if (vol->n_ports >= SPA_AUDIO_MAX_CHANNELS) { + spa_log_error(impl->log, "too many volume controls"); + return -ENOSPC; + } + if (spa_streq(scale, "linear")) { + vol->scale[vol->n_ports] = SCALE_LINEAR; + } else if (spa_streq(scale, "cubic")) { + vol->scale[vol->n_ports] = SCALE_CUBIC; + } else { + spa_log_error(impl->log, "Invalid scale value '%s', use one of linear or cubic", scale); + return -EINVAL; + } + spa_log_info(impl->log, "volume %d: \"%s:%s\" min:%f max:%f scale:%s", vol->n_ports, port->node->name, + port->node->desc->desc->ports[port->p].name, min, max, scale); + + vol->ports[vol->n_ports] = port; + vol->min[vol->n_ports] = min; + vol->max[vol->n_ports] = max; + vol->n_ports++; + + return 0; +} + +/** + * type = ladspa + * name = rev + * plugin = g2reverb + * label = G2reverb + * config = { + * ... + * } + * control = { + * ... + * } + */ +static int load_node(struct graph *graph, struct spa_json *json) +{ + struct impl *impl = graph->impl; + struct spa_json control, config; + struct descriptor *desc; + struct node *node; + const char *val; + char key[256]; + char type[256] = ""; + char name[256] = ""; + char plugin[256] = ""; + char label[256] = ""; + bool have_control = false; + bool have_config = false; + uint32_t i; + int res, len; + + while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) { + if (spa_streq("type", key)) { + if (spa_json_parse_stringn(val, len, type, sizeof(type)) <= 0) { + spa_log_error(impl->log, "type expects a string"); + return -EINVAL; + } + } else if (spa_streq("name", key)) { + if (spa_json_parse_stringn(val, len, name, sizeof(name)) <= 0) { + spa_log_error(impl->log, "name expects a string"); + return -EINVAL; + } + } else if (spa_streq("plugin", key)) { + if (spa_json_parse_stringn(val, len, plugin, sizeof(plugin)) <= 0) { + spa_log_error(impl->log, "plugin expects a string"); + return -EINVAL; + } + } else if (spa_streq("label", key)) { + if (spa_json_parse_stringn(val, len, label, sizeof(label)) <= 0) { + spa_log_error(impl->log, "label expects a string"); + return -EINVAL; + } + } else if (spa_streq("control", key)) { + if (!spa_json_is_object(val, len)) { + spa_log_error(impl->log, "control expects an object"); + return -EINVAL; + } + spa_json_enter(json, &control); + have_control = true; + } else if (spa_streq("config", key)) { + config = SPA_JSON_START(json, val); + have_config = true; + } else { + spa_log_warn(impl->log, "unexpected node key '%s'", key); + } + } + if (spa_streq(type, "builtin")) + snprintf(plugin, sizeof(plugin), "%s", "builtin"); + else if (spa_streq(type, "")) { + spa_log_error(impl->log, "missing plugin type"); + return -EINVAL; + } + + spa_log_info(impl->log, "loading type:%s plugin:%s label:%s", type, plugin, label); + + if ((desc = descriptor_load(graph->impl, type, plugin, label)) == NULL) + return -errno; + + node = calloc(1, sizeof(*node)); + if (node == NULL) + return -errno; + + node->graph = graph; + node->desc = desc; + snprintf(node->name, sizeof(node->name), "%s", name); + + node->input_port = calloc(desc->n_input, sizeof(struct port)); + node->output_port = calloc(desc->n_output, sizeof(struct port)); + node->control_port = calloc(desc->n_control, sizeof(struct port)); + node->notify_port = calloc(desc->n_notify, sizeof(struct port)); + + spa_log_info(impl->log, "loaded n_input:%d n_output:%d n_control:%d n_notify:%d", + desc->n_input, desc->n_output, + desc->n_control, desc->n_notify); + + for (i = 0; i < desc->n_input; i++) { + struct port *port = &node->input_port[i]; + port->node = node; + port->idx = i; + port->external = SPA_ID_INVALID; + port->p = desc->input[i]; + spa_list_init(&port->link_list); + } + for (i = 0; i < desc->n_output; i++) { + struct port *port = &node->output_port[i]; + port->node = node; + port->idx = i; + port->external = SPA_ID_INVALID; + port->p = desc->output[i]; + spa_list_init(&port->link_list); + } + for (i = 0; i < desc->n_control; i++) { + struct port *port = &node->control_port[i]; + port->node = node; + port->idx = i; + port->external = SPA_ID_INVALID; + port->p = desc->control[i]; + spa_list_init(&port->link_list); + port->control_data[0] = desc->default_control[i]; + } + for (i = 0; i < desc->n_notify; i++) { + struct port *port = &node->notify_port[i]; + port->node = node; + port->idx = i; + port->external = SPA_ID_INVALID; + port->p = desc->notify[i]; + spa_list_init(&port->link_list); + } + if (have_config) + if ((res = parse_config(node, &config)) < 0) + spa_log_warn(impl->log, "error parsing config: %s", spa_strerror(res)); + if (have_control) + parse_control(node, &control); + + spa_list_append(&graph->node_list, &node->link); + + return 0; +} + +static void node_cleanup(struct node *node) +{ + const struct spa_fga_descriptor *d = node->desc->desc; + struct impl *impl = node->graph->impl; + uint32_t i; + + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] == NULL) + continue; + spa_log_info(impl->log, "cleanup %s %s[%d]", d->name, node->name, i); + if (d->deactivate) + d->deactivate(node->hndl[i]); + d->cleanup(node->hndl[i]); + node->hndl[i] = NULL; + } +} + +static int port_ensure_data(struct port *port, uint32_t i, uint32_t max_samples) +{ + float *data; + struct node *node = port->node; + const struct spa_fga_descriptor *d = node->desc->desc; + struct impl *impl = node->graph->impl; + + if ((data = port->audio_mem[i]) == NULL) { + data = calloc(max_samples, sizeof(float) + impl->max_align); + if (data == NULL) { + spa_log_error(impl->log, "cannot create port data: %m"); + return -errno; + } + port->audio_mem[i] = data; + port->audio_data[i] = SPA_PTR_ALIGN(data, impl->max_align, void); + } + spa_log_info(impl->log, "connect output port %s[%d]:%s %p", + node->name, i, d->ports[port->p].name, port->audio_data[i]); + d->connect_port(port->node->hndl[i], port->p, port->audio_data[i]); + return 0; +} + +static void port_free_data(struct port *port, uint32_t i) +{ + free(port->audio_mem[i]); + port->audio_mem[i] = NULL; + port->audio_data[i] = NULL; +} + +static void node_free(struct node *node) +{ + uint32_t i, j; + + spa_list_remove(&node->link); + for (i = 0; i < node->n_hndl; i++) { + for (j = 0; j < node->desc->n_output; j++) + port_free_data(&node->output_port[j], i); + } + node_cleanup(node); + descriptor_unref(node->desc); + free(node->input_port); + free(node->output_port); + free(node->control_port); + free(node->notify_port); + free(node->config); + free(node); +} + +static int impl_deactivate(void *object) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + struct node *node; + + if (!graph->activated) + return 0; + + graph->activated = false; + spa_list_for_each(node, &graph->node_list, link) + node_cleanup(node); + return 0; +} + +static int impl_activate(void *object, const struct spa_dict *props) +{ + struct impl *impl = object; + struct graph *graph = &impl->graph; + struct node *node; + struct port *port; + struct link *link; + struct descriptor *desc; + const struct spa_fga_descriptor *d; + const struct spa_fga_plugin *p; + uint32_t i, j, max_samples = impl->quantum_limit; + int res; + float *sd, *dd, *data; + const char *rate; + + if (graph->activated) + return 0; + + graph->activated = true; + + rate = spa_dict_lookup(props, SPA_KEY_AUDIO_RATE); + impl->rate = rate ? atoi(rate) : DEFAULT_RATE; + + /* first make instances */ + spa_list_for_each(node, &graph->node_list, link) { + node_cleanup(node); + + desc = node->desc; + d = desc->desc; + p = desc->plugin->plugin; + + for (i = 0; i < node->n_hndl; i++) { + spa_log_info(impl->log, "instantiate %s %s[%d] rate:%lu", d->name, node->name, i, impl->rate); + errno = EINVAL; + if ((node->hndl[i] = d->instantiate(p, d, impl->rate, i, node->config)) == NULL) { + spa_log_error(impl->log, "cannot create plugin instance %d rate:%lu: %m", i, impl->rate); + res = -errno; + goto error; + } + } + } + + /* then link ports */ + spa_list_for_each(node, &graph->node_list, link) { + desc = node->desc; + d = desc->desc; + if (d->flags & SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) { + sd = dd = NULL; + } + else { + sd = impl->silence_data; + dd = impl->discard_data; + } + for (i = 0; i < node->n_hndl; i++) { + for (j = 0; j < desc->n_input; j++) { + port = &node->input_port[j]; + if (!spa_list_is_empty(&port->link_list)) { + link = spa_list_first(&port->link_list, struct link, input_link); + if ((res = port_ensure_data(link->output, i, max_samples)) < 0) + goto error; + data = link->output->audio_data[i]; + } else { + data = sd; + } + spa_log_info(impl->log, "connect input port %s[%d]:%s %p", + node->name, i, d->ports[port->p].name, data); + d->connect_port(node->hndl[i], port->p, data); + } + for (j = 0; j < desc->n_output; j++) { + port = &node->output_port[j]; + if (port->audio_data[i] == NULL) { + spa_log_info(impl->log, "connect output port %s[%d]:%s %p", + node->name, i, d->ports[port->p].name, dd); + d->connect_port(node->hndl[i], port->p, dd); + } + } + for (j = 0; j < desc->n_control; j++) { + port = &node->control_port[j]; + + if (!spa_list_is_empty(&port->link_list)) { + link = spa_list_first(&port->link_list, struct link, input_link); + data = &link->output->control_data[i]; + } else { + data = &port->control_data[i]; + } + spa_log_info(impl->log, "connect control port %s[%d]:%s %p", + node->name, i, d->ports[port->p].name, data); + d->connect_port(node->hndl[i], port->p, data); + } + for (j = 0; j < desc->n_notify; j++) { + port = &node->notify_port[j]; + spa_log_info(impl->log, "connect notify port %s[%d]:%s %p", + node->name, i, d->ports[port->p].name, + &port->control_data[i]); + d->connect_port(node->hndl[i], port->p, &port->control_data[i]); + } + } + } + + /* now activate */ + spa_list_for_each(node, &graph->node_list, link) { + desc = node->desc; + d = desc->desc; + + for (i = 0; i < node->n_hndl; i++) { + if (d->activate) + d->activate(node->hndl[i]); + if (node->control_changed && d->control_changed) + d->control_changed(node->hndl[i]); + } + } + + spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT); + return 0; +error: + impl_deactivate(impl); + return res; +} + +/* any default values for the controls are set in the first instance + * of the control data. Duplicate this to the other instances now. */ +static void setup_node_controls(struct node *node) +{ + uint32_t i, j; + uint32_t n_hndl = node->n_hndl; + uint32_t n_ports = node->desc->n_control; + struct port *ports = node->control_port; + + for (i = 0; i < n_ports; i++) { + struct port *port = &ports[i]; + for (j = 1; j < n_hndl; j++) + port->control_data[j] = port->control_data[0]; + } +} + +static struct node *find_next_node(struct graph *graph) +{ + struct node *node; + spa_list_for_each(node, &graph->node_list, link) { + if (node->n_deps == 0 && !node->visited) { + node->visited = true; + return node; + } + } + return NULL; +} + +static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs) +{ + struct impl *impl = graph->impl; + struct node *node, *first, *last; + struct port *port; + struct link *link; + struct graph_port *gp; + struct graph_hndl *gh; + uint32_t i, j, n_nodes, n_input, n_output, n_control, n_hndl = 0; + int res; + struct descriptor *desc; + const struct spa_fga_descriptor *d; + char v[256]; + bool allow_unused; + + first = spa_list_first(&graph->node_list, struct node, link); + last = spa_list_last(&graph->node_list, struct node, link); + + /* calculate the number of inputs and outputs into the graph. + * If we have a list of inputs/outputs, just count them. Otherwise + * we count all input ports of the first node and all output + * ports of the last node */ + if (inputs != NULL) + n_input = count_array(inputs); + else + n_input = first->desc->n_input; + + if (outputs != NULL) + n_output = count_array(outputs); + else + n_output = last->desc->n_output; + + /* we allow unconnected ports when not explicitly given and the nodes support + * NULL data */ + allow_unused = inputs == NULL && outputs == NULL && + SPA_FLAG_IS_SET(first->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) && + SPA_FLAG_IS_SET(last->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA); + + if (n_input == 0) { + spa_log_error(impl->log, "no inputs"); + res = -EINVAL; + goto error; + } + if (n_output == 0) { + spa_log_error(impl->log, "no outputs"); + res = -EINVAL; + goto error; + } + + if (impl->info.n_inputs == 0) + impl->info.n_inputs = n_input; + + /* compare to the requested number of inputs and duplicate the + * graph n_hndl times when needed. */ + n_hndl = impl->info.n_inputs / n_input; + + if (impl->info.n_outputs == 0) + impl->info.n_outputs = n_output * n_hndl; + + if (n_hndl != impl->info.n_outputs / n_output) { + spa_log_error(impl->log, "invalid ports. The input stream has %1$d ports and " + "the filter has %2$d inputs. The output stream has %3$d ports " + "and the filter has %4$d outputs. input:%1$d / input:%2$d != " + "output:%3$d / output:%4$d. Check inputs and outputs objects.", + impl->info.n_inputs, n_input, + impl->info.n_outputs, n_output); + res = -EINVAL; + goto error; + } + if (n_hndl > MAX_HNDL) { + spa_log_error(impl->log, "too many ports. %d > %d", n_hndl, MAX_HNDL); + res = -EINVAL; + goto error; + } + if (n_hndl == 0) { + n_hndl = 1; + if (!allow_unused) + spa_log_warn(impl->log, "The input stream has %1$d ports and " + "the filter has %2$d inputs. The output stream has %3$d ports " + "and the filter has %4$d outputs. Some filter ports will be " + "unconnected..", + impl->info.n_inputs, n_input, + impl->info.n_outputs, n_output); + + if (impl->info.n_outputs == 0) + impl->info.n_outputs = n_output * n_hndl; + } + spa_log_info(impl->log, "using %d instances %d %d", n_hndl, n_input, n_output); + + /* now go over all nodes and create instances. */ + n_control = 0; + n_nodes = 0; + spa_list_for_each(node, &graph->node_list, link) { + node->n_hndl = n_hndl; + desc = node->desc; + n_control += desc->n_control; + n_nodes++; + setup_node_controls(node); + } + graph->n_input = 0; + graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port)); + graph->n_output = 0; + graph->output = calloc(n_output * n_hndl, sizeof(struct graph_port)); + + /* now collect all input and output ports for all the handles. */ + for (i = 0; i < n_hndl; i++) { + if (inputs == NULL) { + desc = first->desc; + d = desc->desc; + for (j = 0; j < desc->n_input; j++) { + gp = &graph->input[graph->n_input++]; + spa_log_info(impl->log, "input port %s[%d]:%s", + first->name, i, d->ports[desc->input[j]].name); + gp->desc = d; + gp->hndl = &first->hndl[i]; + gp->port = desc->input[j]; + } + } else { + struct spa_json it = *inputs; + while (spa_json_get_string(&it, v, sizeof(v)) > 0) { + if (spa_streq(v, "null")) { + gp = &graph->input[graph->n_input++]; + gp->desc = NULL; + spa_log_info(impl->log, "ignore input port %d", graph->n_input); + } else if ((port = find_port(first, v, SPA_FGA_PORT_INPUT)) == NULL) { + res = -ENOENT; + spa_log_error(impl->log, "input port %s not found", v); + goto error; + } else { + bool disabled = false; + + desc = port->node->desc; + d = desc->desc; + if (i == 0 && port->external != SPA_ID_INVALID) { + spa_log_error(impl->log, "input port %s[%d]:%s already used as input %d, use mixer", + port->node->name, i, d->ports[port->p].name, + port->external); + res = -EBUSY; + goto error; + } + if (port->n_links > 0) { + spa_log_error(impl->log, "input port %s[%d]:%s already used by link, use mixer", + port->node->name, i, d->ports[port->p].name); + res = -EBUSY; + goto error; + } + + if (d->flags & SPA_FGA_DESCRIPTOR_COPY) { + for (j = 0; j < desc->n_output; j++) { + struct port *p = &port->node->output_port[j]; + struct link *link; + + gp = NULL; + spa_list_for_each(link, &p->link_list, output_link) { + struct port *peer = link->input; + + spa_log_info(impl->log, "copy input port %s[%d]:%s", + port->node->name, i, + d->ports[port->p].name); + peer->external = graph->n_input; + gp = &graph->input[graph->n_input++]; + gp->desc = peer->node->desc->desc; + gp->hndl = &peer->node->hndl[i]; + gp->port = peer->p; + gp->next = true; + disabled = true; + } + if (gp != NULL) + gp->next = false; + } + port->node->disabled = disabled; + } + if (!disabled) { + spa_log_info(impl->log, "input port %s[%d]:%s", + port->node->name, i, d->ports[port->p].name); + port->external = graph->n_input; + gp = &graph->input[graph->n_input++]; + gp->desc = d; + gp->hndl = &port->node->hndl[i]; + gp->port = port->p; + gp->next = false; + } + } + } + } + if (outputs == NULL) { + desc = last->desc; + d = desc->desc; + for (j = 0; j < desc->n_output; j++) { + gp = &graph->output[graph->n_output++]; + spa_log_info(impl->log, "output port %s[%d]:%s", + last->name, i, d->ports[desc->output[j]].name); + gp->desc = d; + gp->hndl = &last->hndl[i]; + gp->port = desc->output[j]; + } + } else { + struct spa_json it = *outputs; + while (spa_json_get_string(&it, v, sizeof(v)) > 0) { + gp = &graph->output[graph->n_output]; + if (spa_streq(v, "null")) { + gp->desc = NULL; + spa_log_info(impl->log, "silence output port %d", graph->n_output); + } else if ((port = find_port(last, v, SPA_FGA_PORT_OUTPUT)) == NULL) { + res = -ENOENT; + spa_log_error(impl->log, "output port %s not found", v); + goto error; + } else { + desc = port->node->desc; + d = desc->desc; + if (i == 0 && port->external != SPA_ID_INVALID) { + spa_log_error(impl->log, "output port %s[%d]:%s already used as output %d, use copy", + port->node->name, i, d->ports[port->p].name, + port->external); + res = -EBUSY; + goto error; + } + if (port->n_links > 0) { + spa_log_error(impl->log, "output port %s[%d]:%s already used by link, use copy", + port->node->name, i, d->ports[port->p].name); + res = -EBUSY; + goto error; + } + spa_log_info(impl->log, "output port %s[%d]:%s", + port->node->name, i, d->ports[port->p].name); + port->external = graph->n_output; + gp->desc = d; + gp->hndl = &port->node->hndl[i]; + gp->port = port->p; + } + graph->n_output++; + } + } + } + + /* order all nodes based on dependencies */ + graph->n_hndl = 0; + graph->hndl = calloc(n_nodes * n_hndl, sizeof(struct graph_hndl)); + graph->n_control = 0; + graph->control_port = calloc(n_control, sizeof(struct port *)); + while (true) { + if ((node = find_next_node(graph)) == NULL) + break; + + desc = node->desc; + d = desc->desc; + + if (!node->disabled) { + for (i = 0; i < n_hndl; i++) { + gh = &graph->hndl[graph->n_hndl++]; + gh->hndl = &node->hndl[i]; + gh->desc = d; + } + } + for (i = 0; i < desc->n_output; i++) { + spa_list_for_each(link, &node->output_port[i].link_list, output_link) + link->input->node->n_deps--; + } + for (i = 0; i < desc->n_notify; i++) { + spa_list_for_each(link, &node->notify_port[i].link_list, output_link) + link->input->node->n_deps--; + } + + /* collect all control ports on the graph */ + for (i = 0; i < desc->n_control; i++) { + graph->control_port[graph->n_control] = &node->control_port[i]; + graph->n_control++; + } + } + res = 0; +error: + return res; +} + +/** + * filter.graph = { + * nodes = [ + * { ... } ... + * ] + * links = [ + * { ... } ... + * ] + * inputs = [ ] + * outputs = [ ] + * input.volumes = [ + * ... + * ] + * output.volumes = [ + * ... + * ] + * } + */ +static int load_graph(struct graph *graph, const struct spa_dict *props) +{ + struct impl *impl = graph->impl; + struct spa_json it[2]; + struct spa_json inputs, outputs, *pinputs = NULL, *poutputs = NULL; + struct spa_json ivolumes, ovolumes, *pivolumes = NULL, *povolumes = NULL; + struct spa_json nodes, *pnodes = NULL, links, *plinks = NULL; + const char *json, *val; + char key[256]; + int res, len; + + spa_list_init(&graph->node_list); + spa_list_init(&graph->link_list); + + if ((json = spa_dict_lookup(props, "filter.graph")) == NULL) { + spa_log_error(impl->log, "missing filter.graph property"); + return -EINVAL; + } + + if (spa_json_begin_object(&it[0], json, strlen(json)) <= 0) { + spa_log_error(impl->log, "filter.graph must be an object"); + return -EINVAL; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq("n_inputs", key)) { + if (spa_json_parse_int(val, len, &res) <= 0) { + spa_log_error(impl->log, "%s expects an integer", key); + return -EINVAL; + } + impl->info.n_inputs = res; + } + else if (spa_streq("n_outputs", key)) { + if (spa_json_parse_int(val, len, &res) <= 0) { + spa_log_error(impl->log, "%s expects an integer", key); + return -EINVAL; + } + impl->info.n_outputs = res; + } + else if (spa_streq("nodes", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &nodes); + pnodes = &nodes; + } + else if (spa_streq("links", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &links); + plinks = &links; + } + else if (spa_streq("inputs", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &inputs); + pinputs = &inputs; + } + else if (spa_streq("outputs", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &outputs); + poutputs = &outputs; + } + else if (spa_streq("capture.volumes", key) || + spa_streq("input.volumes", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &ivolumes); + pivolumes = &ivolumes; + } + else if (spa_streq("playback.volumes", key) || + spa_streq("output.volumes", key)) { + if (!spa_json_is_array(val, len)) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_json_enter(&it[0], &ovolumes); + povolumes = &ovolumes; + } else { + spa_log_warn(impl->log, "unexpected graph key '%s'", key); + } + } + if (pnodes == NULL) { + spa_log_error(impl->log, "filter.graph is missing a nodes array"); + return -EINVAL; + } + while (spa_json_enter_object(pnodes, &it[1]) > 0) { + if ((res = load_node(graph, &it[1])) < 0) + return res; + } + if (plinks != NULL) { + while (spa_json_enter_object(plinks, &it[1]) > 0) { + if ((res = parse_link(graph, &it[1])) < 0) + return res; + } + } + if (pivolumes != NULL) { + while (spa_json_enter_object(pivolumes, &it[1]) > 0) { + if ((res = parse_volume(graph, &it[1], SPA_DIRECTION_INPUT)) < 0) + return res; + } + } + if (povolumes != NULL) { + while (spa_json_enter_object(povolumes, &it[1]) > 0) { + if ((res = parse_volume(graph, &it[1], SPA_DIRECTION_OUTPUT)) < 0) + return res; + } + } + return setup_graph(graph, pinputs, poutputs); +} + +static void graph_free(struct graph *graph) +{ + struct link *link; + struct node *node; + spa_list_consume(link, &graph->link_list, link) + link_free(link); + spa_list_consume(node, &graph->node_list, link) + node_free(node); + free(graph->input); + free(graph->output); + free(graph->hndl); + free(graph->control_port); +} + +static const struct spa_filter_graph_methods impl_filter_graph = { + SPA_VERSION_FILTER_GRAPH_METHODS, + .add_listener = impl_add_listener, + .enum_prop_info = impl_enum_prop_info, + .get_props = impl_get_props, + .set_props = impl_set_props, + .activate = impl_activate, + .deactivate = impl_deactivate, + .reset = impl_reset, + .process = impl_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FilterGraph)) + *interface = &this->filter_graph; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *impl = (struct impl *) handle; + + graph_free(&impl->graph); + + if (impl->dsp) + spa_fga_dsp_free(impl->dsp); + + free(impl->silence_data); + free(impl->discard_data); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *impl; + uint32_t i; + int res; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct impl *) handle; + impl->graph.impl = impl; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(impl->log, &log_topic); + + impl->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + impl->max_align = spa_cpu_get_max_align(impl->cpu); + + impl->dsp = spa_fga_dsp_new(impl->cpu ? spa_cpu_get_flags(impl->cpu) : 0); + + impl->loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); + + spa_list_init(&impl->plugin_list); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &impl->quantum_limit, 0); + if (spa_streq(k, "filter-graph.n_inputs")) + spa_atou32(s, &impl->info.n_inputs, 0); + if (spa_streq(k, "filter-graph.n_outputs")) + spa_atou32(s, &impl->info.n_outputs, 0); + } + if (impl->quantum_limit == 0) + return -EINVAL; + + impl->silence_data = calloc(impl->quantum_limit, sizeof(float)); + if (impl->silence_data == NULL) { + res = -errno; + goto error; + } + + impl->discard_data = calloc(impl->quantum_limit, sizeof(float)); + if (impl->discard_data == NULL) { + res = -errno; + goto error; + } + + if ((res = load_graph(&impl->graph, info)) < 0) { + spa_log_error(impl->log, "can't load graph: %s", spa_strerror(res)); + goto error; + } + + impl->filter_graph.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FilterGraph, + SPA_VERSION_FILTER_GRAPH, + &impl_filter_graph, impl); + spa_hook_list_init(&impl->hooks); + + return 0; +error: + free(impl->silence_data); + free(impl->discard_data); + return res; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_FilterGraph,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_filter_graph_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_filter_graph_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/ladspa.h b/spa/plugins/filter-graph/ladspa.h new file mode 100644 index 0000000..b1a9c4e --- /dev/null +++ b/spa/plugins/filter-graph/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/spa/plugins/filter-graph/ladspa_plugin.c b/spa/plugins/filter-graph/ladspa_plugin.c new file mode 100644 index 0000000..45026c8 --- /dev/null +++ b/spa/plugins/filter-graph/ladspa_plugin.c @@ -0,0 +1,385 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "audio-plugin.h" +#include "ladspa.h" + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_log *log; + + void *hndl; + LADSPA_Descriptor_Function desc_func; +}; + +struct descriptor { + struct spa_fga_descriptor desc; + const LADSPA_Descriptor *d; +}; + +static void *ladspa_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, + unsigned long SampleRate, int index, const char *config) +{ + struct descriptor *d = (struct descriptor *)desc; + return d->d->instantiate(d->d, SampleRate); +} + +static const LADSPA_Descriptor *find_desc(LADSPA_Descriptor_Function desc_func, const char *name) +{ + unsigned long i; + for (i = 0; ;i++) { + const LADSPA_Descriptor *d = desc_func(i); + if (d == NULL) + break; + if (spa_streq(d->Label, name)) + return d; + } + return NULL; +} + +static float get_default(struct spa_fga_port *port, LADSPA_PortRangeHintDescriptor hint, + LADSPA_Data lower, LADSPA_Data upper) +{ + LADSPA_Data def; + + switch (hint & LADSPA_HINT_DEFAULT_MASK) { + case LADSPA_HINT_DEFAULT_MINIMUM: + def = lower; + break; + case LADSPA_HINT_DEFAULT_MAXIMUM: + def = upper; + break; + case LADSPA_HINT_DEFAULT_LOW: + if (LADSPA_IS_HINT_LOGARITHMIC(hint)) + def = (LADSPA_Data) expf(logf(lower) * 0.75f + logf(upper) * 0.25f); + else + def = (LADSPA_Data) (lower * 0.75f + upper * 0.25f); + break; + case LADSPA_HINT_DEFAULT_MIDDLE: + if (LADSPA_IS_HINT_LOGARITHMIC(hint)) + def = (LADSPA_Data) expf(logf(lower) * 0.5f + logf(upper) * 0.5f); + else + def = (LADSPA_Data) (lower * 0.5f + upper * 0.5f); + break; + case LADSPA_HINT_DEFAULT_HIGH: + if (LADSPA_IS_HINT_LOGARITHMIC(hint)) + def = (LADSPA_Data) expf(logf(lower) * 0.25f + logf(upper) * 0.75f); + else + def = (LADSPA_Data) (lower * 0.25f + upper * 0.75f); + break; + case LADSPA_HINT_DEFAULT_0: + def = 0.0f; + break; + case LADSPA_HINT_DEFAULT_1: + def = 1.0f; + break; + case LADSPA_HINT_DEFAULT_100: + def = 100.0f; + break; + case LADSPA_HINT_DEFAULT_440: + def = 440.0f; + break; + default: + if (upper == lower) + def = upper; + else + def = SPA_CLAMPF(0.5f * upper, lower, upper); + break; + } + if (LADSPA_IS_HINT_INTEGER(hint)) + def = roundf(def); + return def; +} + +static void ladspa_port_update_ranges(struct descriptor *dd, struct spa_fga_port *port) +{ + const LADSPA_Descriptor *d = dd->d; + unsigned long p = port->index; + LADSPA_PortRangeHintDescriptor hint = d->PortRangeHints[p].HintDescriptor; + LADSPA_Data lower, upper; + + lower = d->PortRangeHints[p].LowerBound; + upper = d->PortRangeHints[p].UpperBound; + + port->hint = hint; + port->def = get_default(port, hint, lower, upper); + port->min = lower; + port->max = upper; +} + +static void ladspa_free(const struct spa_fga_descriptor *desc) +{ + struct descriptor *d = (struct descriptor*)desc; + free(d->desc.ports); + free(d); +} + +static const struct spa_fga_descriptor *ladspa_plugin_make_desc(void *plugin, const char *name) +{ + struct plugin *p = (struct plugin *)plugin; + struct descriptor *desc; + const LADSPA_Descriptor *d; + uint32_t i; + + d = find_desc(p->desc_func, name); + if (d == NULL) + return NULL; + + desc = calloc(1, sizeof(*desc)); + desc->d = d; + + desc->desc.instantiate = ladspa_instantiate; + desc->desc.cleanup = d->cleanup; + desc->desc.connect_port = d->connect_port; + desc->desc.activate = d->activate; + desc->desc.deactivate = d->deactivate; + desc->desc.run = d->run; + + desc->desc.free = ladspa_free; + + desc->desc.name = d->Label; + desc->desc.flags = 0; + + desc->desc.n_ports = d->PortCount; + desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct spa_fga_port)); + + for (i = 0; i < desc->desc.n_ports; i++) { + desc->desc.ports[i].index = i; + desc->desc.ports[i].name = d->PortNames[i]; + desc->desc.ports[i].flags = d->PortDescriptors[i]; + ladspa_port_update_ranges(desc, &desc->desc.ports[i]); + } + return &desc->desc; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = ladspa_plugin_make_desc, +}; + +static int ladspa_handle_load_by_path(struct plugin *impl, const char *path) +{ + int res; + void *handle = NULL; + LADSPA_Descriptor_Function desc_func; + + handle = dlopen(path, RTLD_NOW); + if (handle == NULL) { + spa_log_debug(impl->log, "failed to open '%s': %s", path, dlerror()); + res = -ENOENT; + goto exit; + } + + spa_log_info(impl->log, "successfully opened '%s'", path); + + desc_func = (LADSPA_Descriptor_Function) dlsym(handle, "ladspa_descriptor"); + if (desc_func == NULL) { + spa_log_warn(impl->log, "cannot find descriptor function in '%s': %s", path, dlerror()); + res = -ENOSYS; + goto exit; + } + + impl->hndl = handle; + impl->desc_func = desc_func; + return 0; + +exit: + if (handle) + dlclose(handle); + return res; +} + +static inline const char *split_walk(const char *str, const char *delimiter, size_t * len, const char **state) +{ + const char *s = *state ? *state : str; + + s += strspn(s, delimiter); + if (*s == '\0') + return NULL; + + *len = strcspn(s, delimiter); + *state = s + *len; + + return s; +} + +static int load_ladspa_plugin(struct plugin *impl, const char *path) +{ + int res = -ENOENT; + + if (path[0] != '/') { + const char *search_dirs, *p, *state = NULL; + char filename[PATH_MAX]; + size_t len; + + search_dirs = getenv("LADSPA_PATH"); + if (!search_dirs) + search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR; + + /* + * set the errno for the case when `ladspa_handle_load_by_path()` + * is never called, which can only happen if the supplied + * LADSPA_PATH contains too long paths + */ + res = -ENAMETOOLONG; + + while ((p = split_walk(search_dirs, ":", &len, &state))) { + int namelen; + + if (len >= sizeof(filename)) + continue; + + namelen = snprintf(filename, sizeof(filename), "%.*s/%s.so", (int) len, p, path); + if (namelen < 0 || (size_t) namelen >= sizeof(filename)) + continue; + + res = ladspa_handle_load_by_path(impl, filename); + if (res >= 0) + break; + } + } + else { + res = ladspa_handle_load_by_path(impl, path); + } + return res; +} + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct plugin *impl = (struct plugin *)handle; + if (impl->hndl) + dlclose(impl->hndl); + impl->hndl = NULL; + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + uint32_t i; + int res; + const char *path = NULL; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "filter.graph.path")) + path = s; + } + if (path == NULL) + return -EINVAL; + + if ((res = load_ladspa_plugin(impl, path)) < 0) { + spa_log_error(impl->log, "failed to load plugin '%s': %s", + path, spa_strerror(res)); + return res; + } + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_plugin_ladspa_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.ladspa", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_plugin_ladspa_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/lv2_plugin.c b/spa/plugins/filter-graph/lv2_plugin.c new file mode 100644 index 0000000..a2af22d --- /dev/null +++ b/spa/plugins/filter-graph/lv2_plugin.c @@ -0,0 +1,604 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#if defined __has_include +# if __has_include () + + #include + #include + #include + #include + #include + +# else + + #include + #include + #include + #include + #include + +# endif + +#endif + +#include "audio-plugin.h" + +static struct context *_context; + +typedef struct URITable { + char **data; + size_t alloc; + size_t len; +} URITable; + +static void uri_table_init(URITable *table) +{ + table->data = NULL; + table->len = table->alloc = 0; +} + +static void uri_table_destroy(URITable *table) +{ + size_t i; + for (i = 0; i < table->len; i++) + free(table->data[i]); + free(table->data); + uri_table_init(table); +} + +static LV2_URID uri_table_map(LV2_URID_Map_Handle handle, const char *uri) +{ + URITable *table = (URITable*)handle; + size_t i; + + for (i = 0; i < table->len; i++) + if (spa_streq(table->data[i], uri)) + return i+1; + + if (table->len == table->alloc) { + table->alloc += 64; + table->data = realloc(table->data, table->alloc * sizeof(char *)); + } + table->data[table->len++] = strdup(uri); + return table->len; +} + +static const char *uri_table_unmap(LV2_URID_Map_Handle handle, LV2_URID urid) +{ + URITable *table = (URITable*)handle; + if (urid > 0 && urid <= table->len) + return table->data[urid-1]; + return NULL; +} + +struct context { + int ref; + LilvWorld *world; + + 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(void) +{ + 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); + + return c; +error: + context_free(c); + return NULL; +} + +static struct context *context_ref(void) +{ + if (_context == NULL) { + _context = context_new(); + 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 spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_loop *main_loop; + + struct context *c; + const LilvPlugin *p; +}; + +struct descriptor { + struct spa_fga_descriptor desc; + struct plugin *p; +}; + +struct instance { + struct descriptor *desc; + struct plugin *p; + + LilvInstance *instance; + LV2_Worker_Schedule work_schedule; + LV2_Feature work_schedule_feature; + LV2_Options_Option options[6]; + LV2_Feature options_feature; + + const LV2_Feature *features[7]; + + const LV2_Worker_Interface *work_iface; + + int32_t block_length; + LV2_Atom empty_atom; +}; + +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; + spa_loop_invoke(i->p->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; + spa_loop_invoke(i->p->main_loop, do_schedule, 1, data, size, false, i); + return LV2_WORKER_SUCCESS; +} + +static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, + unsigned long SampleRate, int index, const char *config) +{ + struct descriptor *d = (struct descriptor*)desc; + struct plugin *p = d->p; + struct context *c = p->c; + struct instance *i; + uint32_t n, 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->p = p; + 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); + } + for (n = 0; n < desc->n_ports; n++) { + const LilvPort *port = lilv_plugin_get_port_by_index(p->p, n); + if (lilv_port_is_a(p->p, port, c->atom_AtomPort)) { + lilv_instance_connect_port(i->instance, n, &i->empty_atom); + } + } + + 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 spa_fga_descriptor *desc) +{ + struct descriptor *d = (struct descriptor*)desc; + free((char*)d->desc.name); + free(d->desc.ports); + free(d); +} + +static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const char *name) +{ + struct plugin *p = (struct plugin *)plugin; + struct context *c = p->c; + 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 spa_fga_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 spa_fga_port *fp = &desc->desc.ports[i]; + + fp->index = i; + fp->name = strdup(lilv_node_as_string(symbol)); + + fp->flags = 0; + if (lilv_port_is_a(p->p, port, c->lv2_InputPort)) + fp->flags |= SPA_FGA_PORT_INPUT; + if (lilv_port_is_a(p->p, port, c->lv2_OutputPort)) + fp->flags |= SPA_FGA_PORT_OUTPUT; + if (lilv_port_is_a(p->p, port, c->lv2_ControlPort)) + fp->flags |= SPA_FGA_PORT_CONTROL; + if (lilv_port_is_a(p->p, port, c->lv2_AudioPort)) + fp->flags |= SPA_FGA_PORT_AUDIO; + + fp->hint = 0; + fp->min = mins[i]; + fp->max = maxes[i]; + fp->def = controls[i]; + } + return &desc->desc; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = lv2_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct plugin *p = (struct plugin *)handle; + context_unref(p->c); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + uint32_t i; + int res; + const char *path = NULL; + const LilvPlugins *plugins; + LilvNode *uri; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + impl->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "filter.graph.path")) + path = s; + } + if (path == NULL) + return -EINVAL; + + impl->c = context_ref(); + if (impl->c == NULL) + return -EINVAL; + + uri = lilv_new_uri(impl->c->world, path); + if (uri == NULL) { + spa_log_warn(impl->log, "invalid URI %s", path); + res = -EINVAL; + goto error_cleanup; + } + + plugins = lilv_world_get_all_plugins(impl->c->world); + impl->p = lilv_plugins_get_by_uri(plugins, uri); + lilv_node_free(uri); + + if (impl->p == NULL) { + spa_log_warn(impl->log, "can't load plugin %s", path); + res = -EINVAL; + goto error_cleanup; + } + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + return 0; + +error_cleanup: + if (impl->c) + context_unref(impl->c); + return res; +} + + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_plugin_lv2_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.lv2", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_plugin_lv2_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/meson.build b/spa/plugins/filter-graph/meson.build new file mode 100644 index 0000000..c346a96 --- /dev/null +++ b/spa/plugins/filter-graph/meson.build @@ -0,0 +1,117 @@ +plugin_dependencies = [] +if get_option('spa-plugins').allowed() + plugin_dependencies += audioconvert_dep +endif + +simd_cargs = [] +simd_dependencies = [] + +if have_sse + filter_graph_sse = static_library('filter_graph_sse', + ['pffft.c', + 'audio-dsp-sse.c' ], + include_directories : [configinc], + c_args : [sse_args, '-O3', '-DHAVE_SSE'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE'] + simd_dependencies += filter_graph_sse +endif +if have_avx + filter_graph_avx = static_library('filter_graph_avx', + ['audio-dsp-avx.c' ], + include_directories : [configinc], + c_args : [avx_args, fma_args,'-O3', '-DHAVE_AVX'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_AVX'] + simd_dependencies += filter_graph_avx +endif +if have_neon + filter_graph_neon = static_library('filter_graph_neon', + ['pffft.c' ], + c_args : [neon_args, '-O3', '-DHAVE_NEON'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_NEON'] + simd_dependencies += filter_graph_neon +endif + +filter_graph_c = static_library('filter_graph_c', + ['pffft.c', + 'audio-dsp.c', + 'audio-dsp-c.c' ], + include_directories : [configinc], + c_args : [simd_cargs, '-O3', '-DPFFFT_SIMD_DISABLE'], + dependencies : [ spa_dep, fftw_dep], + install : false +) +simd_dependencies += filter_graph_c + +spa_filter_graph = shared_library('spa-filter-graph', + ['filter-graph.c' ], + include_directories : [configinc], + dependencies : [ spa_dep, sndfile_dep, plugin_dependencies, mathlib ], + install : true, + install_dir : spa_plugindir / 'filter-graph', + objects : audioconvert_c.extract_objects('biquad.c'), + link_with: simd_dependencies +) + + +filter_graph_dependencies = [ + spa_dep, mathlib, sndfile_dep, plugin_dependencies +] + +spa_filter_graph_plugin_builtin = shared_library('spa-filter-graph-plugin-builtin', + [ 'builtin_plugin.c', + 'convolver.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies ], + objects : audioconvert_c.extract_objects('biquad.c') +) + +spa_filter_graph_plugin_ladspa = shared_library('spa-filter-graph-plugin-ladspa', + [ 'ladspa_plugin.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, dl_lib ] +) + +if libmysofa_dep.found() +spa_filter_graph_plugin_sofa = shared_library('spa-filter-graph-plugin-sofa', + [ 'sofa_plugin.c', + 'convolver.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, libmysofa_dep ] +) +endif + +if lilv_lib.found() +spa_filter_graph_plugin_lv2 = shared_library('spa-filter-graph-plugin-lv2', + [ 'lv2_plugin.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, lilv_lib ] +) +endif + +if ebur128_lib.found() +spa_filter_graph_plugin_ebur128 = shared_library('spa-filter-graph-plugin-ebur128', + [ 'ebur128_plugin.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, lilv_lib, ebur128_lib ] +) +endif + diff --git a/spa/plugins/filter-graph/pffft.c b/spa/plugins/filter-graph/pffft.c new file mode 100644 index 0000000..6ec105c --- /dev/null +++ b/spa/plugins/filter-graph/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 +#include +#include +#include +#include + +#include + +/* 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 +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 +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 + +#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 * (float)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] = cosf(fi * argld); + wa[i - 1] = sinf(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 * (float)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] = cosf(fi * argld); + wa[i] = sinf(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 * (float)M_PI * (m + 1) * k / N; + s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] = + cosf(A); + s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] = + sinf(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 * (float)M_PI * (m + 1) * k / N; + s->e[(2 * (i * 3 + m) + 0) * SIMD_SZ + j] = + cosf(A); + s->e[(2 * (i * 3 + m) + 1) * SIMD_SZ + j] = + sinf(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 = (float)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 = (float)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/spa/plugins/filter-graph/pffft.h b/spa/plugins/filter-graph/pffft.h new file mode 100644 index 0000000..ac8e271 --- /dev/null +++ b/spa/plugins/filter-graph/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 // 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/spa/plugins/filter-graph/sofa_plugin.c b/spa/plugins/filter-graph/sofa_plugin.c new file mode 100644 index 0000000..d348bc9 --- /dev/null +++ b/spa/plugins/filter-graph/sofa_plugin.c @@ -0,0 +1,555 @@ +#include "config.h" + +#include + +#include +#include +#include + +#include "audio-plugin.h" +#include "convolver.h" +#include "audio-dsp.h" + +#include + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_loop *main_loop; + uint32_t quantum_limit; +}; + +struct spatializer_impl { + struct plugin *plugin; + + struct spa_fga_dsp *dsp; + struct spa_log *log; + + unsigned long rate; + float *port[6]; + int n_samples, blocksize, tailsize; + float *tmp[2]; + + struct MYSOFA_EASY *sofa; + unsigned int interpolate:1; + struct convolver *l_conv[3]; + struct convolver *r_conv[3]; +}; + +static void * spatializer_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct spatializer_impl *impl; + struct spa_json it[1]; + const char *val; + char key[256]; + char filename[PATH_MAX] = ""; + int len; + + errno = EINVAL; + if (config == NULL) { + spa_log_error(pl->log, "spatializer: no config was given"); + return NULL; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "spatializer: expected object in config"); + return NULL; + } + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) { + errno = ENOMEM; + return NULL; + } + + impl->plugin = pl; + impl->dsp = pl->dsp; + impl->log = pl->log; + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "blocksize")) { + if (spa_json_parse_int(val, len, &impl->blocksize) <= 0) { + spa_log_error(impl->log, "spatializer:blocksize requires a number"); + errno = EINVAL; + goto error; + } + } + else if (spa_streq(key, "tailsize")) { + if (spa_json_parse_int(val, len, &impl->tailsize) <= 0) { + spa_log_error(impl->log, "spatializer:tailsize requires a number"); + errno = EINVAL; + goto error; + } + } + else if (spa_streq(key, "filename")) { + if (spa_json_parse_stringn(val, len, filename, sizeof(filename)) <= 0) { + spa_log_error(impl->log, "spatializer:filename requires a string"); + errno = EINVAL; + goto error; + } + } + } + if (!filename[0]) { + spa_log_error(impl->log, "spatializer:filename was not given"); + errno = EINVAL; + goto error; + } + + int ret = MYSOFA_OK; + + impl->sofa = mysofa_open_cached(filename, SampleRate, &impl->n_samples, &ret); + + if (ret != MYSOFA_OK) { + const char *reason; + switch (ret) { + case MYSOFA_INVALID_FORMAT: + reason = "Invalid format"; + errno = EINVAL; + break; + case MYSOFA_UNSUPPORTED_FORMAT: + reason = "Unsupported format"; + errno = ENOTSUP; + break; + case MYSOFA_NO_MEMORY: + reason = "No memory"; + errno = ENOMEM; + break; + case MYSOFA_READ_ERROR: + reason = "Read error"; + errno = ENOENT; + break; + case MYSOFA_INVALID_ATTRIBUTES: + reason = "Invalid attributes"; + errno = EINVAL; + break; + case MYSOFA_INVALID_DIMENSIONS: + reason = "Invalid dimensions"; + errno = EINVAL; + break; + case MYSOFA_INVALID_DIMENSION_LIST: + reason = "Invalid dimension list"; + errno = EINVAL; + break; + case MYSOFA_INVALID_COORDINATE_TYPE: + reason = "Invalid coordinate type"; + errno = EINVAL; + break; + case MYSOFA_ONLY_EMITTER_WITH_ECI_SUPPORTED: + reason = "Only emitter with ECI supported"; + errno = ENOTSUP; + break; + case MYSOFA_ONLY_DELAYS_WITH_IR_OR_MR_SUPPORTED: + reason = "Only delays with IR or MR supported"; + errno = ENOTSUP; + break; + case MYSOFA_ONLY_THE_SAME_SAMPLING_RATE_SUPPORTED: + reason = "Only the same sampling rate supported"; + errno = ENOTSUP; + break; + case MYSOFA_RECEIVERS_WITH_RCI_SUPPORTED: + reason = "Receivers with RCI supported"; + errno = ENOTSUP; + break; + case MYSOFA_RECEIVERS_WITH_CARTESIAN_SUPPORTED: + reason = "Receivers with cartesian supported"; + errno = ENOTSUP; + break; + case MYSOFA_INVALID_RECEIVER_POSITIONS: + reason = "Invalid receiver positions"; + errno = EINVAL; + break; + case MYSOFA_ONLY_SOURCES_WITH_MC_SUPPORTED: + reason = "Only sources with MC supported"; + errno = ENOTSUP; + break; + default: + case MYSOFA_INTERNAL_ERROR: + errno = EIO; + reason = "Internal error"; + break; + } + spa_log_error(impl->log, "Unable to load HRTF from %s: %s (%d)", filename, reason, ret); + goto error; + } + + if (impl->blocksize <= 0) + impl->blocksize = SPA_CLAMP(impl->n_samples, 64, 256); + if (impl->tailsize <= 0) + impl->tailsize = SPA_CLAMP(4096, impl->blocksize, 32768); + + spa_log_info(impl->log, "using n_samples:%u %d:%d blocksize sofa:%s", impl->n_samples, + impl->blocksize, impl->tailsize, filename); + + impl->tmp[0] = calloc(impl->plugin->quantum_limit, sizeof(float)); + impl->tmp[1] = calloc(impl->plugin->quantum_limit, sizeof(float)); + impl->rate = SampleRate; + return impl; +error: + if (impl->sofa) + mysofa_close_cached(impl->sofa); + free(impl); + return NULL; +} + +static int +do_switch(struct spa_loop *loop, bool async, uint32_t seq, const void *data, + size_t size, void *user_data) +{ + struct spatializer_impl *impl = user_data; + + if (impl->l_conv[0] == NULL) { + SPA_SWAP(impl->l_conv[0], impl->l_conv[2]); + SPA_SWAP(impl->r_conv[0], impl->r_conv[2]); + } else { + SPA_SWAP(impl->l_conv[1], impl->l_conv[2]); + SPA_SWAP(impl->r_conv[1], impl->r_conv[2]); + } + impl->interpolate = impl->l_conv[0] && impl->l_conv[1]; + + return 0; +} + +static void spatializer_reload(void * Instance) +{ + struct spatializer_impl *impl = Instance; + float *left_ir = calloc(impl->n_samples, sizeof(float)); + float *right_ir = calloc(impl->n_samples, sizeof(float)); + float left_delay; + float right_delay; + float coords[3]; + + for (uint8_t i = 0; i < 3; i++) + coords[i] = impl->port[3 + i][0]; + + spa_log_info(impl->log, "making spatializer with %f %f %f", coords[0], coords[1], coords[2]); + + mysofa_s2c(coords); + mysofa_getfilter_float( + impl->sofa, + coords[0], + coords[1], + coords[2], + left_ir, + right_ir, + &left_delay, + &right_delay + ); + + // TODO: make use of delay + if ((left_delay != 0.0f || right_delay != 0.0f) && (!isnan(left_delay) || !isnan(right_delay))) + spa_log_warn(impl->log, "delay dropped l: %f, r: %f", left_delay, right_delay); + + if (impl->l_conv[2]) + convolver_free(impl->l_conv[2]); + if (impl->r_conv[2]) + convolver_free(impl->r_conv[2]); + + impl->l_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize, + left_ir, impl->n_samples); + impl->r_conv[2] = convolver_new(impl->dsp, impl->blocksize, impl->tailsize, + right_ir, impl->n_samples); + + free(left_ir); + free(right_ir); + + if (impl->l_conv[2] == NULL || impl->r_conv[2] == NULL) { + spa_log_error(impl->log, "reloading left or right convolver failed"); + return; + } + spa_loop_invoke(impl->plugin->data_loop, do_switch, 1, NULL, 0, true, impl); +} + +struct free_data { + void *item[2]; +}; + +static int +do_free(struct spa_loop *loop, bool async, uint32_t seq, const void *data, + size_t size, void *user_data) +{ + const struct free_data *fd = data; + if (fd->item[0]) + convolver_free(fd->item[0]); + if (fd->item[1]) + convolver_free(fd->item[1]); + return 0; +} + +static void spatializer_run(void * Instance, unsigned long SampleCount) +{ + struct spatializer_impl *impl = Instance; + + if (impl->interpolate) { + uint32_t len = SPA_MIN(SampleCount, impl->plugin->quantum_limit); + struct free_data free_data; + float *l = impl->tmp[0], *r = impl->tmp[1]; + + convolver_run(impl->l_conv[0], impl->port[2], impl->port[0], len); + convolver_run(impl->l_conv[1], impl->port[2], l, len); + convolver_run(impl->r_conv[0], impl->port[2], impl->port[1], len); + convolver_run(impl->r_conv[1], impl->port[2], r, len); + + for (uint32_t i = 0; i < SampleCount; i++) { + float t = (float)i / SampleCount; + impl->port[0][i] = impl->port[0][i] * (1.0f - t) + l[i] * t; + impl->port[1][i] = impl->port[1][i] * (1.0f - t) + r[i] * t; + } + free_data.item[0] = impl->l_conv[0]; + free_data.item[1] = impl->r_conv[0]; + impl->l_conv[0] = impl->l_conv[1]; + impl->r_conv[0] = impl->r_conv[1]; + impl->l_conv[1] = impl->r_conv[1] = NULL; + impl->interpolate = false; + + spa_loop_invoke(impl->plugin->main_loop, do_free, 1, &free_data, sizeof(free_data), false, impl); + } else if (impl->l_conv[0] && impl->r_conv[0]) { + convolver_run(impl->l_conv[0], impl->port[2], impl->port[0], SampleCount); + convolver_run(impl->r_conv[0], impl->port[2], impl->port[1], SampleCount); + } +} + +static void spatializer_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct spatializer_impl *impl = Instance; + if (Port > 5) + return; + impl->port[Port] = DataLocation; +} + +static void spatializer_cleanup(void * Instance) +{ + struct spatializer_impl *impl = Instance; + + for (uint8_t i = 0; i < 3; i++) { + if (impl->l_conv[i]) + convolver_free(impl->l_conv[i]); + if (impl->r_conv[i]) + convolver_free(impl->r_conv[i]); + } + if (impl->sofa) + mysofa_close_cached(impl->sofa); + free(impl->tmp[0]); + free(impl->tmp[1]); + + free(impl); +} + +static void spatializer_control_changed(void * Instance) +{ + spatializer_reload(Instance); +} + +static void spatializer_deactivate(void * Instance) +{ + struct spatializer_impl *impl = Instance; + if (impl->l_conv[0]) + convolver_reset(impl->l_conv[0]); + if (impl->r_conv[0]) + convolver_reset(impl->r_conv[0]); + impl->interpolate = false; +} + +static struct spa_fga_port spatializer_ports[] = { + { .index = 0, + .name = "Out L", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Out R", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + + { .index = 3, + .name = "Azimuth", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = 0.0f, .max = 360.0f + }, + { .index = 4, + .name = "Elevation", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -90.0f, .max = 90.0f + }, + { .index = 5, + .name = "Radius", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = 100.0f + }, +}; + +static const struct spa_fga_descriptor spatializer_desc = { + .name = "spatializer", + + .n_ports = 6, + .ports = spatializer_ports, + + .instantiate = spatializer_instantiate, + .connect_port = spatializer_connect_port, + .control_changed = spatializer_control_changed, + .deactivate = spatializer_deactivate, + .run = spatializer_run, + .cleanup = spatializer_cleanup, +}; + +static const struct spa_fga_descriptor * sofa_descriptor(unsigned long Index) +{ + switch(Index) { + case 0: + return &spatializer_desc; + } + return NULL; +} + + +static const struct spa_fga_descriptor *sofa_plugin_make_desc(void *plugin, const char *name) +{ + unsigned long i; + for (i = 0; ;i++) { + const struct spa_fga_descriptor *d = sofa_descriptor(i); + if (d == NULL) + break; + if (spa_streq(d->name, name)) + return d; + } + return NULL; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = sofa_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + impl->quantum_limit = 8192u; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + impl->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + impl->dsp = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioDSP); + + for (uint32_t i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &impl->quantum_limit, 0); + if (spa_streq(k, "filter.graph.audio.dsp")) + sscanf(s, "pointer:%p", &impl->dsp); + } + + if (impl->data_loop == NULL || impl->main_loop == NULL) { + spa_log_error(impl->log, "%p: could not find a data/main loop", impl); + return -EINVAL; + } + if (impl->dsp == NULL) { + spa_log_error(impl->log, "%p: could not find DSP functions", impl); + return -EINVAL; + } + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_sofa_plugin_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.sofa", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_sofa_plugin_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/jack/jack-client.c b/spa/plugins/jack/jack-client.c new file mode 100644 index 0000000..de61c6b --- /dev/null +++ b/spa/plugins/jack/jack-client.c @@ -0,0 +1,101 @@ +/* Spa JACK Client */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#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_log_trace_fp(client->log, "frames %u", nframes); + + spa_jack_client_emit_process(client); + + return 0; +} + +static void jack_shutdown(void* arg) +{ + struct spa_jack_client *client = arg; + + spa_log_warn(client->log, "%p", client); + + 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); + + spa_log_info(client->log, "%p: %s", client, client_name); + + 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_log_info(client->log, "%p:", client); + + 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..decd9ae --- /dev/null +++ b/spa/plugins/jack/jack-client.h @@ -0,0 +1,64 @@ +/* Spa JACK Client */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_JACK_CLIENT_H +#define SPA_JACK_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include + +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..632c00b --- /dev/null +++ b/spa/plugins/jack/jack-device.c @@ -0,0 +1,440 @@ +/* Spa JACK Device */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jack-client.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, "%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->client.log = 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); + + 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..ab50231 --- /dev/null +++ b/spa/plugins/jack/jack-sink.c @@ -0,0 +1,914 @@ +/* Spa jack client */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jack-client.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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; + + 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 inline bool is_following(struct impl *impl) +{ + return impl->position && impl->clock && impl->position->clock.id != impl->clock->id; +} + +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[8]; + 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 Sink"); + 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_ALWAYS_PROCESS, "true"); + items[5] = SPA_DICT_ITEM_INIT("priority.driver", "30001"); + items[6] = SPA_DICT_ITEM_INIT("node.group", "jack-group"); + items[7] = 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 (is_following(this)) + return; + + 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, "%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, "%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, "%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, ¶m, &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(1, 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, "%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, "%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, "%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; + } + return res | SPA_STATUS_NEED_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static 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, "%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 " }, + { 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..67b3a19 --- /dev/null +++ b/spa/plugins/jack/jack-source.c @@ -0,0 +1,939 @@ +/* Spa jack client */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jack-client.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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; + + 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, "%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[8]; + 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 Source"); + 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_ALWAYS_PROCESS, "true"); + items[5] = SPA_DICT_ITEM_INIT("priority.driver", "30000"); + items[6] = SPA_DICT_ITEM_INIT("node.group", "jack-group"); + items[7] = 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 inline bool is_following(struct impl *impl) +{ + return impl->position && impl->clock && impl->position->clock.id != impl->clock->id; +} + +static void client_process(void *data) +{ + struct impl *this = data; + int res; + + if (is_following(this)) + return; + + spa_log_trace_fp(this->log, "%p, process", this); + + res = spa_node_process(&this->node); + + 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, "%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, "%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, "%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, ¶m, &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(1, 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, "%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, "%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, "%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 | 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; + 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, "%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 " }, + { 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..317fcc0 --- /dev/null +++ b/spa/plugins/jack/plugin.c @@ -0,0 +1,37 @@ +/* Spa jack plugin */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp new file mode 100644 index 0000000..5eb46a1 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-device.cpp @@ -0,0 +1,355 @@ +/* Spa libcamera device */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* @author Raghavendra Rao Sidlagatta */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libcamera.h" +#include "libcamera-manager.hpp" + +#include +#include +#include + +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 manager; + std::shared_ptr camera; + + impl(spa_log *log, + std::shared_ptr manager, + std::shared_ptr camera, + std::string device_id); +}; + +} + +static const libcamera::Span cameraDevice(const Camera& camera) +{ + if (auto devices = camera.properties().get(properties::SystemDevices)) + return devices.value(); + + return {}; +} + +static std::string cameraModel(const Camera& camera) +{ + if (auto model = camera.properties().get(properties::Model)) + return std::move(model.value()); + + return camera.id(); +} + +static const char *cameraLoc(const Camera& camera) +{ + if (auto location = camera.properties().get(properties::Location)) { + switch (location.value()) { + case properties::CameraLocationFront: + return "front"; + case properties::CameraLocationBack: + return "back"; + case properties::CameraLocationExternal: + return "external"; + } + } + + return nullptr; +} + +static const char *cameraRot(const Camera& camera) +{ + if (auto rotation = camera.properties().get(properties::Rotation)) { + switch (rotation.value()) { + case 90: + return "90"; + case 180: + return "180"; + case 270: + return "270"; + default: + return "0"; + } + } + + 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]; + Camera& camera = *impl->camera; + + info = SPA_DEVICE_INFO_INIT(); + + info.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + + const auto path = "libcamera:" + impl->device_id; + ADD_ITEM(SPA_KEY_OBJECT_PATH, path.c_str()); + + ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device"); + ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, impl->device_id.c_str()); + + if (auto location = cameraLoc(camera)) + ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location); + if (auto rotation = cameraRot(camera)) + ADD_ITEM(SPA_KEY_API_LIBCAMERA_ROTATION, rotation); + + const auto model = cameraModel(camera); + ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model.c_str()); + ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model.c_str()); + + const auto name = "libcamera_device." + impl->device_id; + ADD_ITEM(SPA_KEY_DEVICE_NAME, name.c_str()); + + auto device_numbers = cameraDevice(camera); + std::string devids; + + if (!device_numbers.empty()) { + std::ostringstream s; + + + /* encode device numbers into a json array */ + s << "[ "; + for (const auto& devid : device_numbers) + s << devid << ' '; + s << ']'; + + devids = std::move(s).str(); + ADD_ITEM(SPA_KEY_DEVICE_DEVIDS, devids.c_str()); + } + +#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 = { + .version = 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(handle)); + return 0; +} + +impl::impl(spa_log *log, + std::shared_ptr manager, + std::shared_ptr 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_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..db2c73d --- /dev/null +++ b/spa/plugins/libcamera/libcamera-manager.cpp @@ -0,0 +1,466 @@ +/* Spa libcamera manager */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace libcamera; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libcamera.h" +#include "libcamera-manager.hpp" + +#define MAX_DEVICES 64 + +namespace { + +struct device { + uint32_t id; + std::shared_ptr 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 manager; + void addCamera(std::shared_ptr camera); + void removeCamera(std::shared_ptr camera); + + struct device devices[MAX_DEVICES]; + uint32_t n_devices = 0; + + struct hotplug_event { + enum class type { add, remove } type; + std::shared_ptr camera; + }; + + std::mutex hotplug_events_lock; + std::queue 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 global_manager; + +std::shared_ptr libcamera_manager_acquire(int& res) +{ + if (auto manager = global_manager.lock()) + return manager; + + auto manager = std::make_shared(); + 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) +{ + 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 = id; + 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; + + 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"); + ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, device->camera->id().c_str()); +#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) +{ + 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(data); + + for (;;) { + std::optional 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) +{ + { + 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) +{ + { + 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 : 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 = { + .version = 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(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_support_find(support, n_support, SPA_TYPE_INTERFACE_Log)); + + auto loop_utils = static_cast(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..71ebd6f --- /dev/null +++ b/spa/plugins/libcamera/libcamera-manager.hpp @@ -0,0 +1,9 @@ +/* Spa libcamera support */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +std::shared_ptr 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..f570499 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-source.cpp @@ -0,0 +1,1067 @@ +/* Spa libcamera source */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* @author Raghavendra Rao Sidlagatta */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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; +}; + +struct port { + struct impl *impl; + + std::optional current_format; + + struct spa_fraction rate = {}; + StreamConfiguration streamConfig; + + uint32_t memtype = 0; + uint32_t buffers_blocks = 1; + + 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_PROPS | 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 PORT_Latency 6 +#define N_PORT_PARAMS 7 + 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); + params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READ); + + info.flags = SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL; + info.params = params; + 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 out_ports; + + struct spa_io_position *position = nullptr; + struct spa_io_clock *clock = nullptr; + + struct spa_latency_info latency[2]; + + std::shared_ptr manager; + std::shared_ptr camera; + + FrameBufferAllocator *allocator = nullptr; + std::vector> requestPool; + std::deque pendingRequests; + + void requestComplete(libcamera::Request *request); + + std::unique_ptr 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 manager, std::shared_ptr camera, std::string device_id); + + struct spa_dll dll; +}; + +} + +#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, ¶m, &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; + if (impl->clock) + SPA_FLAG_SET(impl->clock->flags, SPA_IO_CLOCK_FLAG_NO_RATE); + break; + case SPA_IO_Position: + impl->position = (struct spa_io_position*)data; + 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 void emit_node_info(struct impl *impl, bool full) +{ + 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" }, + }; + 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) +{ + static const struct spa_dict_item info_items[] = { + { SPA_KEY_PORT_GROUP, "stream.0" }, + }; + 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 dict = SPA_DICT_INIT_ARRAY(info_items); + port->info.props = &dict; + 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, ¶m, &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(port->buffers_blocks), + 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; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + param = spa_latency_build(&b, id, &impl->latency[result.index]); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + 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 = { + .version = 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(handle)); + return 0; +} + +impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system, + std::shared_ptr manager, std::shared_ptr 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; + + latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); +} + +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_support_find(support, n_support, SPA_TYPE_INTERFACE_Log)); + auto data_loop = static_cast(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop)); + auto system = static_cast(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..92185c4 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-utils.cpp @@ -0,0 +1,1071 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ +/* @author Raghavendra Rao Sidlagatta */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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; + + impl->config = impl->camera->generateConfiguration({ StreamRole::VideoRecording }); +} + +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 = impl->camera->createRequest(i); + if (!request) { + impl->requestPool.clear(); + return -ENOMEM; + } + impl->requestPool.push_back(std::move(request)); + } + + /* Some devices require data for each output video frame to be + * placed in discontiguous memory buffers. In such cases, one + * video frame has to be addressed using more than one memory. + * address. Therefore, need calculate the number of discontiguous + * memory and allocate the specified amount of memory */ + Stream *stream = impl->config->at(0).stream(); + const std::vector> &bufs = + impl->allocator->buffers(stream); + const std::vector &planes = bufs[0]->planes(); + int fd = -1; + uint32_t buffers_blocks = 0; + + for (const FrameBuffer::Plane &plane : planes) { + const int current_fd = plane.fd.get(); + if (current_fd >= 0 && current_fd != fd) { + buffers_blocks += 1; + fd = current_fd; + } + } + + if (buffers_blocks > 0) { + port->buffers_blocks = buffers_blocks; + } + return res; +} + +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; + port->ring = SPA_RINGBUFFER_INIT(); + + 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, BGR, video, raw), + MAKE_FMT(formats::BGR888, RGB, video, raw), + MAKE_FMT(formats::XRGB8888, BGRx, video, raw), + MAKE_FMT(formats::XBGR8888, RGBx, video, raw), + MAKE_FMT(formats::RGBX8888, xBGR, video, raw), + MAKE_FMT(formats::BGRX8888, xRGB, video, raw), + MAKE_FMT(formats::ARGB8888, BGRA, video, raw), + MAKE_FMT(formats::ABGR8888, RGBA, video, raw), + MAKE_FMT(formats::RGBA8888, ABGR, video, raw), + MAKE_FMT(formats::BGRA8888, ARGB, 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 struct { + uint32_t id; + uint32_t spa_id; +} control_map[] = { + { libcamera::controls::BRIGHTNESS, SPA_PROP_brightness }, + { libcamera::controls::CONTRAST, SPA_PROP_contrast }, + { libcamera::controls::SATURATION, SPA_PROP_saturation }, + { libcamera::controls::EXPOSURE_TIME, SPA_PROP_exposure }, + { libcamera::controls::ANALOGUE_GAIN, SPA_PROP_gain }, + { libcamera::controls::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->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->id; + } + if (prop_id >= SPA_PROP_START_CUSTOM) + return prop_id - SPA_PROP_START_CUSTOM; + return SPA_ID_INVALID; +} + +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, id; + 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; + + id = control_to_prop_id(impl, ctrl_id->id()); + + 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(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(); + else + def = ctrl_info.def().get(); + + 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 max = ctrl_info.max().get(); + float def; + + if (ctrl_info.def().isNone()) + def = (min + max) / 2; + else + def = ctrl_info.def().get(); + + 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 max = ctrl_info.max().get(); + int32_t def; + + if (ctrl_info.def().isNone()) + def = (min + max) / 2; + else + def = ctrl_info.def().get(); + + 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; + uint32_t control_id; + + control_id = prop_id_to_control(impl, prop->key); + if (control_id == SPA_ID_INVALID) + return -ENOENT; + + auto v = info.idmap().find(control_id); + 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 { + Orientation libcamera_orientation; /* clockwise rotation then horizontal mirroring */ + uint32_t spa_transform_value; /* horizontal mirroring then counter-clockwise rotation */ +} orientation_map[] = { + { Orientation::Rotate0, SPA_META_TRANSFORMATION_None }, + { Orientation::Rotate0Mirror, SPA_META_TRANSFORMATION_Flipped }, + { Orientation::Rotate90, SPA_META_TRANSFORMATION_270 }, + { Orientation::Rotate90Mirror, SPA_META_TRANSFORMATION_Flipped90 }, + { Orientation::Rotate180, SPA_META_TRANSFORMATION_180 }, + { Orientation::Rotate180Mirror, SPA_META_TRANSFORMATION_Flipped180 }, + { Orientation::Rotate270, SPA_META_TRANSFORMATION_90 }, + { Orientation::Rotate270Mirror, SPA_META_TRANSFORMATION_Flipped270 }, +}; + +static uint32_t libcamera_orientation_to_spa_transform_value(Orientation orientation) +{ + for (const auto& t : orientation_map) { + if (t.libcamera_orientation == orientation) + 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> &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, "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_orientation_to_spa_transform_value(impl->config->orientation); + spa_log_debug(impl->log, "Setting videotransform for buffer %u to %u", + i, b->videotransform->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].chunk->stride = port->streamConfig.stride; + d[j].chunk->flags = 0; + /* Update parameters according to the plane information */ + unsigned int numPlanes = bufs[i]->planes().size(); + if (buffers[i]->n_datas < numPlanes) { + if (j < buffers[i]->n_datas - 1) { + d[j].maxsize = bufs[i]->planes()[j].length; + d[j].chunk->offset = bufs[i]->planes()[j].offset; + d[j].chunk->size = bufs[i]->planes()[j].length; + } else { + d[j].chunk->offset = bufs[i]->planes()[j].offset; + for (uint8_t k = j; k < numPlanes; k++) { + d[j].maxsize += bufs[i]->planes()[k].length; + d[j].chunk->size += bufs[i]->planes()[k].length; + } + } + } else if (buffers[i]->n_datas == numPlanes) { + d[j].maxsize = bufs[i]->planes()[j].length; + d[j].chunk->offset = bufs[i]->planes()[j].offset; + d[j].chunk->size = bufs[i]->planes()[j].length; + } else { + spa_log_warn(impl->log, "buffer index: i: %d, data member " + "numbers: %d is greater than plane number: %d", + i, buffers[i]->n_datas, numPlanes); + d[j].maxsize = port->streamConfig.frameSize; + d[j].chunk->offset = 0; + d[j].chunk->size = port->streamConfig.frameSize; + } + + if (port->memtype == SPA_DATA_DmaBuf || + port->memtype == SPA_DATA_MemFd) { + d[j].flags |= SPA_DATA_FLAG_MAPPABLE; + d[j].fd = bufs[i]->planes()[j].fd.get(); + spa_log_debug(impl->log, "Got fd = %" PRId64 " 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) { + double target = (double)port->info.rate.num / port->info.rate.denom; + double corr; + + if (impl->dll.bw == 0.0) { + spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom); + impl->clock->next_nsec = fmd.timestamp; + corr = 1.0; + } else { + double diff = ((double)impl->clock->next_nsec - (double)fmd.timestamp) / SPA_NSEC_PER_SEC; + double error = port->info.rate.denom * (diff - target); + corr = spa_dll_update(&impl->dll, SPA_CLAMPD(error, -128., 128.)); + } + /* FIXME, we should follow the driver clock and target_ values. + * for now we ignore and use our own. */ + impl->clock->target_rate = port->rate; + impl->clock->target_duration = 1; + + 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 = corr; + impl->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr); + } + 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, "Exiting %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->dll.bw = 0.0; + + impl->source.func = libcamera_on_fd_events; + impl->source.data = impl; + impl->source.fd = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + 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 &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..207b857 --- /dev/null +++ b/spa/plugins/libcamera/libcamera.c @@ -0,0 +1,38 @@ +/* Spa libcamera support */ +/* SPDX-FileCopyrightText: Copyright © 2020 collabora */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +#include "libcamera.h" + +SPA_LOG_TOPIC_DEFINE(libcamera_log_topic, "spa.libcamera"); + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..7d27981 --- /dev/null +++ b/spa/plugins/libcamera/libcamera.h @@ -0,0 +1,31 @@ +/* Spa libcamera support */ +/* SPDX-FileCopyrightText: Copyright © 2020 collabora */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include +#include + +#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..9f396ab --- /dev/null +++ b/spa/plugins/libcamera/meson.build @@ -0,0 +1,13 @@ +libcamera_sources = [ + 'libcamera.c', + 'libcamera-manager.cpp', + 'libcamera-device.cpp', + 'libcamera-source.cpp' +] + +libcameralib = shared_library('spa-libcamera', + libcamera_sources, + include_directories : [ configinc ], + dependencies : [ spa_dep, libcamera_dep, pthread_lib ], + install : true, + install_dir : spa_plugindir / 'libcamera') diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build new file mode 100644 index 0000000..42aec7e --- /dev/null +++ b/spa/plugins/meson.build @@ -0,0 +1,58 @@ +if alsa_dep.found() and host_machine.system() == 'linux' + 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 have_vulkan + subdir('vulkan') +endif + +v4l2_header_found = cc.has_header('linux/videodev2.h', required: get_option('v4l2')) +summary({'V4L2 kernel header': v4l2_header_found}, bool_yn: true, section: 'Backend') +summary({'V4L2 enabled': v4l2_header_found}, bool_yn: true, section: 'Backend') +if v4l2_header_found + subdir('v4l2') +endif +if libcamera_dep.found() + subdir('libcamera') +endif + +subdir('aec') +subdir('filter-graph') diff --git a/spa/plugins/support/cpu-arm.c b/spa/plugins/support/cpu-arm.c new file mode 100644 index 0000000..bc34f15 --- /dev/null +++ b/spa/plugins/support/cpu-arm.c @@ -0,0 +1,117 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include + +#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-riscv.c b/spa/plugins/support/cpu-riscv.c new file mode 100644 index 0000000..e6ffa0a --- /dev/null +++ b/spa/plugins/support/cpu-riscv.c @@ -0,0 +1,29 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright (c) 2023 Institue of Software Chinese Academy of Sciences (ISCAS). */ +/* SPDX-License-Identifier: MIT */ + +#ifdef HAVE_SYS_AUXV_H +#include +#define HWCAP_RV(letter) (1ul << ((letter) - 'A')) +#endif + +static int +riscv_init(struct impl *impl) +{ + uint32_t flags = 0; + +#ifdef HAVE_SYS_AUXV_H + const unsigned long hwcap = getauxval(AT_HWCAP); + if (hwcap & HWCAP_RV('V')) + flags |= SPA_CPU_FLAG_RISCV_V; +#endif + + impl->flags = flags; + + return 0; +} + +static int riscv_zero_denormals(void *object, bool enable) +{ + return 0; +} diff --git a/spa/plugins/support/cpu-x86.c b/spa/plugins/support/cpu-x86.c new file mode 100644 index 0000000..c1c5385 --- /dev/null +++ b/spa/plugins/support/cpu-x86.c @@ -0,0 +1,184 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +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 +#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..c2c419f --- /dev/null +++ b/spa/plugins/support/cpu.c @@ -0,0 +1,300 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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; +}; + +static 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 +# elif defined (__riscv) +#include "cpu-riscv.c" +#define init(t) riscv_init(t) +#define impl_cpu_zero_denormals riscv_zero_denormals +# else +#define init(t) +#define impl_cpu_zero_denormals NULL +#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..d047fca --- /dev/null +++ b/spa/plugins/support/dbus.c @@ -0,0 +1,578 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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; + + spa_log_debug(impl->log, "impl:%p", 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_log_debug(impl->log, "impl:%p %d", impl, status); + 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_log_debug(impl->log, "wakeup main impl:%p", 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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..4538346 --- /dev/null +++ b/spa/plugins/support/evl-plugin.c @@ -0,0 +1,30 @@ +/* Spa Support plugin */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include + +extern const struct spa_handle_factory spa_support_evl_system_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..b0fade3 --- /dev/null +++ b/spa/plugins/support/evl-system.c @@ -0,0 +1,506 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.evl-system"); + +#define MAX_POLL 512 + +struct poll_entry { + int pfd; + int fd; + uint32_t events; + void *data; + unsigned attached:1; +}; + +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; + pthread_t thread; + 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_free(struct impl *impl) +{ + uint32_t i; + for (i = 0; i < impl->n_entries; i++) { + struct poll_entry *e = &impl->entries[i]; + if (e->fd == -1) + return e; + } + if (impl->n_entries == MAX_POLL) { + errno = ENOSPC; + return NULL; + } + return &impl->entries[impl->n_entries++]; +} + +static inline struct poll_entry *find_entry(struct impl *impl, int pfd, int fd) +{ + uint32_t i; + 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 attach_entry(struct impl *impl, struct poll_entry *e) +{ + if (!e->attached && e->fd != -1) { + int res; + + res = evl_add_pollfd(e->pfd, e->fd, e->events, evl_nil); + if (res < 0) + return res; + + e->attached = true; + } + return 0; +} + +static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data) +{ + struct impl *impl = object; + struct poll_entry *e; + int res = 0; + + if ((e = find_free(impl)) == NULL) + return -errno; + + e->pfd = pfd; + e->fd = fd; + e->events = events; + e->data = data; + if (impl->attached != 0) + attach_entry(impl, e); + return res; +} + +static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data) +{ + 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, evl_nil); +} + +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; + e->attached = false; + 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; + impl->thread = pthread_self(); + + for (i = 0; i < (int)impl->n_entries; i++) { + struct poll_entry *e = &impl->entries[i]; + attach_entry(impl, e); + } + } + + if (timeout == -1) { + 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) +{ + if (oob_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 res, fl; + struct evl_flags flg; + + fl = EVL_CLONE_PUBLIC; + if (flags & SPA_FD_NONBLOCK) + fl |= EVL_CLONE_NONBLOCK; + + res = evl_create_flags(&flg, EVL_CLOCK_MONOTONIC, 0, fl, + "flags-%d-%p-%d", impl->pid, impl, impl->n_xbuf); + if (res < 0) + return res; + + impl->n_xbuf++; + + return res; +} + +static int impl_eventfd_write(void *object, int fd, uint64_t count) +{ + int res; + int flags = count; + struct impl *impl = object; + pthread_t tid = pthread_self(); + + if (impl->thread != tid) + res = write(fd, &flags, sizeof(flags)); + else + res = oob_write(fd, &flags, sizeof(flags)); + + if (res != sizeof(flags)) + res = -errno; + return res; +} + +static int impl_eventfd_read(void *object, int fd, uint64_t *count) +{ + int res; + int flags; + struct impl *impl = object; + pthread_t tid = pthread_self(); + + if (impl->thread != tid) + res = read(fd, &flags, sizeof(flags)); + else + res = oob_read(fd, &flags, sizeof(flags)); + + *count = flags; + if (res != sizeof(flags)) + return -errno; + return 0; +} + +/* 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_init()) < 0) { + spa_log_error(impl->log, "%p: init failed: %s", impl, spa_strerror(res)); + return res; + } + + spa_log_info(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_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..7e99d20 --- /dev/null +++ b/spa/plugins/support/journal.c @@ -0,0 +1,329 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Sergey Bugaev */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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; +}; + +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) +{ + static const char * const levels[] = { "-", "E", "W", "I", "D", "T", "*T*" }; + 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); + impl->chain_log->level = impl->log.level; + 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 (spa_log_level_topic_enabled(&impl->log, topic, SPA_LOG_LEVEL_DEBUG)) { + const char *lev = levels[SPA_CLAMP(level, 0u, SPA_N_ELEMENTS(levels) - 1u)]; + const char *tp = topic ? topic->topic : ""; + + if (file && func) { + const char *f = strrchr(file, '/'); + + f = f ? f+1 : file; + sz = spa_scnprintf(message_buffer, sizeof(message_buffer), + "%s %s%s[%s:%d:%s]: ", + lev, tp, topic ? " " : "", + f, line, func); + } else { + sz = spa_scnprintf(message_buffer, sizeof(message_buffer), + "%s %s%s", lev, tp, topic ? ": " : ""); + } + } else 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, +#ifdef HAVE_GETTID + "TID=%jd", (intmax_t) gettid(), +#endif + 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 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, +}; + +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 SPA_UNUSED *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + 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; + + if (info) { + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL) + impl->log.level = atoi(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, "%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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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/logger.c b/spa/plugins/support/logger.c new file mode 100644 index 0000000..c6e6ca4 --- /dev/null +++ b/spa/plugins/support/logger.c @@ -0,0 +1,428 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC +#elif defined(_MSC_VER) +static inline void setlinebuf(FILE* stream) { + setvbuf(stream, NULL, _IOLBF, 0); +} +#endif + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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]; + + clockid_t clock_id; + + unsigned int have_source:1; + unsigned int colors:1; + unsigned int timestamp:1; + unsigned int local_timestamp:1; + unsigned int line:1; +}; + +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[18] = {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->local_timestamp) { + char buf[64]; + struct timespec now; + struct tm now_tm; + + clock_gettime(impl->clock_id, &now); + localtime_r(&now.tv_sec, &now_tm); + strftime(buf, sizeof(buf), "%H:%M:%S", &now_tm); + spa_scnprintf(timestamp, sizeof(timestamp), "[%s.%06d]", buf, + (int)(now.tv_nsec / SPA_NSEC_PER_USEC)); + } else if (impl->timestamp) { + struct timespec now; + clock_gettime(impl->clock_id, &now); + spa_scnprintf(timestamp, sizeof(timestamp), "[%05jd.%06jd]", + (intmax_t) (now.tv_sec & 0x1FFFFFFF) % 100000, (intmax_t) now.tv_nsec / 1000); + } + + 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 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, +}; + +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; + + 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, *dest = ""; + bool linebuf = false; + bool force_colors = false; + + 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); + + 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) { + str = spa_dict_lookup(info, SPA_KEY_LOG_TIMESTAMP); + if (spa_atob(str) || spa_streq(str, "local")) { + this->clock_id = CLOCK_REALTIME; + this->local_timestamp = true; + } else if (spa_streq(str, "monotonic")) { + this->clock_id = CLOCK_MONOTONIC; + this->timestamp = true; + } else if (spa_streq(str, "monotonic-raw")) { + this->clock_id = CLOCK_MONOTONIC_RAW; + this->timestamp = true; + } else if (spa_streq(str, "realtime")) { + this->clock_id = CLOCK_REALTIME; + this->timestamp = true; + } + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LINE)) != NULL) + this->line = spa_atob(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_COLORS)) != NULL) { + if (spa_streq(str, "force")) { + this->colors = true; + force_colors = true; + } else { + 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) { + dest = str; + if (spa_streq(str, "stderr")) + this->file = stderr; + else if (spa_streq(str, "stdout")) + this->file = stdout; + else { + 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 (this->file == NULL) { + this->file = stderr; + dest = "stderr"; + } else { + linebuf = true; + } + if (linebuf) + setlinebuf(this->file); + + if (this->colors && !force_colors && !isatty(fileno(this->file)) ) { + this->colors = false; + } + + spa_ringbuffer_init(&this->trace_rb); + + spa_log_debug(&this->log, "%p: initialized to %s linebuf:%u", this, dest, linebuf); + + 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..8af61e7 --- /dev/null +++ b/spa/plugins/support/loop.c @@ -0,0 +1,1337 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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 + +/* the number of concurrent queues for invoke. This is also the number + * of threads that can concurrently invoke. When there are more, the + * retry timeout will be used to retry. */ +#define QUEUES_MAX 128 +#define DEFAULT_RETRY (1 * SPA_USEC_PER_SEC) + +/** \cond */ + +struct invoke_item { + size_t item_size; + spa_invoke_func_t func; + uint32_t seq; + uint32_t count; + void *data; + size_t size; + bool block; + void *user_data; + int res; +}; + +static int loop_signal_event(void *object, struct spa_source *source); + +struct queue; + +#define IDX_INVALID ((uint16_t)0xffff) +union tag { + struct { + uint16_t idx; + uint16_t count; + } t; + uint32_t v; +}; + +struct impl { + struct spa_handle handle; + struct spa_loop loop; + 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; + + struct spa_ratelimit rate_limit; + int retry_timeout; + + union tag head; + + uint32_t n_queues; + struct queue *queues[QUEUES_MAX]; + pthread_mutex_t queue_lock; + + int poll_fd; + pthread_t thread; + int enter_count; + + struct spa_source *wakeup; + + uint32_t count; + uint32_t flush_count; + + unsigned int polling:1; +}; + +struct queue { + struct impl *impl; + + uint16_t idx; + uint16_t next; + + int ack_fd; + bool close_fd; + + struct queue *overflow; + + struct spa_ringbuffer buffer; + uint8_t *buffer_data; + uint8_t buffer_mem[DATAS_SIZE + MAX_ALIGN]; +}; + +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 inline uint64_t get_time_ns(struct spa_system *system) +{ + struct timespec ts; + spa_system_clock_gettime(system, CLOCK_MONOTONIC, &ts); + return SPA_TIMESPEC_TO_NSEC(&ts); +} + +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 loop_queue_destroy(void *data) +{ + struct queue *queue = data; + struct impl *impl = queue->impl; + + if (queue->close_fd) + spa_system_close(impl->system, queue->ack_fd); + + if (queue->overflow) + loop_queue_destroy(queue->overflow); + + spa_log_info(impl->log, "%p destroyed queue %p idx:%d", impl, queue, queue->idx); + + free(queue); +} + +static struct queue *loop_create_queue(void *object, bool with_fd) +{ + struct impl *impl = object; + struct queue *queue; + int res; + + queue = calloc(1, sizeof(struct queue)); + if (queue == NULL) + return NULL; + + queue->idx = IDX_INVALID; + queue->next = IDX_INVALID; + queue->impl = impl; + + queue->buffer_data = SPA_PTR_ALIGN(queue->buffer_mem, MAX_ALIGN, uint8_t); + spa_ringbuffer_init(&queue->buffer); + + if (with_fd) { + if ((res = spa_system_eventfd_create(impl->system, + SPA_FD_EVENT_SEMAPHORE | SPA_FD_CLOEXEC)) < 0) { + spa_log_error(impl->log, "%p: can't create ack event: %s", + impl, spa_strerror(res)); + goto error; + } + queue->ack_fd = res; + queue->close_fd = true; + + while (true) { + uint16_t idx = SPA_ATOMIC_LOAD(impl->n_queues); + if (idx >= QUEUES_MAX) { + /* this is pretty bad, there are QUEUES_MAX concurrent threads + * that are doing an invoke */ + spa_log_error(impl->log, "max queues %d exceeded!", idx); + res = -ENOSPC; + goto error; + } + queue->idx = idx; + if (SPA_ATOMIC_CAS(impl->queues[queue->idx], NULL, queue)) { + SPA_ATOMIC_INC(impl->n_queues); + break; + } + } + } + spa_log_info(impl->log, "%p created queue %p idx:%d %p", impl, queue, queue->idx, + (void*)pthread_self()); + + return queue; + +error: + loop_queue_destroy(queue); + errno = -res; + return NULL; +} + + +static inline struct queue *get_queue(struct impl *impl) +{ + union tag head, next; + + head.v = SPA_ATOMIC_LOAD(impl->head.v); + + while (true) { + struct queue *queue; + + if (SPA_UNLIKELY(head.t.idx == IDX_INVALID)) + return NULL; + + queue = impl->queues[head.t.idx]; + next.t.idx = queue->next; + next.t.count = head.t.count+1; + + if (SPA_LIKELY(__atomic_compare_exchange_n(&impl->head.v, &head.v, next.v, + 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))) { + spa_log_trace(impl->log, "%p idx:%d %p", queue, queue->idx, (void*)pthread_self()); + return queue; + } + } + return NULL; +} + +static inline void put_queue(struct impl *impl, struct queue *queue) +{ + union tag head, next; + + spa_log_trace(impl->log, "%p idx:%d %p", queue, queue->idx, (void*)pthread_self()); + + head.v = SPA_ATOMIC_LOAD(impl->head.v); + + while (true) { + queue->next = head.t.idx; + + next.t.idx = queue->idx; + next.t.count = head.t.count+1; + + if (SPA_LIKELY(__atomic_compare_exchange_n(&impl->head.v, &head.v, next.v, + 0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))) + break; + } +} + + +static inline int32_t item_compare(struct invoke_item *a, struct invoke_item *b) +{ + return (int32_t)(a->count - b->count); +} + +static void flush_all_queues(struct impl *impl) +{ + uint32_t flush_count; + int res; + + flush_count = SPA_ATOMIC_INC(impl->flush_count); + while (true) { + struct queue *cqueue, *queue = NULL; + struct invoke_item *citem, *item = NULL; + uint32_t cindex, index; + spa_invoke_func_t func; + bool block; + uint32_t i, n_queues; + + n_queues = SPA_ATOMIC_LOAD(impl->n_queues); + for (i = 0; i < n_queues; i++) { + /* loop over all queues and overflow queues */ + for (cqueue = impl->queues[i]; cqueue != NULL; + cqueue = SPA_ATOMIC_LOAD(cqueue->overflow)) { + if (spa_ringbuffer_get_read_index(&cqueue->buffer, &cindex) < + (int32_t)sizeof(struct invoke_item)) + continue; + + citem = SPA_PTROFF(cqueue->buffer_data, + cindex & (DATAS_SIZE - 1), struct invoke_item); + + if (item == NULL || item_compare(citem, item) < 0) { + item = citem; + queue = cqueue; + index = cindex; + } + } + } + if (item == NULL) + break; + + spa_log_trace_fp(impl->log, "%p: flush item %p", queue, 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. */ + func = spa_steal_ptr(item->func); + 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 != SPA_ATOMIC_LOAD(impl->flush_count)) + break; + + index += item->item_size; + block = item->block; + spa_ringbuffer_read_update(&queue->buffer, index); + + if (block && queue->ack_fd != -1) { + if ((res = spa_system_eventfd_write(impl->system, queue->ack_fd, 1)) < 0) + spa_log_warn(impl->log, "%p: failed to write event fd:%d: %s", + queue, queue->ack_fd, spa_strerror(res)); + } + } +} + +static int +loop_queue_invoke(void *object, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + bool block, + void *user_data) +{ + struct queue *queue = object, *orig = queue, *overflow; + struct impl *impl = queue->impl; + struct invoke_item *item; + int res; + int32_t filled; + uint32_t avail, idx, offset, l0; + bool in_thread; + pthread_t loop_thread, current_thread = pthread_self(); + +again: + loop_thread = impl->thread; + in_thread = (loop_thread == 0 || pthread_equal(loop_thread, current_thread)); + + filled = spa_ringbuffer_get_write_index(&queue->buffer, &idx); + spa_assert_se(filled >= 0 && filled <= DATAS_SIZE && "queue xrun"); + avail = (uint32_t)(DATAS_SIZE - filled); + if (avail < sizeof(struct invoke_item)) + goto xrun; + 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(queue->buffer_data, offset, struct invoke_item); + item->func = func; + item->seq = seq; + item->count = SPA_ATOMIC_INC(impl->count); + item->size = size; + item->block = in_thread ? false : 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(impl->log, "%p: add item %p filled:%d block:%d", queue, item, filled, block); + + if (l0 >= item->item_size) { + /* item + size fit in current ringbuffer idx */ + 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 = queue->buffer_data; + item->item_size = SPA_ROUND_UP_N(l0 + size, ITEM_ALIGN); + } + if (avail < item->item_size) + goto xrun; + + if (data && size > 0) + memcpy(item->data, data, size); + + spa_ringbuffer_write_update(&queue->buffer, idx + item->item_size); + + if (in_thread) { + put_queue(impl, orig); + + /* when there is no thread running the loop we flush the queues from + * this invoking thread but we need to serialize the flushing here with + * a mutex */ + if (loop_thread == 0) + pthread_mutex_lock(&impl->queue_lock); + + flush_all_queues(impl); + + if (loop_thread == 0) + pthread_mutex_unlock(&impl->queue_lock); + + res = item->res; + } else { + loop_signal_event(impl, impl->wakeup); + + if (block && queue->ack_fd != -1) { + uint64_t count = 1; + + spa_loop_control_hook_before(&impl->hooks_list); + + if ((res = spa_system_eventfd_read(impl->system, queue->ack_fd, &count)) < 0) + spa_log_warn(impl->log, "%p: failed to read event fd:%d: %s", + queue, queue->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; + } + put_queue(impl, orig); + } + return res; +xrun: + /* we overflow, make a new queue that shares the same fd + * and place it in the overflow array. We hold the queue so there + * is only ever one writer to the overflow field. */ + overflow = queue->overflow; + if (overflow == NULL) { + overflow = loop_create_queue(impl, false); + if (overflow == NULL) + return -errno; + overflow->ack_fd = queue->ack_fd; + SPA_ATOMIC_STORE(queue->overflow, overflow); + } + queue = overflow; + goto again; +} + +static void wakeup_func(void *data, uint64_t count) +{ + struct impl *impl = data; + flush_all_queues(impl); +} + +static int loop_invoke(void *object, spa_invoke_func_t func, uint32_t seq, + const void *data, size_t size, bool block, void *user_data) +{ + struct impl *impl = object; + struct queue *queue; + int res = 0, suppressed; + uint64_t nsec; + + while (true) { + queue = get_queue(impl); + if (SPA_UNLIKELY(queue == NULL)) + queue = loop_create_queue(impl, true); + if (SPA_UNLIKELY(queue == NULL)) { + if (SPA_UNLIKELY(errno != ENOSPC)) + return -errno; + + /* there was no space for a new queue. This means QUEUE_MAX + * threads are concurrently doing an invoke. We can wait a little + * and retry to get a queue */ + if (impl->retry_timeout == 0) + return -EPIPE; + + nsec = get_time_ns(impl->system); + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, nsec)) >= 0) { + spa_log_warn(impl->log, "%p: out of queues, retrying (%d suppressed)", + impl, suppressed); + } + usleep(impl->retry_timeout); + } else { + res = loop_queue_invoke(queue, func, seq, data, size, block, user_data); + break; + } + } + return res; +} + +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_return_if_fail(SPA_CALLBACK_CHECK(hooks, before, 0)); + spa_return_if_fail(SPA_CALLBACK_CHECK(hooks, after, 0)); + 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(pthread_equal(impl->thread, thread_id)); + impl->enter_count++; + } + spa_log_trace_fp(impl->log, "%p: enter %p", impl, (void *) 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(pthread_equal(impl->thread, thread_id)); + + spa_log_trace_fp(impl->log, "%p: leave %p", impl, (void *) impl->thread); + + if (--impl->enter_count == 0) { + impl->thread = 0; + flush_all_queues(impl); + impl->polling = false; + } +} + +static int loop_check(void *object) +{ + struct impl *impl = object; + pthread_t thread_id = pthread_self(); + return (impl->thread == 0 || pthread_equal(impl->thread, thread_id)) ? 1 : 0; +} + +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_cancel(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 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; + + /* 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; + + 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); + } + for (i = 0; i < nfds; i++) { + struct spa_source *s = ep[i].data; + if (SPA_LIKELY(s)) { + s->rmask = 0; + s->priv = NULL; + } + } + 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) { + // timer initially fires after one interval + its.it_value = *interval; + absolute = false; + } + 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_cancel = { + SPA_VERSION_LOOP_CONTROL_METHODS, + .get_fd = loop_get_fd, + .add_hook = loop_add_hook, + .enter = loop_enter, + .leave = loop_leave, + .iterate = loop_iterate_cancel, + .check = loop_check, +}; + +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, + .check = loop_check, +}; + +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; + uint32_t i; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + impl = (struct impl *) handle; + + spa_log_debug(impl->log, "%p: clear", impl); + + if (impl->enter_count != 0 || impl->polling) + spa_log_warn(impl->log, "%p: loop is entered %d times polling:%d", + impl, impl->enter_count, impl->polling); + + spa_list_consume(source, &impl->source_list, link) + loop_destroy_source(impl, &source->source); + for (i = 0; i < impl->n_queues; i++) + loop_queue_destroy(impl->queues[i]); + + spa_system_close(impl->system, impl->poll_fd); + + pthread_mutex_destroy(&impl->queue_lock); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +#define CHECK(expression,label) \ +do { \ + if ((errno = (expression)) != 0) { \ + res = -errno; \ + spa_log_error(impl->log, #expression ": %s", strerror(errno)); \ + goto label; \ + } \ +} while(false); + +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; + pthread_mutexattr_t attr; + 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->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + impl->rate_limit.burst = 1; + impl->retry_timeout = DEFAULT_RETRY; + if (info) { + if ((str = spa_dict_lookup(info, "loop.cancel")) != NULL && + spa_atob(str)) + impl->control.iface.cb.funcs = &impl_loop_control_cancel; + if ((str = spa_dict_lookup(info, "loop.retry-timeout")) != NULL) + impl->retry_timeout = atoi(str); + } + + CHECK(pthread_mutexattr_init(&attr), error_exit); + CHECK(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), error_exit); + CHECK(pthread_mutex_init(&impl->queue_lock, &attr), error_exit); + + 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_free_mutex; + } + 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_free_mutex; + } + impl->poll_fd = res; + + spa_list_init(&impl->source_list); + spa_list_init(&impl->destroy_list); + spa_hook_list_init(&impl->hooks_list); + + 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; + } + + impl->head.t.idx = IDX_INVALID; + + spa_log_debug(impl->log, "%p: initialized", impl); + + return 0; + +error_exit_free_poll: + spa_system_close(impl->system, impl->poll_fd); +error_exit_free_mutex: + pthread_mutex_destroy(&impl->queue_lock); +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..6788457 --- /dev/null +++ b/spa/plugins/support/meson.build @@ -0,0 +1,72 @@ +spa_support_sources = [ + 'cpu.c', + 'logger.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 + +stdthreads_lib = cc.find_library('stdthreads', required: false) + +spa_support_lib = shared_library('spa-support', + spa_support_sources, + c_args : [ simd_cargs ], + include_directories : [ configinc ], + dependencies : [ spa_dep, pthread_lib, epoll_shim_dep, mathlib, stdthreads_lib ], + install : true, + install_dir : spa_plugindir / 'support') +spa_support_dep = declare_dependency(link_with: spa_support_lib) + +if get_option('evl').allowed() + evl_inc = include_directories('/usr/include') + evl_lib = cc.find_library('evl', + dirs: ['/usr/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', + ] + + spa_journal_lib = shared_library('spa-journal', + spa_journal_sources, + include_directories : [ configinc ], + 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..c8777ca --- /dev/null +++ b/spa/plugins/support/node-driver.c @@ -0,0 +1,778 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver"); + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define DEFAULT_FREEWHEEL false +#define DEFAULT_FREEWHEEL_WAIT 5 +#define DEFAULT_CLOCK_PREFIX "clock.system" +#define DEFAULT_CLOCK_ID CLOCK_MONOTONIC +#define DEFAULT_RESYNC_MS 10 + +#define CLOCK_OFFSET_NAVG 20 +#define CLOCK_OFFSET_MAX_ERR (50 * SPA_NSEC_PER_USEC) + +#define CLOCKFD 3 +#define FD_TO_CLOCKID(fd) ((~(clockid_t) (fd) << 3) | CLOCKFD) +#define CLOCKID_TO_FD(clk) ((unsigned int) ~((clk) >> 3)) + +#define BW_PERIOD (3 * SPA_NSEC_PER_SEC) +#define MAX_ERROR_MS 1 + +struct props { + bool freewheel; + char clock_name[64]; + clockid_t clock_id; + uint32_t freewheel_wait; + float resync_ms; +}; + +struct clock_offset { + int64_t offset; + int64_t err; +}; + +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; + int clock_fd; + + bool started; + bool following; + bool tracking; + clockid_t timer_clockid; + uint64_t next_time; + uint64_t last_time; + uint64_t base_time; + struct spa_dll dll; + double max_error; + double max_resync; + + struct clock_offset nsec_offset; +}; + +static void reset_props(struct props *props) +{ + props->freewheel = DEFAULT_FREEWHEEL; + spa_zero(props->clock_name); + props->clock_id = CLOCK_MONOTONIC; + props->freewheel_wait = DEFAULT_FREEWHEEL_WAIT; + props->resync_ms = DEFAULT_RESYNC_MS; +} + +static const struct clock_info { + const char *name; + clockid_t id; +} clock_info[] = { + { "realtime", CLOCK_REALTIME }, +#ifdef CLOCK_TAI + { "tai", CLOCK_TAI }, +#endif + { "monotonic", CLOCK_MONOTONIC }, +#ifdef CLOCK_MONOTONIC_RAW + { "monotonic-raw", CLOCK_MONOTONIC_RAW }, +#endif +#ifdef CLOCK_BOOTTIME + { "boottime", CLOCK_BOOTTIME }, +#endif +}; + +static bool clock_for_timerfd(clockid_t id) +{ + return id == CLOCK_REALTIME || +#ifdef CLOCK_BOOTTIME + id == CLOCK_BOOTTIME || +#endif + id == CLOCK_MONOTONIC; +} + +static clockid_t clock_name_to_id(const char *name) +{ + SPA_FOR_EACH_ELEMENT_VAR(clock_info, i) { + if (spa_streq(i->name, name)) + return i->id; + } + return -1; +} +static const char *clock_id_to_name(clockid_t id) +{ + SPA_FOR_EACH_ELEMENT_VAR(clock_info, i) { + if (i->id == id) + return i->name; + } + return "custom"; +} + + +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 inline uint64_t gettime_nsec(struct impl *this, clockid_t clock_id) +{ + struct timespec now = { 0 }; + uint64_t nsec; + if (spa_system_clock_gettime(this->data_system, clock_id, &now) < 0) + return 0; + nsec = SPA_TIMESPEC_TO_NSEC(&now); + spa_log_trace(this->log, "%p now:%"PRIu64, this, nsec); + return nsec; +} + +static int set_timers(struct impl *this) +{ + this->next_time = gettime_nsec(this, this->timer_clockid); + + spa_log_debug(this->log, "%p now:%"PRIu64, this, this->next_time); + + if (this->following || !this->started) { + 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_set_timers(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 int64_t get_nsec_offset(struct impl *this, uint64_t *now) +{ + struct timespec ts1, ts2, ts3; + int64_t t1, t2, t3; + + /* Offset between timer clock and monotonic */ + if (this->timer_clockid == CLOCK_MONOTONIC) + return 0; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts1); + spa_system_clock_gettime(this->data_system, this->timer_clockid, &ts2); + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts3); + + t1 = SPA_TIMESPEC_TO_NSEC(&ts1); + t2 = SPA_TIMESPEC_TO_NSEC(&ts2); + t3 = SPA_TIMESPEC_TO_NSEC(&ts3); + + if (now) + *now = t3; + + return t1 + (t3 - t1) / 2 - t2; +} + +static int64_t clock_offset_update(struct clock_offset *off, int64_t offset, struct spa_log *log) +{ + const int64_t max_resync = CLOCK_OFFSET_MAX_ERR; + const int64_t n = CLOCK_OFFSET_NAVG; + int64_t err; + + /* Moving average smoothing, discarding outliers */ + err = offset - off->offset; + + if (SPA_ABS(err) > max_resync) { + /* Clock jump */ + spa_log_info(log, "nsec err %"PRIi64" > max_resync %"PRIi64", resetting", + err, max_resync); + off->offset = offset; + off->err = 0; + err = 0; + } else if (SPA_ABS(err) / 2 <= off->err) { + off->offset += err / n; + } + + off->err += (SPA_ABS(err) - off->err) / n; + + spa_log_trace(log, "clock offset %"PRIi64" err:%"PRIi64" abs-err:%"PRIi64, + off->offset, err, off->err); + + return off->offset; +} + +static int64_t smooth_nsec_offset(struct impl *this, uint64_t *now) +{ + int64_t offset; + + if (this->timer_clockid == CLOCK_MONOTONIC) + return 0; + + offset = get_nsec_offset(this, now); + return clock_offset_update(&this->nsec_offset, offset, this->log); +} + +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, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_set_timers, 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 inline uint64_t scale_u64(uint64_t val, uint32_t num, uint32_t denom) +{ +#if 0 + return ((__uint128_t)val * num) / denom; +#else + return (uint64_t)((double)val / denom * num); +#endif +} + +static void on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t expirations, nsec, duration, current_time, current_position, position; + uint32_t rate; + double corr = 1.0, err = 0.0; + int res; + + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, "%p: timerfd error: %s", + this, spa_strerror(res)); + return; + } + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.target_duration; + rate = this->position->clock.target_rate.denom; + } else { + duration = 1024; + rate = 48000; + } + if (this->props.freewheel) + nsec = gettime_nsec(this, this->timer_clockid); + else + nsec = this->next_time; + + if (this->tracking) + /* we are actually following another clock */ + current_time = gettime_nsec(this, this->props.clock_id); + else + current_time = nsec; + + current_position = scale_u64(current_time, rate, SPA_NSEC_PER_SEC); + + if (this->last_time == 0) { + spa_dll_set_bw(&this->dll, SPA_DLL_BW_MIN, duration, rate); + this->max_error = rate * MAX_ERROR_MS / 1000; + this->max_resync = rate * this->props.resync_ms / 1000; + position = current_position; + } else if (SPA_LIKELY(this->clock)) { + position = this->clock->position + this->clock->duration; + } else { + position = current_position; + } + + this->last_time = current_time; + + if (this->props.freewheel) { + corr = 1.0; + this->next_time = nsec + this->props.freewheel_wait * SPA_NSEC_PER_SEC; + } else if (this->tracking) { + /* check the elapsed time of the other clock against + * the graph clock elapsed time, feed this error into the + * dll and adjust the timeout of our MONOTONIC clock. */ + err = (double)position - (double)current_position; + if (fabs(err) > this->max_error) { + if (fabs(err) > this->max_resync) { + spa_log_warn(this->log, "err %f > max_resync %f, resetting", + err, this->max_resync); + spa_dll_set_bw(&this->dll, SPA_DLL_BW_MIN, duration, rate); + position = current_position; + err = 0.0; + } else { + err = SPA_CLAMPD(err, -this->max_error, this->max_error); + } + } + corr = spa_dll_update(&this->dll, err); + this->next_time = (uint64_t)(nsec + duration / corr * 1e9 / rate); + } else { + corr = 1.0; + this->next_time = scale_u64(position + duration, SPA_NSEC_PER_SEC, rate); + } + + if (SPA_UNLIKELY((this->next_time - this->base_time) > BW_PERIOD)) { + this->base_time = this->next_time; + spa_log_debug(this->log, "%p: rate:%f " + "bw:%f dur:%"PRIu64" max:%f drift:%f", + this, corr, this->dll.bw, duration, + this->max_error, err); + } + + if (SPA_LIKELY(this->clock)) { + uint64_t nsec_now = nsec; + int64_t nsec_offset = smooth_nsec_offset(this, &nsec_now); + + this->clock->nsec = SPA_MIN(nsec + nsec_offset, nsec_now); + this->clock->rate = this->clock->target_rate; + this->clock->position = position; + this->clock->duration = duration; + this->clock->delay = 0; + this->clock->rate_diff = corr; + this->clock->next_nsec = this->next_time + nsec_offset; + } + + 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); + this->started = true; + this->last_time = 0; + spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + return 0; +} + +static int do_stop(struct impl *this) +{ + if (!this->started) + return 0; + this->started = false; + spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + 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 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[3]; + + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + items[1] = SPA_DICT_ITEM_INIT("clock.id", clock_id_to_name(this->props.clock_id)); + items[2] = SPA_DICT_ITEM_INIT("clock.name", this->props.clock_name); + + this->info.props = &SPA_DICT_INIT(items, 3); + 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; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_log_trace(this->log, "process %d", this->props.freewheel); + + if (this->props.freewheel && + !SPA_FLAG_IS_SET(this->position->clock.flags, SPA_IO_CLOCK_FLAG_XRUN_RECOVER)) { + this->next_time = gettime_nsec(this, this->timer_clockid); + 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); + + if (this->clock_fd != -1) + close(this->clock_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 get_phc_index(struct spa_system *s, const char *name) { +#ifdef ETHTOOL_GET_TS_INFO + struct ethtool_ts_info info = {0}; + struct ifreq ifr = {0}; + int fd, err; + + info.cmd = ETHTOOL_GET_TS_INFO; + strncpy(ifr.ifr_name, name, IFNAMSIZ - 1); + ifr.ifr_data = (char *) &info; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return -errno; + + err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr); + close(fd); + if (err < 0) + return -errno; + + return info.phc_index; +#else + return -ENOTSUP; +#endif +} + +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); + this->clock_fd = -1; + spa_dll_init(&this->dll); + + 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; + + 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") && this->clock_fd < 0) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), "%s", s); + } else if (spa_streq(k, "clock.id") && this->clock_fd < 0) { + this->props.clock_id = clock_name_to_id(s); + if (this->props.clock_id == -1) { + spa_log_warn(this->log, "unknown clock id '%s'", s); + this->props.clock_id = DEFAULT_CLOCK_ID; + } + } else if (spa_streq(k, "clock.device")) { + if (this->clock_fd >= 0) { + close(this->clock_fd); + } + this->clock_fd = open(s, O_RDONLY); + + if (this->clock_fd == -1) { + spa_log_warn(this->log, "failed to open clock device '%s': %m", s); + } else { + this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + } + } else if (spa_streq(k, "clock.interface") && this->clock_fd < 0) { + int phc_index = get_phc_index(this->data_system, s); + if (phc_index < 0) { + spa_log_warn(this->log, "failed to get phc device index for interface '%s': %s", + s, spa_strerror(phc_index)); + } else { + char dev[19]; + spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index); + this->clock_fd = open(dev, O_RDONLY); + if (this->clock_fd == -1) { + spa_log_warn(this->log, "failed to open clock device '%s' " + "for interface '%s': %m", dev, s); + } else { + this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + } + } + } else if (spa_streq(k, "freewheel.wait")) { + this->props.freewheel_wait = atoi(s); + } else if (spa_streq(k, "resync.ms")) { + this->props.resync_ms = (float)atof(s); + } + } + if (this->props.clock_name[0] == '\0') { + spa_scnprintf(this->props.clock_name, sizeof(this->props.clock_name), + "%s.%s", DEFAULT_CLOCK_PREFIX, + clock_id_to_name(this->props.clock_id)); + } + + this->tracking = !clock_for_timerfd(this->props.clock_id); + this->timer_clockid = this->tracking ? CLOCK_MONOTONIC : this->props.clock_id; + this->max_error = 128; + + this->nsec_offset.offset = get_nsec_offset(this, NULL); + this->nsec_offset.err = 0; + + this->timer_source.func = on_timeout; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, + this->timer_clockid, 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); + + 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..997a668 --- /dev/null +++ b/spa/plugins/support/null-audio-sink.c @@ -0,0 +1,1003 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.null-audio-sink"); + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + uint32_t format; + uint32_t channels; + uint32_t rate; + uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + char clock_name[64]; + unsigned int debug:1; + unsigned int driver:1; +}; + +static void reset_props(struct props *props) +{ + props->format = 0; + props->channels = 0; + props->rate = 0; + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->debug = false; + props->driver = true; +} + +#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 || !this->started) { + 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_set_timers(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, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_set_timers, 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, "%p: timerfd error: %s", + this, spa_strerror(res)); + return; + } + + nsec = this->next_time; + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.target_duration; + rate = this->position->clock.target_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->rate = this->clock->target_rate; + this->clock->position += this->clock->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); + this->started = true; + spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + return 0; +} + +static int do_stop(struct impl *this) +{ + if (!this->started) + return 0; + this->started = false; + spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + 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 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) { + const struct spa_dict_item node_info_items[] = { + { SPA_KEY_NODE_DRIVER, this->props.driver ? "true" : "false" }, + }; + 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), + 0); + if (this->props.format != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_format, SPA_POD_Id(this->props.format), + 0); + } else { + spa_pod_builder_add(builder, + 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.channels != 0) { + spa_pod_builder_prop(builder, SPA_FORMAT_AUDIO_position, 0); + spa_pod_builder_array(builder, sizeof(uint32_t), SPA_TYPE_Id, + this->props.channels, 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, ¶m, &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, "%p: clear buffers", this); + port->n_buffers = 0; + this->started = false; + } + return 0; +} + +static int calc_width(struct spa_audio_info *info) +{ + switch (info->info.raw.format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_U8P: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_S8P: + case SPA_AUDIO_FORMAT_ULAW: + case SPA_AUDIO_FORMAT_ALAW: + return 1; + case SPA_AUDIO_FORMAT_S16P: + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + return 2; + case SPA_AUDIO_FORMAT_S24P: + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + return 3; + case SPA_AUDIO_FORMAT_F64P: + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return 8; + default: + return 4; + } +} + +static int +port_set_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 (this->props.format != 0) { + if (this->props.format != info.info.raw.format) + return -EINVAL; + } else if (info.info.raw.format != SPA_AUDIO_FORMAT_F32P && + info.info.raw.format != SPA_AUDIO_FORMAT_F32) { + return -EINVAL; + } + + port->bpf = calc_width(&info); + if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) { + port->blocks = info.info.raw.channels; + } else { + port->blocks = 1; + port->bpf *= info.info.raw.channels; + } + 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, "%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 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_FORMAT)) { + this->props.format = spa_type_audio_format_from_short_name(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { + this->props.channels = atoi(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { + this->props.rate = atoi(s); + } else if (spa_streq(k, SPA_KEY_NODE_DRIVER)) { + this->props.driver = spa_atob(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + spa_audio_parse_position(s, strlen(s), this->props.pos, &this->props.channels); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), + "%s", s); + } + } + spa_log_info(this->log, "%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 " }, + { 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..89e3dba --- /dev/null +++ b/spa/plugins/support/plugin.c @@ -0,0 +1,50 @@ +/* Spa Support plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include +#include + +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_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..767e0b4 --- /dev/null +++ b/spa/plugins/support/system.c @@ -0,0 +1,366 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "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, err; + 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); + err = -errno; /* save errno in case it is overwritten before return */ + spa_log_debug(impl->log, "%p: new fd:%d", impl, res); + return res < 0 ? err : 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..f71dc3d --- /dev/null +++ b/spa/plugins/test/fakesink.c @@ -0,0 +1,828 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, "%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, "%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, "%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, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Format: + if ((res = port_get_format(this, port, + result.index, filter, ¶m, &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, "%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, "%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, "%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->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, + "fakesink", + 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..d9658c9 --- /dev/null +++ b/spa/plugins/test/fakesrc.c @@ -0,0 +1,858 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, "%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, "%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, "%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, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Format: + if ((res = port_get_format(this, port, + result.index, filter, ¶m, &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, "%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, "%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, "%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, + "fakesrc", + 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..e770428 --- /dev/null +++ b/spa/plugins/test/plugin.c @@ -0,0 +1,33 @@ +/* Spa Test plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_fakesrc_factory; +extern const struct spa_handle_factory spa_fakesink_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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/test/test-helper.h b/spa/plugins/test/test-helper.h new file mode 100644 index 0000000..8c789bd --- /dev/null +++ b/spa/plugins/test/test-helper.h @@ -0,0 +1,97 @@ +#include + +#include +#include +#include +#include +#include + +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/v4l2/meson.build b/spa/plugins/v4l2/meson.build new file mode 100644 index 0000000..22746a1 --- /dev/null +++ b/spa/plugins/v4l2/meson.build @@ -0,0 +1,19 @@ +v4l2_sources = ['v4l2.c', + 'v4l2-device.c', + 'v4l2-source.c'] +v4l2_dependencies = [ spa_dep, libinotify_dep, mathlib ] + +if libudev_dep.found() + v4l2_sources += [ 'v4l2-udev.c' ] + v4l2_dependencies += [ libudev_dep ] + if logind_dep.found() + v4l2_dependencies += [ logind_dep ] + endif +endif + +v4l2lib = shared_library('spa-v4l2', + v4l2_sources, + include_directories : [ configinc ], + dependencies : v4l2_dependencies, + 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..2379d21 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-device.c @@ -0,0 +1,285 @@ +/* Spa V4l2 Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "v4l2.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.v4l2-device"); + +static const char default_device[] = "/dev/video0"; + +struct props { + char device[64]; + char devnum[32]; + char product_id[7]; + char vendor_id[7]; + 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[13]; + 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], devices_str[16]; + struct spa_strbuf buf; + + 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); + + /* encode device number into a json array */ + spa_strbuf_init(&buf, devices_str, sizeof(devices_str)); + spa_strbuf_append(&buf, "[ "); + spa_strbuf_append(&buf, "%s", this->props.devnum); + spa_strbuf_append(&buf, " ]"); + ADD_ITEM(SPA_KEY_DEVICE_DEVIDS, devices_str); + + 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, sizeof(this->props.device)-1); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_DEVIDS))) + strncpy(this->props.devnum, str, sizeof(this->props.devnum)-1); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_PRODUCT_ID))) + strncpy(this->props.product_id, str, sizeof(this->props.product_id)-1); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_VENDOR_ID))) + strncpy(this->props.vendor_id, str, sizeof(this->props.vendor_id)-1); + + 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..b06dda9 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-source.c @@ -0,0 +1,1129 @@ +/* Spa V4l2 Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "v4l2.h" + +static const char default_device[] = "/dev/video0"; +static const char default_clock_name[] = "api.v4l2.unknown"; + +struct props { + char device[64]; + char device_name[128]; + int device_fd; + char clock_name[64]; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, sizeof(props->device)); + strncpy(props->clock_name, default_clock_name, sizeof(props->clock_name)); +} + +#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 spa_meta_videotransform *vt; + struct v4l2_buffer v4l2_buffer; + void *ptr; + void *mmap_ptr; +}; + +#define MAX_CONTROLS 64 + +struct control { + uint32_t id; + uint32_t ctrl_id; + uint32_t type; + int32_t value; +}; + +struct port { + struct impl *impl; + + bool alloc_buffers; + bool probed_expbuf; + bool have_expbuf; + bool first_buffer; + uint32_t max_buffers; + + 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_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; + + enum spa_meta_videotransform_value transform; + + 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]; + + struct spa_dll dll; +}; + +#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 void emit_node_info(struct impl *this, bool full) +{ + 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" }, + }; + 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) +{ + static const struct spa_dict_item info_items[] = { + { SPA_KEY_PORT_GROUP, "stream.0" }, + }; + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + port->info.props = &SPA_DICT_INIT_ARRAY(info_items); + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b.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.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.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; + struct spa_pod_frame f; + struct port *port = &this->out_ports[0]; + uint32_t i; + + if ((res = spa_v4l2_update_controls(this)) < 0) { + spa_log_error(this->log, "error: %s", spa_strerror(res)); + return res; + } + + switch (result.index) { + case 0: + spa_pod_builder_push_object(&b.b, &f, SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b.b, + 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), + 0); + for (i = 0; i < port->n_controls; i++) { + struct control *c = &port->controls[i]; + + spa_pod_builder_prop(&b.b, c->id, 0); + switch (c->type) { + case SPA_TYPE_Int: + spa_pod_builder_int(&b.b, c->value); + break; + case SPA_TYPE_Bool: + spa_pod_builder_bool(&b.b, c->value); + break; + default: + spa_pod_builder_int(&b.b, c->value); + break; + } + } + param = spa_pod_builder_pop(&b.b, &f); + 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, ¶m, &b.b)) <= 0) + return res; + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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; + } + } + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[NODE_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, true); + 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; + if (this->clock) { + SPA_FLAG_SET(this->clock->flags, SPA_IO_CLOCK_FLAG_NO_RATE); + spa_scnprintf(this->clock->name, sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + 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 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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_reset(&b.b, &state); + + 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, ¶m, &b.b)) <= 0) + return res; + break; + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + if (port->max_buffers == 0) + return -EIO; + + param = spa_pod_builder_add_object(&b.b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(SPA_MIN(4u, port->max_buffers), + 1, port->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.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.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 = spa_pod_builder_add_object(&b.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.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.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.b, id, &this->latency[result.index]); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b.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 = 0; + + spa_zero(info); + + 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; + port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + if (port->have_format) { + uint64_t latency; + latency = port->info.rate.num * SPA_NSEC_PER_SEC / port->info.rate.denom; + this->latency[SPA_DIRECTION_OUTPUT] = + SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT, + .min_ns = latency, + .max_ns = latency); + 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 { + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + 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 res; +} + +static int impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + int res = 0; + 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 (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; + 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; + struct port *port; + uint32_t i; + int res; + bool have_clock = false; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + 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_PROPS | + 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->probed_expbuf = false; + port->have_query_ext_ctrl = true; + port->dev.log = this->log; + port->dev.fd = -1; + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, SPA_KEY_API_V4L2_PATH)) { + strncpy(this->props.device, s, 63); + } else if (spa_streq(k, "meta.videotransform.transform")) { + this->transform = spa_debug_type_find_type_short(spa_type_meta_videotransform_type, s); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), "%s", s); + have_clock = true; + } + } + if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0) + return res; + spa_v4l2_close(&port->dev); + + if (!have_clock) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), "api.v4l2.%s", port->dev.cap.bus_info); + } + return 0; +} + +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..c45ef06 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-udev.c @@ -0,0 +1,825 @@ +/* Spa V4l2 udev monitor */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "v4l2.h" + +#ifdef HAVE_LOGIND +#include +#endif + +#define MAX_DEVICES 64 + +enum action { + ACTION_CHANGE, + ACTION_REMOVE, +}; + +struct device { + uint32_t id; + struct udev_device *dev; + int inotify_wd; + unsigned int accessible: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; +#ifdef HAVE_LOGIND + struct spa_source logind; + sd_login_monitor *logind_monitor; +#endif +}; + +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 void start_watching_device(struct impl *this, struct device *device) +{ + if (this->notify.fd < 0 || device->inotify_wd >= 0) + return; + + char path[64]; + snprintf(path, sizeof(path), "/dev/video%" PRIu32, device->id); + + device->inotify_wd = inotify_add_watch(this->notify.fd, path, IN_ATTRIB); +} + +static void stop_watching_device(struct impl *this, struct device *device) +{ + if (device->inotify_wd < 0) + return; + + spa_assert(this->notify.fd >= 0); + + inotify_rm_watch(this->notify.fd, device->inotify_wd); + device->inotify_wd = -1; +} + +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; + device->inotify_wd = -1; + + start_watching_device(this, device); + + 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) +{ + device->dev = udev_device_unref(device->dev); + stop_watching_device(this, device); + *device = this->devices[--this->n_devices]; +} + +static void clear_devices(struct impl *this) +{ + while (this->n_devices > 0) + remove_device(this, &this->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[21]; + uint32_t n_items = 0; + char devnum[32]; + + 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)); + snprintf(devnum, sizeof(devnum), "%" PRId64, (int64_t)udev_device_get_devnum(dev)); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DEVIDS, devnum); + + 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, enum action action, struct device *device) +{ + switch (action) { + case ACTION_CHANGE: + check_access(this, device); + if (device->accessible && !device->emitted) { + emit_object_info(this, device); + } else if (!device->accessible && device->emitted) { + device->emitted = false; + spa_device_emit_object_info(&this->hooks, device->id, NULL); + } + break; + case ACTION_REMOVE: { + bool emitted = device->emitted; + uint32_t id = device->id; + + remove_device(this, device); + + if (emitted) + spa_device_emit_object_info(&this->hooks, id, NULL); + break; + } + } +} + +static void process_udev_device(struct impl *this, enum action action, struct udev_device *udev_device) +{ + struct device *device; + uint32_t id; + + if ((id = get_device_id(this, udev_device)) == SPA_ID_INVALID) + return; + + device = find_device(this, id); + if (action == ACTION_CHANGE && !device) + device = add_device(this, id, udev_device); + + if (!device) + return; + + process_device(this, action, device); +} + +static int stop_inotify(struct impl *this) +{ + if (this->notify.fd == -1) + return 0; + spa_log_info(this->log, "stop inotify"); + + for (size_t i = 0; i < this->n_devices; i++) + stop_watching_device(this, &this->devices[i]); + + 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) +{ + 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) + break; + + e = SPA_PTROFF(&buf, len, void); + + for (p = &buf; p < e; + p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) { + event = (const struct inotify_event *) p; + struct device *device = NULL; + + for (size_t i = 0; i < this->n_devices; i++) { + if (this->devices[i].inotify_wd == event->wd) { + device = &this->devices[i]; + break; + } + } + + if (!device) + continue; + + if (event->mask & IN_ATTRIB) + process_device(this, ACTION_CHANGE, device); + + if (event->mask & IN_IGNORED) + device->inotify_wd = -1; + } + } +} + +static int start_inotify(struct impl *this) +{ + int notify_fd; + +#ifdef HAVE_LOGIND + /* Do not use inotify when using logind session monitoring */ + if (this->logind_monitor) + return 0; +#endif + + if (this->notify.fd != -1) + return 0; + + if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0) + return -errno; + + 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; +} + +#ifdef HAVE_LOGIND +static void impl_on_logind_events(struct spa_source *source) +{ + struct impl *this = source->data; + + /* Recheck access on all v4l2 devices on logind session changes */ + for (size_t i = 0; i < this->n_devices; i++) + process_device(this, ACTION_CHANGE, &this->devices[i]); + + sd_login_monitor_flush(this->logind_monitor); +} + +static int start_logind(struct impl *this) +{ + int res; + + if (this->logind_monitor) + return 0; + + /* If we are not actually running logind become a NOP */ + if (access("/run/systemd/seats/", F_OK) < 0) + return 0; + + res = sd_login_monitor_new("session", &this->logind_monitor); + if (res < 0) + return res; + + spa_log_info(this->log, "start logind monitoring"); + + this->logind.func = impl_on_logind_events; + this->logind.data = this; + this->logind.fd = sd_login_monitor_get_fd(this->logind_monitor); + this->logind.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->logind); + + return 0; +} + +static void stop_logind(struct impl *this) +{ + if (this->logind_monitor) { + spa_loop_remove_source(this->main_loop, &this->logind); + sd_login_monitor_unref(this->logind_monitor); + this->logind_monitor = NULL; + } +} +#else +/* Stubs to avoid more ifdefs below */ +static int start_logind(struct impl *this) +{ + return 0; +} + +static void stop_logind(struct impl *this) +{ +} +#endif + +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); + + if (spa_streq(action, "add") || + spa_streq(action, "change")) { + process_udev_device(this, ACTION_CHANGE, dev); + } else if (spa_streq(action, "remove")) { + process_udev_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_logind(this)) < 0) + return res; + + 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); + stop_logind(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_udev_device(this, ACTION_CHANGE, 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 = 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; + + 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; +#ifdef HAVE_LOGIND + this->logind_monitor = NULL; +#endif + + 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..be38ab3 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -0,0 +1,1946 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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 (b->mmap_ptr) + munmap(b->mmap_ptr, b->v4l2_buffer.length); + 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 int +enum_filter_format(uint32_t media_type, int32_t media_subtype, + const struct spa_pod *filter, uint32_t index) +{ + uint32_t video_format = SPA_VIDEO_FORMAT_UNKNOWN; + + 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 -ENOENT; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + if (val->type != SPA_TYPE_Id || n_values == 0) + 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.min, min) < 0 || + compare_fraction(&frmival->stepwise.max, max) > 0) + return false; + + if (compare_fraction(&frmival->stepwise.max, min) < 0) { + frmival->stepwise.max.denominator = min->num; + frmival->stepwise.max.numerator = min->denom; + } + if (compare_fraction(&frmival->stepwise.min, max) > 0) { + frmival->stepwise.min.denominator = max->num; + frmival->stepwise.min.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; + struct spa_v4l2_device *dev = &port->dev; + uint8_t buffer[1024]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + 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; + + res = enum_filter_format(filter_media_type, + filter_media_subtype, + filter, port->fmtdesc.index); + if (res == -ENOENT) + goto do_enum_fmt; + if (res < 0) + goto exit; + if (res == SPA_VIDEO_FORMAT_UNKNOWN) + goto enum_end; + + info = find_format_info_by_media_type(filter_media_type, + filter_media_subtype, + res, 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 { +do_enum_fmt: + 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 || n_vals == 0) + 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 == ENOTTY) + goto next_fmtdesc; + if (errno == EINVAL) { + if (port->frmsize.index == 0) { + port->frmsize.type = V4L2_FRMSIZE_TYPE_CONTINUOUS; + port->frmsize.stepwise.min_width = 16; + port->frmsize.stepwise.min_height = 16; + port->frmsize.stepwise.max_width = 16384; + port->frmsize.stepwise.max_height = 16384; + port->frmsize.stepwise.step_width = 16; + port->frmsize.stepwise.step_height = 16; + port->fmtdesc.index++; + port->next_fmtdesc = true; + goto do_frmsize_filter; + } + else + goto next_fmtdesc; + } + + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMESIZES: %m", + this->props.device); + goto exit; + } +do_frmsize_filter: + 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 || n_values == 0) + 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_reset(&b.b, &state); + spa_pod_builder_push_object(&b.b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b.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.b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id(&b.b, info->format); + } + + spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_size, 0); + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + spa_pod_builder_rectangle(&b.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.b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b.b, &f[1]); + + spa_pod_builder_rectangle(&b.b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b.b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b.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.b, + port->frmsize.stepwise.max_width, + port->frmsize.stepwise.max_height); + } + spa_pod_builder_pop(&b.b, &f[1]); + } + + spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_framerate, 0); + + n_fractions = 0; + + spa_pod_builder_push_choice(&b.b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b.b, &f[1]); + port->frmival.index = 0; + + while (true) { + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMEINTERVALS, &port->frmival)) < 0) { + res = -errno; + port->frmsize.index++; + port->next_frmsize = true; + if (errno == EINVAL || errno == ENOTTY) { + if (port->frmival.index == 0) { + port->frmival.type = V4L2_FRMIVAL_TYPE_CONTINUOUS; + port->frmival.stepwise.min.denominator = 120; + port->frmival.stepwise.min.numerator = 1; + port->frmival.stepwise.max.denominator = 1; + port->frmival.stepwise.max.numerator = 1; + goto do_frminterval_filter; + } + else + break; + } + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMEINTERVALS: %m", + this->props.device); + goto exit; + } +do_frminterval_filter: + 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 || n_values == 0) + 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.b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + spa_pod_builder_fraction(&b.b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + port->frmival.index++; + n_fractions++; + } else if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS || + port->frmival.type == V4L2_FRMIVAL_TYPE_STEPWISE) { + if (n_fractions == 0) { + struct spa_fraction f = { 25, 1 }; + if (compare_fraction(&port->frmival.stepwise.max, &f) > 0) { + f.denom = port->frmival.stepwise.max.numerator; + f.num = port->frmival.stepwise.max.denominator; + } + if (compare_fraction(&port->frmival.stepwise.min, &f) < 0) { + f.denom = port->frmival.stepwise.min.numerator; + f.num = port->frmival.stepwise.min.denominator; + } + + spa_pod_builder_fraction(&b.b, f.num, f.denom); + } + spa_pod_builder_fraction(&b.b, + port->frmival.stepwise.max.denominator, + port->frmival.stepwise.max.numerator); + spa_pod_builder_fraction(&b.b, + port->frmival.stepwise.min.denominator, + port->frmival.stepwise.min.numerator); + + if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS) { + choice->body.type = SPA_CHOICE_Range; + n_fractions += 2; + } else { + choice->body.type = SPA_CHOICE_Step; + spa_pod_builder_fraction(&b.b, + port->frmival.stepwise.step.denominator, + port->frmival.stepwise.step.numerator); + n_fractions += 3; + } + + port->frmsize.index++; + port->next_frmsize = true; + break; + } + } + if (n_fractions == 0) + goto next_frmsize; + if (n_fractions == 1) + choice->body.type = SPA_CHOICE_None; + spa_pod_builder_pop(&b.b, &f[1]); + + result.param = spa_pod_builder_pop(&b.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 probe_expbuf(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_requestbuffers reqbuf; + struct v4l2_exportbuffer expbuf; + + if (port->probed_expbuf) + return 0; + port->probed_expbuf = true; + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = V4L2_MEMORY_MMAP; + reqbuf.count = port->max_buffers = MAX_BUFFERS; + + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS: %m", this->props.device); + return -errno; + } + port->max_buffers = reqbuf.count; + + spa_zero(expbuf); + expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + expbuf.index = 0; + expbuf.flags = O_CLOEXEC | O_RDONLY; + if (xioctl(dev->fd, VIDIOC_EXPBUF, &expbuf) < 0) { + spa_log_info(this->log, "'%s' EXPBUF not supported: %m", this->props.device); + port->have_expbuf = false; + port->alloc_buffers = false; + } else { + port->have_expbuf = true; + port->alloc_buffers = true; + } + + reqbuf.count = 0; + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS: %m", this->props.device); + return -errno; + } + return 0; +} + +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, "%s: unknown media type %d %d %d", + this->props.device, 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_info(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, "%s: VIDIOC_S_PARM: %m", this->props.device); + + 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, "%s: wanted %.4s %dx%d, got %.4s %dx%d", + this->props.device, + (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; + + if (streamparm.parm.capture.timeperframe.denominator == 0) + streamparm.parm.capture.timeperframe.denominator = 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; + + probe_expbuf(this); + + 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.num = streamparm.parm.capture.timeperframe.numerator; + port->info.rate.denom = streamparm.parm.capture.timeperframe.denominator; + + 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 (res == 0 || 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; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + 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; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + 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_reset(&b.b, &state); + + 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); + + switch (queryctrl.type) { + case V4L2_CTRL_TYPE_INTEGER: + port->controls[port->n_controls].type = SPA_TYPE_Int; + param = spa_pod_builder_add_object(&b.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: + port->controls[port->n_controls].type = SPA_TYPE_Bool; + param = spa_pod_builder_add_object(&b.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; + + port->controls[port->n_controls].type = SPA_TYPE_Int; + spa_pod_builder_push_object(&b.b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add(&b.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.b, SPA_PROP_INFO_labels, 0); + + spa_pod_builder_push_struct(&b.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.b, querymenu.index); + spa_pod_builder_string(&b.b, (const char *)querymenu.name); + } + } + spa_pod_builder_pop(&b.b, &f[1]); + param = spa_pod_builder_pop(&b.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; + + } + + port->n_controls++; + + if (spa_pod_filter(&b.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_update_controls(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + int res; + uint32_t i; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + for (i = 0; i < port->n_controls; i++) { + struct control *c = &port->controls[i]; + struct v4l2_control control; + + spa_zero(control); + control.id = c->ctrl_id; + if (xioctl(dev->fd, VIDIOC_G_CTRL, &control) < 0) { + /* Write only controls like relative pan/tilt return EACCES */ + if (errno == EACCES) { + c->value = 0; + continue; + } + res = -errno; + goto done; + } + c->value = control.value; + } + res = 0; +done: + 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_Float: + { + float val; + if ((res = spa_pod_get_float(&prop->value, &val)) < 0) + goto done; + control.value = (int32_t) val; + break; + } + case SPA_TYPE_Int: + { + int32_t val; + 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; + + spa_log_trace(this->log, "v4l2 %p: have output %d/%d", this, buf.index, buf.sequence); + + /* Drop the first frame in order to work around common firmware + * timestamp issues */ + if (port->first_buffer) { + port->first_buffer = false; + if (xioctl(dev->fd, VIDIOC_QBUF, &buf) < 0) + spa_log_warn(this->log, "v4l2 %p: error qbuf: %m", this); + return 0; + } + + pts = SPA_TIMEVAL_TO_NSEC(&buf.timestamp); + + + if (this->clock) { + double target = (double)port->info.rate.num / port->info.rate.denom; + double corr; + + if (this->dll.bw == 0.0) { + spa_dll_set_bw(&this->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom); + this->clock->next_nsec = pts; + corr = 1.0; + } else { + double diff = ((double)this->clock->next_nsec - (double)pts) / SPA_NSEC_PER_SEC; + double error = port->info.rate.denom * (diff - target); + corr = spa_dll_update(&this->dll, SPA_CLAMPD(error, -128., 128.)); + } + + /* FIXME, we should follow the driver clock and target_ values. + * for now we ignore and use our own. */ + this->clock->target_rate = port->info.rate; + this->clock->target_duration = 1; + + this->clock->nsec = pts; + this->clock->rate = port->info.rate; + this->clock->position = buf.sequence; + this->clock->duration = 1; + this->clock->delay = 0; + this->clock->rate_diff = corr; + this->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr); + } + + 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; + } + if (b->vt) { + b->vt->transform = this->transform; + } + + d = b->outbuf->datas; + d[0].chunk->offset = 0; + d[0].chunk->size = SPA_MIN(buf.bytesused, d[0].maxsize); + 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; + + if (b->mmap_ptr && b->ptr) + memcpy(b->ptr, b->mmap_ptr, d[0].chunk->size); + + 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; + int res; + + 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 ((res = mmap_read(this)) < 0) { + spa_log_warn(this->log, "v4l2 %p: mmap read error:%s", this, spa_strerror(res)); + 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, "%s: can't use buffers of type %d", + this->props.device, 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) { + if (port->memtype != V4L2_MEMORY_USERPTR) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS %m", this->props.device); + return -errno; + } + /* some drivers (v4l2loopback) don't support USERPTR + * and so we need to try again with MMAP and memcpy */ + 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 < 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)); + b->vt = spa_buffer_find_meta_data(buffers[i], SPA_META_VideoTransform, sizeof(*b->vt)); + + spa_log_debug(this->log, "%s: import buffer %p", this->props.device, buffers[i]); + + if (buffers[i]->n_datas < 1) { + spa_log_error(this->log, "%s: invalid memory on buffer %p", + this->props.device, 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 || + port->memtype == V4L2_MEMORY_MMAP) { + 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; + + if (port->memtype == V4L2_MEMORY_USERPTR) { + b->v4l2_buffer.m.userptr = (unsigned long) b->ptr; + b->v4l2_buffer.length = d[0].maxsize; + } + else { + if (xioctl(dev->fd, VIDIOC_QUERYBUF, &b->v4l2_buffer) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_QUERYBUF: %m", this->props.device); + return -errno; + } + b->mmap_ptr = mmap(NULL, + b->v4l2_buffer.length, + PROT_READ, MAP_PRIVATE, + dev->fd, b->v4l2_buffer.m.offset); + if (b->mmap_ptr == MAP_FAILED) { + spa_log_error(this->log, "'%s' mmap: %m", this->props.device); + return -errno; + } + } + } + else if (port->memtype == V4L2_MEMORY_DMABUF) { + b->v4l2_buffer.m.fd = d[0].fd; + } + else { + spa_log_error(this->log, "%s: invalid port memory %d", + this->props.device, 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, "%s: invalid buffer data", + this->props.device); + 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)); + b->vt = spa_buffer_find_meta_data(buffers[i], SPA_META_VideoTransform, sizeof(*b->vt)); + + 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); + +again: + if (port->have_expbuf && + d[0].type != SPA_ID_INVALID && + (d[0].type & ((1u << SPA_DATA_DmaBuf)|(1u<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 again; + } + spa_log_error(this->log, "'%s' VIDIOC_EXPBUF: %m", this->props.device); + return -errno; + } + if (d[0].type & (1u<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)) { + 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, "%s: unsupported data type:%08x", + this->props.device, d[0].type); + port->alloc_buffers = false; + 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, "%s: invalid capabilities %08x", + this->props.device, 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"); + + port->first_buffer = true; + mmap_read(this); + + 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; + } + this->dll.bw = 0.0; + + 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..7148932 --- /dev/null +++ b/spa/plugins/v4l2/v4l2.c @@ -0,0 +1,48 @@ +/* Spa V4l2 support */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +#include "config.h" +#include "v4l2.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; + +SPA_LOG_TOPIC_DEFINE(v4l2_log_topic, "spa.v4l2"); + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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: +#ifdef HAVE_LIBUDEV + *factory = &spa_v4l2_udev_factory; + break; +#else + (*index)++; + SPA_FALLTHROUGH; +#endif + 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..04065e6 --- /dev/null +++ b/spa/plugins/v4l2/v4l2.h @@ -0,0 +1,31 @@ +/* Spa V4l2 support */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include + +#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..c345257 --- /dev/null +++ b/spa/plugins/videoconvert/meson.build @@ -0,0 +1,26 @@ +videoconvert_sources = [ + 'videoadapter.c', + 'videoconvert-dummy.c', + 'plugin.c' +] + +extra_cargs = [] +extra_dependencies = [] + +if avcodec_dep.found() and avutil_dep.found() and swscale_dep.found() + videoconvert_ffmpeg = static_library('videoconvert_fmmpeg', + ['videoconvert-ffmpeg.c' ], + dependencies : [ spa_dep, avcodec_dep, avutil_dep, swscale_dep ], + install : false + ) + extra_cargs += '-D HAVE_VIDEOCONVERT_FFMPEG' + extra_dependencies += videoconvert_ffmpeg +endif + +videoconvertlib = shared_library('spa-videoconvert', + videoconvert_sources, + c_args : extra_cargs, + dependencies : [ spa_dep, mathlib ], + link_with : extra_dependencies, + install : true, + install_dir : spa_plugindir / 'videoconvert') diff --git a/spa/plugins/videoconvert/plugin.c b/spa/plugins/videoconvert/plugin.c new file mode 100644 index 0000000..d3d717b --- /dev/null +++ b/spa/plugins/videoconvert/plugin.c @@ -0,0 +1,41 @@ +/* Spa videoconvert plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_videoadapter_factory; +extern const struct spa_handle_factory spa_videoconvert_dummy_factory; +#if HAVE_VIDEOCONVERT_FFMPEG +extern const struct spa_handle_factory spa_videoconvert_ffmpeg_factory; +#endif + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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; + case 1: + *factory = &spa_videoconvert_dummy_factory; + break; +#if HAVE_VIDEOCONVERT_FFMPEG + case 2: + *factory = &spa_videoconvert_ffmpeg_factory; + break; +#endif + 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..ebc11e1 --- /dev/null +++ b/spa/plugins/videoconvert/videoadapter.c @@ -0,0 +1,2043 @@ +/* SPA */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoadapter"); + +#define DEFAULT_ALIGN 16 + +#define MAX_PORTS (1+1) +#define MAX_RETRY 64 + +/** \cond */ + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + struct spa_plugin_loader *ploader; + + uint32_t max_align; + enum spa_direction direction; + + struct spa_node *target; + + struct spa_node *follower; + struct spa_hook follower_listener; + uint64_t follower_flags; + struct spa_video_info follower_current_format; + struct spa_video_info default_format; + int in_set_param; + + struct spa_handle *hnd_convert; + struct spa_node *convert; + struct spa_hook convert_listener; + uint64_t convert_port_flags; + char *convertname; + + 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 IDX_Tag 8 +#define N_NODE_PARAMS 9 + struct spa_param_info params[N_NODE_PARAMS]; + uint32_t convert_params_flags[N_NODE_PARAMS]; + uint32_t follower_params_flags[N_NODE_PARAMS]; + uint64_t follower_port_flags; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + unsigned int add_listener:1; + unsigned int have_format:1; + unsigned int recheck_format:1; + unsigned int started:1; + unsigned int ready:1; + unsigned int async:1; + unsigned int passthrough:1; + unsigned int follower_removing:1; + unsigned int in_recalc; + + unsigned int warned:1; + unsigned int driver: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 && + this->follower != this->target) { + if ((res = spa_node_enum_params_sync(this->target, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + result->next = 0x100000; + } + 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; + + if (this->convert == NULL) + return 0; + + f1 = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction)); + + 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]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + 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++; + res = 1; + 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: + case SPA_PARAM_Tag: + 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) + return res; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + count++; + + if (count != num) + goto next; + + return 0; +} + +static int link_io(struct impl *this) +{ + int res; + struct spa_io_rate_match *rate_match; + size_t rate_match_size; + + spa_log_debug(this->log, "%p: controls", this); + + spa_zero(this->io_rate_match); + this->io_rate_match.rate = 1.0; + + if (this->follower == this->target) { + rate_match = NULL; + rate_match_size = 0; + } else { + rate_match = &this->io_rate_match; + rate_match_size = sizeof(this->io_rate_match); + } + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_RateMatch, + rate_match, rate_match_size)) < 0) { + spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this, + res, spa_strerror(res)); + } + else if (this->follower != this->target) { + if ((res = spa_node_port_set_io(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_RateMatch, + rate_match, rate_match_size)) < 0) { + spa_log_warn(this->log, "%p: set RateMatch on target failed %d %s", this, + res, spa_strerror(res)); + } + } + return 0; +} + +static int activate_io(struct impl *this, bool active) +{ + int res; + struct spa_io_buffers *data = active ? &this->io_buffers : NULL; + uint32_t size = active ? sizeof(this->io_buffers) : 0; + + if (this->follower == this->target) + return 0; + + if (active) + this->io_buffers = SPA_IO_BUFFERS_INIT; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_Buffers, data, size)) < 0) { + spa_log_warn(this->log, "%p: set Buffers on follower failed %d %s", this, + res, spa_strerror(res)); + return res; + } + else if ((res = spa_node_port_set_io(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_Buffers, data, size)) < 0) { + spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this, + 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) { + struct spa_dict_item *items; + uint32_t n_items = 0; + + if (this->info.props) + n_items = this->info.props->n_items; + items = alloca((n_items + 2) * sizeof(struct spa_dict_item)); + for (i = 0; i < n_items; i++) + items[i] = this->info.props->items[i]; + items[n_items++] = SPA_DICT_ITEM_INIT("adapter.auto-port-config", NULL); + items[n_items++] = SPA_DICT_ITEM_INIT("video.adapt.follower", NULL); + this->info.props = &SPA_DICT_INIT(items, n_items); + + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + 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; + spa_zero(this->info.props); + } +} + +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_ERROR, 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, ¶m, &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_ERROR, 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; + struct spa_meta metas[1]; + uint64_t follower_flags, conv_flags; + + spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); + + if (this->follower == this->target) + return 0; + + if (this->n_buffers > 0) + return 0; + + state = 0; + param = NULL; + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &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->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &b)) != 1) { + debug_params(this, this->target, + 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_port_flags; + conv_flags = this->convert_port_flags; + + follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + + 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; + + if (this->async) + buffers = SPA_MAX(2u, buffers); + + 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; + } + metas[0].type = SPA_META_Header; + metas[0].size = sizeof(struct spa_meta_header); + + free(this->buffers); + this->buffers = spa_buffer_alloc_array(buffers, flags, 1, metas, blocks, datas, aligns); + if (this->buffers == NULL) + return -errno; + this->n_buffers = buffers; + + if ((res = spa_node_port_use_buffers(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + this->buffers, this->n_buffers)) < 0) + 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; + + activate_io(this, true); + + return 0; +} + +static void clear_buffers(struct impl *this) +{ + free(this->buffers); + this->buffers = NULL; + this->n_buffers = 0; +} + +static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format) +{ + uint8_t buffer[4096]; + int res; + + spa_log_debug(this->log, "%p: configure format:", this); + + if (format == NULL) { + if (!this->have_format) + return 0; + activate_io(this, false); + } + else { + spa_debug_log_format(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + } + + if ((res = spa_node_port_set_param(this->follower, + this->direction, 0, + 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) { + if ((res = spa_node_port_set_param(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + } + + this->have_format = format != NULL; + clear_buffers(this); + + if (format != NULL) + 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); +} + +static const struct spa_node_events follower_node_events; + +static int recalc_latency(struct impl *this, struct spa_node *src, enum spa_direction direction, + uint32_t port_id, struct spa_node *dst) +{ + 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: %d:%d", this, direction, port_id); + + 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(src, + direction, port_id, SPA_PARAM_Latency, + &index, NULL, ¶m, &b)) != 1) { + param = NULL; + break; + } + if ((res = spa_latency_parse(param, &latency)) < 0) + return res; + if (latency.direction == direction) + break; + } + if ((res = spa_node_port_set_param(dst, + SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Latency, 0, param)) < 0) + return res; + + return 0; +} + +static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_direction direction, + uint32_t port_id, struct spa_node *dst) +{ + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + uint8_t buffer[2048]; + struct spa_pod *param; + uint32_t index = 0; + struct spa_tag_info info; + int res; + + spa_log_debug(this->log, "%p: %d:%d", this, direction, port_id); + + if (this->target == this->follower) + return 0; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 2048); + spa_pod_builder_get_state(&b.b, &state); + + while (true) { + void *tag_state = NULL; + spa_pod_builder_reset(&b.b, &state); + if ((res = spa_node_port_enum_params_sync(src, + direction, port_id, SPA_PARAM_Tag, + &index, NULL, ¶m, &b.b)) != 1) { + param = NULL; + break; + } + if ((res = spa_tag_parse(param, &info, &tag_state)) < 0) + return res; + if (info.direction == direction) + break; + } + return spa_node_port_set_param(dst, SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Tag, 0, param); +} + + +static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode, + enum spa_direction direction, struct spa_pod *format) +{ + int res = 0; + struct spa_hook l; + bool passthrough = mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough; + + spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough); + + if (!passthrough && this->convert == NULL) + return -ENOTSUP; + + if (this->passthrough != passthrough) { + if (passthrough) { + /* remove converter split/merge ports */ + 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, mode); + } + link_io(this); + } + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + SPA_FLAG_CLEAR(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE); + SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, + this->async && this->follower == this->target); + this->params[IDX_Props].user++; + + emit_node_info(this, false); + + spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough); + + 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 (spa_format_video_parse(param, &info) < 0) + return -EINVAL; + if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + 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_video_parse(format, &info)) < 0) + return res; + + 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, mode, dir, format)) < 0) + return res; + break; + case SPA_PARAM_PORT_CONFIG_MODE_convert: + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + if ((res = reconfigure_mode(this, mode, 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; + + res = recalc_latency(this, this->follower, this->direction, 0, this->target); + } + break; + } + + case SPA_PARAM_Props: + { + int in_set_param = ++this->in_set_param; + res = spa_node_set_param(this->follower, id, flags, param); + if (this->target != this->follower && this->in_set_param == in_set_param) + res2 = spa_node_set_param(this->target, 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 fstate, tstate; + struct spa_pod *format, *def; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + int res, fres; + + spa_log_debug(this->log, "%p: have_format:%d recheck:%d", this, this->have_format, + this->recheck_format); + + if (this->target == this->follower) + return 0; + + if (this->have_format && !this->recheck_format) + return 0; + + this->recheck_format = false; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); + + /* first try the ideal converter format, which is likely passthrough */ + tstate = 0; + fres = spa_node_port_enum_params_sync(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, + NULL, &format, &b); + if (fres == 1) { + fstate = 0; + res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, + format, &format, &b); + if (res == 1) + goto found; + } + + /* then try something the follower can accept */ + for (fstate = 0;;) { + format = NULL; + res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, + NULL, &format, &b); + + if (res == -ENOENT) + format = NULL; + else if (res <= 0) + break; + + tstate = 0; + fres = spa_node_port_enum_params_sync(this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, + format, &format, &b); + if (fres == 0 && res == 1) + continue; + + res = fres; + break; + } +found: + if (format == NULL) { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_EnumFormat, format, "follower format", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, format, "convert format", res); + res = -ENOTSUP; + goto done; + } + def = spa_format_video_build(&b, + SPA_PARAM_Format, &this->default_format); + + format = merge_objects(this, &b, SPA_PARAM_Format, + (struct spa_pod_object*)format, + (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; + + 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; + this->ready = true; + this->warned = false; + break; + case SPA_NODE_COMMAND_Suspend: + spa_log_debug(this->log, "%p: suspending", this); + break; + case SPA_NODE_COMMAND_Pause: + spa_log_debug(this->log, "%p: pausing", this); + break; + case SPA_NODE_COMMAND_Flush: + spa_log_debug(this->log, "%p: flushing", this); + this->io_buffers.status = SPA_STATUS_OK; + 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)); + } + + if (res >= 0 && 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)); + } + } + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (res < 0) { + spa_log_debug(this->log, "%p: start failed", this); + this->ready = false; + configure_format(this, 0, NULL); + } else { + this->started = true; + spa_log_debug(this->log, "%p: started", this); + } + break; + case SPA_NODE_COMMAND_Suspend: + configure_format(this, 0, NULL); + this->started = false; + this->warned = false; + this->ready = false; + spa_log_debug(this->log, "%p: suspended", this); + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + this->warned = false; + this->ready = false; + 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 follower_convert_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 (info == NULL) + return; + + spa_log_debug(this->log, "%p: convert port info %s %p %08"PRIx64, this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask); + + this->convert_port_flags = info->flags; + + if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_Latency: + idx = IDX_Latency; + break; + case SPA_PARAM_Tag: + idx = IDX_Tag; + break; + default: + continue; + } + + if (!this->add_listener && + this->convert_params_flags[idx] == info->params[i].flags) + continue; + + this->convert_params_flags[idx] = info->params[i].flags; + + if (this->add_listener) + continue; + + if (idx == IDX_Latency) { + this->in_recalc++; + res = recalc_latency(this, this->target, direction, port_id, this->follower); + this->in_recalc--; + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_Tag) { + this->in_recalc++; + res = recalc_tag(this, this->target, direction, port_id, this->follower); + this->in_recalc--; + spa_log_debug(this->log, "tag: %d (%s)", res, + spa_strerror(res)); + } + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } +} + +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; + struct spa_port_info pi; + + if (direction != this->direction) { + if (port_id == 0) { + /* handle the converter output port into the follower separately */ + follower_convert_port_info(this, direction, port_id, info); + return; + } else + /* the monitor ports are exposed */ + port_id--; + } else if (info) { + pi = *info; + pi.flags = this->follower_port_flags & + (SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL); + info = π + } + + 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_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, + this->async && this->follower == this->target); + + 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 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 (info == NULL) + return; + + if (this->follower_removing) { + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return; + } + + this->follower_port_flags = info->flags; + + spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64" recalc:%u", this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask, + this->in_recalc); + + 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; + case SPA_PARAM_Tag: + idx = IDX_Tag; + 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 && this->in_recalc == 0) { + res = recalc_latency(this, this->follower, direction, port_id, this->target); + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_Tag && this->in_recalc == 0) { + res = recalc_tag(this, this->follower, direction, port_id, this->target); + spa_log_debug(this->log, "tag: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_EnumFormat) { + spa_log_debug(this->log, "new formats"); + /* we will renegotiate when restarting */ + this->recheck_format = true; + } + + 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: + case SPA_NODE_EVENT_RequestProcess: + /* Forward errors and process requests */ + 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->ready) { + 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 = MAX_RETRY; + while (retry--) { + status = spa_node_process_fast(this->target); + if (status & SPA_STATUS_HAVE_DATA) + break; + + if (status & SPA_STATUS_NEED_DATA) { + status = spa_node_process_fast(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) + res = spa_node_port_reuse_buffer(this->target, port_id, buffer_id); + else + res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id); + + 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->follower != this->target) { + spa_zero(l); + spa_node_add_listener(this->target, &l, &convert_node_events, this); + spa_hook_remove(&l); + } + this->add_listener = false; + + emit_node_info(this, true); + } + 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 follower_port_enum_params(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t idx, + struct spa_result_node_params *result, + const struct spa_pod *filter, + struct spa_pod_builder *builder) +{ + int res; + if (result->next < 0x100000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + if ((res = spa_node_port_enum_params_sync(this->follower, direction, port_id, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + result->next = 0x100000; + } + if (result->next < 0x200000 && + this->follower != this->target) { + result->next &= 0xfffff; + if ((res = spa_node_port_enum_params_sync(this->target, direction, port_id, + id, &result->next, filter, &result->param, builder)) == 1) { + result->next |= 0x100000; + return res; + } + result->next = 0x200000; + } + return 0; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + uint8_t buffer[4096]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + struct spa_pod_builder_state state; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + if (direction != this->direction) + port_id++; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); + + spa_pod_builder_reset(&b.b, &state); + + switch (id) { + case SPA_PARAM_EnumFormat: + res = follower_port_enum_params(this, direction, port_id, + id, IDX_EnumFormat, &result, filter, &b.b); + break; + default: + return spa_node_port_enum_params(this->target, seq, direction, port_id, id, + start, num, filter); + } + if (res != 1) + return res; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + count++; + + if (count != num) + goto next; + + return 0; +} + +static int +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, " %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + return spa_node_port_set_param(this->target, direction, port_id, id, + flags, param); +} + +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 = MAX_RETRY; + + if (!this->ready) { + if (!this->warned) + spa_log_warn(this->log, "%p: scheduling stopped node", this); + this->warned = true; + 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_fast(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 = spa_node_process_fast(this->target); + /* 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_fast(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 = spa_node_process_fast(this->target); + if (status == 0) + status = SPA_STATUS_NEED_DATA; + else if (status < 0) + break; + + done = (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)); + if (done) + break; + + if (status & SPA_STATUS_NEED_DATA) { + /* the converter needs more data, schedule the + * follower */ + fstatus = spa_node_process_fast(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; + } + } + if (!done) + spa_node_call_xrun(&this->callbacks, 0, 0, NULL); + + } else { + status = spa_node_process_fast(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 load_plugin_from(struct impl *this, const struct spa_dict *info, + const char *convertname, struct spa_handle **handle, struct spa_node **iface) +{ + struct spa_handle *hnd_convert = NULL; + void *iface_conv = NULL; + hnd_convert = spa_plugin_loader_load(this->ploader, convertname, info); + if (!hnd_convert) + return -EINVAL; + + spa_handle_get_interface(hnd_convert, SPA_TYPE_INTERFACE_Node, &iface_conv); + if (iface_conv == NULL) { + spa_plugin_loader_unload(this->ploader, hnd_convert); + return -EINVAL; + } + + *handle = hnd_convert; + *iface = iface_conv; + + return 0; +} + +static int load_converter(struct impl *this, const struct spa_dict *info) +{ + int ret; + if (!this->ploader || !info) + return -EINVAL; + + const char* factory_name = spa_dict_lookup(info, "video.adapt.converter"); + + if (factory_name) { + ret = load_plugin_from(this, info, factory_name, &this->hnd_convert, &this->convert); + if (ret >= 0) { + this->convertname = strdup(factory_name); + return ret; + } + } + return 0; +} + +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_plugin_loader_unload(this->ploader, this->hnd_convert); + free(this->convertname); + } + + clear_buffers(this); + return 0; +} + + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + size_t 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; + const char *str; + int ret; + + 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); + + this->ploader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); + + 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); + + ret = load_converter(this, info); + spa_log_info(this->log, "%p: loaded converter %s, hnd %p, convert %p", this, + this->convertname, this->hnd_convert, this->convert); + if (ret < 0) + return ret; + + if (this->convert == NULL) { + this->target = this->follower; + this->passthrough = true; + } else { + this->target = this->convert; + this->passthrough = false; + } + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.flags = SPA_NODE_FLAG_RT | + 0; + //SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + 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->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, 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); + + // TODO: adapt port bootstrap for arbitrary converter (incl. dummy) + if (this->convert) { + spa_node_add_listener(this->convert, + &this->convert_listener, &convert_node_events, this); + + if (strcmp(this->convertname, "video.convert.dummy") == 0) { + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL); + } else { + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_convert); + } + } else { + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, 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/videoconvert/videoconvert-dummy.c b/spa/plugins/videoconvert/videoconvert-dummy.c new file mode 100644 index 0000000..fab51d7 --- /dev/null +++ b/spa/plugins/videoconvert/videoconvert-dummy.c @@ -0,0 +1,720 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoconvert.dummy"); + +#define MAX_PORTS 1 + +struct props { +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define IDX_Tag 6 +#define N_PORT_PARAMS 7 + struct spa_param_info params[N_PORT_PARAMS]; +}; + +struct dir { + struct port ports[MAX_PORTS]; + uint32_t n_ports; + + enum spa_direction direction; + enum spa_param_port_config_mode mode; + + struct spa_video_info format; + unsigned int have_profile:1; + struct spa_pod *tag; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + struct props props; + + struct spa_io_position *io_position; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumPortConfig 0 +#define IDX_PortConfig 1 +#define IDX_PropInfo 2 +#define IDX_Props 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct dir dir[2]; +}; + +#define CHECK_PORT(this,d,p) ((p) < this->dir[d].n_ports) + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_MEDIA_CLASS, "Video/Filter" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(this->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[1]; + + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float RGBA video"); + port->info.props = &SPA_DICT_INIT(items, 1); + spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + { + struct dir *dir; + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_none), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(false), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(false)); + break; + } + case SPA_PARAM_PortConfig: + { + struct dir *dir; + struct spa_pod_frame f[1]; + + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); + spa_pod_builder_add(&b, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(false), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(false), + 0); + + param = spa_pod_builder_pop(&b, &f[0]); + break; + } + case SPA_PARAM_PropInfo: + { + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("video.convert.converter"), + SPA_PROP_INFO_description, SPA_POD_String("Name of the used videoconverter"), + SPA_PROP_INFO_type, SPA_POD_String("dummy"), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + default: + return 0; + } + break; + } + default: + return 0; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p/%zu", this, id, data, size); + + switch (id) { + case SPA_IO_Position: + if (size > 0 && size < sizeof(struct spa_io_position)) + return -EINVAL; + this->io_position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode, + enum spa_direction direction, struct spa_video_info *info) +{ + struct dir *dir; + uint32_t i; + + dir = &this->dir[direction]; + + if (dir->have_profile && dir->mode == mode && + (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0)) + return 0; + + spa_log_info(this->log, "%p: port config direction:%d mode:%d %d %p", this, + direction, mode, dir->n_ports, info); + + for (i = 0; i < dir->n_ports; i++) { + spa_node_emit_port_info(&this->hooks, direction, i, NULL); + } + + dir->have_profile = true; + dir->mode = mode; + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_none: + break; + default: + return -ENOTSUP; + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_Props].user++; + this->params[IDX_PortConfig].user++; + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (param == NULL) + return 0; + + switch (id) { + case SPA_PARAM_PortConfig: + { + struct spa_video_info info = { 0, }, *infop = NULL; + struct spa_pod *format = NULL; + enum spa_direction direction; + enum spa_param_port_config_mode mode; + bool monitor = false, control = false; + int res; + + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; + + if (format) { + if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) + return -EINVAL; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_video_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format == 0) + return -EINVAL; + + infop = &info; + } + + if ((res = reconfigure_mode(this, mode, direction, infop)) < 0) + return res; + + emit_node_info(this, false); + break; + } + + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->dir[0].ports[0], true); + emit_port_info(this, &this->dir[1].ports[0], true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + return -ENOTSUP; +} + + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_log_debug(this->log, "%p: enum params port %d.%d %d %u", + this, direction, port_id, seq, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int port_set_format(struct impl *this, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + return -ENOTSUP; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set param port %d.%d %u", + this, direction, port_id, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + return -ENOTSUP; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return -ENOTSUP; + +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return -ENOTSUP; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct dir *dir; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + // props_reset(&this->props); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_OUT_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + dir = &this->dir[SPA_DIRECTION_INPUT]; + dir->direction = SPA_DIRECTION_INPUT; + + dir = &this->dir[SPA_DIRECTION_OUTPUT]; + dir->direction = SPA_DIRECTION_OUTPUT; + + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_none, SPA_DIRECTION_INPUT, NULL); + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_none, SPA_DIRECTION_OUTPUT, NULL); + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Columbarius " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Dummy video convert plugin" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_videoconvert_dummy_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_VIDEO_CONVERT_DUMMY, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/videoconvert/videoconvert-ffmpeg.c b/spa/plugins/videoconvert/videoconvert-ffmpeg.c new file mode 100644 index 0000000..955bfa9 --- /dev/null +++ b/spa/plugins/videoconvert/videoconvert-ffmpeg.c @@ -0,0 +1,2082 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoconvert.ffmpeg"); + +#define MAX_ALIGN 64u +#define MAX_BUFFERS 32 +#define MAX_DATAS 4 +#define MAX_PORTS (1+1) + +struct props { + unsigned int dummy:1; +}; + +static void props_reset(struct props *props) +{ + props->dummy = false; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1<<0) + uint32_t flags; + struct spa_list link; + struct spa_buffer *buf; + void *datas[MAX_DATAS]; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define IDX_Tag 6 +#define N_PORT_PARAMS 7 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_latency_info latency[2]; + unsigned int have_latency:1; + + struct spa_video_info format; + unsigned int valid:1; + unsigned int have_format:1; + unsigned int is_dsp:1; + unsigned int is_monitor:1; + unsigned int is_control:1; + + uint32_t blocks; + uint32_t stride; + uint32_t maxsize; + + struct spa_list queue; +}; + +struct dir { + struct port *ports[MAX_PORTS]; + uint32_t n_ports; + + enum spa_direction direction; + enum spa_param_port_config_mode mode; + + struct spa_video_info format; + unsigned int have_format:1; + unsigned int have_profile:1; + struct spa_pod *tag; + enum AVPixelFormat pix_fmt; + int width; + int height; + + unsigned int control:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + struct spa_loop *data_loop; + + uint32_t cpu_flags; + uint32_t max_align; + uint32_t quantum_limit; + enum spa_direction direction; + + struct spa_ratelimit rate_limit; + + struct props props; + + struct spa_io_position *io_position; + struct spa_io_rate_match *io_rate_match; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumPortConfig 0 +#define IDX_PortConfig 1 +#define IDX_PropInfo 2 +#define IDX_Props 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + + unsigned int monitor:1; + + struct dir dir[2]; + + unsigned int started:1; + unsigned int setup:1; + unsigned int fmt_passthrough:1; + unsigned int drained:1; + unsigned int port_ignore_latency:1; + unsigned int monitor_passthrough:1; + + char group_name[128]; + + struct { + const AVCodec *codec; + AVCodecContext *context; + AVPacket *packet; + AVFrame *frame; + } decoder; + struct { + struct SwsContext *context; + AVFrame *frame; + } convert; + struct { + const AVCodec *codec; + AVCodecContext *context; + AVFrame *frame; + AVPacket *packet; + } encoder; +}; + +#define CHECK_PORT(this,d,p) ((p) < this->dir[d].n_ports) +#define GET_PORT(this,d,p) (this->dir[d].ports[p]) +#define GET_IN_PORT(this,p) GET_PORT(this,SPA_DIRECTION_INPUT,p) +#define GET_OUT_PORT(this,p) GET_PORT(this,SPA_DIRECTION_OUTPUT,p) + +#define PORT_IS_DSP(this,d,p) (GET_PORT(this,d,p)->is_dsp) +#define PORT_IS_CONTROL(this,d,p) (GET_PORT(this,d,p)->is_control) + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(this->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[5]; + uint32_t n_items = 0; + + if (PORT_IS_DSP(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float video"); + if (port->is_monitor) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true"); + if (this->port_ignore_latency) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); + } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + } + if (this->group_name[0] != '\0') + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); + port->info.props = &SPA_DICT_INIT(items, n_items); + + if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(port->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id, + bool is_dsp, bool is_monitor, bool is_control) +{ + struct port *port = GET_PORT(this, direction, port_id); + + spa_assert(port_id < MAX_PORTS); + + if (port == NULL) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + this->dir[direction].ports[port_id] = port; + } + port->direction = direction; + port->id = port_id; + port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_DYNAMIC_DATA; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->n_buffers = 0; + port->have_format = false; + port->is_monitor = is_monitor; + port->is_dsp = is_dsp; + if (port->is_dsp) { + port->format.media_type = SPA_MEDIA_TYPE_video; + port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp; + port->format.info.dsp.format = SPA_VIDEO_FORMAT_DSP_F32; + port->blocks = 1; + port->stride = 16; + } + port->is_control = is_control; + if (port->is_control) { + port->format.media_type = SPA_MEDIA_TYPE_application; + port->format.media_subtype = SPA_MEDIA_SUBTYPE_control; + port->blocks = 1; + port->stride = 1; + } + port->valid = true; + spa_list_init(&port->queue); + + spa_log_debug(this->log, "%p: add port %d:%d %d %d %d", + this, direction, port_id, is_dsp, is_monitor, is_control); + emit_port_info(this, port, true); + + return 0; +} + +static int deinit_port(struct impl *this, enum spa_direction direction, uint32_t port_id) +{ + struct port *port = GET_PORT(this, direction, port_id); + if (port == NULL || !port->valid) + return -ENOENT; + port->valid = false; + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + { + struct dir *dir; + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_CHOICE_ENUM_Id(4, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_dsp, + SPA_PARAM_PORT_CONFIG_MODE_convert), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_CHOICE_Bool(false), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_CHOICE_Bool(false)); + break; + } + case SPA_PARAM_PortConfig: + { + struct dir *dir; + struct spa_pod_frame f[1]; + + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); + spa_pod_builder_add(&b, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(this->monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(dir->control), + 0); + + if (dir->have_format) { + spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0); + spa_format_video_build(&b, SPA_PARAM_PORT_CONFIG_format, + &dir->format); + } + param = spa_pod_builder_pop(&b, &f[0]); + break; + } + case SPA_PARAM_PropInfo: + { + switch (result.index) { + default: + return 0; + } + break; + } + + case SPA_PARAM_Props: + { + struct spa_pod_frame f[2]; + + switch (result.index) { + case 0: + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_OBJECT_Props, id); + param = spa_pod_builder_pop(&b, &f[0]); + break; + default: + return 0; + } + break; + } + default: + return 0; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size); + + switch (id) { + case SPA_IO_Position: + this->io_position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int videoconvert_set_param(struct impl *this, const char *k, const char *s) +{ + return 0; +} + +static int parse_prop_params(struct impl *this, struct spa_pod *params) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + int changed = 0; + + spa_pod_parser_pod(&prs, params); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + struct spa_pod *pod; + char value[512]; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + + if (spa_pod_parser_get_pod(&prs, &pod) < 0) + break; + + if (spa_pod_is_string(pod)) { + spa_pod_copy_string(pod, sizeof(value), value); + } else if (spa_pod_is_float(pod)) { + spa_dtoa(value, sizeof(value), + SPA_POD_VALUE(struct spa_pod_float, pod)); + } else if (spa_pod_is_double(pod)) { + spa_dtoa(value, sizeof(value), + SPA_POD_VALUE(struct spa_pod_double, pod)); + } else if (spa_pod_is_int(pod)) { + snprintf(value, sizeof(value), "%d", + SPA_POD_VALUE(struct spa_pod_int, pod)); + } else if (spa_pod_is_bool(pod)) { + snprintf(value, sizeof(value), "%s", + SPA_POD_VALUE(struct spa_pod_bool, pod) ? + "true" : "false"); + } else if (spa_pod_is_none(pod)) { + spa_zero(value); + } else + continue; + + spa_log_info(this->log, "key:'%s' val:'%s'", name, value); + changed += videoconvert_set_param(this, name, value); + } + return changed; +} + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + int changed = 0; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_params: + changed += parse_prop_params(this, &prop->value); + break; + default: + break; + } + } + return changed; +} + +static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode, + enum spa_direction direction, bool monitor, bool control, struct spa_video_info *info) +{ + struct dir *dir; + uint32_t i; + + dir = &this->dir[direction]; + + if (dir->have_profile && this->monitor == monitor && dir->mode == mode && + dir->control == control && + (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0)) + return 0; + + spa_log_debug(this->log, "%p: port config direction:%d monitor:%d " + "control:%d mode:%d %d", this, direction, monitor, + control, mode, dir->n_ports); + + for (i = 0; i < dir->n_ports; i++) { + deinit_port(this, direction, i); + if (this->monitor && direction == SPA_DIRECTION_INPUT) + deinit_port(this, SPA_DIRECTION_OUTPUT, i+1); + } + + this->monitor = monitor; + this->setup = false; + dir->control = control; + dir->have_profile = true; + dir->mode = mode; + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + { + if (info) { + dir->n_ports = 1; + dir->format = *info; + dir->format.info.dsp.format = SPA_VIDEO_FORMAT_DSP_F32; + dir->have_format = true; + } else { + dir->n_ports = 0; + } + + if (this->monitor && direction == SPA_DIRECTION_INPUT) + this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1; + + for (i = 0; i < dir->n_ports; i++) { + init_port(this, direction, i, true, false, false); + if (this->monitor && direction == SPA_DIRECTION_INPUT) + init_port(this, SPA_DIRECTION_OUTPUT, i+1, true, true, false); + } + break; + } + case SPA_PARAM_PORT_CONFIG_MODE_convert: + { + dir->n_ports = 1; + dir->have_format = false; + init_port(this, direction, 0, false, false, false); + break; + } + case SPA_PARAM_PORT_CONFIG_MODE_none: + break; + default: + return -ENOTSUP; + } + if (direction == SPA_DIRECTION_INPUT && dir->control) { + i = dir->n_ports++; + init_port(this, direction, i, false, false, true); + } + /* when output is convert mode, we are in OUTPUT (merge) mode, we always output all + * the incoming data to output. When output is DSP, we need to output quantum size + * chunks. */ + this->direction = this->dir[SPA_DIRECTION_OUTPUT].mode == SPA_PARAM_PORT_CONFIG_MODE_convert ? + SPA_DIRECTION_OUTPUT : SPA_DIRECTION_INPUT; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_Props].user++; + this->params[IDX_PortConfig].user++; + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (param == NULL) + return 0; + + switch (id) { + case SPA_PARAM_PortConfig: + { + struct spa_video_info info = { 0, }, *infop = NULL; + struct spa_pod *format = NULL; + enum spa_direction direction; + enum spa_param_port_config_mode mode; + bool monitor = false, control = false; + int res; + + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; + + if (format) { + if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) + return -EINVAL; + + if ((res = spa_format_video_parse(format, &info)) < 0) + return res; + + infop = &info; + } + + if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0) + return res; + + emit_node_info(this, false); + break; + } + case SPA_PARAM_Props: + if (apply_props(this, param) > 0) + emit_node_info(this, false); + break; + default: + return -ENOENT; + } + return 0; +} + +static enum AVPixelFormat format_to_pix_fmt(uint32_t format) +{ + switch (format) { + case SPA_VIDEO_FORMAT_I420: + return AV_PIX_FMT_YUV420P; + case SPA_VIDEO_FORMAT_YV12: + break; + case SPA_VIDEO_FORMAT_YUY2: + return AV_PIX_FMT_YUYV422; + case SPA_VIDEO_FORMAT_UYVY: + return AV_PIX_FMT_UYVY422; + case SPA_VIDEO_FORMAT_AYUV: + break; + case SPA_VIDEO_FORMAT_RGBx: + return AV_PIX_FMT_RGB0; + case SPA_VIDEO_FORMAT_BGRx: + return AV_PIX_FMT_BGR0; + case SPA_VIDEO_FORMAT_xRGB: + return AV_PIX_FMT_0RGB; + case SPA_VIDEO_FORMAT_xBGR: + return AV_PIX_FMT_0BGR; + case SPA_VIDEO_FORMAT_RGBA: + return AV_PIX_FMT_RGBA; + case SPA_VIDEO_FORMAT_BGRA: + return AV_PIX_FMT_BGRA; + case SPA_VIDEO_FORMAT_ARGB: + return AV_PIX_FMT_ARGB; + case SPA_VIDEO_FORMAT_ABGR: + return AV_PIX_FMT_ABGR; + case SPA_VIDEO_FORMAT_RGB: + return AV_PIX_FMT_RGB24; + case SPA_VIDEO_FORMAT_BGR: + return AV_PIX_FMT_BGR24; + case SPA_VIDEO_FORMAT_Y41B: + return AV_PIX_FMT_YUV411P; + case SPA_VIDEO_FORMAT_Y42B: + return AV_PIX_FMT_YUV422P; + case SPA_VIDEO_FORMAT_YVYU: + return AV_PIX_FMT_YVYU422; + case SPA_VIDEO_FORMAT_Y444: + return AV_PIX_FMT_YUV444P; + case SPA_VIDEO_FORMAT_v210: + case SPA_VIDEO_FORMAT_v216: + break; + case SPA_VIDEO_FORMAT_NV12: + return AV_PIX_FMT_NV12; + case SPA_VIDEO_FORMAT_NV21: + return AV_PIX_FMT_NV21; + case SPA_VIDEO_FORMAT_GRAY8: + return AV_PIX_FMT_GRAY8; + case SPA_VIDEO_FORMAT_GRAY16_BE: + return AV_PIX_FMT_GRAY16BE; + case SPA_VIDEO_FORMAT_GRAY16_LE: + return AV_PIX_FMT_GRAY16LE; + case SPA_VIDEO_FORMAT_v308: + break; + case SPA_VIDEO_FORMAT_RGB16: + return AV_PIX_FMT_RGB565; + case SPA_VIDEO_FORMAT_BGR16: + break; + case SPA_VIDEO_FORMAT_RGB15: + return AV_PIX_FMT_RGB555; + case SPA_VIDEO_FORMAT_BGR15: + case SPA_VIDEO_FORMAT_UYVP: + break; + case SPA_VIDEO_FORMAT_A420: + return AV_PIX_FMT_YUVA420P; + case SPA_VIDEO_FORMAT_RGB8P: + return AV_PIX_FMT_PAL8; + case SPA_VIDEO_FORMAT_YUV9: + return AV_PIX_FMT_YUV410P; + case SPA_VIDEO_FORMAT_YVU9: + case SPA_VIDEO_FORMAT_IYU1: + case SPA_VIDEO_FORMAT_ARGB64: + case SPA_VIDEO_FORMAT_AYUV64: + case SPA_VIDEO_FORMAT_r210: + break; + case SPA_VIDEO_FORMAT_I420_10BE: + return AV_PIX_FMT_YUV420P10BE; + case SPA_VIDEO_FORMAT_I420_10LE: + return AV_PIX_FMT_YUV420P10LE; + case SPA_VIDEO_FORMAT_I422_10BE: + return AV_PIX_FMT_YUV422P10BE; + case SPA_VIDEO_FORMAT_I422_10LE: + return AV_PIX_FMT_YUV422P10LE; + case SPA_VIDEO_FORMAT_Y444_10BE: + return AV_PIX_FMT_YUV444P10BE; + case SPA_VIDEO_FORMAT_Y444_10LE: + return AV_PIX_FMT_YUV444P10LE; + case SPA_VIDEO_FORMAT_GBR: + return AV_PIX_FMT_GBRP; + case SPA_VIDEO_FORMAT_GBR_10BE: + return AV_PIX_FMT_GBRP10BE; + case SPA_VIDEO_FORMAT_GBR_10LE: + return AV_PIX_FMT_GBRP10LE; + case SPA_VIDEO_FORMAT_NV16: + case SPA_VIDEO_FORMAT_NV24: + case SPA_VIDEO_FORMAT_NV12_64Z32: + break; + case SPA_VIDEO_FORMAT_A420_10BE: + return AV_PIX_FMT_YUVA420P10BE; + case SPA_VIDEO_FORMAT_A420_10LE: + return AV_PIX_FMT_YUVA420P10LE; + case SPA_VIDEO_FORMAT_A422_10BE: + return AV_PIX_FMT_YUVA422P10BE; + case SPA_VIDEO_FORMAT_A422_10LE: + return AV_PIX_FMT_YUVA422P10LE; + case SPA_VIDEO_FORMAT_A444_10BE: + return AV_PIX_FMT_YUVA444P10BE; + case SPA_VIDEO_FORMAT_A444_10LE: + return AV_PIX_FMT_YUVA444P10LE; + case SPA_VIDEO_FORMAT_NV61: + case SPA_VIDEO_FORMAT_P010_10BE: + case SPA_VIDEO_FORMAT_P010_10LE: + case SPA_VIDEO_FORMAT_IYU2: + case SPA_VIDEO_FORMAT_VYUY: + break; + case SPA_VIDEO_FORMAT_GBRA: + return AV_PIX_FMT_GBRAP; + case SPA_VIDEO_FORMAT_GBRA_10BE: + return AV_PIX_FMT_GBRAP10BE; + case SPA_VIDEO_FORMAT_GBRA_10LE: + return AV_PIX_FMT_GBRAP10LE; + case SPA_VIDEO_FORMAT_GBR_12BE: + return AV_PIX_FMT_GBRP12BE; + case SPA_VIDEO_FORMAT_GBR_12LE: + return AV_PIX_FMT_GBRP12LE; + case SPA_VIDEO_FORMAT_GBRA_12BE: + return AV_PIX_FMT_GBRAP12BE; + case SPA_VIDEO_FORMAT_GBRA_12LE: + return AV_PIX_FMT_GBRAP12LE; + case SPA_VIDEO_FORMAT_I420_12BE: + return AV_PIX_FMT_YUV420P12BE; + case SPA_VIDEO_FORMAT_I420_12LE: + return AV_PIX_FMT_YUV420P12LE; + case SPA_VIDEO_FORMAT_I422_12BE: + return AV_PIX_FMT_YUV422P12BE; + case SPA_VIDEO_FORMAT_I422_12LE: + return AV_PIX_FMT_YUV422P12LE; + case SPA_VIDEO_FORMAT_Y444_12BE: + return AV_PIX_FMT_YUV444P12BE; + case SPA_VIDEO_FORMAT_Y444_12LE: + return AV_PIX_FMT_YUV444P12LE; + + case SPA_VIDEO_FORMAT_RGBA_F16: + case SPA_VIDEO_FORMAT_RGBA_F32: + break; + + case SPA_VIDEO_FORMAT_xRGB_210LE: + return AV_PIX_FMT_X2RGB10LE; + case SPA_VIDEO_FORMAT_xBGR_210LE: + return AV_PIX_FMT_X2BGR10LE; + + case SPA_VIDEO_FORMAT_RGBx_102LE: + case SPA_VIDEO_FORMAT_BGRx_102LE: + case SPA_VIDEO_FORMAT_ARGB_210LE: + case SPA_VIDEO_FORMAT_ABGR_210LE: + case SPA_VIDEO_FORMAT_RGBA_102LE: + case SPA_VIDEO_FORMAT_BGRA_102LE: + break; + default: + break; + } + return AV_PIX_FMT_NONE; +} + +static int get_format(struct dir *dir, int *width, int *height, uint32_t *format) +{ + if (dir->have_format) { + switch (dir->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + *width = dir->format.info.raw.size.width; + *height = dir->format.info.raw.size.height; + *format = dir->format.info.raw.format; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + *width = dir->format.info.mjpg.size.width; + *height = dir->format.info.mjpg.size.height; + break; + case SPA_MEDIA_SUBTYPE_h264: + *width = dir->format.info.h264.size.width; + *height = dir->format.info.h264.size.height; + break; + default: + *width = *height = 0; + break; + } + } else { + *width = *height = 0; + } + return 0; +} + + +static int setup_convert(struct impl *this) +{ + struct dir *in, *out; + uint32_t format; + + in = &this->dir[SPA_DIRECTION_INPUT]; + out = &this->dir[SPA_DIRECTION_OUTPUT]; + + spa_log_debug(this->log, "%p: setup:%d in_format:%d out_format:%d", this, + this->setup, in->have_format, out->have_format); + + if (this->setup) + return 0; + + if (!in->have_format || !out->have_format) + return -EIO; + + switch (in->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + in->pix_fmt = format_to_pix_fmt(in->format.info.raw.format); + switch (out->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); + break; + case SPA_MEDIA_SUBTYPE_mjpg: + if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG)) == NULL) { + spa_log_error(this->log, "failed to find MJPEG encoder"); + return -ENOTSUP; + } + out->format.media_subtype = SPA_MEDIA_SUBTYPE_raw; + out->format.info.raw.format = SPA_VIDEO_FORMAT_I420; + out->format.info.raw.size = in->format.info.raw.size; + out->pix_fmt = AV_PIX_FMT_YUVJ420P; + break; + case SPA_MEDIA_SUBTYPE_h264: + if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_H264)) == NULL) { + spa_log_error(this->log, "failed to find H264 encoder"); + return -ENOTSUP; + } + break; + default: + return -ENOTSUP; + } + break; + case SPA_MEDIA_SUBTYPE_mjpg: + switch (out->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_mjpg: + /* passthrough */ + break; + case SPA_MEDIA_SUBTYPE_raw: + out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); + if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG)) == NULL) { + spa_log_error(this->log, "failed to find MJPEG decoder"); + return -ENOTSUP; + } + break; + default: + return -ENOTSUP; + } + break; + case SPA_MEDIA_SUBTYPE_h264: + switch (out->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_h264: + /* passthrough */ + break; + case SPA_MEDIA_SUBTYPE_raw: + out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); + if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_H264)) == NULL) { + spa_log_error(this->log, "failed to find H264 decoder"); + return -ENOTSUP; + } + break; + default: + return -ENOTSUP; + } + break; + default: + return -ENOTSUP; + } + + get_format(in, &in->width, &in->height, &format); + get_format(out, &out->width, &out->height, &format); + + if (this->decoder.codec) { + if ((this->decoder.context = avcodec_alloc_context3(this->decoder.codec)) == NULL) + return -EIO; + + if ((this->decoder.packet = av_packet_alloc()) == NULL) + return -EIO; + + this->decoder.context->flags2 |= AV_CODEC_FLAG2_FAST; + + if (avcodec_open2(this->decoder.context, this->decoder.codec, NULL) < 0) { + spa_log_error(this->log, "failed to open decoder codec"); + return -EIO; + } + } + if ((this->decoder.frame = av_frame_alloc()) == NULL) + return -EIO; + if (this->encoder.codec) { + if ((this->encoder.context = avcodec_alloc_context3(this->encoder.codec)) == NULL) + return -EIO; + + if ((this->encoder.packet = av_packet_alloc()) == NULL) + return -EIO; + if ((this->encoder.frame = av_frame_alloc()) == NULL) + return -EIO; + + this->encoder.context->flags2 |= AV_CODEC_FLAG2_FAST; + this->encoder.context->time_base.num = 1; + this->encoder.context->width = out->width; + this->encoder.context->height = out->height; + this->encoder.context->pix_fmt = out->pix_fmt; + + if (avcodec_open2(this->encoder.context, this->encoder.codec, NULL) < 0) { + spa_log_error(this->log, "failed to open encoder codec"); + return -EIO; + } + } + if ((this->convert.frame = av_frame_alloc()) == NULL) + return -EIO; + + + this->setup = true; + + emit_node_info(this, false); + + return 0; +} + +static void reset_node(struct impl *this) +{ +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (this->started) + return 0; + if ((res = setup_convert(this)) < 0) + return res; + this->started = true; + break; + case SPA_NODE_COMMAND_Suspend: + this->setup = false; + SPA_FALLTHROUGH; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + case SPA_NODE_COMMAND_Flush: + reset_node(this); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + uint32_t i; + struct spa_hook_list save; + struct port *p; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + if ((p = GET_IN_PORT(this, i)) && p->valid) + emit_port_info(this, p, true); + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + if ((p = GET_OUT_PORT(this, i)) && p->valid) + emit_port_info(this, p, true); + } + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + struct dir *other = &this->dir[SPA_DIRECTION_REVERSE(direction)]; + struct spa_pod_frame f[1]; + int width, height; + uint32_t format = 0; + + get_format(other, &width, &height, &format); + + switch (index) { + case 0: + if (PORT_IS_DSP(this, direction, port_id)) { + struct spa_video_info_dsp info; + info.format = SPA_VIDEO_FORMAT_DSP_F32; + *param = spa_format_video_dsp_build(builder, + SPA_PARAM_EnumFormat, &info); + } else if (PORT_IS_CONTROL(this, direction, port_id)) { + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int( + (1u<have_format) { + *param = spa_format_video_build(builder, SPA_PARAM_EnumFormat, &other->format); + } else { + *param = NULL; + } + } + break; + case 1: + if (PORT_IS_DSP(this, direction, port_id) || + PORT_IS_CONTROL(this, direction, port_id)) + return 0; + + spa_pod_builder_push_object(builder, &f[0], + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, + format, + SPA_VIDEO_FORMAT_YUY2, + SPA_VIDEO_FORMAT_I420, + SPA_VIDEO_FORMAT_UYVY, + SPA_VIDEO_FORMAT_YVYU, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_BGRx), + 0); + if (width != 0 && height != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(width, height), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(INT32_MAX, INT32_MAX)), + 0); + } + *param = spa_pod_builder_pop(builder, &f[0]); + break; + case 2: + if (PORT_IS_DSP(this, direction, port_id) || + PORT_IS_CONTROL(this, direction, port_id)) + return 0; + + spa_pod_builder_push_object(builder, &f[0], + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg), + 0); + if (width != 0 && height != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(width, height), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(INT32_MAX, INT32_MAX)), + 0); + } + *param = spa_pod_builder_pop(builder, &f[0]); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port, *other; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_log_debug(this->log, "%p: enum params port %d.%d %d %u", + this, direction, port_id, seq, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(object, direction, port_id, result.index, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + if (PORT_IS_DSP(this, direction, port_id)) + param = spa_format_video_dsp_build(&b, id, &port->format.info.dsp); + else if (PORT_IS_CONTROL(this, direction, port_id)) + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, id, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + SPA_FORMAT_CONTROL_types, SPA_POD_Int( + (1u<format); + break; + case SPA_PARAM_Buffers: + { + uint32_t size, min, max; + + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + if (PORT_IS_DSP(this, direction, port_id)) { + size = 1024 * 1024 * 16; + } else { + size = 1024 * 1024 * 4; + } + + other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id); + if (other->n_buffers > 0) { + min = max = other->n_buffers; + } else { + min = 2; + max = MAX_BUFFERS; + } + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, min, max), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + size * port->stride, + 16 * port->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + uint32_t idx = result.index; + param = spa_latency_build(&b, id, &port->latency[idx]); + break; + } + default: + return 0; + } + break; + case SPA_PARAM_Tag: + switch (result.index) { + case 0: case 1: + { + uint32_t idx = result.index; + if (port->is_monitor) + idx = idx ^ 1; + param = this->dir[idx].tag; + if (param == NULL) + goto next; + break; + } + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, "%p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int port_set_latency(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *latency) +{ + struct impl *this = object; + struct port *port, *oport; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct spa_latency_info info; + bool have_latency, emit = false;; + uint32_t i; + + spa_log_debug(this->log, "%p: set latency direction:%d id:%d %p", + this, direction, port_id, latency); + + port = GET_PORT(this, direction, port_id); + if (latency == NULL) { + info = SPA_LATENCY_INFO(other); + have_latency = false; + } else { + if (spa_latency_parse(latency, &info) < 0 || + info.direction != other) + return -EINVAL; + have_latency = true; + } + emit = spa_latency_info_compare(&info, &port->latency[other]) != 0 || + port->have_latency == have_latency; + + port->latency[other] = info; + port->have_latency = have_latency; + + spa_log_debug(this->log, "%p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, + info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + info.min_quantum, info.max_quantum, + info.min_rate, info.max_rate, + info.min_ns, info.max_ns); + + if (this->monitor_passthrough) { + if (port->is_monitor) + oport = GET_PORT(this, other, port_id-1); + else if (this->monitor && direction == SPA_DIRECTION_INPUT) + oport = GET_PORT(this, other, port_id+1); + else + return 0; + + if (oport != NULL && + spa_latency_info_compare(&info, &oport->latency[other]) != 0) { + oport->latency[other] = info; + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Latency].user++; + emit_port_info(this, oport, false); + } + } else { + spa_latency_info_combine_start(&info, other); + for (i = 0; i < this->dir[direction].n_ports; i++) { + oport = GET_PORT(this, direction, i); + if ((oport->is_monitor) || !oport->have_latency) + continue; + spa_log_debug(this->log, "%p: combine %d", this, i); + spa_latency_info_combine(&info, &oport->latency[other]); + } + spa_latency_info_combine_finish(&info); + + spa_log_debug(this->log, "%p: combined %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, + info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + info.min_quantum, info.max_quantum, + info.min_rate, info.max_rate, + info.min_ns, info.max_ns); + + for (i = 0; i < this->dir[other].n_ports; i++) { + oport = GET_PORT(this, other, i); + if (oport->is_monitor) + continue; + spa_log_debug(this->log, "%p: change %d", this, i); + if (spa_latency_info_compare(&info, &oport->latency[other]) != 0) { + oport->latency[other] = info; + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Latency].user++; + emit_port_info(this, oport, false); + } + } + } + if (emit) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].user++; + emit_port_info(this, port, false); + } + return 0; +} + +static int port_set_tag(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *tag) +{ + struct impl *this = object; + struct port *port, *oport; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + uint32_t i; + + spa_log_debug(this->log, "%p: set tag direction:%d id:%d %p", + this, direction, port_id, tag); + + port = GET_PORT(this, direction, port_id); + if (port->is_monitor && !this->monitor_passthrough) + return 0; + + if (tag != NULL) { + struct spa_tag_info info; + void *state = NULL; + if (spa_tag_parse(tag, &info, &state) < 0 || + info.direction != other) + return -EINVAL; + } + if (spa_tag_compare(tag, this->dir[other].tag) != 0) { + free(this->dir[other].tag); + this->dir[other].tag = tag ? spa_pod_copy(tag) : NULL; + + for (i = 0; i < this->dir[other].n_ports; i++) { + oport = GET_PORT(this, other, i); + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Tag].user++; + emit_port_info(this, oport, false); + } + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Tag].user++; + emit_port_info(this, port, false); + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: %d:%d set format", this, direction, port_id); + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_video_info info = { 0 }; + spa_debug_format(2, NULL, format); + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + if (PORT_IS_DSP(this, direction, port_id)) { + if (info.media_type != SPA_MEDIA_TYPE_video || + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + if ((res = spa_format_video_dsp_parse(format, &info.info.dsp)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32) { + spa_log_error(this->log, "unexpected format %d<->%d", + info.info.dsp.format, SPA_VIDEO_FORMAT_DSP_F32); + return -EINVAL; + } + port->blocks = 1; + port->stride = 16; + } + else if (PORT_IS_CONTROL(this, direction, port_id)) { + if (info.media_type != SPA_MEDIA_TYPE_application || + info.media_subtype != SPA_MEDIA_SUBTYPE_control) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + port->blocks = 1; + port->stride = 1; + } + else { + struct dir *dir = &this->dir[direction]; + struct dir *odir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; + + if (info.media_type != SPA_MEDIA_TYPE_video) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + if ((res = spa_format_video_parse(format, &info)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + port->stride = 2; + port->stride *= info.info.raw.size.width; + port->blocks = 1; + dir->format = info; + dir->have_format = true; + if (odir->have_format) { + if (memcmp(&odir->format, &dir->format, sizeof(dir->format)) == 0) + this->fmt_passthrough = true; + } + this->setup = false; + } + port->format = info; + port->have_format = true; + + spa_log_debug(this->log, "%p: %d %d %d", this, + port_id, port->stride, port->blocks); + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + SPA_FLAG_UPDATE(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS, this->fmt_passthrough); + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set param port %d.%d %u", + this, direction, port_id, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Latency: + return port_set_latency(this, direction, port_id, flags, param); + case SPA_PARAM_Tag: + return port_set_tag(this, direction, port_id, flags, param); + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } +} + +static inline void queue_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d", + this, id, port->id, b->flags); + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); +} + +static inline struct buffer *peek_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_log_trace_fp(this->log, "%p: peek buffer %d/%d on port %d %u", + this, b->id, port->n_buffers, port->id, b->flags); + return b; +} + +static inline void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u", + this, b->id, port->id, b->flags); + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return; + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i, j, maxsize; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", + this, n_buffers, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + maxsize = this->quantum_limit * sizeof(float); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + uint32_t n_datas = buffers[i]->n_datas; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->buf = buffers[i]; + + if (n_datas != port->blocks) { + spa_log_error(this->log, "%p: invalid blocks %d on buffer %d", + this, n_datas, i); + return -EINVAL; + } + if (SPA_FLAG_IS_SET(flags, SPA_NODE_BUFFERS_FLAG_ALLOC)) { + struct port *other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id); + + if (other->n_buffers <= 0) + return -EIO; + *b->buf = *other->buffers[i % other->n_buffers].buf; + b->datas[0] = other->buffers[i % other->n_buffers].datas[0]; + } else { + for (j = 0; j < n_datas; j++) { + void *data = d[j].data; + if (data == NULL && SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_MAPPABLE)) { + data = mmap(NULL, d[j].maxsize, + PROT_READ, MAP_SHARED, d[j].fd, + d[j].mapoffset); + if (data == MAP_FAILED) { + spa_log_error(this->log, "%p: mmap failed %d on buffer %d %d %p: %m", + this, j, i, d[j].type, data); + return -EINVAL; + } + } + if (data != NULL && !SPA_IS_ALIGNED(data, this->max_align)) { + spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned", + this, j, i); + } + b->datas[j] = data; + maxsize = SPA_MAX(maxsize, d[j].maxsize); + } + } + if (direction == SPA_DIRECTION_OUTPUT) + queue_buffer(this, port, i); + } + port->maxsize = maxsize; + port->n_buffers = n_buffers; + + return 0; +} + +struct io_data { + struct port *port; + void *data; + size_t size; +}; + +static int do_set_port_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + const struct io_data *d = user_data; + d->port->io = d->data; + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set io %d on port %d:%d %p", + this, id, direction, port_id, data); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + if (this->data_loop) { + struct io_data d = { .port = port, .data = data, .size = size }; + spa_loop_invoke(this->data_loop, do_set_port_io, 0, NULL, 0, true, &d); + } + else + port->io = data; + break; + case SPA_IO_RateMatch: + this->io_rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_OUT_PORT(this, port_id); + queue_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *in_port, *out_port; + struct spa_io_buffers *input, *output; + struct buffer *dbuf, *sbuf; + struct dir *in, *out; + struct AVFrame *f; + void *datas[8]; + uint32_t sizes[8], strides[8]; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + in = &this->dir[SPA_DIRECTION_INPUT]; + out = &this->dir[SPA_DIRECTION_OUTPUT]; + + out_port = GET_OUT_PORT(this, 0); + if ((output = out_port->io) == NULL) + return -EIO; + + if (output->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* recycle */ + if (output->buffer_id < out_port->n_buffers) { + queue_buffer(this, out_port, output->buffer_id); + output->buffer_id = SPA_ID_INVALID; + } + + in_port = GET_IN_PORT(this, 0); + if ((input = in_port->io) == NULL) + return -EIO; + + if (input->status != SPA_STATUS_HAVE_DATA) + return SPA_STATUS_NEED_DATA; + + if (input->buffer_id >= in_port->n_buffers) { + input->status = -EINVAL; + return -EINVAL; + } + + sbuf = &in_port->buffers[input->buffer_id]; + + if ((dbuf = peek_buffer(this, out_port)) == NULL) { + spa_log_error(this->log, "%p: out of buffers", this); + return -EPIPE; + } + dbuf = &out_port->buffers[input->buffer_id]; + + spa_log_trace(this->log, "%d %p:%p %d %d %d", input->buffer_id, sbuf->buf->datas[0].chunk, + dbuf->buf->datas[0].chunk, sbuf->buf->datas[0].chunk->size, + sbuf->id, dbuf->id); + + /* do decoding */ + if (this->decoder.codec) { + this->decoder.packet->data = sbuf->datas[0]; + this->decoder.packet->size = sbuf->buf->datas[0].chunk->size; + + if ((res = avcodec_send_packet(this->decoder.context, this->decoder.packet)) < 0) { + spa_log_error(this->log, "failed to send frame to codec: %d %p:%d", + res, this->decoder.packet->data, this->decoder.packet->size); + return -EIO; + } + + f = this->decoder.frame; + if (avcodec_receive_frame(this->decoder.context, f) < 0) { + spa_log_error(this->log, "failed to receive frame from codec"); + return -EIO; + } + + in->pix_fmt = f->format; + in->width = f->width; + in->height = f->height; + } else { + f = this->decoder.frame; + f->format = in->pix_fmt; + f->width = in->width; + f->height = in->height; + f->data[0] = sbuf->datas[0]; + f->linesize[0] = sbuf->buf->datas[0].chunk->stride; + } + + /* do conversion */ + if (f->format != out->pix_fmt || + f->width != out->width || + f->height != out->height) { + if (this->convert.context == NULL) { + this->convert.context = sws_getContext( + f->width, f->height, f->format, + out->width, out->height, out->pix_fmt, + 0, NULL, NULL, NULL); + } + sws_scale_frame(this->convert.context, this->convert.frame, f); + f = this->convert.frame; + } + /* do encoding */ + if (this->encoder.codec) { + if ((res = avcodec_send_frame(this->encoder.context, f)) < 0) { + spa_log_error(this->log, "failed to send frame to codec: %d", res); + return -EIO; + } + if (avcodec_receive_packet(this->encoder.context, this->encoder.packet) < 0) { + spa_log_error(this->log, "failed to receive frame from codec"); + return -EIO; + } + datas[0] = this->encoder.packet->data; + sizes[0] = this->encoder.packet->size; + strides[0] = 1; + + } else { + datas[0] = f->data[0]; + strides[0] = f->linesize[0]; + sizes[0] = strides[0] * out->height; + } + + /* write to output */ + for (uint_fast32_t i = 0; i < dbuf->buf->n_datas; ++i) { + if (SPA_FLAG_IS_SET(dbuf->buf->datas[i].flags, SPA_DATA_FLAG_DYNAMIC)) + dbuf->buf->datas[i].data = datas[i]; + else if (datas[i] && dbuf->datas[i] && dbuf->datas[i] != datas[i]) + memcpy(dbuf->datas[i], datas[i], sizes[i]); + + if (dbuf->buf->datas[i].chunk != sbuf->buf->datas[i].chunk) { + dbuf->buf->datas[i].chunk->stride = strides[i]; + dbuf->buf->datas[i].chunk->size = sizes[i]; + } + } + + dequeue_buffer(this, out_port, dbuf); + output->buffer_id = dbuf->id; + output->status = SPA_STATUS_HAVE_DATA; + + input->status = SPA_STATUS_NEED_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static void free_dir(struct dir *dir) +{ + uint32_t i; + for (i = 0; i < MAX_PORTS; i++) + free(dir->ports[i]); + free(dir->tag); +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + free_dir(&this->dir[SPA_DIRECTION_INPUT]); + free_dir(&this->dir[SPA_DIRECTION_OUTPUT]); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + if (this->cpu) { + this->cpu_flags = spa_cpu_get_flags(this->cpu); + this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); + } + props_reset(&this->props); + + this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + this->rate_limit.burst = 1; + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + else if (spa_streq(k, SPA_KEY_PORT_IGNORE_LATENCY)) + this->port_ignore_latency = spa_atob(s); + else if (spa_streq(k, SPA_KEY_PORT_GROUP)) + spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s); + else if (spa_streq(k, "monitor.passthrough")) + this->monitor_passthrough = spa_atob(s); + else + videoconvert_set_param(this, k, s); + } + + this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT; + this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT; + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_OUT_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL); + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_videoconvert_ffmpeg_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_VIDEO_CONVERT".ffmpeg", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/videotestsrc/draw.c b/spa/plugins/videotestsrc/draw.c new file mode 100644 index 0000000..14bdb64 --- /dev/null +++ b/spa/plugins/videotestsrc/draw.c @@ -0,0 +1,268 @@ +/* Spa Video Test Source */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +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..10b1b06 --- /dev/null +++ b/spa/plugins/videotestsrc/plugin.c @@ -0,0 +1,29 @@ +/* Spa Video Test Source plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_videotestsrc_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..aff3f53 --- /dev/null +++ b/spa/plugins/videotestsrc/videotestsrc.c @@ -0,0 +1,972 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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_loop_utils *loop_utils; + + 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 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) +{ + 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), + 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_loop_utils_update_timer(this->loop_utils, this->timer_source, &this->timerspec.it_value, &this->timerspec.it_interval, true); + } +} + +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 (spa_list_is_empty(&port->empty)) { + set_timer(this, false); + spa_log_error(this->log, "%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, "%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(void* data, uint64_t expirations) +{ + struct impl *this = 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, ¶m, &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, "%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, "%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, "%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_loop_utils_destroy_source(this->loop_utils, this->timer_source); + + 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->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + + 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 = spa_loop_utils_add_timer(this->loop_utils, on_output, this); + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + 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 " }, + { 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, + "videotestsrc", + &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..0791894 --- /dev/null +++ b/spa/plugins/volume/plugin.c @@ -0,0 +1,29 @@ +/* Spa Volume plugin */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_volume_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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..164e46a --- /dev/null +++ b/spa/plugins/volume/volume.c @@ -0,0 +1,876 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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, ¶m, &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, "%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, "%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, "%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, "%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] = (int16_t)(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, "%p: out of buffers", this); + return -EPIPE; + } + + sbuf = &in_port->buffers[input->buffer_id]; + + spa_log_trace(this->log, "%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, + "volume", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/dmabuf.h b/spa/plugins/vulkan/dmabuf.h new file mode 100644 index 0000000..2bc68a1 --- /dev/null +++ b/spa/plugins/vulkan/dmabuf.h @@ -0,0 +1,62 @@ +// Copyright (c) 2023 The wlroots contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to 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. +// +// Obtained from https://gitlab.freedesktop.org/wlroots/wlroots/ + +/* SPDX-FileCopyrightText: Copyright © 2023 The wlroots contributors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef RENDER_DMABUF_H +#define RENDER_DMABUF_H + +#include +#include + +#include "spa/support/log.h" + +// Copied from to avoid #ifdef soup +#define DMA_BUF_SYNC_READ (1 << 0) +#define DMA_BUF_SYNC_WRITE (2 << 0) +#define DMA_BUF_SYNC_RW (DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE) + +/** + * Check whether DMA-BUF import/export from/to sync_file is available. + * + * If this function returns true, dmabuf_import_sync_file() is supported. + */ +bool dmabuf_check_sync_file_import_export(struct spa_log *log); + +/** + * Import a sync_file into a DMA-BUF with DMA_BUF_IOCTL_IMPORT_SYNC_FILE. + * + * This can be used to make explicit sync interoperate with implicit sync. + */ +bool dmabuf_import_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags, int sync_file_fd); + +/** + * Export a sync_file from a DMA-BUF with DMA_BUF_IOCTL_EXPORT_SYNC_FILE. + * + * The sync_file FD is returned on success, -1 is returned on error. + * + * This can be used to make explicit sync interoperate with implicit sync. + */ +int dmabuf_export_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags); + +#endif diff --git a/spa/plugins/vulkan/dmabuf_fallback.c b/spa/plugins/vulkan/dmabuf_fallback.c new file mode 100644 index 0000000..5493cef --- /dev/null +++ b/spa/plugins/vulkan/dmabuf_fallback.c @@ -0,0 +1,42 @@ +// Copyright (c) 2023 The wlroots contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to 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. +// +// Obtained from https://gitlab.freedesktop.org/wlroots/wlroots/ + +/* SPDX-FileCopyrightText: Copyright © 2023 The wlroots contributors */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + + +bool dmabuf_check_sync_file_import_export(struct spa_log *log) { + return false; +} + +bool dmabuf_import_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags, int sync_file_fd) { + spa_log_error(log, "DMA-BUF sync_file import IOCTL not available on this system"); + return false; +} + +int dmabuf_export_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags) { + spa_log_error(log, "DMA-BUF sync_file export IOCTL not available on this system"); + return false; +} diff --git a/spa/plugins/vulkan/dmabuf_linux.c b/spa/plugins/vulkan/dmabuf_linux.c new file mode 100644 index 0000000..f3b9c6f --- /dev/null +++ b/spa/plugins/vulkan/dmabuf_linux.c @@ -0,0 +1,127 @@ +// Copyright (c) 2023 The wlroots contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to 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. +// +// Obtained from https://gitlab.freedesktop.org/wlroots/wlroots/ + +/* SPDX-FileCopyrightText: Copyright © 2023 The wlroots contributors */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "dmabuf.h" + +bool dmabuf_check_sync_file_import_export(struct spa_log *log) { + /* Unfortunately there's no better way to check the availability of the + * IOCTL than to check the kernel version. See the discussion at: + * https://lore.kernel.org/dri-devel/20220601161303.64797-1-contact@emersion.fr/ + */ + + struct utsname utsname = {0}; + if (uname(&utsname) != 0) { + spa_log_warn(log, "uname failed"); + return false; + } + + if (strcmp(utsname.sysname, "Linux") != 0) { + return false; + } + + // Trim release suffix if any, e.g. "-arch1-1" + for (size_t i = 0; utsname.release[i] != '\0'; i++) { + char ch = utsname.release[i]; + if ((ch < '0' || ch > '9') && ch != '.') { + utsname.release[i] = '\0'; + break; + } + } + + char *rel = strtok(utsname.release, "."); + int major = atoi(rel); + + int minor = 0; + rel = strtok(NULL, "."); + if (rel != NULL) { + minor = atoi(rel); + } + + int patch = 0; + rel = strtok(NULL, "."); + if (rel != NULL) { + patch = atoi(rel); + } + + return KERNEL_VERSION(major, minor, patch) >= KERNEL_VERSION(5, 20, 0); +} + +// TODO: drop these definitions once widespread + +#if !defined(DMA_BUF_IOCTL_IMPORT_SYNC_FILE) + +struct dma_buf_import_sync_file { + __u32 flags; + __s32 fd; +}; + +#define DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW(DMA_BUF_BASE, 3, struct dma_buf_import_sync_file) + +#endif + +#if !defined(DMA_BUF_IOCTL_EXPORT_SYNC_FILE) + +struct dma_buf_export_sync_file { + __u32 flags; + __s32 fd; +}; + +#define DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR(DMA_BUF_BASE, 2, struct dma_buf_export_sync_file) + +#endif + +bool dmabuf_import_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags, int sync_file_fd) { + struct dma_buf_import_sync_file data = { + .flags = flags, + .fd = sync_file_fd, + }; + if (drmIoctl(dmabuf_fd, DMA_BUF_IOCTL_IMPORT_SYNC_FILE, &data) != 0) { + spa_log_error(log, "drmIoctl(IMPORT_SYNC_FILE) failed with %d (%s)", errno, spa_strerror(-errno)); + return false; + } + return true; +} + +int dmabuf_export_sync_file(struct spa_log *log, int dmabuf_fd, uint32_t flags) { + struct dma_buf_export_sync_file data = { + .flags = flags, + .fd = -1, + }; + if (drmIoctl(dmabuf_fd, DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &data) != 0) { + spa_log_error(log, "drmIoctl(EXPORT_SYNC_FILE) failed with %d (%s)", errno, spa_strerror(-errno)); + return -1; + } + return data.fd; +} diff --git a/spa/plugins/vulkan/meson.build b/spa/plugins/vulkan/meson.build new file mode 100644 index 0000000..17ab1d6 --- /dev/null +++ b/spa/plugins/vulkan/meson.build @@ -0,0 +1,26 @@ +spa_vulkan_sources = [ + 'plugin.c', + 'pixel-formats.c', + 'vulkan-compute-filter.c', + 'vulkan-compute-source.c', + 'vulkan-compute-utils.c', + 'vulkan-blit-filter.c', + 'vulkan-blit-dsp-filter.c', + 'vulkan-blit-utils.c', + 'vulkan-utils.c', + 'utils.c', +] + +drm = dependency('libdrm') + +if cc.has_header('linux/dma-buf.h') and target_machine.system() == 'linux' + spa_vulkan_sources += files('dmabuf_linux.c') +else + spa_vulkan_sources += files('dmabuf_fallback.c') +endif + +spa_vulkan = shared_library('spa-vulkan', + spa_vulkan_sources, + dependencies : [ spa_dep, vulkan_dep, mathlib, drm ], + install : true, + install_dir : spa_plugindir / 'vulkan') diff --git a/spa/plugins/vulkan/pixel-formats.c b/spa/plugins/vulkan/pixel-formats.c new file mode 100644 index 0000000..06144d7 --- /dev/null +++ b/spa/plugins/vulkan/pixel-formats.c @@ -0,0 +1,33 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include "pixel-formats.h" + +#include +#include + +struct pixel_info { + uint32_t format; + uint32_t bpp; +} pixel_infos[] = { + { SPA_VIDEO_FORMAT_RGBA_F32, 16 }, + { SPA_VIDEO_FORMAT_BGRA, 4 }, + { SPA_VIDEO_FORMAT_RGBA, 4 }, + { SPA_VIDEO_FORMAT_BGRx, 4 }, + { SPA_VIDEO_FORMAT_RGBx, 4 }, + { SPA_VIDEO_FORMAT_BGR, 3 }, + { SPA_VIDEO_FORMAT_RGB, 3 }, +}; + +bool get_pixel_format_info(uint32_t format, struct pixel_format_info *info) +{ + struct pixel_info *p; + SPA_FOR_EACH_ELEMENT(pixel_infos, p) { + if (p->format != format) + continue; + info->bpp = p->bpp; + return true; + } + return false; +} diff --git a/spa/plugins/vulkan/pixel-formats.h b/spa/plugins/vulkan/pixel-formats.h new file mode 100644 index 0000000..9497331 --- /dev/null +++ b/spa/plugins/vulkan/pixel-formats.h @@ -0,0 +1,12 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +struct pixel_format_info { + uint32_t bpp; // bytes per pixel +}; + +bool get_pixel_format_info(uint32_t format, struct pixel_format_info *info); diff --git a/spa/plugins/vulkan/plugin.c b/spa/plugins/vulkan/plugin.c new file mode 100644 index 0000000..89a36e2 --- /dev/null +++ b/spa/plugins/vulkan/plugin.c @@ -0,0 +1,41 @@ +/* Spa vulkan plugin */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_vulkan_compute_filter_factory; +extern const struct spa_handle_factory spa_vulkan_compute_source_factory; +extern const struct spa_handle_factory spa_vulkan_blit_filter_factory; +extern const struct spa_handle_factory spa_vulkan_blit_dsp_filter_factory; + +SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED; + +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; + case 2: + *factory = &spa_vulkan_blit_filter_factory; + break; + case 3: + *factory = &spa_vulkan_blit_dsp_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 0000000000000000000000000000000000000000..5bfd24593dd343b062b9282e9facfacd2ad25bb2 GIT binary patch literal 4884 zcmZ9Nhj&y}5QiTmp#)H{cNegO*uaKD05zh~MC^54l1;L>?8fXS*aflo-h1!8_g?=1 z{}PYo9MAFl-oD{HbNBqt%>3@$nYnZCecLv9{?sIylyoMulHZcnn3GI~Ns<}KlvH;2 zoZhpj(HvN`c*((fY?ZX7j{0nkPY1FctS?n+PAmqq;pOIlLI(ZY_)SKBi_xA;Pr7@G zE7q(mHp(NV(NeQqYz&tM%A>{l+I8jrW<#G&eY$&lyL*l=_K%Dd8_mnBh-0e@_yrkisH~@i)Ms1ds(P)_EY&PO?T(zI*>c;$*$;B0YweXw9~9fe^EL-yCJX?!nGWz}%0Rx4Mn%^Dv@)&~b0 zW%hp}O!RX;ydyaKDd#x{XFuhpwqr*t(e+z}v@UDhnQon*>8{NWdm!hU3$h7)twq~=?XdPD(!8$G zUW@b$w9Rn|(tHzhguN@ft}?Hm`TQ<8;d%mP3i9$?g`}e_w8wi|6b_&&j;bZH@bX1%eN1@T&&p--8K7y zShGL6e5^SDU9OLc3&@4QIP&(`TIM0;)qQimGjsS3jsU)uCBPis%_D*QNLJTT8Rr_m zcT0gWJsa29t7CwC#61?>xZz{mWk5dSF3-5l#5G12Fphj5+;rp$VE!&({hqU&d3>M8 z0lBE(ILBvv)O!N9_2zxLvE{!c&PwEoz;*I3vnRRLK>knio{V(-!<_s{K+b!p9`*E~ zTTkrUdQJiIQO~L9)|2nH`p{h`|2H`zm+}v8uJxS;tgi#;ZynZpI?`dS`m6zR z)~fz5Ul(;b&(A)e1FYK~`!=+X;#4Lh&jnin-;i^z@$H5CfNuPL=8fyToclNu#P`7(9W%fY)HDq_Gt)bf zvw-{F8X4b>GP--w)_+~jcFn_?Z7ud`5Ew_hfE)szrTcPzIOF6Ulfcc{U9Ow^7|&P0(yp-`8=Xm&iSU?v-@+OQNs=B6MJ_foP6}|W^}omfW5mF z>9BXs-2(0b##WE>y#qaezIS4qE6(>WbbaLGeEl|h_IHEZfj+ShIrm|HbD1;taWDGB zecT5pANzO!UG9G1J|03k+=p`yg13ON)y?aBcof8*9z)OX>2YlHI`18R0=UNW(|!`@ zBOm*I3f(x)#lBC=&h}|I=N+-PXVBvgT9fwEAZmIRJzvvv*!ey`kFAgV#C$KnIq$oT zd@rJ#&--jXZT%wOOX&H0FJqf8dhiOmJ&13_tLSpVJdWtkYv``mC;Ia`x;dPiSnnHf z&dW!=Z=zeT@5p+!_4BTH#-69=8uh=8p0EEM?0mo8#nwkY^7rz!EMUI}oEOeH^1XW> z+gkiy#F>78?mCC{=_42Qe~9iH`yFdOLU)a^U89d&toax{_8e`yb z{texl*&C4dNf|jlMYk{R#okRpeg@?AnT`A$%mdaQK3`;fe6PO5_S+%9DZA%ip&QG& znM5}B*TDU@1LyC^e7`~e8u;ywyx*cv%=;ajeB}Ke-MsSg4fp}wc+U9^_zCIo8{piJ fK+ayM+eiECx5&Q5oYMVHMg9!*(RXHM$A9&Id= 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 0000000000000000000000000000000000000000..5da17558e3a51b30bcf0383667d69a0b9de67ebb GIT binary patch literal 9980 zcmZ9R2b@*K6~-^@E=52^5yV(_!2+mQz=8!~K~@w6?0tQ1fz@RnZUJiqd&ioHy~o~@ zsEH-Un1;rf9%Ev9F)`I1P5%G;=B(Fu+4Gz4`_7p&XXehG``)8w;P}B=HXvIo8<9Oe zAj_|j*+7&m8c2<+e8M6*Cb+onEl2S*Pj{UH8(bYy` z^~sE{;om^o=OMM((5z`*^UMPqn|s=pmby#5ZOuK4ORa6)&E?}yXlv>1F=j1en&!`M znzwgz%hILIJ-w%Nwl#NlbhS0Nc61Zfv8>IwA%(b(u9nWe)?z>%5ycwovf|Q+BVmj5*d&NtTr{@i5xfgoo3%Bs_w)FX6Rmm*u!F8%4Xg z)U$ZXg!UbFPyY33U9dX-ZA6>To$;p<;22}ky4!ldgR;$X+{N1T7nsLvqE1o9Zz`s! zx@>T^CD{8hcS&hcTb^@S`;K|ux@>cN?cJqCv&x<2Zm{ET(XX)#xmKG^r0qDkV+qTf z$81eItK8MoTk7)GuF>Pq>q{VF=ix&4f#-MCl z_=3Kk#W4;Py*8Ud+cBq-(0ZrI4{YlxclPyml)Lhnz18+F_w`V)eVX7c-d5T=_Q!s- zi#nT|I?KnGI_GvRE4P&FJ9l>OuggxPE%&h#D$A1R=tOgCF3DqRGuuyiUFJG)kNP@$ z^OdGgO?DdENqKxtc8~s&?wo9vx2&UZD||snr%V_o&*Cg61Ao^Eq##j@Zjh^mI=R!#2)+wfTM+ z2lmqM{%FWqg+Ioi#`|oq&lBNz4diqT@o3N>+InmZ{W+X-uD>2`U20kbY-qyj)n>fs zZ{%ozua~&G`A5?_o@-$I7+T}ijo&bF^#<0`dFqd`%+UZhhxMDo`B&iv4$kJO9Wx9qj#-8`p?=na{IG&Gk2* z+;dHB8%9x|gW0EOuGv0l^_Z9UWiG8+?45nV12A=S%lAu~wHP}O^RkxxX^mG8|H7n= zVn#=R$Fdqe=hkOz@p8^;=JKraGS;!>J{w-v>Urfj_K}}V>l({rovs6C*^0e_-h4=UWpu+cz_z!cwrk@YxCQg|$rN93$_;ssSPxroV z0(LFESN6Bv0&Pa(?u!X-s3Xo=o<04||rX zY3|FNIiCj7)?!}XgZEJCXx($;Xq&KjRy~hStm2&oZv2wOXOhLU!wSs#EFR;?^&@kX1r&J@vhG zNK7s2H_uT?k9v=WTW>Mu7`XZ~LvcQYroW=#EE#x2o$eSlB>I84obD2{rL z2U}0f+j?3s^{A&6T)lpLw9UVFL_O_r`>DUr7%`UZ+w64fTZCDk&w=sBVbt(IR zEXLHVRs2ylAjfLnzXsYa%(`9UC5cbtt_%e8H6;Ju;WMPqKBH-uVrusBd6VlqiMAK> zJs_V;y9{$b%%^tW3CokF?lk~Aomk%wr(pVHoKwN(Z^3U~{c6r*SIqNaFnk@%Ybdr0 zH4UL%tAK|U@bCg2QNU}1U8i+vJzLCi8fJd$u{|9#Z}2m~=5rtG|14&Vx@%*9wTM3x z?3lqJf7e*qGqwf*mjNp9hZo#_EsQ z^TDoDb`2y}TZ-b{!TmH&n&k1XsHei@I+Ho7?{ShclY{=oZZNaE**P zH_hix@GaOKSj651Hr9Qu-+8JP=eZHwYeUR=j-efkInU3~`n)*zdoafj{y-IfqQHH= zp046ACGHw{&fJSRj%P;$?R`m8w`R{M=iwUPpR`6=<9zM`H1f~{c$rr$iSuX@boIj}W8i$(6QfuF_D!&Q zm~-)7e+$$1A{Kk<+u*28|79umJ79g!V3GH`iK}UmcMVvdeVm(nMNRBJc^Pv~*6=+{ ze~k0}q`g8L^Zo(Y{V<%9-Fg2Iv&J2;S23SGW8yQ(kCJ9wi#KM z&Df|_E#iL$F4p>UxSHcft-kJLtoVJTd?~n_QdbN-XC@6>Kgt&Y5F49AHe3*7jyn2xVT1tf~y%DYor$O ze+C!V=r3@!;u`%GUR0xSHcfeg6d) z*XSd-nlZ7DJ_g4c>5nz~U(#Kp53mn0V`Gi%9LRe9&K!_(Yj#Q$her-NU(M2iyGDg7uRTgxZ|tG8jVVtzR0x!*nIk8&ZEJ_ zH5vm~Gd9*pE#fx>8y`8x!quFMp9SQ_&jOnf>$Nc!pS3qB;7!4PE~uxC&(Z$o?Kv&? z_XU6V4n73k-@b#7g!}t=jjpfEx~I3-r{#Sc|B&X zd`)Si? ztU28$>XCCASS`Mv><;EH|9+*<->Kp=%O1&RKI6>iK2wi;dx72S`rKpk@XZ9f_w~&H z%fmMd?A-LZ_vGQ*8*GjG+=ue;HG!=`pLWme;@EVjz{Zw=C!|Z#`{bd6S?M! zvAEa!g1r}E@ttQsbYt~L9<|6bPmIN9^8LYT8h`n{ir51Xim~(IY8roeY<%BbfM70T zo=DHbLa@4L=)276Ah4IuhQ0$aHJ=UfO!|3#91M2cd0=btv&tcG^`}zqL&0jk|9w6q zk8S+Uav0cg>M`RT!#a+j^|B6Q4#(82LmcxzQk=NY`%&QHGkY{#Z6P&T<1t|Sc!hRs z(v(AMPMXif0(Qe_+T+mdZ@hkUd;dzb`r<4&9=s9p>QQG4cw;P{_g1i)vF6aH<~qeZ zZRO)=owu#|oM#)@d9Gky@^-M=RIvHwbj_LbH9Pc0JyCvo|9*ggy znjWThaQyQ9FSgJ~P3~1@8Ohj4FO%6~Cy8ud3o#Rq<=9_+3@}o+^HC;?BQ; zdQOBpk64dR_%wJtb4%cAnwO2wZv$-?#Qw&(*3MlmaxVp|c`i2)e-c=&@*b#Y-SCOY z+!GDN^}yAQH?M1`<})_|e=pb?i_csintH@6OEC+HvmW>Say0e$K6*0Pe(Lt~c|HZ~ zc*WeO!qwyZ$!TCU`$xai!O_o}<;KO>p9N1Of8;n5?&mJ`&{l%gS77m(`7AJh`EOGC zBj4HJV!m_W=2MS+=YrLX`Obs$mw%VnA7}LWV8?32XH2|HF97TFo;fe~ubSAs>*o_| za?f83R||d#*mysq>c13hjC#BaF9Y+JzgzX2*JnUI@>~v9i+Af4VE*!VtG?ZGU*-3x zE0fQ6t8wP@8B&jYSA*4pUjr`Y{~VmZJioE}qu$RapZSe5zt5z4?1k0f*b9!~-n$M> zJ;uBqYz?u_H-OcQi##`i%~RaNH^J2-&&^=Rk36@4)r?!2*8Ntn`SnG=+rarcrWlf`3Fa@qU;3TDXOMd2{sP$h9yQ$!R`dODyl0YH#NP)thrWAh<^I2b zd**(y-vjKU<}*_2~ZqSS{XJ4}#UaLVGA_%Aq}+G|&2Yw>^Spf8+JL9**g`uP^>T F;QtMgi=qGk literal 0 HcmV?d00001 diff --git a/spa/plugins/vulkan/utils.c b/spa/plugins/vulkan/utils.c new file mode 100644 index 0000000..2ccd2a7 --- /dev/null +++ b/spa/plugins/vulkan/utils.c @@ -0,0 +1,91 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include "utils.h" + +#include +#include +#include + +// This function enumerates the available formats in vulkan_state::formats, announcing all formats capable to support DmaBufs +// first and then falling back to those supported with SHM buffers. +bool find_EnumFormatInfo(struct vulkan_format_infos *fmtInfos, uint32_t index, uint32_t caps, uint32_t *fmt_idx, bool *has_modifier) +{ + int64_t fmtIterator = 0; + int64_t maxIterator = 0; + if (caps & VULKAN_BUFFER_TYPE_CAP_SHM) + maxIterator += fmtInfos->formatCount; + if (caps & VULKAN_BUFFER_TYPE_CAP_DMABUF) + maxIterator += fmtInfos->formatCount; + // Count available formats until index underflows, while fmtIterator indexes the current format. + // Iterate twice over formats first time with modifiers, second time without if both caps are supported. + while (index < (uint32_t)-1 && fmtIterator < maxIterator) { + const struct vulkan_format_info *f_info = &fmtInfos->infos[fmtIterator%fmtInfos->formatCount]; + if (caps & VULKAN_BUFFER_TYPE_CAP_DMABUF && fmtIterator < fmtInfos->formatCount) { + // First round, check for modifiers + if (f_info->modifierCount > 0) { + index--; + } + } else if (caps & VULKAN_BUFFER_TYPE_CAP_SHM) { + // Second round, every format should be supported. + index--; + } + fmtIterator++; + } + + if (index != (uint32_t)-1) { + // No more formats available + return false; + } + // Undo end of loop increment + fmtIterator--; + *fmt_idx = fmtIterator%fmtInfos->formatCount; + // Loop finished in first round + *has_modifier = caps & VULKAN_BUFFER_TYPE_CAP_DMABUF && fmtIterator < fmtInfos->formatCount; + return true; +} + +struct spa_pod *build_dsp_EnumFormat(const struct vulkan_format_info *fmt, bool with_modifiers, struct spa_pod_builder *builder) +{ + struct spa_pod_frame f[2]; + uint32_t i, c; + + spa_pod_builder_push_object(builder, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add(builder, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), 0); + spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_format, SPA_POD_Id(fmt->spa_format), 0); + if (with_modifiers && fmt->modifierCount > 0) { + spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); + spa_pod_builder_push_choice(builder, &f[1], SPA_CHOICE_Enum, 0); + for (i = 0, c = 0; i < fmt->modifierCount; i++) { + spa_pod_builder_long(builder, fmt->infos[i].props.drmFormatModifier); + if (c++ == 0) + spa_pod_builder_long(builder, fmt->infos[i].props.drmFormatModifier); + } + spa_pod_builder_pop(builder, &f[1]); + } + return spa_pod_builder_pop(builder, &f[0]); +} + +struct spa_pod *build_raw_EnumFormat(const struct vulkan_format_info *fmt, bool with_modifiers, struct spa_pod_builder *builder) +{ + struct spa_pod_frame f[2]; + uint32_t i, c; + + spa_pod_builder_push_object(builder, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add(builder, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); + spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_format, SPA_POD_Id(fmt->spa_format), 0); + if (with_modifiers && fmt->modifierCount > 0) { + spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); + spa_pod_builder_push_choice(builder, &f[1], SPA_CHOICE_Enum, 0); + for (i = 0, c = 0; i < fmt->modifierCount; i++) { + spa_pod_builder_long(builder, fmt->infos[i].props.drmFormatModifier); + if (c++ == 0) + spa_pod_builder_long(builder, fmt->infos[i].props.drmFormatModifier); + } + spa_pod_builder_pop(builder, &f[1]); + } + return spa_pod_builder_pop(builder, &f[0]); +} diff --git a/spa/plugins/vulkan/utils.h b/spa/plugins/vulkan/utils.h new file mode 100644 index 0000000..9716804 --- /dev/null +++ b/spa/plugins/vulkan/utils.h @@ -0,0 +1,11 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include "vulkan-types.h" +#include "spa/pod/builder.h" + +bool find_EnumFormatInfo(struct vulkan_format_infos *fmtInfos, uint32_t index, uint32_t caps, uint32_t *fmt_idx, bool *has_modifier); + +struct spa_pod *build_dsp_EnumFormat(const struct vulkan_format_info *fmt, bool with_modifiers, struct spa_pod_builder *builder); +struct spa_pod *build_raw_EnumFormat(const struct vulkan_format_info *fmt, bool with_modifiers, struct spa_pod_builder *builder); diff --git a/spa/plugins/vulkan/vulkan-blit-dsp-filter.c b/spa/plugins/vulkan/vulkan-blit-dsp-filter.c new file mode 100644 index 0000000..3ba2ed7 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-blit-dsp-filter.c @@ -0,0 +1,954 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pixel-formats.h" +#include "vulkan-blit-utils.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.vulkan.blit-dsp-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; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffer 4 +#define N_PORT_PARAMS 5 + struct spa_param_info params[N_PORT_PARAMS]; + + 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; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + // Synchronization between main and data thread + atomic_bool started; + pthread_rwlock_t renderlock; + + struct vulkan_blit_state state; + struct vulkan_pass pass; + struct port port[2]; +}; + +#define CHECK_PORT(this,d,p) ((p) < 1) + +static int lock_init(struct impl *this) +{ + return pthread_rwlock_init(&this->renderlock, NULL); +} + +static void lock_destroy(struct impl *this) +{ + pthread_rwlock_destroy(&this->renderlock); +} + +static int lock_renderer(struct impl *this) +{ + spa_log_info(this->log, "Lock renderer"); + return pthread_rwlock_wrlock(&this->renderlock); +} + +static int unlock_renderer(struct impl *this) +{ + spa_log_info(this->log, "Unlock renderer"); + return pthread_rwlock_unlock(&this->renderlock); +} + +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, "%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_blit_start(&this->state); + // The main thread needs to lock the renderer before changing its state + break; + + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + lock_renderer(this); + spa_vulkan_blit_stop(&this->state); + this->started = false; + unlock_renderer(this); + // Locking the renderer from the renderer is no longer required + 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 bool port_has_fixated_format(struct port *p) +{ + if (!p->have_format) + return false; + return p->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && p->current_format.info.dsp.flags ^ SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; +} + +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) +{ + struct impl *this = object; + + if (port_has_fixated_format(&this->port[port_id])) { + if (index == 0) { + spa_log_info(this->log, "enum_formats fixated format idx: %d, format %d, has_modifier 1", + index, this->port[port_id].current_format.info.dsp.format); + *param = spa_format_video_dsp_build(builder, SPA_PARAM_EnumFormat, &this->port[port_id].current_format.info.dsp); + return 1; + } + return spa_vulkan_blit_enumerate_formats(&this->state, index-1, spa_vulkan_blit_get_buffer_caps(&this->state, direction), param, builder); + } else { + return spa_vulkan_blit_enumerate_formats(&this->state, index, spa_vulkan_blit_get_buffer_caps(&this->state, direction), param, builder); + } +} + +static int port_get_buffer_props(struct impl *this, struct port *port, + uint32_t *blocks, uint32_t *size, uint32_t *stride, bool *is_dmabuf) +{ + if (this->position == NULL) + return -EIO; + + spa_log_debug(this->log, "%p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + if (port->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + *is_dmabuf = true; + + struct vulkan_modifier_info *mod_info = spa_vulkan_blit_get_modifier_info(&this->state, + &port->current_format); + *blocks = mod_info->props.drmFormatModifierPlaneCount; + } else { + *is_dmabuf = false; + *blocks = 1; + *size = this->position->video.stride * this->position->video.size.height; + *stride = this->position->video.stride; + } + 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 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, ¶m, &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 (result.index > 0) + return 0; + + int ret; + uint32_t blocks, size, stride; + bool is_dmabuf; + + if ((ret = port_get_buffer_props(this, port, &blocks, &size, &stride, &is_dmabuf)) < 0) + return ret; + + if (is_dmabuf) { + 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(blocks), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<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", this); + lock_renderer(this); + spa_vulkan_blit_use_buffers(&this->state, &this->state.streams[port->stream_id], 0, &port->current_format, 0, NULL); + spa_vulkan_blit_clear_pass(&this->state, &this->pass); + unlock_renderer(this); + port->n_buffers = 0; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + } + return 0; +} + +static int port_set_dsp_format(struct impl *this, struct port *port, + uint32_t flags, struct spa_video_info *info, + bool *has_modifier, bool *modifier_fixed, + const struct spa_pod *format) +{ + 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.streams[port->stream_id].dim.width = this->position->video.size.width; + this->state.streams[port->stream_id].dim.height = this->position->video.size.height; + this->state.streams[port->stream_id].bpp = 16; + *has_modifier = SPA_FLAG_IS_SET(info->info.dsp.flags, SPA_VIDEO_FLAG_MODIFIER); + + // fixate modifier + if (port->direction == SPA_DIRECTION_OUTPUT + && info->info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && info->info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) + return -EINVAL; + + const struct spa_pod *mod_pod = &mod_prop->value; + uint32_t modifierCount = SPA_POD_CHOICE_N_VALUES(mod_pod); + uint64_t *modifiers = SPA_POD_CHOICE_VALUES(mod_pod); + if (modifierCount <= 1) + return -EINVAL; + // SPA_POD_CHOICE carries the "preferred" value at position 0 + modifierCount -= 1; + modifiers++; + uint64_t fixed_modifier; + if (spa_vulkan_blit_fixate_modifier(&this->state, &this->state.streams[port->stream_id], info, modifierCount, modifiers, &fixed_modifier) != 0) + return -EINVAL; + + spa_log_info(this->log, "modifier fixated %"PRIu64, fixed_modifier); + + info->info.dsp.modifier = fixed_modifier; + info->info.dsp.flags &= ~SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + *modifier_fixed = true; + } + + 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) + return -EINVAL; + + bool has_modifier = false; + bool modifier_fixed = false; + if ((res = port_set_dsp_format(this, port, flags, &info, &has_modifier, &modifier_fixed, format)) < 0) + return res; + + if (has_modifier) { + SPA_FLAG_SET(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + } else { + SPA_FLAG_CLEAR(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + + port->current_format = info; + port->have_format = true; + + if (modifier_fixed) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_EnumFormat].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + return 0; + } + } + + 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_Buffer] = 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_Buffer] = 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; + + lock_renderer(this); + 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_blit_use_buffers(&this->state, &this->state.streams[port->stream_id], flags, &port->current_format, n_buffers, buffers); + port->n_buffers = n_buffers; + if (n_buffers > 0) + spa_vulkan_blit_init_pass(&this->state, &this->pass); + unlock_renderer(this); + + 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); + spa_return_val_if_fail(this->started, -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, "%p: out of buffers", this); + return -EPIPE; + } + + if (pthread_rwlock_tryrdlock(&this->renderlock) < 0) { + return -EBUSY; + } + + b = &inport->buffers[inio->buffer_id]; + this->pass.in_stream_id = SPA_DIRECTION_INPUT; + this->pass.in_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->pass.out_stream_id = SPA_DIRECTION_OUTPUT; + this->pass.out_buffer_id = b->id; + + spa_log_debug(this->log, "filter into %d", b->id); + + spa_vulkan_blit_process(&this->state, &this->pass); + spa_vulkan_blit_reset_pass(&this->state, &this->pass); + + 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; + + pthread_rwlock_unlock(&this->renderlock); + + return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_vulkan_blit_unprepare(&this->state); + spa_vulkan_blit_deinit(&this->state); + lock_destroy(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; + 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; + + 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[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; + + lock_init(this); + + port = &this->port[SPA_DIRECTION_INPUT]; + port->stream_id = SPA_DIRECTION_INPUT; + 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; + 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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_vulkan_blit_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[SPA_DIRECTION_OUTPUT]; + port->stream_id = SPA_DIRECTION_OUTPUT; + 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[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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + spa_vulkan_blit_init_stream(&this->state, &this->state.streams[port->stream_id], + SPA_DIRECTION_OUTPUT, NULL); + + this->state.n_streams = 2; + spa_vulkan_blit_init(&this->state); + spa_vulkan_blit_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, "Columbarius " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Convert video frames using a vulkan blit" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_vulkan_blit_dsp_filter_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_VULKAN_BLIT_DSP_FILTER, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/vulkan-blit-filter.c b/spa/plugins/vulkan/vulkan-blit-filter.c new file mode 100644 index 0000000..48db271 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-blit-filter.c @@ -0,0 +1,1056 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pixel-formats.h" +#include "vulkan-blit-utils.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.vulkan.blit-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; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffer 4 +#define N_PORT_PARAMS 5 + struct spa_param_info params[N_PORT_PARAMS]; + + 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; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + // Synchronization between main and data thread + atomic_bool started; + pthread_rwlock_t renderlock; + + struct vulkan_blit_state state; + struct vulkan_pass pass; + struct port port[2]; +}; + +#define CHECK_PORT(this,d,p) ((p) < 1) + +static int lock_init(struct impl *this) +{ + return pthread_rwlock_init(&this->renderlock, NULL); +} + +static void lock_destroy(struct impl *this) +{ + pthread_rwlock_destroy(&this->renderlock); +} + +static int lock_renderer(struct impl *this) +{ + spa_log_info(this->log, "Lock renderer"); + return pthread_rwlock_wrlock(&this->renderlock); +} + +static int unlock_renderer(struct impl *this) +{ + spa_log_info(this->log, "Unlock renderer"); + return pthread_rwlock_unlock(&this->renderlock); +} + +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, "%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_blit_start(&this->state); + // The main thread needs to lock the renderer before changing its state + break; + + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + lock_renderer(this); + spa_vulkan_blit_stop(&this->state); + this->started = false; + unlock_renderer(this); + // Locking the renderer from the renderer is no longer required + 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 bool port_has_fixated_format(struct port *p) +{ + if (!p->have_format) + return false; + switch (p->current_format.media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: + return p->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && p->current_format.info.dsp.flags ^ SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + case SPA_MEDIA_SUBTYPE_raw: + return p->current_format.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER + && p->current_format.info.raw.flags ^ SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + } + return false; +} + +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) +{ + struct impl *this = object; + + if (port_has_fixated_format(&this->port[port_id])) { + if (index == 0) { + if (this->port[port_id].current_format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) { + spa_log_info(this->log, "enum_formats fixated format idx: %d, format %d, has_modifier 1", + index, this->port[port_id].current_format.info.dsp.format); + *param = spa_format_video_dsp_build(builder, SPA_PARAM_EnumFormat, &this->port[port_id].current_format.info.dsp); + } else { + spa_log_info(this->log, "enum_formats fixated format idx: %d, format %d, has_modifier 1", + index, this->port[port_id].current_format.info.raw.format); + *param = spa_format_video_raw_build(builder, SPA_PARAM_EnumFormat, &this->port[port_id].current_format.info.raw); + } + return 1; + } + return spa_vulkan_blit_enumerate_formats(&this->state, index-1, spa_vulkan_blit_get_buffer_caps(&this->state, direction), param, builder); + } else { + return spa_vulkan_blit_enumerate_formats(&this->state, index, spa_vulkan_blit_get_buffer_caps(&this->state, direction), param, builder); + } +} + +static int port_get_buffer_props(struct impl *this, struct port *port, + uint32_t *blocks, uint32_t *size, uint32_t *stride, bool *is_dmabuf) +{ + if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) { + if (this->position == NULL) + return -EIO; + + spa_log_debug(this->log, "%p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + if (port->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + *is_dmabuf = true; + + struct vulkan_modifier_info *mod_info = spa_vulkan_blit_get_modifier_info(&this->state, + &port->current_format); + *blocks = mod_info->props.drmFormatModifierPlaneCount; + } else { + *is_dmabuf = false; + *blocks = 1; + *size = this->position->video.stride * this->position->video.size.height; + *stride = this->position->video.stride; + } + return 0; + } else if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + spa_log_debug(this->log, "%p: %dx%d", this, + port->current_format.info.raw.size.width, + port->current_format.info.raw.size.height); + + if (port->current_format.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER) { + *is_dmabuf = true; + + struct vulkan_modifier_info *mod_info = spa_vulkan_blit_get_modifier_info(&this->state, + &port->current_format); + *blocks = mod_info->props.drmFormatModifierPlaneCount; + } else { + struct pixel_format_info pInfo = {0}; + if (!get_pixel_format_info(port->current_format.info.raw.format, &pInfo)) + return -EINVAL; + uint32_t buffer_stride = pInfo.bpp * port->current_format.info.raw.size.width; + *is_dmabuf = false; + *blocks = 1; + *size = buffer_stride * port->current_format.info.raw.size.height; + *stride = buffer_stride; + } + return 0; + } else { + return -EINVAL; + } +} + +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, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) { + param = spa_format_video_dsp_build(&b, id, &port->current_format.info.dsp); + } else if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + param = spa_format_video_raw_build(&b, id, &port->current_format.info.raw); + } else { + return -EINVAL; + } + break; + + case SPA_PARAM_Buffers: + { + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + int ret; + uint32_t blocks, size, stride; + bool is_dmabuf; + + if ((ret = port_get_buffer_props(this, port, &blocks, &size, &stride, &is_dmabuf)) < 0) + return ret; + + if (is_dmabuf) { + 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(blocks), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<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", this); + lock_renderer(this); + spa_vulkan_blit_use_buffers(&this->state, &this->state.streams[port->stream_id], 0, &port->current_format, 0, NULL); + spa_vulkan_blit_clear_pass(&this->state, &this->pass); + unlock_renderer(this); + port->n_buffers = 0; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + } + return 0; +} + +static int port_set_dsp_format(struct impl *this, struct port *port, + uint32_t flags, struct spa_video_info *info, + bool *has_modifier, bool *modifier_fixed, + const struct spa_pod *format) +{ + 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.streams[port->stream_id].dim.width = this->position->video.size.width; + this->state.streams[port->stream_id].dim.height = this->position->video.size.height; + this->state.streams[port->stream_id].bpp = 16; + *has_modifier = SPA_FLAG_IS_SET(info->info.dsp.flags, SPA_VIDEO_FLAG_MODIFIER); + + // fixate modifier + if (port->direction == SPA_DIRECTION_OUTPUT + && info->info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && info->info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) + return -EINVAL; + + const struct spa_pod *mod_pod = &mod_prop->value; + uint32_t modifierCount = SPA_POD_CHOICE_N_VALUES(mod_pod); + uint64_t *modifiers = SPA_POD_CHOICE_VALUES(mod_pod); + if (modifierCount <= 1) + return -EINVAL; + // SPA_POD_CHOICE carries the "preferred" value at position 0 + modifierCount -= 1; + modifiers++; + uint64_t fixed_modifier; + if (spa_vulkan_blit_fixate_modifier(&this->state, &this->state.streams[port->stream_id], info, modifierCount, modifiers, &fixed_modifier) != 0) + return -EINVAL; + + spa_log_info(this->log, "modifier fixated %"PRIu64, fixed_modifier); + + info->info.dsp.modifier = fixed_modifier; + info->info.dsp.flags &= ~SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + *modifier_fixed = true; + } + + return 0; +} + +static int port_set_raw_format(struct impl *this, struct port *port, + uint32_t flags, struct spa_video_info *info, + bool *has_modifier, bool *modifier_fixed, + const struct spa_pod *format) +{ + if (spa_format_video_raw_parse(format, &info->info.raw) < 0) + return -EINVAL; + + struct pixel_format_info pInfo; + if (!get_pixel_format_info(info->info.raw.format, &pInfo)) + return -EINVAL; + this->state.streams[port->stream_id].dim = info->info.raw.size; + this->state.streams[port->stream_id].bpp = pInfo.bpp; + *has_modifier = SPA_FLAG_IS_SET(info->info.raw.flags, SPA_VIDEO_FLAG_MODIFIER); + + // fixate modifier + if (port->direction == SPA_DIRECTION_OUTPUT + && info->info.raw.flags & SPA_VIDEO_FLAG_MODIFIER + && info->info.raw.flags & SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) + return -EINVAL; + + const struct spa_pod *mod_pod = &mod_prop->value; + uint32_t modifierCount = SPA_POD_CHOICE_N_VALUES(mod_pod); + uint64_t *modifiers = SPA_POD_CHOICE_VALUES(mod_pod); + if (modifierCount <= 1) + return -EINVAL; + // SPA_POD_CHOICE carries the "preferred" value at position 0 + modifierCount -= 1; + modifiers++; + uint64_t fixed_modifier; + if (spa_vulkan_blit_fixate_modifier(&this->state, &this->state.streams[port->stream_id], info, modifierCount, modifiers, &fixed_modifier) != 0) + return -EINVAL; + + spa_log_info(this->log, "modifier fixated %"PRIu64, fixed_modifier); + + info->info.raw.modifier = fixed_modifier; + info->info.raw.flags &= ~SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + *modifier_fixed = true; + } + + 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) + return -EINVAL; + + bool has_modifier = false; + bool modifier_fixed = false; + if (info.media_subtype == SPA_MEDIA_SUBTYPE_dsp) { + if ((res = port_set_dsp_format(this, port, flags, &info, &has_modifier, &modifier_fixed, format)) < 0) + return res; + } else if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + if ((res = port_set_raw_format(this, port, flags, &info, &has_modifier, &modifier_fixed, format)) < 0) + return res; + } else { + return -EINVAL; + } + + if (has_modifier) { + SPA_FLAG_SET(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + } else { + SPA_FLAG_CLEAR(port->info.flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + + port->current_format = info; + port->have_format = true; + + if (modifier_fixed) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_EnumFormat].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + return 0; + } + } + + 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_Buffer] = 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_Buffer] = 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; + + lock_renderer(this); + 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_blit_use_buffers(&this->state, &this->state.streams[port->stream_id], flags, &port->current_format, n_buffers, buffers); + port->n_buffers = n_buffers; + if (n_buffers > 0) + spa_vulkan_blit_init_pass(&this->state, &this->pass); + unlock_renderer(this); + + 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); + spa_return_val_if_fail(this->started, -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, "%p: out of buffers", this); + return -EPIPE; + } + + if (pthread_rwlock_tryrdlock(&this->renderlock) < 0) { + return -EBUSY; + } + + b = &inport->buffers[inio->buffer_id]; + this->pass.in_stream_id = SPA_DIRECTION_INPUT; + this->pass.in_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->pass.out_stream_id = SPA_DIRECTION_OUTPUT; + this->pass.out_buffer_id = b->id; + + spa_log_debug(this->log, "filter into %d", b->id); + + spa_vulkan_blit_process(&this->state, &this->pass); + spa_vulkan_blit_reset_pass(&this->state, &this->pass); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = b->outbuf->datas[0].maxsize; + if (outport->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + b->outbuf->datas[0].chunk->stride = + this->state.streams[outport->stream_id].bpp * outport->current_format.info.raw.size.width; + } else { + b->outbuf->datas[0].chunk->stride = this->position->video.stride; + } + + outio->buffer_id = b->id; + outio->status = SPA_STATUS_HAVE_DATA; + + pthread_rwlock_unlock(&this->renderlock); + + return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_vulkan_blit_unprepare(&this->state); + spa_vulkan_blit_deinit(&this->state); + lock_destroy(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; + 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; + + 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[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; + + lock_init(this); + + port = &this->port[SPA_DIRECTION_INPUT]; + port->stream_id = SPA_DIRECTION_INPUT; + 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; + 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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_vulkan_blit_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[SPA_DIRECTION_OUTPUT]; + port->stream_id = SPA_DIRECTION_OUTPUT; + 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[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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + spa_vulkan_blit_init_stream(&this->state, &this->state.streams[port->stream_id], + SPA_DIRECTION_OUTPUT, NULL); + + this->state.n_streams = 2; + spa_vulkan_blit_init(&this->state); + spa_vulkan_blit_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, "Columbarius " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Convert video frames using a vulkan blit" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_vulkan_blit_filter_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_VULKAN_BLIT_FILTER, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/vulkan-blit-utils.c b/spa/plugins/vulkan/vulkan-blit-utils.c new file mode 100644 index 0000000..f8a6049 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-blit-utils.c @@ -0,0 +1,683 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "vulkan-blit-utils.h" +#include "vulkan-utils.h" +#include "utils.h" + +#define VULKAN_INSTANCE_FUNCTION(name) \ + PFN_##name name = (PFN_##name)vkGetInstanceProcAddr(s->base.instance, #name) + +#define GET_BUFFER_ID_FROM_STREAM(s, pass) \ + (s->direction == SPA_DIRECTION_INPUT ? pass->in_buffer_id : pass->out_buffer_id) + +static int runImportSHMBuffers(struct vulkan_blit_state *s, struct vulkan_pass *pass) { + struct vulkan_stream *p = &s->streams[pass->in_stream_id]; + + if (p->buffer_type == SPA_DATA_MemPtr) { + struct spa_buffer *spa_buf = p->spa_buffers[pass->in_buffer_id]; + struct vulkan_write_pixels_info writeInfo = { + .data = spa_buf->datas[0].data, + .offset = 0, + .stride = p->bpp * p->dim.width, + .bytes_per_pixel = p->bpp, + .size.width = p->dim.width, + .size.height = p->dim.height, + .copies = &pass->in_copy, + }; + CHECK(vulkan_write_pixels(&s->base, &writeInfo, &pass->in_staging_buffer)); + } + + return 0; +} + +static int runExportSHMBuffers(struct vulkan_blit_state *s, struct vulkan_pass *pass) { + struct vulkan_stream *p = &s->streams[pass->out_stream_id]; + + if (p->buffer_type == SPA_DATA_MemPtr) { + struct spa_buffer *spa_buf = p->spa_buffers[pass->out_buffer_id]; + struct vulkan_read_pixels_info readInfo = { + .data = spa_buf->datas[0].data, + .offset = 0, + .stride = p->bpp * p->dim.width, + .bytes_per_pixel = p->bpp, + .size.width = p->dim.width, + .size.height = p->dim.height, + }; + CHECK(vulkan_read_pixels(&s->base, &readInfo, &p->buffers[pass->out_buffer_id])); + } + + return 0; +} + +static int runImportSync(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + int ret = 0; + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + uint32_t current_buffer_id = GET_BUFFER_ID_FROM_STREAM(p, pass); + struct vulkan_buffer *current_buffer = &p->buffers[current_buffer_id]; + + if (p->buffer_type != SPA_DATA_DmaBuf) + continue; + + if (vulkan_buffer_import_implicit_syncfd(&s->base, current_buffer) >= 0) + continue; + if (vulkan_buffer_wait_dmabuf_fence(&s->base, current_buffer) < 0) { + spa_log_warn(s->log, "Failed to wait for foreign buffer DMA-BUF fence"); + ret = -1; + } + } + return ret; +} + +static int runExportSync(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + int ret = 0; + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + if (p->buffer_type != SPA_DATA_DmaBuf) + continue; + + if (!vulkan_sync_export_dmabuf(&s->base, &p->buffers[GET_BUFFER_ID_FROM_STREAM(p, pass)], pass->sync_fd)) { + ret = -1; + } + } + + return ret; +} + +static int runCommandBuffer(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + VULKAN_INSTANCE_FUNCTION(vkQueueSubmit2KHR); + VULKAN_INSTANCE_FUNCTION(vkGetSemaphoreFdKHR); + + 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(pass->commandBuffer, &beginInfo)); + + uint32_t i; + struct vulkan_stream *stream_input = &s->streams[pass->in_stream_id]; + struct vulkan_stream *stream_output = &s->streams[pass->out_stream_id]; + + VkImage src_image = stream_input->buffers[pass->in_buffer_id].image; + VkImage dst_image = stream_output->buffers[pass->out_buffer_id].image; + + if (stream_input->buffer_type == SPA_DATA_MemPtr) { + vkCmdCopyBufferToImage(pass->commandBuffer, pass->in_staging_buffer.buffer, src_image, + VK_IMAGE_LAYOUT_GENERAL, 1, &pass->in_copy); + + VkImageMemoryBarrier copy_barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = s->base.queueFamilyIndex, + .dstQueueFamilyIndex = s->base.queueFamilyIndex, + .image = src_image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + vkCmdPipelineBarrier(pass->commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, NULL, 0, NULL, + 1, ©_barrier); + } + + VkImageBlit imageBlitRegion = { + .srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .srcSubresource.layerCount = 1, + .srcOffsets[0] = { + .x = 0, + .y = 0, + .z = 0, + }, + .srcOffsets[1] = { + .x = stream_input->dim.width, + .y = stream_input->dim.height, + .z = 1, + }, + .dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .dstSubresource.layerCount = 1, + .dstOffsets[1] = { + .x = stream_output->dim.width, + .y = stream_output->dim.height, + .z = 1, + } + }; + spa_log_trace_fp(s->log, "Blitting from (%p, %d, %d) %d,%dx%d,%d to (%p, %d, %d) %d,%dx%d,%d", + stream_input, pass->in_buffer_id, stream_input->direction, 0, 0, stream_input->dim.width, stream_input->dim.height, + stream_output, pass->out_buffer_id, stream_output->direction, 0, 0, stream_output->dim.width, stream_output->dim.height); + vkCmdBlitImage(pass->commandBuffer, src_image, VK_IMAGE_LAYOUT_GENERAL, + dst_image, VK_IMAGE_LAYOUT_GENERAL, + 1, &imageBlitRegion, VK_FILTER_NEAREST); + + VkImageMemoryBarrier acquire_barrier[s->n_streams]; + VkImageMemoryBarrier release_barrier[s->n_streams]; + VkSemaphoreSubmitInfo semaphore_wait_info[s->n_streams]; + uint32_t semaphore_wait_info_len = 0; + VkSemaphoreSubmitInfo semaphore_signal_info[1]; + uint32_t semaphore_signal_info_len = 0; + + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + struct vulkan_buffer *current_buffer = &p->buffers[GET_BUFFER_ID_FROM_STREAM(p, pass)]; + + VkAccessFlags access_flags; + VkAccessFlags release_flags; + if (p->direction == SPA_DIRECTION_INPUT) { + access_flags = p->buffer_type == SPA_DATA_DmaBuf + ? VK_ACCESS_TRANSFER_READ_BIT + : VK_ACCESS_TRANSFER_WRITE_BIT; + release_flags = VK_ACCESS_TRANSFER_READ_BIT; + } else { + access_flags = VK_ACCESS_TRANSFER_WRITE_BIT; + release_flags = VK_ACCESS_TRANSFER_WRITE_BIT; + } + + acquire_barrier[i]= (VkImageMemoryBarrier) { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .dstQueueFamilyIndex = s->base.queueFamilyIndex, + .image = current_buffer->image, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = 0, + .dstAccessMask = access_flags, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + release_barrier[i]= (VkImageMemoryBarrier) { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = s->base.queueFamilyIndex, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .image = current_buffer->image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = release_flags, + .dstAccessMask = 0, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + if (current_buffer->foreign_semaphore != VK_NULL_HANDLE) { + semaphore_wait_info[semaphore_wait_info_len++] = (VkSemaphoreSubmitInfo) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = current_buffer->foreign_semaphore, + .stageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + }; + } + } + + vkCmdPipelineBarrier(pass->commandBuffer, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, + 0, 0, NULL, 0, NULL, + s->n_streams, acquire_barrier); + + vkCmdPipelineBarrier(pass->commandBuffer, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, NULL, 0, NULL, + s->n_streams, release_barrier); + + VK_CHECK_RESULT(vkEndCommandBuffer(pass->commandBuffer)); + + VK_CHECK_RESULT(vkResetFences(s->base.device, 1, &pass->fence)); + + semaphore_signal_info[semaphore_signal_info_len++] = (VkSemaphoreSubmitInfo) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = pass->pipelineSemaphore, + }; + + VkCommandBufferSubmitInfoKHR commandBufferInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .commandBuffer = pass->commandBuffer, + }; + + const VkSubmitInfo2KHR submitInfo = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2_KHR, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &commandBufferInfo, + .waitSemaphoreInfoCount = semaphore_wait_info_len, + .pWaitSemaphoreInfos = semaphore_wait_info, + .signalSemaphoreInfoCount = semaphore_signal_info_len, + .pSignalSemaphoreInfos = semaphore_signal_info, + }; + VK_CHECK_RESULT(vkQueueSubmit2KHR(s->base.queue, 1, &submitInfo, pass->fence)); + s->started = true; + + VkSemaphoreGetFdInfoKHR get_fence_fd_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR, + .semaphore = pass->pipelineSemaphore, + .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + }; + VK_CHECK_RESULT(vkGetSemaphoreFdKHR(s->base.device, &get_fence_fd_info, &pass->sync_fd)); + + return 0; +} + +static void clear_buffers(struct vulkan_blit_state *s, struct vulkan_stream *p) +{ + uint32_t i; + + for (i = 0; i < p->n_buffers; i++) { + vulkan_buffer_clear(&s->base, &p->buffers[i]); + p->spa_buffers[i] = NULL; + } + p->n_buffers = 0; + p->buffer_type = SPA_DATA_Invalid; + p->maxsize = 0; +} + +static void clear_streams(struct vulkan_blit_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_blit_fixate_modifier(struct vulkan_blit_state *s, struct vulkan_stream *p, struct spa_video_info *info, + uint32_t modifierCount, uint64_t *modifiers, uint64_t *modifier) +{ + VkFormat format; + struct spa_rectangle size; + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: + format = vulkan_id_to_vkformat(info->info.dsp.format); + size.width = p->dim.width; + size.height = p->dim.height; + break; + case SPA_MEDIA_SUBTYPE_raw: + format = vulkan_id_to_vkformat(info->info.raw.format); + size.width = p->dim.width; + size.height = p->dim.height; + break; + default: + spa_log_warn(s->log, "Unsupported media subtype %d", info->media_subtype); + return -1; + } + if (format == VK_FORMAT_UNDEFINED) { + return -1; + } + + struct dmabuf_fixation_info fixation_info = { + .format = format, + .modifierCount = modifierCount, + .modifiers = modifiers, + .size = size, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, + }; + return vulkan_fixate_modifier(&s->base, &fixation_info, modifier); +} + +int spa_vulkan_blit_use_buffers(struct vulkan_blit_state *s, struct vulkan_stream *p, uint32_t flags, + struct spa_video_info *info, uint32_t n_buffers, struct spa_buffer **buffers) +{ + struct external_buffer_info externalBufferInfo = {0}; + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: + externalBufferInfo.format = vulkan_id_to_vkformat(info->info.dsp.format); + externalBufferInfo.size.width = p->dim.width; + externalBufferInfo.size.height = p->dim.height; + if (info->info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) + externalBufferInfo.modifier = info->info.dsp.modifier; + break; + case SPA_MEDIA_SUBTYPE_raw: + externalBufferInfo.format = vulkan_id_to_vkformat(info->info.raw.format); + externalBufferInfo.size.width = p->dim.width; + externalBufferInfo.size.height = p->dim.height; + if (info->info.raw.flags & SPA_VIDEO_FLAG_MODIFIER) + externalBufferInfo.modifier = info->info.raw.modifier; + break; + default: + spa_log_warn(s->log, "Unsupported media subtype %d", info->media_subtype); + return -1; + } + if (externalBufferInfo.format == VK_FORMAT_UNDEFINED) + return -1; + + vulkan_wait_idle(&s->base); + clear_buffers(s, p); + + if (n_buffers == 0) + return 0; + + bool alloc = flags & SPA_NODE_BUFFERS_FLAG_ALLOC; + int ret; + for (uint32_t i = 0; i < n_buffers; i++) { + if (p->buffer_type == SPA_DATA_Invalid) { + p->buffer_type = buffers[i]->datas->type; + } else { + if (p->buffer_type != buffers[i]->datas->type) { + spa_log_error(s->log, "Buffers are of different type %d:%d", p->buffer_type, buffers[i]->datas[0].type); + return -1; + } + } + p->maxsize = SPA_MAX(p->maxsize, buffers[i]->datas[0].maxsize); + externalBufferInfo.usage = p->direction == SPA_DIRECTION_OUTPUT + ? VK_IMAGE_USAGE_TRANSFER_DST_BIT + : VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + externalBufferInfo.spa_buf = buffers[i]; + if (alloc) { + if (SPA_FLAG_IS_SET(buffers[i]->datas[0].type, 1<base, &externalBufferInfo, &p->buffers[i]); + } else { + spa_log_error(s->log, "Unsupported buffer type mask %d", buffers[i]->datas[0].type); + return -1; + } + } else { + switch (buffers[i]->datas[0].type) { + case SPA_DATA_DmaBuf:; + ret = vulkan_import_dmabuf(&s->base, &externalBufferInfo, &p->buffers[i]); + break; + case SPA_DATA_MemPtr:; + if (p->direction == SPA_DIRECTION_OUTPUT) { + externalBufferInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } else { + externalBufferInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + } + ret = vulkan_import_memptr(&s->base, &externalBufferInfo, &p->buffers[i]); + break; + default: + spa_log_error(s->log, "Unsupported buffer type %d", buffers[i]->datas[0].type); + return -1; + } + } + if (ret != 0) { + spa_log_error(s->log, "Failed to use buffer %d", i); + return ret; + } + p->spa_buffers[i] = buffers[i]; + p->n_buffers++; + } + + return 0; +} + +int spa_vulkan_blit_enumerate_raw_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + uint32_t fmt_idx; + bool has_modifier; + if (!find_EnumFormatInfo(&s->formatInfosRaw, index, caps, &fmt_idx, &has_modifier)) + return 0; + *param = build_raw_EnumFormat(&s->formatInfosRaw.infos[fmt_idx], has_modifier, builder); + return 1; +} + +int spa_vulkan_blit_enumerate_dsp_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + uint32_t fmt_idx; + bool has_modifier; + if (!find_EnumFormatInfo(&s->formatInfosDSP, index, caps, &fmt_idx, &has_modifier)) + return 0; + *param = build_dsp_EnumFormat(&s->formatInfosDSP.infos[fmt_idx], has_modifier, builder); + return 1; +} + +int spa_vulkan_blit_enumerate_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + uint32_t fmt_idx; + bool has_modifier; + uint32_t raw_offset = 0; + if ((caps & VULKAN_BUFFER_TYPE_CAP_SHM) > 0) + raw_offset += s->formatInfosDSP.formatCount; + if ((caps & VULKAN_BUFFER_TYPE_CAP_DMABUF) > 0) + raw_offset += s->formatInfosDSP.formatsWithModifiersCount; + if (index < raw_offset) { + if (find_EnumFormatInfo(&s->formatInfosDSP, index, caps, &fmt_idx, &has_modifier)) { + *param = build_dsp_EnumFormat(&s->formatInfosDSP.infos[fmt_idx], has_modifier, builder); + return 1; + } + } else { + if (find_EnumFormatInfo(&s->formatInfosRaw, index - raw_offset, caps, &fmt_idx, &has_modifier)) { + *param = build_raw_EnumFormat(&s->formatInfosRaw.infos[fmt_idx], has_modifier, builder); + return 1; + } + } + return 0; +} + +static int vulkan_stream_init(struct vulkan_stream *stream, enum spa_direction direction, + struct spa_dict *props) +{ + spa_zero(*stream); + stream->direction = direction; + stream->maxsize = 0; + return 0; +} + +int spa_vulkan_blit_init_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + pass->in_buffer_id = SPA_ID_INVALID; + pass->in_stream_id = SPA_ID_INVALID; + pass->out_buffer_id = SPA_ID_INVALID; + pass->out_stream_id = SPA_ID_INVALID; + + pass->sync_fd = -1; + + CHECK(vulkan_fence_create(&s->base, &pass->fence)); + CHECK(vulkan_commandBuffer_create(&s->base, s->commandPool, &pass->commandBuffer)); + + VkExportSemaphoreCreateInfo export_info = { + .sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + }; + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = &export_info, + }; + VK_CHECK_RESULT(vkCreateSemaphore(s->base.device, &semaphore_info, NULL, &pass->pipelineSemaphore)); + + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + if (p->direction == SPA_DIRECTION_OUTPUT || p->buffer_type != SPA_DATA_MemPtr) + continue; + vulkan_staging_buffer_create(&s->base, p->maxsize, &pass->in_staging_buffer); + } + + return 0; +} + +int spa_vulkan_blit_reset_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + pass->in_buffer_id = SPA_ID_INVALID; + pass->in_stream_id = SPA_ID_INVALID; + pass->out_buffer_id = SPA_ID_INVALID; + pass->out_stream_id = SPA_ID_INVALID; + + if (pass->sync_fd != -1) { + close(pass->sync_fd); + pass->sync_fd = -1; + } + + return 0; +} + +int spa_vulkan_blit_clear_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + pass->in_buffer_id = SPA_ID_INVALID; + pass->in_stream_id = SPA_ID_INVALID; + pass->out_buffer_id = SPA_ID_INVALID; + pass->out_stream_id = SPA_ID_INVALID; + + if (pass->sync_fd != -1) { + close(pass->sync_fd); + pass->sync_fd = -1; + } + + vkDestroyFence(s->base.device, pass->fence, NULL); + pass->fence = VK_NULL_HANDLE; + vkFreeCommandBuffers(s->base.device, s->commandPool, 1, &pass->commandBuffer); + pass->commandBuffer = VK_NULL_HANDLE; + vkDestroySemaphore(s->base.device, pass->pipelineSemaphore, NULL); + pass->pipelineSemaphore = VK_NULL_HANDLE; + vulkan_staging_buffer_destroy(&s->base, &pass->in_staging_buffer); + pass->in_staging_buffer.buffer = VK_NULL_HANDLE; + + return 0; +} + +int spa_vulkan_blit_init_stream(struct vulkan_blit_state *s, struct vulkan_stream *stream, + enum spa_direction direction, struct spa_dict *props) +{ + return vulkan_stream_init(stream, direction, props); +} + +int spa_vulkan_blit_prepare(struct vulkan_blit_state *s) +{ + if (!s->prepared) { + CHECK(vulkan_commandPool_create(&s->base, &s->commandPool)); + s->prepared = true; + } + return 0; +} + +int spa_vulkan_blit_unprepare(struct vulkan_blit_state *s) +{ + if (s->prepared) { + vkDestroyCommandPool(s->base.device, s->commandPool, NULL); + s->prepared = false; + } + return 0; +} + +int spa_vulkan_blit_start(struct vulkan_blit_state *s) +{ + return 0; +} + +int spa_vulkan_blit_stop(struct vulkan_blit_state *s) +{ + VK_CHECK_RESULT(vkDeviceWaitIdle(s->base.device)); + clear_streams(s); + s->started = false; + return 0; +} + +int spa_vulkan_blit_process(struct vulkan_blit_state *s, struct vulkan_pass *pass) +{ + if (!s->initialized) { + spa_log_warn(s->log, "Renderer not initialized"); + return -1; + } + if (!s->prepared) { + spa_log_warn(s->log, "Renderer not prepared"); + return -1; + } + CHECK(runImportSync(s, pass)); + CHECK(runImportSHMBuffers(s, pass)); + CHECK(runCommandBuffer(s, pass)); + if (pass->sync_fd != -1) { + runExportSync(s, pass); + } + CHECK(vulkan_wait_idle(&s->base)); + CHECK(runExportSHMBuffers(s, pass)); + + return 0; +} + +int spa_vulkan_blit_get_buffer_caps(struct vulkan_blit_state *s, enum spa_direction direction) +{ + switch (direction) { + case SPA_DIRECTION_INPUT: + return VULKAN_BUFFER_TYPE_CAP_DMABUF | VULKAN_BUFFER_TYPE_CAP_SHM; + case SPA_DIRECTION_OUTPUT: + return VULKAN_BUFFER_TYPE_CAP_DMABUF | VULKAN_BUFFER_TYPE_CAP_SHM; + } + return 0; +} + +struct vulkan_modifier_info *spa_vulkan_blit_get_modifier_info(struct vulkan_blit_state *s, struct spa_video_info *info) { + VkFormat format; + uint64_t modifier; + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: + format = vulkan_id_to_vkformat(info->info.dsp.format); + modifier = info->info.dsp.modifier; + return vulkan_modifierInfo_find(&s->formatInfosDSP, format, modifier); + case SPA_MEDIA_SUBTYPE_raw: + format = vulkan_id_to_vkformat(info->info.raw.format); + modifier = info->info.raw.modifier; + return vulkan_modifierInfo_find(&s->formatInfosRaw, format, modifier); + default: + spa_log_warn(s->log, "Unsupported media subtype %d", info->media_subtype); + return NULL; + } +} + +int spa_vulkan_blit_init(struct vulkan_blit_state *s) +{ + int ret; + s->base.log = s->log; + struct vulkan_base_info baseInfo = { + .queueFlags = VK_QUEUE_TRANSFER_BIT, + }; + if ((ret = vulkan_base_init(&s->base, &baseInfo)) < 0) + return ret; + + uint32_t dsp_formats [] = { + SPA_VIDEO_FORMAT_DSP_F32 + }; + vulkan_format_infos_init(&s->base, SPA_N_ELEMENTS(dsp_formats), dsp_formats, &s->formatInfosDSP); + uint32_t raw_formats [] = { + SPA_VIDEO_FORMAT_BGRA, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_BGR, + SPA_VIDEO_FORMAT_RGB, + }; + vulkan_format_infos_init(&s->base, SPA_N_ELEMENTS(raw_formats), raw_formats, &s->formatInfosRaw); + s->initialized = true; + return 0; +} + +void spa_vulkan_blit_deinit(struct vulkan_blit_state *s) +{ + vulkan_format_infos_deinit(&s->formatInfosRaw); + vulkan_format_infos_deinit(&s->formatInfosDSP); + vulkan_base_deinit(&s->base); + s->initialized = false; +} diff --git a/spa/plugins/vulkan/vulkan-blit-utils.h b/spa/plugins/vulkan/vulkan-blit-utils.h new file mode 100644 index 0000000..f7582c0 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-blit-utils.h @@ -0,0 +1,96 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 columbarius */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include + +#include "vulkan-utils.h" + +#define MAX_STREAMS 2 + +struct vulkan_pass { + uint32_t in_buffer_id; + uint32_t in_stream_id; + + uint32_t out_buffer_id; + uint32_t out_stream_id; + + VkBufferImageCopy in_copy; + struct vulkan_staging_buffer in_staging_buffer; + + VkCommandBuffer commandBuffer; + VkSemaphore pipelineSemaphore; + VkFence fence; + + int sync_fd; +}; + +struct vulkan_stream { + enum spa_direction direction; + + enum spa_data_type buffer_type; + struct spa_rectangle dim; + uint32_t bpp; + uint32_t maxsize; + + struct vulkan_buffer buffers[MAX_BUFFERS]; + struct spa_buffer *spa_buffers[MAX_BUFFERS]; + uint32_t n_buffers; +}; + +struct vulkan_blit_state { + struct spa_log *log; + + struct vulkan_base base; + + struct vulkan_format_infos formatInfosRaw; + struct vulkan_format_infos formatInfosDSP; + + VkCommandPool commandPool; + + unsigned int initialized:1; + unsigned int prepared:1; + unsigned int started:1; + + uint32_t n_streams; + struct vulkan_stream streams[MAX_STREAMS]; +}; + +int spa_vulkan_blit_init_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass); +int spa_vulkan_blit_reset_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass); +int spa_vulkan_blit_clear_pass(struct vulkan_blit_state *s, struct vulkan_pass *pass); + +int spa_vulkan_blit_init_stream(struct vulkan_blit_state *s, struct vulkan_stream *stream, enum spa_direction, + struct spa_dict *props); + +int spa_vulkan_blit_fixate_modifier(struct vulkan_blit_state *s, struct vulkan_stream *p, struct spa_video_info *info, + uint32_t modifierCount, uint64_t *modifiers, uint64_t *modifier); +int spa_vulkan_blit_use_buffers(struct vulkan_blit_state *s, struct vulkan_stream *stream, uint32_t flags, + struct spa_video_info *info, uint32_t n_buffers, struct spa_buffer **buffers); +int spa_vulkan_blit_enumerate_raw_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder); +int spa_vulkan_blit_enumerate_dsp_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder); +int spa_vulkan_blit_enumerate_formats(struct vulkan_blit_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder); +int spa_vulkan_blit_prepare(struct vulkan_blit_state *s); +int spa_vulkan_blit_unprepare(struct vulkan_blit_state *s); + +int spa_vulkan_blit_start(struct vulkan_blit_state *s); +int spa_vulkan_blit_stop(struct vulkan_blit_state *s); +int spa_vulkan_blit_ready(struct vulkan_blit_state *s); +int spa_vulkan_blit_process(struct vulkan_blit_state *s, struct vulkan_pass *pass); +int spa_vulkan_blit_cleanup(struct vulkan_blit_state *s); + +int spa_vulkan_blit_get_buffer_caps(struct vulkan_blit_state *s, enum spa_direction direction); +struct vulkan_modifier_info *spa_vulkan_blit_get_modifier_info(struct vulkan_blit_state *s, + struct spa_video_info *info); + +int spa_vulkan_blit_init(struct vulkan_blit_state *s); +void spa_vulkan_blit_deinit(struct vulkan_blit_state *s); diff --git a/spa/plugins/vulkan/vulkan-compute-filter.c b/spa/plugins/vulkan/vulkan-compute-filter.c new file mode 100644 index 0000000..3970036 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-filter.c @@ -0,0 +1,863 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vulkan-compute-utils.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffer 4 +#define N_PORT_PARAMS 5 + struct spa_param_info params[N_PORT_PARAMS]; + + 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; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + bool started; + + struct vulkan_compute_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, "%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_compute_start(&this->state); + break; + + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + this->started = false; + spa_vulkan_compute_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) +{ + struct impl *this = object; + + if (this->port[port_id].have_format + && this->port[port_id].current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && this->port[port_id].current_format.info.dsp.flags ^ SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + if (index == 0) { + spa_log_info(this->log, "vulkan-compute-filter: enum_formats fixated format idx: %d, format %d, has_modifier 1", + index, this->port[port_id].current_format.info.dsp.format); + *param = spa_format_video_dsp_build(builder, SPA_PARAM_EnumFormat, &this->port[port_id].current_format.info.dsp); + return 1; + } + return spa_vulkan_compute_enumerate_formats(&this->state, index-1, spa_vulkan_compute_get_buffer_caps(&this->state, direction), param, builder); + } else { + return spa_vulkan_compute_enumerate_formats(&this->state, index, spa_vulkan_compute_get_buffer_caps(&this->state, direction), param, builder); + } +} + +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, ¶m, &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, "%p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + if (port->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + struct vulkan_modifier_info *mod_info = spa_vulkan_compute_get_modifier_info(&this->state, + &port->current_format.info.dsp); + 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(mod_info->props.drmFormatModifierPlaneCount), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<position->video.stride * + this->position->video.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<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", this); + spa_vulkan_compute_stop(&this->state); + spa_vulkan_compute_use_buffers(&this->state, &this->state.streams[port->stream_id], 0, &port->current_format.info.dsp, 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_compute_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; + + bool modifier_fixed = false; + if (port->direction == SPA_DIRECTION_OUTPUT + && info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) + return -EINVAL; + + const struct spa_pod *mod_pod = &mod_prop->value; + uint32_t modifierCount = SPA_POD_CHOICE_N_VALUES(mod_pod); + uint64_t *modifiers = SPA_POD_CHOICE_VALUES(mod_pod); + if (modifierCount <= 1) + return -EINVAL; + // SPA_POD_CHOICE carries the "preferred" value at position 0 + modifierCount -= 1; + modifiers++; + uint64_t fixed_modifier; + if (spa_vulkan_compute_fixate_modifier(&this->state, &this->state.streams[port->stream_id], &info.info.dsp, modifierCount, modifiers, &fixed_modifier) != 0) + return -EINVAL; + + spa_log_info(this->log, "modifier fixated %"PRIu64, fixed_modifier); + + info.info.dsp.modifier = fixed_modifier; + info.info.dsp.flags &= ~SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + modifier_fixed = true; + } + + if (info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + port->info.flags |= SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + } else { + port->info.flags &= ~SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + + port->current_format = info; + port->have_format = true; + + if (modifier_fixed) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_EnumFormat].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + return 0; + } + } + + 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_Buffer] = 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_Buffer] = 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_compute_use_buffers(&this->state, &this->state.streams[port->stream_id], flags, &port->current_format.info.dsp, 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, "%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.025f; + this->state.constants.frame++; + + spa_log_debug(this->log, "filter into %d", b->id); + + spa_vulkan_compute_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) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_vulkan_compute_deinit(&this->state); + 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[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[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; + 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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_vulkan_compute_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[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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + spa_vulkan_compute_init_stream(&this->state, &this->state.streams[port->stream_id], + SPA_DIRECTION_OUTPUT, NULL); + + this->state.n_streams = 2; + spa_vulkan_compute_init(&this->state); + spa_vulkan_compute_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 " }, + { 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..8512c15 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-source.c @@ -0,0 +1,1070 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vulkan-compute-utils.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.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; + + enum spa_direction direction; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffer 4 +#define N_PORT_PARAMS 5 + struct spa_param_info params[N_PORT_PARAMS]; + + 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; +#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_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_compute_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, "%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_compute_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, "%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, "%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_compute_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, "%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, "%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_compute_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_compute_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) +{ + struct impl *this = object; + + if (this->port.have_format + && this->port.current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && this->port.current_format.info.dsp.flags ^ SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + if (index == 0) { + spa_log_info(this->log, "vulkan-compute-source: enum_formats fixated format idx: %d, format %d, has_modifier 1", + index, this->port.current_format.info.dsp.format); + *param = spa_format_video_dsp_build(builder, SPA_PARAM_EnumFormat, &this->port.current_format.info.dsp); + return 1; + } + return spa_vulkan_compute_enumerate_formats(&this->state, index-1, spa_vulkan_compute_get_buffer_caps(&this->state, direction), param, builder); + } else { + return spa_vulkan_compute_enumerate_formats(&this->state, index, spa_vulkan_compute_get_buffer_caps(&this->state, direction), param, builder); + } +} + +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, ¶m, &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, "%p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + + if (port->current_format.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + struct vulkan_modifier_info *mod_info = spa_vulkan_compute_get_modifier_info(&this->state, + &port->current_format.info.dsp); + 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(mod_info->props.drmFormatModifierPlaneCount), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<position->video.stride * + this->position->video.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<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", this); + spa_vulkan_compute_use_buffers(&this->state, &this->state.streams[0], 0, &port->current_format.info.dsp, 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_compute_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; + + bool modifier_fixed = false; + if (info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER + && info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED) { + const struct spa_pod_prop *mod_prop; + if ((mod_prop = spa_pod_find_prop(format, NULL, SPA_FORMAT_VIDEO_modifier)) == NULL) + return -EINVAL; + + const struct spa_pod *mod_pod = &mod_prop->value; + uint32_t modifierCount = SPA_POD_CHOICE_N_VALUES(mod_pod); + uint64_t *modifiers = SPA_POD_CHOICE_VALUES(mod_pod); + if (modifierCount <= 1) + return -EINVAL; + // SPA_POD_CHOICE carries the "preferred" value at position 0 + modifierCount -= 1; + modifiers++; + + uint64_t fixed_modifier; + if (spa_vulkan_compute_fixate_modifier(&this->state, &this->state.streams[0], &info.info.dsp, modifierCount, modifiers, &fixed_modifier) != 0) + return -EINVAL; + + spa_log_info(this->log, "modifier fixated %"PRIu64, fixed_modifier); + + info.info.dsp.modifier = fixed_modifier; + info.info.dsp.flags &= ~SPA_VIDEO_FLAG_MODIFIER_FIXATION_REQUIRED; + modifier_fixed = true; + } + + if (info.info.dsp.flags & SPA_VIDEO_FLAG_MODIFIER) { + port->info.flags |= SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + } else { + port->info.flags &= ~SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + + port->current_format = info; + port->have_format = true; + spa_vulkan_compute_prepare(&this->state); + + if (modifier_fixed) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_EnumFormat].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + return 0; + } + } + + 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_Buffer] = 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_Buffer] = 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_log_info(this->log, "%p: %d:%d add buffer %p", port, direction, port_id, b); + spa_list_append(&port->empty, &b->link); + } + spa_vulkan_compute_use_buffers(&this->state, &this->state.streams[0], flags, &port->current_format.info.dsp, 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; + + spa_vulkan_compute_deinit(&this->state); + + 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[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; + 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; + if (this->props.live) + 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_Buffer] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + + this->state.log = this->log; + spa_vulkan_compute_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; + spa_vulkan_compute_init(&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 " }, + { 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-compute-utils.c b/spa/plugins/vulkan/vulkan-compute-utils.c new file mode 100644 index 0000000..49705be --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-utils.c @@ -0,0 +1,751 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "vulkan-compute-utils.h" +#include "vulkan-utils.h" +#include "utils.h" +#include "pixel-formats.h" + +#define VULKAN_INSTANCE_FUNCTION(name) \ + PFN_##name name = (PFN_##name)vkGetInstanceProcAddr(s->base.instance, #name) + +static int createDescriptors(struct vulkan_compute_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->base.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->base.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->base.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->base.device, &samplerInfo, NULL, &s->sampler)); + + return 0; +} + +static int updateDescriptors(struct vulkan_compute_state *s) +{ + uint32_t i; + VkDescriptorImageInfo descriptorImageInfo[s->n_streams]; + VkWriteDescriptorSet writeDescriptorSet[s->n_streams]; + uint32_t descriptorSetLen = 0; + + 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[descriptorSetLen] = (VkDescriptorImageInfo) { + .sampler = s->sampler, + .imageView = p->buffers[p->current_buffer_id].view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + writeDescriptorSet[descriptorSetLen] = (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], + }; + descriptorSetLen++; + } + vkUpdateDescriptorSets(s->base.device, descriptorSetLen, + writeDescriptorSet, 0, NULL); + + return 0; +} + +static VkShaderModule createShaderModule(struct vulkan_compute_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->base.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_compute_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->base.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->base.device, VK_NULL_HANDLE, + 1, &pipelineCreateInfo, NULL, + &s->pipeline)); + return 0; +} + +static int createCommandBuffer(struct vulkan_compute_state *s) +{ + CHECK(vulkan_commandPool_create(&s->base, &s->commandPool)); + CHECK(vulkan_commandBuffer_create(&s->base, s->commandPool, &s->commandBuffer)); + + return 0; +} + +static int runImportSHMBuffers(struct vulkan_compute_state *s) { + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + if (p->direction == SPA_DIRECTION_OUTPUT) + continue; + + struct pixel_format_info pixel_info; + CHECK(get_pixel_format_info(p->format, &pixel_info)); + + if (p->spa_buffers[p->current_buffer_id]->datas[0].type == SPA_DATA_MemPtr) { + struct vulkan_buffer *vk_buf = &p->buffers[p->current_buffer_id]; + struct spa_buffer *spa_buf = p->spa_buffers[p->current_buffer_id]; + VkBufferImageCopy copy; + struct vulkan_write_pixels_info writeInfo = { + .data = spa_buf->datas[0].data, + .offset = spa_buf->datas[0].chunk->offset, + .stride = spa_buf->datas[0].chunk->stride, + .bytes_per_pixel = pixel_info.bpp, + .size.width = s->constants.width, + .size.height = s->constants.height, + .copies = ©, + }; + CHECK(vulkan_write_pixels(&s->base, &writeInfo, &s->staging_buffer)); + + vkCmdCopyBufferToImage(s->commandBuffer, s->staging_buffer.buffer, vk_buf->image, + VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } + } + + return 0; +} + +static int runExportSHMBuffers(struct vulkan_compute_state *s) { + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + if (p->direction == SPA_DIRECTION_INPUT) + continue; + + struct pixel_format_info pixel_info; + CHECK(get_pixel_format_info(p->format, &pixel_info)); + + if (p->spa_buffers[p->current_buffer_id]->datas[0].type == SPA_DATA_MemPtr) { + struct spa_buffer *spa_buf = p->spa_buffers[p->current_buffer_id]; + struct vulkan_read_pixels_info readInfo = { + .data = spa_buf->datas[0].data, + .offset = spa_buf->datas[0].chunk->offset, + .stride = spa_buf->datas[0].chunk->stride, + .bytes_per_pixel = pixel_info.bpp, + .size.width = s->constants.width, + .size.height = s->constants.height, + }; + CHECK(vulkan_read_pixels(&s->base, &readInfo, &p->buffers[p->current_buffer_id])); + } + } + + return 0; +} + +/** runCommandBuffer + * The return value of this functions means the following: + * ret < 0: Error + * ret = 0: queueSubmit was successful, but manual synchronization is required + * ret = 1: queueSubmit was successful and buffers can be released without synchronization + */ +static int runCommandBuffer(struct vulkan_compute_state *s) +{ + VULKAN_INSTANCE_FUNCTION(vkQueueSubmit2KHR); + VULKAN_INSTANCE_FUNCTION(vkGetSemaphoreFdKHR); + + 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)); + + CHECK(runImportSHMBuffers(s)); + + 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); + + VkImageMemoryBarrier acquire_barrier[s->n_streams]; + VkImageMemoryBarrier release_barrier[s->n_streams]; + VkSemaphoreSubmitInfo semaphore_wait_info[s->n_streams]; + uint32_t semaphore_wait_info_len = 0; + VkSemaphoreSubmitInfo semaphore_signal_info[1]; + uint32_t semaphore_signal_info_len = 0; + + uint32_t i; + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + struct vulkan_buffer *current_buffer = &p->buffers[p->current_buffer_id]; + struct spa_buffer *current_spa_buffer = p->spa_buffers[p->current_buffer_id]; + + VkAccessFlags access_flags; + if (p->direction == SPA_DIRECTION_INPUT) { + access_flags = VK_ACCESS_SHADER_READ_BIT; + } else { + access_flags = VK_ACCESS_SHADER_WRITE_BIT; + } + + acquire_barrier[i]= (VkImageMemoryBarrier) { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .dstQueueFamilyIndex = s->base.queueFamilyIndex, + .image = current_buffer->image, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = 0, + .dstAccessMask = access_flags, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + release_barrier[i]= (VkImageMemoryBarrier) { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = s->base.queueFamilyIndex, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .image = current_buffer->image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = access_flags, + .dstAccessMask = 0, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + if (current_spa_buffer->datas[0].type != SPA_DATA_DmaBuf) + continue; + + if (vulkan_sync_foreign_dmabuf(&s->base, current_buffer) < 0) { + spa_log_warn(s->log, "Failed to wait for foreign buffer DMA-BUF fence"); + } else { + if (current_buffer->foreign_semaphore != VK_NULL_HANDLE) { + semaphore_wait_info[semaphore_wait_info_len++] = (VkSemaphoreSubmitInfo) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = current_buffer->foreign_semaphore, + .stageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + }; + } + } + } + + vkCmdPipelineBarrier(s->commandBuffer, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, NULL, 0, NULL, + s->n_streams, acquire_barrier); + + vkCmdPipelineBarrier(s->commandBuffer, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, NULL, 0, NULL, + s->n_streams, release_barrier); + + VK_CHECK_RESULT(vkEndCommandBuffer(s->commandBuffer)); + + VK_CHECK_RESULT(vkResetFences(s->base.device, 1, &s->fence)); + + if (s->pipelineSemaphore == VK_NULL_HANDLE) { + VkExportSemaphoreCreateInfo export_info = { + .sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + }; + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = &export_info, + }; + VK_CHECK_RESULT(vkCreateSemaphore(s->base.device, &semaphore_info, NULL, &s->pipelineSemaphore)); + } + + semaphore_signal_info[semaphore_signal_info_len++] = (VkSemaphoreSubmitInfo) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = s->pipelineSemaphore, + }; + + VkCommandBufferSubmitInfoKHR commandBufferInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .commandBuffer = s->commandBuffer, + }; + + const VkSubmitInfo2KHR submitInfo = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2_KHR, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &commandBufferInfo, + .waitSemaphoreInfoCount = semaphore_wait_info_len, + .pWaitSemaphoreInfos = semaphore_wait_info, + .signalSemaphoreInfoCount = semaphore_signal_info_len, + .pSignalSemaphoreInfos = semaphore_signal_info, + }; + VK_CHECK_RESULT(vkQueueSubmit2KHR(s->base.queue, 1, &submitInfo, s->fence)); + s->started = true; + + VkSemaphoreGetFdInfoKHR get_fence_fd_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR, + .semaphore = s->pipelineSemaphore, + .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + }; + int sync_file_fd = -1; + VK_CHECK_RESULT(vkGetSemaphoreFdKHR(s->base.device, &get_fence_fd_info, &sync_file_fd)); + + int ret = 1; + for (uint32_t i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + struct spa_buffer *current_spa_buffer = p->spa_buffers[p->current_buffer_id]; + + if (current_spa_buffer->datas[0].type != SPA_DATA_DmaBuf) + continue; + + if (!vulkan_sync_export_dmabuf(&s->base, &p->buffers[p->current_buffer_id], sync_file_fd)) { + ret = 0; + } + } + close(sync_file_fd); + + return ret; +} + +static void clear_buffers(struct vulkan_compute_state *s, struct vulkan_stream *p) +{ + uint32_t i; + + for (i = 0; i < p->n_buffers; i++) { + vulkan_buffer_clear(&s->base, &p->buffers[i]); + p->spa_buffers[i] = NULL; + } + p->n_buffers = 0; + if (p->direction == SPA_DIRECTION_INPUT) { + vulkan_staging_buffer_destroy(&s->base, &s->staging_buffer); + s->staging_buffer.buffer = VK_NULL_HANDLE; + } +} + +static void clear_streams(struct vulkan_compute_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_compute_fixate_modifier(struct vulkan_compute_state *s, struct vulkan_stream *p, struct spa_video_info_dsp *dsp_info, + uint32_t modifierCount, uint64_t *modifiers, uint64_t *modifier) +{ + VkFormat format = vulkan_id_to_vkformat(dsp_info->format); + if (format == VK_FORMAT_UNDEFINED) { + return -1; + } + + struct dmabuf_fixation_info fixation_info = { + .format = format, + .modifierCount = modifierCount, + .modifiers = modifiers, + .size.width = s->constants.width, + .size.height = s->constants.height, + .usage = VK_IMAGE_USAGE_STORAGE_BIT, + }; + return vulkan_fixate_modifier(&s->base, &fixation_info, modifier); +} + +int spa_vulkan_compute_use_buffers(struct vulkan_compute_state *s, struct vulkan_stream *p, uint32_t flags, + struct spa_video_info_dsp *dsp_info, uint32_t n_buffers, struct spa_buffer **buffers) +{ + VkFormat format = vulkan_id_to_vkformat(dsp_info->format); + if (format == VK_FORMAT_UNDEFINED) + return -1; + + vulkan_wait_idle(&s->base); + clear_buffers(s, p); + p->format = SPA_VIDEO_FORMAT_UNKNOWN; + + if (n_buffers == 0) + return 0; + + bool alloc = flags & SPA_NODE_BUFFERS_FLAG_ALLOC; + int ret; + for (uint32_t i = 0; i < n_buffers; i++) { + if (alloc) { + if (SPA_FLAG_IS_SET(buffers[i]->datas[0].type, 1<modifier, + .size.width = s->constants.width, + .size.height = s->constants.height, + .usage = p->direction == SPA_DIRECTION_OUTPUT + ? VK_IMAGE_USAGE_STORAGE_BIT + : VK_IMAGE_USAGE_SAMPLED_BIT, + .spa_buf = buffers[i], + }; + struct vulkan_modifier_info *modifierInfo = vulkan_modifierInfo_find(&s->formatInfos, format, dsp_info->modifier); + CHECK(vulkan_validate_dmabuf_properties(modifierInfo, &dmabufInfo.spa_buf->n_datas, &dmabufInfo.size)); + ret = vulkan_create_dmabuf(&s->base, &dmabufInfo, &p->buffers[i]); + } else { + spa_log_error(s->log, "Unsupported buffer type mask %d", buffers[i]->datas[0].type); + return -1; + } + } else { + switch (buffers[i]->datas[0].type) { + case SPA_DATA_DmaBuf:; + struct external_buffer_info dmabufInfo = { + .format = format, + .modifier = dsp_info->modifier, + .size.width = s->constants.width, + .size.height = s->constants.height, + .usage = p->direction == SPA_DIRECTION_OUTPUT + ? VK_IMAGE_USAGE_STORAGE_BIT + : VK_IMAGE_USAGE_SAMPLED_BIT, + .spa_buf = buffers[i], + }; + struct vulkan_modifier_info *modifierInfo = vulkan_modifierInfo_find(&s->formatInfos, format, dsp_info->modifier); + CHECK(vulkan_validate_dmabuf_properties(modifierInfo, &dmabufInfo.spa_buf->n_datas, &dmabufInfo.size)); + ret = vulkan_import_dmabuf(&s->base, &dmabufInfo, &p->buffers[i]); + break; + case SPA_DATA_MemPtr:; + struct external_buffer_info memptrInfo = { + .format = format, + .size.width = s->constants.width, + .size.height = s->constants.height, + .usage = p->direction == SPA_DIRECTION_OUTPUT + ? VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT + : VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .spa_buf = buffers[i], + }; + ret = vulkan_import_memptr(&s->base, &memptrInfo, &p->buffers[i]); + break; + default: + spa_log_error(s->log, "Unsupported buffer type %d", buffers[i]->datas[0].type); + return -1; + } + } + if (ret != 0) { + spa_log_error(s->log, "Failed to use buffer %d", i); + return ret; + } + p->spa_buffers[i] = buffers[i]; + p->n_buffers++; + } + if (p->direction == SPA_DIRECTION_INPUT && buffers[0]->datas[0].type == SPA_DATA_MemPtr) { + ret = vulkan_staging_buffer_create(&s->base, buffers[0]->datas[0].maxsize, &s->staging_buffer); + if (ret < 0) { + spa_log_error(s->log, "Failed to create staging buffer"); + return ret; + } + } + p->format = dsp_info->format; + + return 0; +} + +int spa_vulkan_compute_enumerate_formats(struct vulkan_compute_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + uint32_t fmt_idx; + bool has_modifier; + if (!find_EnumFormatInfo(&s->formatInfos, index, caps, &fmt_idx, &has_modifier)) + return 0; + *param = build_dsp_EnumFormat(&s->formatInfos.infos[fmt_idx], has_modifier, builder); + return 1; +} + +static int vulkan_stream_init(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_compute_init_stream(struct vulkan_compute_state *s, struct vulkan_stream *stream, + enum spa_direction direction, struct spa_dict *props) +{ + return vulkan_stream_init(stream, direction, props); +} + +int spa_vulkan_compute_prepare(struct vulkan_compute_state *s) +{ + if (!s->prepared) { + CHECK(vulkan_fence_create(&s->base, &s->fence)); + CHECK(createDescriptors(s)); + CHECK(createComputePipeline(s, s->shaderName)); + CHECK(createCommandBuffer(s)); + s->prepared = true; + } + return 0; +} + +int spa_vulkan_compute_unprepare(struct vulkan_compute_state *s) +{ + if (s->prepared) { + vkDestroyShaderModule(s->base.device, s->computeShaderModule, NULL); + vkDestroySampler(s->base.device, s->sampler, NULL); + vkDestroyDescriptorPool(s->base.device, s->descriptorPool, NULL); + vkDestroyDescriptorSetLayout(s->base.device, s->descriptorSetLayout, NULL); + vkDestroyPipelineLayout(s->base.device, s->pipelineLayout, NULL); + vkDestroyPipeline(s->base.device, s->pipeline, NULL); + vkDestroyCommandPool(s->base.device, s->commandPool, NULL); + vkDestroyFence(s->base.device, s->fence, NULL); + s->prepared = false; + } + return 0; +} + +int spa_vulkan_compute_start(struct vulkan_compute_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_compute_stop(struct vulkan_compute_state *s) +{ + VK_CHECK_RESULT(vkDeviceWaitIdle(s->base.device)); + clear_streams(s); + s->started = false; + return 0; +} + +int spa_vulkan_compute_ready(struct vulkan_compute_state *s) +{ + uint32_t i; + VkResult result; + + if (!s->started) + return 0; + + result = vkGetFenceStatus(s->base.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_compute_process(struct vulkan_compute_state *s) +{ + CHECK(updateDescriptors(s)); + CHECK(runCommandBuffer(s)); + VK_CHECK_RESULT(vkDeviceWaitIdle(s->base.device)); + CHECK(runExportSHMBuffers(s)); + + return 0; +} + +int spa_vulkan_compute_get_buffer_caps(struct vulkan_compute_state *s, enum spa_direction direction) +{ + switch (direction) { + case SPA_DIRECTION_INPUT: + return VULKAN_BUFFER_TYPE_CAP_DMABUF | VULKAN_BUFFER_TYPE_CAP_SHM; + case SPA_DIRECTION_OUTPUT: + return VULKAN_BUFFER_TYPE_CAP_DMABUF | VULKAN_BUFFER_TYPE_CAP_SHM; + } + return 0; +} + +struct vulkan_modifier_info *spa_vulkan_compute_get_modifier_info(struct vulkan_compute_state *s, struct spa_video_info_dsp *info) { + VkFormat vk_format = vulkan_id_to_vkformat(info->format); + return vulkan_modifierInfo_find(&s->formatInfos, vk_format, info->modifier); +} + +int spa_vulkan_compute_init(struct vulkan_compute_state *s) +{ + int ret; + s->base.log = s->log; + uint32_t dsp_formats[] = { + SPA_VIDEO_FORMAT_DSP_F32 + }; + struct vulkan_base_info baseInfo = { + .queueFlags = VK_QUEUE_COMPUTE_BIT, + }; + if ((ret = vulkan_base_init(&s->base, &baseInfo)) < 0) + return ret; + return vulkan_format_infos_init(&s->base, SPA_N_ELEMENTS(dsp_formats), dsp_formats, &s->formatInfos); + +} + +void spa_vulkan_compute_deinit(struct vulkan_compute_state *s) +{ + vulkan_format_infos_deinit(&s->formatInfos); + vulkan_base_deinit(&s->base); +} diff --git a/spa/plugins/vulkan/vulkan-compute-utils.h b/spa/plugins/vulkan/vulkan-compute-utils.h new file mode 100644 index 0000000..9c79c55 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-utils.h @@ -0,0 +1,100 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include + +#include "vulkan-utils.h" + +#define MAX_STREAMS 2 +#define WORKGROUP_SIZE 32 + +struct pixel { + float r, g, b, a; +}; + +struct push_constants { + float time; + int frame; + int width; + int height; +}; + +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; + + uint32_t format; + + struct vulkan_buffer buffers[MAX_BUFFERS]; + struct spa_buffer *spa_buffers[MAX_BUFFERS]; + uint32_t n_buffers; +}; + +struct vulkan_compute_state { + struct spa_log *log; + + struct push_constants constants; + + struct vulkan_base base; + + struct vulkan_format_infos formatInfos; + + VkPipeline pipeline; + VkPipelineLayout pipelineLayout; + const char *shaderName; + VkShaderModule computeShaderModule; + + VkCommandPool commandPool; + VkCommandBuffer commandBuffer; + struct vulkan_staging_buffer staging_buffer; + + VkFence fence; + VkSemaphore pipelineSemaphore; + unsigned int initialized:1; + 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_compute_init_stream(struct vulkan_compute_state *s, struct vulkan_stream *stream, enum spa_direction, + struct spa_dict *props); + +int spa_vulkan_compute_fixate_modifier(struct vulkan_compute_state *s, struct vulkan_stream *p, struct spa_video_info_dsp *dsp_info, + uint32_t modifierCount, uint64_t *modifiers, uint64_t *modifier); +int spa_vulkan_compute_use_buffers(struct vulkan_compute_state *s, struct vulkan_stream *stream, uint32_t flags, + struct spa_video_info_dsp *dsp_info, uint32_t n_buffers, struct spa_buffer **buffers); +int spa_vulkan_compute_enumerate_formats(struct vulkan_compute_state *s, uint32_t index, uint32_t caps, + struct spa_pod **param, struct spa_pod_builder *builder); +int spa_vulkan_compute_prepare(struct vulkan_compute_state *s); +int spa_vulkan_compute_unprepare(struct vulkan_compute_state *s); + +int spa_vulkan_compute_start(struct vulkan_compute_state *s); +int spa_vulkan_compute_stop(struct vulkan_compute_state *s); +int spa_vulkan_compute_ready(struct vulkan_compute_state *s); +int spa_vulkan_compute_process(struct vulkan_compute_state *s); +int spa_vulkan_compute_cleanup(struct vulkan_compute_state *s); + +int spa_vulkan_compute_get_buffer_caps(struct vulkan_compute_state *s, enum spa_direction direction); +struct vulkan_modifier_info *spa_vulkan_compute_get_modifier_info(struct vulkan_compute_state *s, + struct spa_video_info_dsp *dsp_info); + +int spa_vulkan_compute_init(struct vulkan_compute_state *s); +void spa_vulkan_compute_deinit(struct vulkan_compute_state *s); diff --git a/spa/plugins/vulkan/vulkan-types.h b/spa/plugins/vulkan/vulkan-types.h new file mode 100644 index 0000000..e8b7ff5 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-types.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include +#include + +#define MAX_BUFFERS 16 +#define DMABUF_MAX_PLANES 1 + +enum buffer_type_caps { + VULKAN_BUFFER_TYPE_CAP_SHM = 1<<0, + VULKAN_BUFFER_TYPE_CAP_DMABUF = 1<<1, +}; + +struct vulkan_modifier_info { + VkDrmFormatModifierPropertiesEXT props; + VkExtent2D max_extent; +}; + +struct vulkan_format_info { + uint32_t spa_format; + VkFormat vk_format; + uint32_t modifierCount; + struct vulkan_modifier_info *infos; +}; + +struct vulkan_format_infos { + uint32_t formatCount; + struct vulkan_format_info *infos; + + uint32_t formatsWithModifiersCount; +}; + +struct vulkan_buffer { + int fd; + VkImage image; + VkImageView view; + VkDeviceMemory memory; + VkSemaphore foreign_semaphore; +}; + +struct vulkan_staging_buffer { + VkBuffer buffer; + VkDeviceMemory memory; +}; + +struct vulkan_base_info { + uint32_t queueFlags; +}; + +struct vulkan_base { + struct spa_log *log; + + VkInstance instance; + + VkPhysicalDevice physicalDevice; + + VkQueue queue; + uint32_t queueFamilyIndex; + VkDevice device; + + bool implicit_sync_interop; + + unsigned int initialized:1; +}; diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c new file mode 100644 index 0000000..6432313 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-utils.c @@ -0,0 +1,996 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "vulkan-utils.h" +#include "dmabuf.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; + } +} + +static struct { + VkFormat format; + uint32_t id; +} vk_video_format_convs[] = { + { VK_FORMAT_R32G32B32A32_SFLOAT, SPA_VIDEO_FORMAT_RGBA_F32 }, + { VK_FORMAT_B8G8R8A8_SRGB, SPA_VIDEO_FORMAT_BGRA }, + { VK_FORMAT_R8G8B8A8_SRGB, SPA_VIDEO_FORMAT_RGBA }, + { VK_FORMAT_B8G8R8A8_SRGB, SPA_VIDEO_FORMAT_BGRx }, + { VK_FORMAT_R8G8B8A8_SRGB, SPA_VIDEO_FORMAT_RGBx }, + { VK_FORMAT_B8G8R8_SRGB, SPA_VIDEO_FORMAT_BGR }, + { VK_FORMAT_R8G8B8_SRGB, SPA_VIDEO_FORMAT_RGB }, +}; + +static int createInstance(struct vulkan_base *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 int findPhysicalDevice(struct vulkan_base *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]; + + return 0; +} + +static int getComputeQueueFamilyIndex(struct vulkan_base *s, uint32_t queueFlags, uint32_t *queueFamilyIndex) +{ + 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 & queueFlags) == queueFlags)) + break; + } + if (i == queueFamilyCount) + return -ENODEV; + + *queueFamilyIndex = i; + return 0; +} + +static int createDevice(struct vulkan_base *s, struct vulkan_base_info *info) +{ + + CHECK(getComputeQueueFamilyIndex(s, info->queueFlags, &s->queueFamilyIndex)); + + const VkDeviceQueueCreateInfo queueCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .queueFamilyIndex = s->queueFamilyIndex, + .queueCount = 1, + .pQueuePriorities = (const float[]) { 1.0f } + }; + const VkPhysicalDeviceSynchronization2FeaturesKHR sync2_features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR, + .synchronization2 = VK_TRUE, + }; + static const char * const extensions[] = { + VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME, + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, + VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME, + VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, + VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, + VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME, + VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, + }; + + const VkDeviceCreateInfo deviceCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &queueCreateInfo, + .enabledExtensionCount = SPA_N_ELEMENTS(extensions), + .ppEnabledExtensionNames = extensions, + .pNext = &sync2_features, + }; + + VK_CHECK_RESULT(vkCreateDevice(s->physicalDevice, &deviceCreateInfo, NULL, &s->device)); + + vkGetDeviceQueue(s->device, s->queueFamilyIndex, 0, &s->queue); + + return 0; +} + +int vulkan_write_pixels(struct vulkan_base *s, struct vulkan_write_pixels_info *info, struct vulkan_staging_buffer *vk_sbuf) +{ + void *vmap; + VK_CHECK_RESULT(vkMapMemory(s->device, vk_sbuf->memory, 0, VK_WHOLE_SIZE, 0, &vmap)); + + // upload data + memcpy(vmap, info->data, info->stride * info->size.height); + + info->copies[0] = (VkBufferImageCopy) { + .imageExtent.width = info->size.width, + .imageExtent.height = info->size.height, + .imageExtent.depth = 1, + .imageOffset.x = 0, + .imageOffset.y = 0, + .imageOffset.z = 0, + .bufferOffset = 0, + .bufferRowLength = info->size.width, + .bufferImageHeight = info->size.height, + .imageSubresource.mipLevel = 0, + .imageSubresource.baseArrayLayer = 0, + .imageSubresource.layerCount = 1, + .imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + }; + + vkUnmapMemory(s->device, vk_sbuf->memory); + + return 0; +} + +int vulkan_read_pixels(struct vulkan_base *s, struct vulkan_read_pixels_info *info, struct vulkan_buffer *vk_buf) +{ + VkImageSubresource img_sub_res = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .arrayLayer = 0, + .mipLevel = 0, + }; + VkSubresourceLayout img_sub_layout; + vkGetImageSubresourceLayout(s->device, vk_buf->image, &img_sub_res, &img_sub_layout); + + void *v; + VK_CHECK_RESULT(vkMapMemory(s->device, vk_buf->memory, 0, VK_WHOLE_SIZE, 0, &v)); + + const char *d = (const char *)v + img_sub_layout.offset; + unsigned char *p = (unsigned char *)info->data + info->offset; + uint32_t pack_stride = img_sub_layout.rowPitch; + spa_log_trace_fp(s->log, "Read pixels: %p to %p, stride: %d, width %d, height %d, offset %d, pack_stride %d", d, p, info->stride, info->size.width, info->size.height, info->offset, pack_stride); + if (pack_stride == info->stride) { + memcpy(p, d, info->stride * info->size.height); + } else { + for (uint32_t i = 0; i < info->size.height; i++) { + memcpy(p + i * info->stride, d + i * pack_stride, info->size.width * info->bytes_per_pixel); + } + } + vkUnmapMemory(s->device, vk_buf->memory); + return 0; +} + +int vulkan_sync_foreign_dmabuf(struct vulkan_base *s, struct vulkan_buffer *vk_buf) +{ + if (!s->implicit_sync_interop) { + return vulkan_buffer_wait_dmabuf_fence(s, vk_buf); + } + + return vulkan_buffer_import_implicit_syncfd(s, vk_buf); +} + +bool vulkan_sync_export_dmabuf(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd) +{ + return vulkan_buffer_set_implicit_syncfd(s, vk_buf, sync_file_fd); +} + +int vulkan_fence_create(struct vulkan_base *s, VkFence *fence) +{ + VkFenceCreateInfo createInfo = { + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .flags = 0, + }; + VK_CHECK_RESULT(vkCreateFence(s->device, &createInfo, NULL, fence)); + + return 0; +} + +int vulkan_commandPool_create(struct vulkan_base *s, VkCommandPool *commandPool) +{ + 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, + commandPool)); + + return 0; +} + +int vulkan_commandBuffer_create(struct vulkan_base *s, VkCommandPool commandPool, VkCommandBuffer *commandBuffer) +{ + const VkCommandBufferAllocateInfo commandBufferAllocateInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VK_CHECK_RESULT(vkAllocateCommandBuffers(s->device, + &commandBufferAllocateInfo, + commandBuffer)); + + return 0; +} + +uint32_t vulkan_memoryType_find(struct vulkan_base *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; +} + +struct vulkan_format_info *vulkan_formatInfo_find(struct vulkan_format_infos *fmtInfo, VkFormat format) +{ + for (uint32_t i = 0; i < fmtInfo->formatCount; i++) { + if (fmtInfo->infos[i].vk_format == format) + return &fmtInfo->infos[i]; + } + return NULL; +} + +struct vulkan_modifier_info *vulkan_modifierInfo_find(struct vulkan_format_infos *fmtInfo, VkFormat format, uint64_t mod) +{ + struct vulkan_format_info *f_info = vulkan_formatInfo_find(fmtInfo, format); + if (!f_info) + return NULL; + for (uint32_t i = 0; i < f_info->modifierCount; i++) { + if (f_info->infos[i].props.drmFormatModifier == mod) + return &f_info->infos[i]; + } + return NULL; +} + +int vulkan_buffer_get_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf) +{ + if (!s->implicit_sync_interop) + return -1; + + return dmabuf_export_sync_file(s->log, vk_buf->fd, DMA_BUF_SYNC_READ); +} + +bool vulkan_buffer_set_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd) +{ + if (!s->implicit_sync_interop) + return false; + + return dmabuf_import_sync_file(s->log, vk_buf->fd, DMA_BUF_SYNC_WRITE, sync_file_fd); +} + +int vulkan_buffer_import_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf) +{ + int sync_file_fd = vulkan_buffer_get_implicit_syncfd(s, vk_buf); + if (sync_file_fd < 0) { + spa_log_error(s->log, "Failed to extract for DMA-BUF fence"); + return -1; + } + return vulkan_buffer_import_syncfd(s, vk_buf, sync_file_fd); +} + +int vulkan_buffer_wait_dmabuf_fence(struct vulkan_base *s, struct vulkan_buffer *vk_buf) +{ + struct pollfd pollfd = { + .fd = vk_buf->fd, + .events = POLLIN, + }; + int timeout_ms = 1000; + int ret = poll(&pollfd, 1, timeout_ms); + if (ret < 0) { + spa_log_error(s->log, "Failed to wait for DMA-BUF fence"); + return -1; + } else if (ret == 0) { + spa_log_error(s->log, "Timed out waiting for DMA-BUF fence"); + return -1; + } + return 0; +} + +int vulkan_buffer_import_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd) +{ + VULKAN_INSTANCE_FUNCTION(vkImportSemaphoreFdKHR); + + if (vk_buf->foreign_semaphore == VK_NULL_HANDLE) { + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VK_CHECK_RESULT_WITH_CLEANUP(vkCreateSemaphore(s->device, &semaphore_info, NULL, &vk_buf->foreign_semaphore), close(sync_file_fd)); + } + + VkImportSemaphoreFdInfoKHR import_info = { + .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR, + .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + .flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT_KHR, + .semaphore = vk_buf->foreign_semaphore, + .fd = sync_file_fd, + }; + VK_CHECK_RESULT_WITH_CLEANUP(vkImportSemaphoreFdKHR(s->device, &import_info), close(sync_file_fd)); + + return 0; +} + +void vulkan_buffer_clear(struct vulkan_base *s, struct vulkan_buffer *buffer) +{ + if (buffer->fd != -1) + close(buffer->fd); + vkFreeMemory(s->device, buffer->memory, NULL); + vkDestroyImage(s->device, buffer->image, NULL); + vkDestroyImageView(s->device, buffer->view, NULL); +} + +static VkImageAspectFlagBits mem_plane_aspect(uint32_t i) +{ + switch (i) { + case 0: return VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT; + case 1: return VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT; + case 2: return VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT; + case 3: return VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT; + default: abort(); // unreachable + } +} + +int vulkan_staging_buffer_create(struct vulkan_base *s, uint32_t size, struct vulkan_staging_buffer *s_buf) { + VkBufferCreateInfo buf_info = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = size, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + VK_CHECK_RESULT(vkCreateBuffer(s->device, &buf_info, NULL, &s_buf->buffer)); + + VkMemoryRequirements memoryRequirements; + vkGetBufferMemoryRequirements(s->device, s_buf->buffer, &memoryRequirements); + + VkMemoryAllocateInfo mem_info = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memoryRequirements.size, + .memoryTypeIndex = vulkan_memoryType_find(s, + memoryRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), + }; + VK_CHECK_RESULT(vkAllocateMemory(s->device, &mem_info, NULL, &s_buf->memory)); + VK_CHECK_RESULT(vkBindBufferMemory(s->device, s_buf->buffer, s_buf->memory, 0)); + + return 0; +} + +void vulkan_staging_buffer_destroy(struct vulkan_base *s, struct vulkan_staging_buffer *s_buf) { + if (s_buf->buffer == VK_NULL_HANDLE) + return; + vkFreeMemory(s->device, s_buf->memory, NULL); + vkDestroyBuffer(s->device, s_buf->buffer, NULL); +} + +static int allocate_dmabuf(struct vulkan_base *s, VkFormat format, uint32_t modifierCount, uint64_t *modifiers, VkImageUsageFlags usage, struct spa_rectangle *size, struct vulkan_buffer *vk_buf) +{ + VkImageDrmFormatModifierListCreateInfoEXT imageDrmFormatModifierListCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT, + .drmFormatModifierCount = modifierCount, + .pDrmFormatModifiers = modifiers, + }; + + VkExternalMemoryImageCreateInfo extMemoryImageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + extMemoryImageCreateInfo.pNext = &imageDrmFormatModifierListCreateInfo; + + VkImageCreateInfo imageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent.width = size->width, + .extent.height = size->height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, + .usage = usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + imageCreateInfo.pNext = &extMemoryImageCreateInfo; + + VK_CHECK_RESULT(vkCreateImage(s->device, + &imageCreateInfo, NULL, &vk_buf->image)); + + VkMemoryRequirements memoryRequirements = {0}; + vkGetImageMemoryRequirements(s->device, + vk_buf->image, &memoryRequirements); + + VkExportMemoryAllocateInfo exportAllocateInfo = { + .sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + VkMemoryAllocateInfo allocateInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memoryRequirements.size, + .memoryTypeIndex = vulkan_memoryType_find(s, + memoryRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT), + }; + allocateInfo.pNext = &exportAllocateInfo; + + VK_CHECK_RESULT(vkAllocateMemory(s->device, + &allocateInfo, NULL, &vk_buf->memory)); + + VK_CHECK_RESULT(vkBindImageMemory(s->device, + vk_buf->image, vk_buf->memory, 0)); + + return 0; +} + +int vulkan_validate_dmabuf_properties(const struct vulkan_modifier_info *modInfo, uint32_t *planeCount, struct spa_rectangle *dim) +{ + if (planeCount) { + if (*planeCount != modInfo->props.drmFormatModifierPlaneCount) + return -1; + } + if (dim) { + if (dim->width > modInfo->max_extent.width || dim->height > modInfo->max_extent.height) + return -1; + } + return 0; +} + +int vulkan_fixate_modifier(struct vulkan_base *s, struct dmabuf_fixation_info *info, uint64_t *modifier) +{ + VULKAN_INSTANCE_FUNCTION(vkGetImageDrmFormatModifierPropertiesEXT); + + struct vulkan_buffer vk_buf; + vk_buf.fd = -1; + vk_buf.view = VK_NULL_HANDLE; + VK_CHECK_RESULT(allocate_dmabuf(s, info->format, info->modifierCount, info->modifiers, info->usage, &info->size, &vk_buf)); + + VkImageDrmFormatModifierPropertiesEXT mod_prop = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT, + }; + VK_CHECK_RESULT(vkGetImageDrmFormatModifierPropertiesEXT(s->device, vk_buf.image, &mod_prop)); + + *modifier = mod_prop.drmFormatModifier; + + vulkan_buffer_clear(s, &vk_buf); + + return 0; +} + +int vulkan_create_dmabuf(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf) +{ + VULKAN_INSTANCE_FUNCTION(vkGetMemoryFdKHR); + + if (info->spa_buf->n_datas != 1) + return -1; + + VK_CHECK_RESULT(allocate_dmabuf(s, info->format, 1, &info->modifier, info->usage, &info->size, vk_buf)); + + const VkMemoryGetFdInfoKHR getFdInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, + .memory = vk_buf->memory, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT + }; + int fd = -1; + VK_CHECK_RESULT(vkGetMemoryFdKHR(s->device, &getFdInfo, &fd)); + + VkMemoryRequirements memoryRequirements = {0}; + vkGetImageMemoryRequirements(s->device, + vk_buf->image, &memoryRequirements); + + spa_log_info(s->log, "export DMABUF %" PRIu64, memoryRequirements.size); + + for (uint32_t i = 0; i < info->spa_buf->n_datas; i++) { + VkImageSubresource subresource = { + .aspectMask = mem_plane_aspect(i), + }; + VkSubresourceLayout subresLayout = {0}; + vkGetImageSubresourceLayout(s->device, vk_buf->image, &subresource, &subresLayout); + + info->spa_buf->datas[i].type = SPA_DATA_DmaBuf; + info->spa_buf->datas[i].fd = fd; + info->spa_buf->datas[i].flags = SPA_DATA_FLAG_READABLE; + info->spa_buf->datas[i].mapoffset = 0; + info->spa_buf->datas[i].chunk->offset = subresLayout.offset; + info->spa_buf->datas[i].chunk->stride = subresLayout.rowPitch; + info->spa_buf->datas[i].chunk->size = subresLayout.size; + info->spa_buf->datas[i].maxsize = memoryRequirements.size; + } + vk_buf->fd = fd; + + VkImageViewCreateInfo viewInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = vk_buf->image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = info->format, + .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, &vk_buf->view)); + return 0; +} + +int vulkan_import_dmabuf(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf) +{ + + if (info->spa_buf->n_datas == 0 || info->spa_buf->n_datas > DMABUF_MAX_PLANES) + return -1; + + uint32_t planeCount = info->spa_buf->n_datas; + + VkSubresourceLayout planeLayouts[DMABUF_MAX_PLANES] = {0}; + for (uint32_t i = 0; i < planeCount; i++) { + planeLayouts[i].offset = info->spa_buf->datas[i].chunk->offset; + planeLayouts[i].rowPitch = info->spa_buf->datas[i].chunk->stride; + planeLayouts[i].size = 0; + } + + VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT, + .drmFormatModifierPlaneCount = planeCount, + .drmFormatModifier = info->modifier, + .pPlaneLayouts = planeLayouts, + }; + + VkExternalMemoryImageCreateInfo extInfo = { + .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + .pNext = &modInfo, + }; + + VkImageCreateInfo imageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = info->format, + .extent.width = info->size.width, + .extent.height = info->size.height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, + .usage = info->usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .pNext = &extInfo, + }; + + VK_CHECK_RESULT(vkCreateImage(s->device, + &imageCreateInfo, NULL, &vk_buf->image)); + + VkMemoryRequirements memoryRequirements; + vkGetImageMemoryRequirements(s->device, + vk_buf->image, &memoryRequirements); + + vk_buf->fd = fcntl(info->spa_buf->datas[0].fd, F_DUPFD_CLOEXEC, 0); + VkImportMemoryFdInfoKHR importInfo = { + .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + .fd = fcntl(info->spa_buf->datas[0].fd, F_DUPFD_CLOEXEC, 0), + }; + + VkMemoryAllocateInfo allocateInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memoryRequirements.size, + .memoryTypeIndex = vulkan_memoryType_find(s, + memoryRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT), + }; + allocateInfo.pNext = &importInfo; + + spa_log_info(s->log, "import DMABUF"); + + VK_CHECK_RESULT(vkAllocateMemory(s->device, + &allocateInfo, NULL, &vk_buf->memory)); + VK_CHECK_RESULT(vkBindImageMemory(s->device, + vk_buf->image, vk_buf->memory, 0)); + + VkImageViewCreateInfo viewInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = vk_buf->image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = info->format, + .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, &vk_buf->view)); + return 0; +} + +int vulkan_import_memptr(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf) +{ + VkImageCreateInfo imageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = info->format, + .extent.width = info->size.width, + .extent.height = info->size.height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_LINEAR, + .usage = info->usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + + VK_CHECK_RESULT(vkCreateImage(s->device, + &imageCreateInfo, NULL, &vk_buf->image)); + + VkMemoryRequirements memoryRequirements; + vkGetImageMemoryRequirements(s->device, + vk_buf->image, &memoryRequirements); + + VkMemoryAllocateInfo allocateInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memoryRequirements.size, + .memoryTypeIndex = vulkan_memoryType_find(s, + memoryRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), + }; + + vk_buf->fd = -1; + spa_log_info(s->log, "import MemPtr"); + + VK_CHECK_RESULT(vkAllocateMemory(s->device, + &allocateInfo, NULL, &vk_buf->memory)); + VK_CHECK_RESULT(vkBindImageMemory(s->device, + vk_buf->image, vk_buf->memory, 0)); + + VkImageViewCreateInfo viewInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = vk_buf->image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = info->format, + .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, &vk_buf->view)); + return 0; +} + +uint32_t vulkan_vkformat_to_id(VkFormat format) +{ + SPA_FOR_EACH_ELEMENT_VAR(vk_video_format_convs, f) { + if (f->format == format) + return f->id; + } + return SPA_VIDEO_FORMAT_UNKNOWN; +} + +VkFormat vulkan_id_to_vkformat(uint32_t id) +{ + SPA_FOR_EACH_ELEMENT_VAR(vk_video_format_convs, f) { + if (f->id == id) + return f->format; + } + return VK_FORMAT_UNDEFINED; +} + +int vulkan_vkresult_to_errno(VkResult result) +{ + return vkresult_to_errno(result); +} + +int vulkan_wait_fence(struct vulkan_base *s, VkFence fence) +{ + VK_CHECK_RESULT(vkWaitForFences(s->device, 1, &fence, VK_TRUE, UINT64_MAX)); + + return 0; +} + +int vulkan_wait_idle(struct vulkan_base *s) +{ + VK_CHECK_RESULT(vkDeviceWaitIdle(s->device)); + + return 0; +} + +int vulkan_format_infos_init(struct vulkan_base *s, uint32_t formatCount, uint32_t *formats, + struct vulkan_format_infos *info) +{ + if (info->infos) + return 0; + + + info->infos = calloc(formatCount, sizeof(struct vulkan_format_info)); + if (!info->infos) + return -ENOMEM; + + uint32_t i; + for (i = 0; i < formatCount; i++) { + VkFormat format = vulkan_id_to_vkformat(formats[i]); + if (format == VK_FORMAT_UNDEFINED) + continue; + struct vulkan_format_info *f_info = &info->infos[info->formatCount++]; + f_info->spa_format = formats[i]; + f_info->vk_format = format; + spa_log_info(s->log, "Adding format %d (spa_format %d)", format, formats[i]); + + VkDrmFormatModifierPropertiesListEXT modPropsList = { + .sType = VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT, + }; + VkFormatProperties2 fmtProps = { + .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2, + .pNext = &modPropsList, + }; + vkGetPhysicalDeviceFormatProperties2(s->physicalDevice, format, &fmtProps); + + if (modPropsList.drmFormatModifierCount == 0) { + spa_log_info(s->log, "Format has no modifiers"); + continue; + } + + modPropsList.pDrmFormatModifierProperties = calloc(modPropsList.drmFormatModifierCount, + sizeof(modPropsList.pDrmFormatModifierProperties[0])); + if (!modPropsList.pDrmFormatModifierProperties) { + spa_log_info(s->log, "Failed to allocate DrmFormatModifierProperties"); + continue; + } + vkGetPhysicalDeviceFormatProperties2(s->physicalDevice, format, &fmtProps); + + f_info->infos = calloc(modPropsList.drmFormatModifierCount, sizeof(f_info->infos[0])); + if (!f_info->infos) { + spa_log_info(s->log, "Failed to allocate modifier infos"); + free(modPropsList.pDrmFormatModifierProperties); + continue; + } + + spa_log_info(s->log, "Found %d modifiers", modPropsList.drmFormatModifierCount); + for (uint32_t j = 0; j < modPropsList.drmFormatModifierCount; j++) { + VkDrmFormatModifierPropertiesEXT props = modPropsList.pDrmFormatModifierProperties[j]; + + if (!(props.drmFormatModifierTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) + continue; + + if (props.drmFormatModifierPlaneCount > DMABUF_MAX_PLANES) + continue; + + VkPhysicalDeviceImageDrmFormatModifierInfoEXT modInfo = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT, + .drmFormatModifier = props.drmFormatModifier, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + VkPhysicalDeviceExternalImageFormatInfo extImgFmtInfo = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO, + .pNext = &modInfo, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + VkPhysicalDeviceImageFormatInfo2 imgFmtInfo = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, + .pNext = &extImgFmtInfo, + .type = VK_IMAGE_TYPE_2D, + .format = format, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, + }; + + VkExternalImageFormatProperties extImgFmtProps = { + .sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES, + }; + VkImageFormatProperties2 imgFmtProps = { + .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, + .pNext = &extImgFmtProps, + }; + + VK_CHECK_RESULT_LOOP(vkGetPhysicalDeviceImageFormatProperties2(s->physicalDevice, &imgFmtInfo, &imgFmtProps)) + + VkExternalMemoryFeatureFlags extMemFeatures = + extImgFmtProps.externalMemoryProperties.externalMemoryFeatures; + if (!(extMemFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT)) { + continue; + } + + VkExtent3D max_extent = imgFmtProps.imageFormatProperties.maxExtent; + f_info->infos[f_info->modifierCount++] = (struct vulkan_modifier_info){ + .props = props, + .max_extent = { .width = max_extent.width, .height = max_extent.height }, + }; + spa_log_info(s->log, "Adding modifier %"PRIu64, props.drmFormatModifier); + + } + free(modPropsList.pDrmFormatModifierProperties); + } + for (i = 0; i < info->formatCount; i++) { + if (info->infos[i].modifierCount > 0) + info->formatsWithModifiersCount++; + } + return 0; +} + +void vulkan_format_infos_deinit(struct vulkan_format_infos *info) +{ + for (uint32_t i = 0; i < info->formatCount; i++) { + free(info->infos[i].infos); + } + free(info->infos); +} + +int vulkan_base_init(struct vulkan_base *s, struct vulkan_base_info *info) +{ + if (!s->initialized) { + CHECK(createInstance(s)); + CHECK(findPhysicalDevice(s)); + CHECK(createDevice(s, info)); + s->implicit_sync_interop = dmabuf_check_sync_file_import_export(s->log); + s->initialized = true; + } + return 0; +} + +void vulkan_base_deinit(struct vulkan_base *s) +{ + if (s->initialized) { + vkDestroyDevice(s->device, NULL); + vkDestroyInstance(s->instance, NULL); + s->initialized = false; + } +} diff --git a/spa/plugins/vulkan/vulkan-utils.h b/spa/plugins/vulkan/vulkan-utils.h new file mode 100644 index 0000000..60b0923 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-utils.h @@ -0,0 +1,126 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#pragma once + +#include + +#include +#include + +#include "vulkan-types.h" + +#define VK_CHECK_RESULT(f) \ +{ \ + VkResult _result = (f); \ + int _r = -vulkan_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 VK_CHECK_RESULT_WITH_CLEANUP(f, c) \ +{ \ + 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)); \ + (c); \ + return _r; \ + } \ +} +#define VK_CHECK_RESULT_LOOP(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)); \ + continue; \ + } \ +} +#define CHECK(f) \ +{ \ + int _res = (f); \ + if (_res < 0) \ + return _res; \ +} + +struct vulkan_write_pixels_info { + struct spa_rectangle size; + uint32_t offset; + uint32_t stride; + uint32_t bytes_per_pixel; + + VkBufferImageCopy *copies; + void *data; +}; + +struct vulkan_read_pixels_info { + struct spa_rectangle size; + void *data; + uint32_t offset; + uint32_t stride; + uint32_t bytes_per_pixel; +}; + +struct dmabuf_fixation_info { + VkFormat format; + uint64_t modifierCount; + uint64_t *modifiers; + struct spa_rectangle size; + VkImageUsageFlags usage; +}; + +struct external_buffer_info { + VkFormat format; + uint64_t modifier; + struct spa_rectangle size; + VkImageUsageFlags usage; + struct spa_buffer *spa_buf; +}; + +int vulkan_write_pixels(struct vulkan_base *s, struct vulkan_write_pixels_info *info, struct vulkan_staging_buffer *vk_sbuf); +int vulkan_read_pixels(struct vulkan_base *s, struct vulkan_read_pixels_info *info, struct vulkan_buffer *vk_buf); + +int vulkan_sync_foreign_dmabuf(struct vulkan_base *s, struct vulkan_buffer *vk_buf); +bool vulkan_sync_export_dmabuf(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd); + +int vulkan_staging_buffer_create(struct vulkan_base *s, uint32_t size, struct vulkan_staging_buffer *s_buf); +void vulkan_staging_buffer_destroy(struct vulkan_base *s, struct vulkan_staging_buffer *s_buf); + +int vulkan_validate_dmabuf_properties(const struct vulkan_modifier_info *modInfo, uint32_t *planeCount, struct spa_rectangle *dim); +int vulkan_fixate_modifier(struct vulkan_base *s, struct dmabuf_fixation_info *info, uint64_t *modifier); +int vulkan_create_dmabuf(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf); +int vulkan_import_dmabuf(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf); +int vulkan_import_memptr(struct vulkan_base *s, struct external_buffer_info *info, struct vulkan_buffer *vk_buf); + +int vulkan_fence_create(struct vulkan_base *s, VkFence *fence); +int vulkan_commandPool_create(struct vulkan_base *s, VkCommandPool *commandPool); +int vulkan_commandBuffer_create(struct vulkan_base *s, VkCommandPool commandPool, VkCommandBuffer *commandBuffer); + +uint32_t vulkan_memoryType_find(struct vulkan_base *s, + uint32_t memoryTypeBits, VkMemoryPropertyFlags properties); +struct vulkan_format_info *vulkan_formatInfo_find(struct vulkan_format_infos *fmtInfo, VkFormat format); +struct vulkan_modifier_info *vulkan_modifierInfo_find(struct vulkan_format_infos *fmtInfo, VkFormat format, uint64_t modifier); + +int vulkan_buffer_get_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf); +bool vulkan_buffer_set_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd); +int vulkan_buffer_import_implicit_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf); +int vulkan_buffer_wait_dmabuf_fence(struct vulkan_base *s, struct vulkan_buffer *vk_buf); +int vulkan_buffer_import_syncfd(struct vulkan_base *s, struct vulkan_buffer *vk_buf, int sync_file_fd); +void vulkan_buffer_clear(struct vulkan_base *s, struct vulkan_buffer *buffer); + +uint32_t vulkan_vkformat_to_id(VkFormat vkFormat); +VkFormat vulkan_id_to_vkformat(uint32_t id); + +int vulkan_vkresult_to_errno(VkResult result); + +int vulkan_wait_fence(struct vulkan_base *s, VkFence fence); +int vulkan_wait_idle(struct vulkan_base *s); + +int vulkan_format_infos_init(struct vulkan_base *s, uint32_t formatCount, uint32_t *formats, + struct vulkan_format_infos *info); +void vulkan_format_infos_deinit(struct vulkan_format_infos *info); +int vulkan_base_init(struct vulkan_base *s, struct vulkan_base_info *info); +void vulkan_base_deinit(struct vulkan_base *s); diff --git a/spa/tests/benchmark-dict.c b/spa/tests/benchmark-dict.c new file mode 100644 index 0000000..2fd3c44 --- /dev/null +++ b/spa/tests/benchmark-dict.c @@ -0,0 +1,125 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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..9979228 --- /dev/null +++ b/spa/tests/benchmark-pod.c @@ -0,0 +1,274 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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..6fb7dd4 --- /dev/null +++ b/spa/tests/stress-ringbuffer.c @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define DEFAULT_SIZE 0x2000 +#define ARRAY_SIZE 63 +#define MAX_VALUE 0x10000 + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) || defined (__GNU__) +#include +#if (__FreeBSD_version >= 1400000 && __FreeBSD_version < 1400043) \ + || (__FreeBSD_version < 1300523) || defined(__MidnightBSD__) \ + || defined (__GNU__) +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..9508e65 --- /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) + +spa_json_dump_exe = 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..fd562b5 --- /dev/null +++ b/spa/tools/spa-inspect.c @@ -0,0 +1,299 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 \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..a1f2e61 --- /dev/null +++ b/spa/tools/spa-json-dump.c @@ -0,0 +1,226 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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; + bool toplevel = false; + int count = 0, res; + char key[1024]; + + if (!value) { + toplevel = true; + value = "{"; + len = 1; + } + + 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, ""); + if ((res = dump(file, indent+2, &sub, value, len)) < 0) + return res; + } + fprintf(file, "%s%*s]", count > 0 ? "\n" : "", + count > 0 ? indent : 0, ""); + } else if (spa_json_is_object(value, len)) { + fprintf(file, "{"); + if (!toplevel) + spa_json_enter(it, &sub); + else + sub = *it; + while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { + fprintf(file, "%s\n%*s", + count++ > 0 ? "," : "", + indent+2, ""); + encode_string(file, key, strlen(key)); + fprintf(file, ": "); + res = dump(file, indent+2, &sub, value, len); + if (res < 0) { + if (toplevel) + *it = sub; + return res; + } + } + if (toplevel) + *it = sub; + 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); + } + + if (spa_json_get_error(it, NULL, NULL)) + return -EINVAL; + + return 0; +} + +static int process_json(const char *filename, void *buf, size_t size) +{ + int len, res; + struct spa_json it; + const char *value; + + if ((len = spa_json_begin(&it, buf, size, &value)) <= 0) { + fprintf(stderr, "not a valid file '%s': %s\n", filename, spa_strerror(len)); + return -EINVAL; + } + if (!spa_json_is_container(value, len)) { + spa_json_init(&it, buf, size); + value = NULL; + len = 0; + } + res = dump(stdout, 0, &it, value, len); + if (spa_json_next(&it, &value) < 0) + res = -EINVAL; + + fprintf(stdout, "\n"); + fflush(stdout); + + if (res < 0) { + struct spa_error_location loc; + + if (spa_json_get_error(&it, buf, &loc)) + spa_debug_file_error_location(stderr, &loc, + "syntax error in file '%s': %s", + filename, loc.reason); + else + fprintf(stderr, "error parsing file '%s': %s\n", filename, spa_strerror(res)); + + return -EINVAL; + } + return 0; +} + +static int process_stdin(void) +{ + uint8_t *buf = NULL, *p; + size_t alloc = 0, size = 0, read_size, res; + int err; + + do { + alloc += 1024 + alloc; + p = realloc(buf, alloc); + if (!p) { + fprintf(stderr, "error: %m\n"); + goto error; + } + buf = p; + + read_size = alloc - size; + res = fread(buf + size, 1, read_size, stdin); + size += res; + } while (res == read_size); + + if (ferror(stdin)) { + fprintf(stderr, "error: %m\n"); + goto error; + } + + err = process_json("-", buf, size); + free(buf); + + return (err == 0) ? EXIT_SUCCESS : EXIT_FAILURE; + +error: + free(buf); + return EXIT_FAILURE; +} + +int main(int argc, char *argv[]) +{ + int fd, res, exit_code = EXIT_FAILURE; + void *data; + struct stat sbuf; + + if (argc < 1) { + fprintf(stderr, "usage: %s [spa-json-file]\n", argv[0]); + goto error; + } + if (argc == 1) + return process_stdin(); + 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; + } + + res = process_json(argv[1], data, sbuf.st_size); + if (res < 0) + exit_code = EXIT_FAILURE; + else + exit_code = EXIT_SUCCESS; + + 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..0f4faba --- /dev/null +++ b/spa/tools/spa-monitor.c @@ -0,0 +1,209 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +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 \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.conf.avail/20-upmix.conf.in b/src/daemon/client.conf.avail/20-upmix.conf.in new file mode 100644 index 0000000..064eba1 --- /dev/null +++ b/src/daemon/client.conf.avail/20-upmix.conf.in @@ -0,0 +1,8 @@ +# Enables upmixing +stream.properties = { + channelmix.upmix = true + channelmix.upmix-method = psd # none, simple + channelmix.lfe-cutoff = 150 + channelmix.fc-cutoff = 12000 + channelmix.rear-delay = 12.0 +} diff --git a/src/daemon/client.conf.avail/meson.build b/src/daemon/client.conf.avail/meson.build new file mode 100644 index 0000000..503212b --- /dev/null +++ b/src/daemon/client.conf.avail/meson.build @@ -0,0 +1,12 @@ +conf_files = [ + '20-upmix.conf', +] + +foreach c : conf_files + res = configure_file(input : '@0@.in'.format(c), + output : c, + configuration : conf_config, + install_dir : pipewire_confdatadir / 'client.conf.avail') + test(f'validate-json-client-@c@', spa_json_dump_exe, args : res) +endforeach + diff --git a/src/daemon/client.conf.in b/src/daemon/client.conf.in new file mode 100644 index 0000000..46874af --- /dev/null +++ b/src/daemon/client.conf.in @@ -0,0 +1,148 @@ +# 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 = { + # = + # + # 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 + video.convert.* = videoconvert/libspa-videoconvert +} + +context.modules = [ + #{ name = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Loads a module with the given parameters. + # If ifexists is given, the module is ignored when it is not found. + # If nofail is given, module initialization failures are ignored. + # + # Uses realtime scheduling to boost the audio thread priorities + { name = libpipewire-module-rt + args = { + #rt.prio = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + } + flags = [ ifexists nofail ] + condition = [ { module.rt = !false } ] + } + + # 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 + condition = [ { module.client-node = !false } ] + } + + # Allows creating devices that run in the context of the + # client. Is used by the session manager. + { name = libpipewire-module-client-device + condition = [ { module.client-device = !false } ] + } + + # Makes a factory for wrapping nodes in an adapter with a + # converter and resampler. + { name = libpipewire-module-adapter + condition = [ { module.adapter = !false } ] + } + + # Allows applications to create metadata objects. It creates + # a factory for Metadata objects. + { name = libpipewire-module-metadata + condition = [ { module.metadata = !false } ] + } + + # Provides factories to make session manager objects. + { name = libpipewire-module-session-manager + condition = [ { module.session-manager = !false } ] + } +] + +filter.properties = { + #node.latency = 1024/48000 +} + +stream.properties = { + #node.latency = 1024/48000 + #node.autoconnect = true + #resample.quality = 4 + #channelmix.normalize = false + #channelmix.mix-lfe = true + #channelmix.upmix = true + #channelmix.upmix-method = psd # none, simple + #channelmix.lfe-cutoff = 150 + #channelmix.fc-cutoff = 12000 + #channelmix.rear-delay = 12.0 + #channelmix.stereo-widen = 0.0 + #channelmix.hilbert-taps = 0 + #dither.noise = 0 +} + +stream.rules = [ + { matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + #application.name = "pw-cat" + #node.name = "~Google Chrome$" + } + ] + actions = { + update-props = { + #node.latency = 512/48000 + } + } + } +] + +alsa.properties = { + #alsa.deny = false + # ALSA params take a single value, an array [] of values + # or a range { min=.. max=... } + #alsa.access = [ MMAP_INTERLEAVED MMAP_NONINTERLEAVED RW_INTERLEAVED RW_NONINTERLEAVED ] + #alsa.format = [ FLOAT S32 S24 S24_3 S16 U8 ] + #alsa.rate = { min=1 max=384000 } # or [ 44100 48000 .. ] + #alsa.channels = { min=1 max=64 } # or [ 2 4 6 .. ] + #alsa.period-bytes = { min=128 max=2097152 } # or [ 128 256 1024 .. ] + #alsa.buffer-bytes = { min=256 max=4194304 } # or [ 256 512 4096 .. ] + + #alsa.volume-method = cubic # linear, cubic +} + +# client specific properties +alsa.rules = [ + { matches = [ { application.process.binary = "resolve" } ] + actions = { + update-props = { + alsa.buffer-bytes = 131072 + } + } + } +] diff --git a/src/daemon/filter-chain.conf.in b/src/daemon/filter-chain.conf.in new file mode 100644 index 0000000..4808875 --- /dev/null +++ b/src/daemon/filter-chain.conf.in @@ -0,0 +1,63 @@ +# 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 = { + # = + # + # 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 = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Loads a module with the given parameters. + # If ifexists is given, the module is ignored when it is not found. + # If nofail is given, module initialization failures are ignored. + # + # Uses realtime scheduling to boost the audio thread priorities + { name = libpipewire-module-rt + args = { + #rt.prio = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + } + flags = [ ifexists nofail ] + } + + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # Allows creating nodes that run in the context of the + # client. Is used by all clients that want to provide + # data to PipeWire. + { name = libpipewire-module-client-node } + + # 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/35-ebur128.conf b/src/daemon/filter-chain/35-ebur128.conf new file mode 100644 index 0000000..f12c4a9 --- /dev/null +++ b/src/daemon/filter-chain/35-ebur128.conf @@ -0,0 +1,63 @@ +context.modules = [ + { name = libpipewire-module-filter-chain + args = { + node.description = "EBU R128 Normalizer" + media.name = "EBU R128 Normalizer" + filter.graph = { + nodes = [ + { + name = ebur128 + type = ebur128 + label = ebur128 + } + { + name = lufsL + type = ebur128 + label = lufs2gain + control = { + "Target LUFS" = -16.0 + } + } + { + name = lufsR + type = ebur128 + label = lufs2gain + control = { + "Target LUFS" = -16.0 + } + } + { + name = volumeL + type = builtin + label = linear + } + { + name = volumeR + type = builtin + label = linear + } + ] + links = [ + { output = "ebur128:Out FL" input = "volumeL:In" } + { output = "ebur128:Global LUFS" input = "lufsL:LUFS" } + { output = "lufsL:Gain" input = "volumeL:Mult" } + { output = "ebur128:Out FR" input = "volumeR:In" } + { output = "ebur128:Global LUFS" input = "lufsR:LUFS" } + { output = "lufsR:Gain" input = "volumeR:Mult" } + ] + inputs = [ "ebur128:In FL" "ebur128:In FR" ] + outputs = [ "volumeL:Out" "volumeR:Out" ] + } + capture.props = { + node.name = "effect_input.ebur128_normalize" + audio.position = [ FL FR ] + media.class = Audio/Sink + } + playback.props = { + node.name = "effect_output.ebur128_normalize" + audio.position = [ FL FR ] + node.passive = true + } + } + } +] diff --git a/src/daemon/filter-chain/36-dcblock.conf b/src/daemon/filter-chain/36-dcblock.conf new file mode 100644 index 0000000..b3b6feb --- /dev/null +++ b/src/daemon/filter-chain/36-dcblock.conf @@ -0,0 +1,59 @@ +context.modules = [ + { name = libpipewire-module-filter-chain + args = { + node.description = "DCBlock Filter" + media.name = "DCBlock Filter" + filter.graph = { + nodes = [ + { + name = dcblock + type = builtin + label = dcblock + control = { + "R" = 0.995 + } + } + { + # add a short 20ms ramp + name = ramp + type = builtin + label = ramp + control = { + "Start" = 0.0 + "Stop" = 1.0 + "Duration (s)" = 0.020 + } + } + { + name = volumeL + type = builtin + label = mult + } + { + name = volumeR + type = builtin + label = mult + } + ] + links = [ + { output = "dcblock:Out 1" input = "volumeL:In 1" } + { output = "dcblock:Out 2" input = "volumeR:In 1" } + { output = "ramp:Out" input = "volumeL:In 2" } + { output = "ramp:Out" input = "volumeR:In 2" } + ] + inputs = [ "dcblock:In 1" "dcblock:In 2" ] + outputs = [ "volumeL:Out" "volumeR:Out" ] + } + capture.props = { + node.name = "effect_input.dcblock" + audio.position = [ FL FR ] + media.class = Audio/Sink + } + playback.props = { + node.name = "effect_output.dcblock" + audio.position = [ FL FR ] + node.passive = true + } + } + } +] diff --git a/src/daemon/filter-chain/demonic.conf b/src/daemon/filter-chain/demonic.conf new file mode 100644 index 0000000..351d153 --- /dev/null +++ b/src/daemon/filter-chain/demonic.conf @@ -0,0 +1,64 @@ +# 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 + flags = [ nofail ] + 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..0e63406 --- /dev/null +++ b/src/daemon/filter-chain/meson.build @@ -0,0 +1,21 @@ +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' ], + [ 'sink-upmix-5.1-filter.conf', 'sink-upmix-5.1-filter.conf' ], +] + +foreach c : conf_files + res = configure_file(input : c.get(0), + output : c.get(1), + configuration : conf_config, + install_dir : pipewire_confdatadir / 'filter-chain') + test('validate-json-filter-chain-' + c.get(0), spa_json_dump_exe, args : res) +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..f513958 --- /dev/null +++ b/src/daemon/filter-chain/sink-dolby-surround.conf @@ -0,0 +1,47 @@ +# 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 + flags = [ nofail ] + 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..b2b66eb --- /dev/null +++ b/src/daemon/filter-chain/sink-matrix-spatialiser.conf @@ -0,0 +1,42 @@ +# Matrix Spatialiser sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# ( Jean-Philippe Guillemin ) +# + +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + 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-upmix-5.1-filter.conf b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf new file mode 100644 index 0000000..1977330 --- /dev/null +++ b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf @@ -0,0 +1,151 @@ +# Stereo to 5.1 upmix sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +context.modules = [ + { name = libpipewire-module-filter-chain + args = { + node.description = "Upmix Sink" + filter.graph = { + nodes = [ + { type = builtin name = copyFL label = copy } + { type = builtin name = copyFR label = copy } + { type = builtin name = copyOFL label = copy } + { type = builtin name = copyOFR label = copy } + { + # this mixes the front left and right together + # for filtering the center and subwoofer signal- + name = mixF + type = builtin + label = mixer + control = { + "Gain 1" = 0.707 + "Gain 2" = 0.707 + } + } + { + # filtering of the FC and LFE channel. We use a 2 channel + # parametric equalizer with custom filters for each channel. + # This makes it possible to run the filters in parallel. + type = builtin + name = eq_FC_LFE + label = param_eq + config = { + filters1 = [ + # FC is a crossover filter (with 2 lowpass biquads) + { type = bq_lowpass freq = 12000 }, + { type = bq_lowpass freq = 12000 }, + ] + filters2 = [ + # LFE is first a gain adjustment (with a highself) and + # then a crossover filter (with 2 lowpass biquads) + { type = bq_highshelf freq = 0 gain = -20.0 }, # gain -20dB + { type = bq_lowpass freq = 120 }, + { type = bq_lowpass freq = 120 }, + ] + } + } + { + # for the rear channels, we subtract the front channels. Do this + # with a mixer with negative gain to flip the sign. + name = subR + type = builtin + label = mixer + control = { + "Gain 1" = 0.707 + "Gain 2" = -0.707 + } + } + { + # a delay for the rear Left channel. This can be + # replaced with the convolver below. */ + type = builtin + name = delayRL + label = delay + config = { "max-delay" = 1 } + control = { "Delay (s)" = 0.012 } + } + { + # a delay for the rear Right channel. This can be + # replaced with the convolver below. */ + type = builtin + name = delayRR + label = delay + config = { "max-delay" = 1 } + control = { "Delay (s)" = 0.012 } + } + { + # an optional convolver with a hilbert curve to + # change the phase. It also has a delay, making the above + # left delay filter optional. + type = builtin + name = convRL + label = convolver + config = { + gain = 1.0 + delay = 0.012 + filename = "/hilbert" + length = 33 + } + } + { + # an optional convolver with a hilbert curve to + # change the phase. It also has a delay, making the above + # right delay filter optional. + type = builtin + name = convRR + label = convolver + config = { + gain = -1.0 + delay = 0.012 + filename = "/hilbert" + length = 33 + } + } + ] + links = [ + { output = "copyFL:Out" input="mixF:In 1" } + { output = "copyFR:Out" input="mixF:In 2" } + { output = "copyFL:Out" input="copyOFR:In" } + { output = "copyFR:Out" input="copyOFL:In" } + { output = "mixF:Out" input="eq_FC_LFE:In 1" } + { output = "mixF:Out" input="eq_FC_LFE:In 2" } + { output = "copyFL:Out" input="subR:In 1" } + { output = "copyFR:Out" input="subR:In 2" } + # here we can choose to just delay or also convolve + # + #{ output = "subR:Out" input="delayRL:In" } + #{ output = "subR:Out" input="delayRR:In" } + { output = "subR:Out" input="convRL:In" } + { output = "subR:Out" input="convRR:In" } + ] + inputs = [ "copyFL:In" "copyFR:In" ] + outputs = [ + "copyOFL:Out" + "copyOFR:Out" + "eq_FC_LFE:Out 1" + "eq_FC_LFE:Out 2" + # here we can choose to just delay or also convolve + # + #"delayRL:Out" + #"delayRR:Out" + "convRL:Out" + "convRR:Out" + ] + } + capture.props = { + node.name = "effect_input.upmix_5.1" + media.class = "Audio/Sink" + audio.position = [ FL FR ] + } + playback.props = { + node.name = "effect_output.upmix_5.1" + audio.position = [ FL FR FC LFE RL RR ] + stream.dont-remix = true + node.passive = true + } + } + } +] + diff --git a/src/daemon/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..ee3333d --- /dev/null +++ b/src/daemon/filter-chain/sink-virtual-surround-5.1-kemar.conf @@ -0,0 +1,180 @@ +# Convolver sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# Adjust the paths to the convolver files to match your system +# +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + 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..3630c23 --- /dev/null +++ b/src/daemon/filter-chain/sink-virtual-surround-7.1-hesuvi.conf @@ -0,0 +1,104 @@ +# Convolver sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# Adjust the paths to the convolver files to match your system +# +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + 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..f3c2c71 --- /dev/null +++ b/src/daemon/filter-chain/source-rnnoise.conf @@ -0,0 +1,44 @@ +# Noise canceling source +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# Adjust the paths to the rnnoise plugin to match your system +# +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + args = { + node.description = "Noise Canceling source" + media.name = "Noise Canceling source" + filter.graph = { + nodes = [ + { + type = ladspa + name = rnnoise + # The path to the plugin. The suffix .so is appended to + # this string and then the file is then located in the directories + # listed in the environment variable LADSPA_PATH or + # /usr/lib64/ladspa, /usr/lib/ladspa or the system library directory + # as a fallback. + # You might want to use an absolute path here to avoid problems. + 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/filter-chain/spatializer-7.1.conf b/src/daemon/filter-chain/spatializer-7.1.conf new file mode 100644 index 0000000..944ed62 --- /dev/null +++ b/src/daemon/filter-chain/spatializer-7.1.conf @@ -0,0 +1,160 @@ +# Headphone surround sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# Adjust the paths to the sofa file to match your system. +# +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + args = { + node.description = "Spatial Sink" + media.name = "Spatial Sink" + filter.graph = { + nodes = [ + { + type = sofa + label = spatializer + name = spFL + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 30.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spFR + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 330.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spFC + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 0.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spRL + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 150.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spRR + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 210.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spSL + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 90.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spSR + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 270.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + { + type = sofa + label = spatializer + name = spLFE + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 0.0 + "Elevation" = -60.0 + "Radius" = 3.0 + } + } + + { type = builtin label = mixer name = mixL } + { type = builtin label = mixer name = mixR } + ] + links = [ + # output + { output = "spFL:Out L" input="mixL:In 1" } + { output = "spFL:Out R" input="mixR:In 1" } + { output = "spFR:Out L" input="mixL:In 2" } + { output = "spFR:Out R" input="mixR:In 2" } + { output = "spFC:Out L" input="mixL:In 3" } + { output = "spFC:Out R" input="mixR:In 3" } + { output = "spRL:Out L" input="mixL:In 4" } + { output = "spRL:Out R" input="mixR:In 4" } + { output = "spRR:Out L" input="mixL:In 5" } + { output = "spRR:Out R" input="mixR:In 5" } + { output = "spSL:Out L" input="mixL:In 6" } + { output = "spSL:Out R" input="mixR:In 6" } + { output = "spSR:Out L" input="mixL:In 7" } + { output = "spSR:Out R" input="mixR:In 7" } + { output = "spLFE:Out L" input="mixL:In 8" } + { output = "spLFE:Out R" input="mixR:In 8" } + ] + inputs = [ "spFL:In" "spFR:In" "spFC:In" "spLFE:In" "spRL:In" "spRR:In", "spSL:In", "spSR:In" ] + outputs = [ "mixL:Out" "mixR:Out" ] + } + capture.props = { + node.name = "effect_input.spatializer" + media.class = Audio/Sink + audio.channels = 8 + audio.position = [ FL FR FC LFE RL RR SL SR ] + } + playback.props = { + node.name = "effect_output.spatializer" + node.passive = true + audio.channels = 2 + audio.position = [ FL FR ] + } + } + } +] diff --git a/src/daemon/filter-chain/spatializer-single.conf b/src/daemon/filter-chain/spatializer-single.conf new file mode 100644 index 0000000..1f5c288 --- /dev/null +++ b/src/daemon/filter-chain/spatializer-single.conf @@ -0,0 +1,48 @@ +# A virtual sound source sink +# Useful for testing spatial effects by moving it around with controls +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +# Adjust the paths to the sofa files to match your system +# +context.modules = [ + { name = libpipewire-module-filter-chain + flags = [ nofail ] + args = { + node.description = "3D Sink" + media.name = "3D Sink" + filter.graph = { + nodes = [ + { + type = sofa + label = spatializer + name = sp + config = { + filename = "~/.config/hrtf-sofa/hrtf b_nh724.sofa" + } + control = { + "Azimuth" = 220.0 + "Elevation" = 0.0 + "Radius" = 3.0 + } + } + ] + inputs = [ "sp:In" ] + outputs = [ "sp:Out L" "sp:Out R" ] + } + capture.props = { + node.name = "effect_input.3d" + media.class = Audio/Sink + audio.channels = 1 + audio.position = [ FC ] + } + playback.props = { + node.name = "effect_output.3d" + node.passive = true + audio.channels = 2 + audio.position = [ FL FR ] + } + } + } +] diff --git a/src/daemon/jack.conf.in b/src/daemon/jack.conf.in new file mode 100644 index 0000000..32d8076 --- /dev/null +++ b/src/daemon/jack.conf.in @@ -0,0 +1,140 @@ +# 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 = { + # = + # + # 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 = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # 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 = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + } + flags = [ ifexists nofail ] + } + + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # Allows creating nodes that run in the context of the + # client. Is used by all clients that want to provide + # data to PipeWire. + { name = libpipewire-module-client-node } + + # Allows 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.show-midi = 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 + # + # allow: Allow connect request of other ports + # fail: Fail connect requests of other ports + # ignore: Ignore connect requests of other ports + #jack.other-connect-mode = allow + #jack.locked-process = true + #jack.default-as-system = false + #jack.fix-midi-events = true + #jack.global-buffer-size = false + #jack.max-client-ports = 768 + #jack.fill-aliases = false + #jack.writable-input = true + #jack.flag-midi2 = false +} + +# client specific properties +jack.rules = [ + { matches = [ + { + # all keys must match the value. ! negates. ~ 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.always-process = false # makes qsynth idle + node.pause-on-idle = false # makes audio fade out when idle + node.passive = out # 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..b2ebb93 --- /dev/null +++ b/src/daemon/meson.build @@ -0,0 +1,166 @@ +pipewire_daemon_sources = [ + 'pipewire.c', +] + +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.set('rtprio_server', get_option('rtprio-server')) +conf_config.set('rtprio_client', get_option('rtprio-client')) + +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, '') + +build_vk = get_option('vulkan').enabled() + +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', + 'filter-chain.conf', + 'jack.conf', + 'minimal.conf', + 'pipewire-pulse.conf', + 'pipewire-avb.conf', + 'pipewire-aes67.conf', +] + +if build_vk + conf_files += 'pipewire-vulkan.conf' +endif + +foreach c : conf_files + res = configure_file(input : '@0@.in'.format(c), + output : c, + configuration : conf_config, + install_dir : pipewire_confdatadir) + test(f'validate-json-@c@', spa_json_dump_exe, args : res) +endforeach + +res = configure_file(input : 'pipewire.conf.in', + output : 'pipewire-uninstalled.conf', + configuration : conf_config_uninstalled) +test('validate-json-pipewire-uninstalled.conf', spa_json_dump_exe, args : res) + +conf_avail_folders = [ + 'pipewire.conf.avail', + 'client.conf.avail', + 'pipewire-pulse.conf.avail', +] + +foreach c : conf_avail_folders + subdir(c) +endforeach + +pipewire_exec = executable('pipewire', + pipewire_daemon_sources, + install: true, + include_directories : [ configinc ], + dependencies : [ spa_dep, pipewire_dep, ], +) + +ln = find_program('ln') + +pipewire_aliases = [ + 'pipewire-pulse', + 'pipewire-avb', + 'pipewire-aes67', +] + +if build_vk + pipewire_aliases += 'pipewire-vulkan' +endif + +foreach alias : pipewire_aliases + custom_target( + alias, + build_by_default: true, + install: false, + command: [ln, '-sf', meson.project_build_root() + '/@INPUT@', '@OUTPUT@'], + input: pipewire_exec, + output: alias, + ) + install_symlink( + alias, + pointing_to: pipewire_exec.name(), + install_dir: pipewire_bindir, + ) +endforeach + +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..cfaab1c --- /dev/null +++ b/src/daemon/minimal.conf.in @@ -0,0 +1,476 @@ +# 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.clock.quantum-floor = 4 + #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 + + # This config can use udev or hardcoded ALSA devices. Make sure to + # change the alsa device below when disabling udev + minimal.use-udev = true + + # Load the pulseaudio emulation daemon + minimal.use-pulse = true +} + +context.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + # These overrides are only applied when running in a vm. + default.clock.min-quantum = 1024 + } + } + } +] + +context.spa-libs = { + # = + # + # 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 + audio.adapt = audioconvert/libspa-audioconvert + api.alsa.* = alsa/libspa-alsa + support.* = support/libspa-support +} + +context.modules = [ + #{ name = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # 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 = @rtprio_server@ + #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 } + + { name = libpipewire-module-spa-device-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 } + + { name = libpipewire-module-protocol-pulse + condition = [ { minimal.use-pulse = true } ] + } +] + +pulse.properties = { + # the addresses this server listens on + server.address = [ + "unix:native" + ] +} + +stream.properties = { + adapter.auto-port-config = { mode = dsp } +} + +context.objects = [ + #{ factory = + # ( args = { = ... } ) + # ( flags = [ ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Creates an object from a PipeWire factory with the given parameters. + # If nofail is given, errors are ignored (and no object is created). + # + #{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } } + #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] } + #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } } + #{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } } + #{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false } } } + #{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } } + + # Make a default metadata store + { factory = metadata + args = { + metadata.name = default + # metadata.values = [ + # { key = default.audio.sink value = { name = somesink } } + # { key = default.audio.source value = { name = somesource } } + # ] + } + } + + # A default dummy driver. This handles nodes marked with the "node.always-process" + # 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 + #freewheel.wait = 10 + } + } + + # This creates a ALSA udev device that will enumerate all + # ALSA devices. Because it is using ACP and has the auto-profile + # property set, this will enable a profile and create associated + # nodes, which will be automatically configured to their best + # configuration with the auto-port-config settings. + # Extra node and device params can be given with node.param and + # device.param prefixes. + { factory = spa-device-factory + args = { + factory.name = api.alsa.enum.udev + alsa.use-acp = true + device.object.properties = { + api.acp.auto-profile = true + api.acp.auto-port = true + device.object.properties = { + node.adapter = audio.adapt + resample.disable = false + adapter.auto-port-config = { + mode = dsp + monitor = false + control = false + position = preserve # unknown, aux + } + #node.param.Props = { + # channelVolumes = [ 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.6 ] + #} + } + #device.param.Profile = { + # #idx = 0 + # name = pro-audio + #} + } + } + condition = [ { minimal.use-udev = 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:4" + #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 = false + #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 ] + # } + #} + #node.param.Props = { + # channelVolumes = [ 0.5 0.4 0.3 0.5 ] + #} + } + condition = [ { minimal.use-udev = false } ] + } + { factory = adapter + args = { + factory.name = api.alsa.pcm.sink + node.name = "system" + node.description = "system" + media.class = "Audio/Sink" + api.alsa.path = "hw:4" + #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 = false + #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 + # } + #} + #node.param.Props = { + # channelVolumes = [ 0.5 0.4 0.3 0.5 ] + #} + } + condition = [ { minimal.use-udev = false } ] + } + # 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" + # monitor.passthrough = true + # 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 + # } + #} + #{ factory = link-factory + # args = { + # link.output.node = system + # link.output.port = capture_2 + # link.input.node = my-mic + # link.input.port = input_FR + # } + #} +] + +context.exec = [ + #{ path = + # ( args = "" | [ ... ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # 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" } +] + +node.rules = [ + { matches = [ + { + # all keys must match the value. ! negates. ~ starts regex. + #alsa.card_name = "ICUSBAUDIO7D" + #api.alsa.pcm.stream = "playback" + } + ] + actions = { + update-props = { + #node.name = "alsa_playback.ICUSBAUDIO7D" + } + } + } +] +device.rules = [ + { matches = [ + { + #alsa.card_name = "ICUSBAUDIO7D" + } + ] + actions = { + update-props = { + #device.name = "alsa_card.ICUSBAUDIO7D" + #api.acp.auto-profile = false + #api.acp.auto-port = false + #device.param.Profile = { + # #idx = 0 + # name = off + #} + } + } + } +] diff --git a/src/daemon/pipewire-aes67.conf.in b/src/daemon/pipewire-aes67.conf.in new file mode 100644 index 0000000..479c12c --- /dev/null +++ b/src/daemon/pipewire-aes67.conf.in @@ -0,0 +1,157 @@ +# AES67 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-aes67.conf.d/ for system-wide changes or in +# ~/.config/pipewire/pipewire-aes67.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 = { + support.* = support/libspa-support +} + +context.objects = [ + # An example clock reading from /dev/ptp0. You can also specify the network interface name, + # pipewire will query the interface for the current active PHC index. Another option is to + # sync the ptp clock to CLOCK_TAI and then set clock.id = tai, keep in mind that tai may + # also be synced by a NTP client. + # The precedence is: device, interface, id + { factory = spa-node-factory + args = { + factory.name = support.node.driver + node.name = PTP0-Driver + node.group = pipewire.ptp0 + # This driver should only be used for network nodes marked with group + priority.driver = 100000 + clock.name = "clock.system.ptp0" + ### Please select the PTP hardware clock here + # Interface name is the preferred method of specifying the PHC + clock.interface = "eth0" + #clock.device = "/dev/ptp0" + #clock.id = tai + # Lower this in case of periodic out-of-sync + resync.ms = 1.5 + object.export = true + } + } +] + +context.modules = [ + { name = libpipewire-module-rt + args = { + nice.level = -11 + #rt.prio = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + } + flags = [ ifexists nofail ] + } + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-client-node } + { name = libpipewire-module-spa-node-factory } + { name = libpipewire-module-adapter } + { name = libpipewire-module-rtp-sap + args = { + ### Please select the interface here + local.ifname = eth0 + sap.ip = 239.255.255.255 + sap.port = 9875 + net.ttl = 32 + net.loop = false + # If you use another PTPv2 daemon supporting management + # messages over a UNIX socket, specify its path here + ptp.management-socket = "/var/run/ptp4lro" + + stream.rules = [ + { + matches = [ + { + rtp.session = "~.*" + } + ] + actions = { + create-stream = { + node.virtual = false + media.class = "Audio/Source" + device.api = aes67 + # You can adjust the latency buffering here. Use integer values only + sess.latency.msec = 3 + node.group = pipewire.ptp0 + } + } + }, + { + matches = [ + { + sess.sap.announce = true + } + ] + actions = { + announce-stream = {} + } + } + ] + } + }, + { name = libpipewire-module-rtp-sink + args = { + ### Please select the interface here + local.ifname = eth0 + ### If you want to create multiple output streams, please copy the whole + ### module-rtp-sink block, but change this multicast IP to another unused + ### one keeping 239.69.x.x range unless you know you need another one + destination.ip = 239.69.150.243 + destination.port = 5004 + net.mtu = 1280 + net.ttl = 32 + net.loop = false + # These should typically be equal + # You can customize packet length, but 1 ms should work for every device + # Consult receiver documentation to ensure it supports the value you set + sess.min-ptime = 1 + sess.max-ptime = 1 + ### Please change this, especially if you create multiple sinks + sess.name = "PipeWire RTP stream" + sess.media = "audio" + # This property is used if you aren't using ptp4l 4 + sess.ts-refclk = "ptp=traceable" + sess.ts-offset = 0 + # You can adjust the latency buffering here. Use integer values only + sess.latency.msec = 3 + audio.format = "S24BE" + audio.rate = 48000 + audio.channels = 2 + # These channel names will be visible both to applications and AES67 receivers + node.channel-names = ["CH1", "CH2"] + # Uncomment this and comment node.group in send/recv stream.props to allow + # separate drivers for the RTP sink and PTP sending (i.e. force rate matching on + # the AES67 node rather than other nodes) + #aes67.driver-group = "pipewire.ptp0" + + stream.props = { + ### Please change the sink name, this is necessary when you create multiple sinks + node.name = "rtp-sink" + media.class = "Audio/Sink" + node.virtual = false + device.api = aes67 + sess.sap.announce = true + node.always-process = true + node.group = pipewire.ptp0 + rtp.ntp = 0 + rtp.fetch-ts-refclk = true + } + } + }, +] diff --git a/src/daemon/pipewire-avb.conf.in b/src/daemon/pipewire-avb.conf.in new file mode 100644 index 0000000..ed697de --- /dev/null +++ b/src/daemon/pipewire-avb.conf.in @@ -0,0 +1,80 @@ +# 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 = @rtprio_client@ + #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" +} + +avb.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + # These overrides are only applied when running in a vm. + } + } + } +] diff --git a/src/daemon/pipewire-pulse.conf.avail/20-upmix.conf.in b/src/daemon/pipewire-pulse.conf.avail/20-upmix.conf.in new file mode 100644 index 0000000..064eba1 --- /dev/null +++ b/src/daemon/pipewire-pulse.conf.avail/20-upmix.conf.in @@ -0,0 +1,8 @@ +# Enables upmixing +stream.properties = { + channelmix.upmix = true + channelmix.upmix-method = psd # none, simple + channelmix.lfe-cutoff = 150 + channelmix.fc-cutoff = 12000 + channelmix.rear-delay = 12.0 +} diff --git a/src/daemon/pipewire-pulse.conf.avail/meson.build b/src/daemon/pipewire-pulse.conf.avail/meson.build new file mode 100644 index 0000000..a9e264f --- /dev/null +++ b/src/daemon/pipewire-pulse.conf.avail/meson.build @@ -0,0 +1,12 @@ +conf_files = [ + '20-upmix.conf', +] + +foreach c : conf_files + res = configure_file(input : '@0@.in'.format(c), + output : c, + configuration : conf_config, + install_dir : pipewire_confdatadir / 'pipewire-pulse.conf.avail') + test(f'validate-json-pulse-@c@', spa_json_dump_exe, args : res) +endforeach + diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in new file mode 100644 index 0000000..affb993 --- /dev/null +++ b/src/daemon/pipewire-pulse.conf.in @@ -0,0 +1,187 @@ +# 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 = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + #uclamp.min = 0 + #uclamp.max = 1024 + } + 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 = " " +# ( flags = [ nofail ] ) +# ( condition = [ { = , ... } ... ] ) +# conditions will check the pulse.properties key/values. +pulse.cmd = [ + { cmd = "load-module" args = "module-always-sink" flags = [ ] + condition = [ { pulse.cmd.always-sink = !false } ] } + { cmd = "load-module" args = "module-device-manager" flags = [ ] + condition = [ { pulse.cmd.device-manager = !false } ] } + { cmd = "load-module" args = "module-device-restore" flags = [ ] + condition = [ { pulse.cmd.device-restore = !false } ] } + { cmd = "load-module" args = "module-stream-restore" flags = [ ] + condition = [ { pulse.cmd.stream-restore = !false } ] } + #{ cmd = "load-module" args = "module-switch-on-connect" } + #{ cmd = "load-module" args = "module-gsettings" flags = [ nofail ] } +] + +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 + #} + ] + #server.dbus-name = "org.pulseaudio.Server" + #pulse.allow-module-loading = true + #pulse.min.req = 128/48000 # 2.7ms + #pulse.default.req = 960/48000 # 20 milliseconds + #pulse.min.frag = 128/48000 # 2.7ms + #pulse.default.frag = 96000/48000 # 2 seconds + #pulse.default.tlength = 96000/48000 # 2 seconds + #pulse.min.quantum = 128/48000 # 2.7ms + #pulse.idle.timeout = 0 # don't pause after underruns + #pulse.default.format = F32 + #pulse.default.position = [ FL FR ] +} + +pulse.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + # These overrides are only applied when running in a vm. + pulse.min.quantum = 1024/48000 # 22ms + } + } + } +] + +# client/stream specific properties +pulse.rules = [ + { + matches = [ + { + # all keys must match the value. ! negates. ~ 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 + # block-source-volume blocks updates to source volume + # block-sink-volume blocks updates to sink volume + #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 = "teams-for-linux" } + { 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 + } + } + } + #{ + # matches = [ { application.process.binary = "Discord" } ] + # actions = { quirks = [ block-source-volume ] } + #} +] diff --git a/src/daemon/pipewire-vulkan.conf.in b/src/daemon/pipewire-vulkan.conf.in new file mode 100644 index 0000000..388a4fc --- /dev/null +++ b/src/daemon/pipewire-vulkan.conf.in @@ -0,0 +1,99 @@ +# Config file for PipeWire version "0.3.77" # +# +# This config file should start the vulkan-compute-source/filter as proxied +# clients +# + +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 + #mem.warn-mlock = false + #mem.allow-mlock = true + #mem.mlock-all = false + #clock.power-of-two-quantum = true + #log.level = 4 + #cpu.zero.denormals = false + + #default.clock.quantum-limit = 8192 +} + +context.spa-libs = { + # = + # + # Used to find spa factory names. It maps an spa factory name + # regular expression to a library name that should contain + # that factory. + # + api.vulkan.* = vulkan/libspa-vulkan + support.* = support/libspa-support + video.convert.* = videoconvert/libspa-videoconvert +} + +context.modules = [ + #{ name = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # 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. + # If condition is given, the module is loaded only when the context + # properties all match the match rules. + # + + # 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 = @rtprio_client@ + #rt.time.soft = -1 + #rt.time.hard = -1 + } + flags = [ ifexists nofail ] + } + + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # 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 } + + # Makes a factory for wrapping nodes in an adapter with a + # converter and resampler. + { name = libpipewire-module-adapter } +] + +context.objects = [ + #{ factory = + # ( args = { = ... } ) + # ( flags = [ ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Creates an object from a PipeWire factory with the given parameters. + # If nofail is given, errors are ignored (and no object is created). + # If condition is given, the object is created only when the context properties + # all match the match rules. + # + #{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } } + #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] } + #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } } + #{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } } + #{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false } } } + { factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = vulkan-compute-source object.export = true } } + { factory = spa-node-factory args = { factory.name = api.vulkan.compute.filter node.name = vulkan-compute-filter object.export = true } } + { factory = spa-node-factory args = { factory.name = api.vulkan.blit.filter node.name = vulkan-blit-filter object.export = true } } + { factory = spa-node-factory args = { factory.name = api.vulkan.blit.dsp-filter node.name = vulkan-blit-dsp-filter object.export = true } } +] diff --git a/src/daemon/pipewire.c b/src/daemon/pipewire.c new file mode 100644 index 0000000..9193d81 --- /dev/null +++ b/src/daemon/pipewire.c @@ -0,0 +1,142 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#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" + " -v, --verbose Increase verbosity by one level\n" + " --version Show version\n" + " -c, --config Load config (Default %s)\n" + " -P --properties Set context properties\n"), + 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' }, + { "properties", required_argument, NULL, 'P' }, + + { NULL, 0, NULL, 0} + }; + int c, res = 0; + char path[PATH_MAX]; + const char *config_name; + enum spa_log_level level; + struct spa_error_location loc; + + 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); + + level = pw_log_level; + + properties = pw_properties_new( + PW_KEY_CONFIG_NAME, config_name, + NULL); + + while ((c = getopt_long(argc, argv, "hVc:vP:", 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; + pw_properties_set(properties, PW_KEY_CONFIG_NAME, config_name); + break; + + case 'P': + if (pw_properties_update_string_checked(properties, optarg, strlen(optarg), &loc) < 0) { + spa_debug_file_error_location(stderr, &loc, + "error: syntax error in --properties: %s", + loc.reason); + goto done; + } + break; + + default: + res = -EINVAL; + goto done; + } + } + + 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), spa_steal_ptr(properties), 0); + + 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.avail/10-rates.conf.in b/src/daemon/pipewire.conf.avail/10-rates.conf.in new file mode 100644 index 0000000..5ede733 --- /dev/null +++ b/src/daemon/pipewire.conf.avail/10-rates.conf.in @@ -0,0 +1,4 @@ +# Adds more common rates +context.properties = { + default.clock.allowed-rates = [ 44100 48000 88200 96000 ] +} diff --git a/src/daemon/pipewire.conf.avail/20-upmix.conf.in b/src/daemon/pipewire.conf.avail/20-upmix.conf.in new file mode 100644 index 0000000..064eba1 --- /dev/null +++ b/src/daemon/pipewire.conf.avail/20-upmix.conf.in @@ -0,0 +1,8 @@ +# Enables upmixing +stream.properties = { + channelmix.upmix = true + channelmix.upmix-method = psd # none, simple + channelmix.lfe-cutoff = 150 + channelmix.fc-cutoff = 12000 + channelmix.rear-delay = 12.0 +} diff --git a/src/daemon/pipewire.conf.avail/meson.build b/src/daemon/pipewire.conf.avail/meson.build new file mode 100644 index 0000000..8b071ba --- /dev/null +++ b/src/daemon/pipewire.conf.avail/meson.build @@ -0,0 +1,13 @@ +conf_files = [ + '10-rates.conf', + '20-upmix.conf', +] + +foreach c : conf_files + res = configure_file(input : '@0@.in'.format(c), + output : c, + configuration : conf_config, + install_dir : pipewire_confdatadir / 'pipewire.conf.avail') + test(f'validate-json-pipewire-@c@', spa_json_dump_exe, args : res) +endforeach + diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in new file mode 100644 index 0000000..14f7c6e --- /dev/null +++ b/src/daemon/pipewire.conf.in @@ -0,0 +1,371 @@ +# 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 + + #loop.rt-prio = -1 # -1 = use module-rt prio, 0 disable rt + #loop.class = data.rt + #thread.affinity = [ 0 1 ] # optional array of CPUs + #context.num-data-loops = 1 # -1 = num-cpus, 0 = no data loops + # + #context.data-loops = [ + # { loop.rt-prio = -1 + # loop.class = [ data.rt audio.rt ] + # #library.name.system = support/libspa-support + # thread.name = data-loop.0 + # #thread.affinity = [ 0 1 ] # optional array of CPUs + # } + #] + + 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.clock.quantum-floor = 4 + #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 +} + +context.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + # These overrides are only applied when running in a vm. + default.clock.min-quantum = 1024 + } + } + } +] + +context.spa-libs = { + # = + # + # 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 + video.convert.* = videoconvert/libspa-videoconvert + #filter.graph = filter-graph/libspa-filter-graph + #videotestsrc = videotestsrc/libspa-videotestsrc + #audiotestsrc = audiotestsrc/libspa-audiotestsrc +} + +context.modules = [ + #{ name = + # ( args = { = ... } ) + # ( flags = [ ( ifexists ) ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # 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. + # If condition is given, the module is loaded only when the context + # properties all match the match rules. + # + + # Uses realtime scheduling to boost the audio thread priorities. This uses + # RTKit if the user doesn't have permission to use regular realtime + # scheduling. You can also clamp utilisation values to improve scheduling + # on embedded and heterogeneous systems, e.g. Arm big.LITTLE devices. + # use module.rt.args = { ... } to override the arguments. + { name = libpipewire-module-rt + args = { + nice.level = -11 + rt.prio = @rtprio_server@ + #rt.time.soft = -1 + #rt.time.hard = -1 + #uclamp.min = 0 + #uclamp.max = 1024 + } + flags = [ ifexists nofail ] + condition = [ { module.rt = !false } ] + } + + # The native communication protocol. + { name = libpipewire-module-protocol-native + args = { + # List of server Unix sockets, and optionally permissions + #sockets = [ { name = "pipewire-0" }, { name = "pipewire-0-manager" } ] + } + } + + # The profile module. Allows application to access profiler + # and performance data. It provides an interface that is used + # by pw-top and pw-profiler. + # use module.profiler.args = { ... } to override the arguments. + { name = libpipewire-module-profiler + args = { + #profile.interval.ms = 0 + } + condition = [ { module.profiler = !false } ] + } + + # Allows applications to create metadata objects. It creates + # a factory for Metadata objects. + { name = libpipewire-module-metadata + condition = [ { module.metadata = !false } ] + } + + # Creates a factory for making devices that run in the + # context of the PipeWire server. + { name = libpipewire-module-spa-device-factory + condition = [ { module.spa-device-factory = !false } ] + } + + # Creates a factory for making nodes that run in the + # context of the PipeWire server. + { name = libpipewire-module-spa-node-factory + condition = [ { module.spa-node-factory = !false } ] + } + + # Allows creating nodes that run in the context of the + # client. Is used by all clients that want to provide + # data to PipeWire. + { name = libpipewire-module-client-node + condition = [ { module.client-node = !false } ] + } + + # Allows creating devices that run in the context of the + # client. Is used by the session manager. + { name = libpipewire-module-client-device + condition = [ { module.client-device = !false } ] + } + + # The portal module monitors the PID of the portal process + # and tags connections with the same PID as portal + # connections. + { name = libpipewire-module-portal + flags = [ ifexists nofail ] + condition = [ { module.portal = !false } ] + } + + # The access module can perform access checks and block + # new clients. + { name = libpipewire-module-access + args = { + # Socket-specific access permissions + #access.socket = { pipewire-0 = "default", pipewire-0-manager = "unrestricted" } + + # Deprecated legacy mode (not socket-based), + # for now enabled by default if access.socket is not specified + #access.legacy = true + } + condition = [ { module.access = !false } ] + } + + # Makes a factory for wrapping nodes in an adapter with a + # converter and resampler. + { name = libpipewire-module-adapter + condition = [ { module.adapter = !false } ] + } + + # Makes a factory for creating links between ports. + # use module.link-factory.args = { ... } to override the arguments. + { name = libpipewire-module-link-factory + args = { + #allow.link.passive = false + } + condition = [ { module.link-factory = !false } ] + } + + # Provides factories to make session manager objects. + { name = libpipewire-module-session-manager + condition = [ { module.session-manager = !false } ] + } + + # 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 ] + condition = [ { module.x11.bell = !false } ] + } + # The JACK DBus detection module. When jackdbus is started, this + # will automatically make PipeWire become a JACK client. + # use module.jackdbus-detect.args = { ... } to override the arguments. + { name = libpipewire-module-jackdbus-detect + args = { + #jack.library = libjack.so.0 + #jack.server = null + #jack.client-name = PipeWire + #jack.connect = true + #tunnel.mode = duplex # source|sink|duplex + source.props = { + #audio.channels = 2 + #midi.ports = 1 + #audio.position = [ FL FR ] + # extra sink properties + } + sink.props = { + #audio.channels = 2 + #midi.ports = 1 + #audio.position = [ FL FR ] + # extra sink properties + } + } + flags = [ ifexists nofail ] + condition = [ { module.jackdbus-detect = !false } ] + } +] + +context.objects = [ + #{ factory = + # ( args = { = ... } ) + # ( flags = [ ( nofail ) ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Creates an object from a PipeWire factory with the given parameters. + # If nofail is given, errors are ignored (and no object is created). + # If condition is given, the object is created only when the context properties + # all match the match rules. + # + #{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc node.description = videotestsrc node.param.Props = { patternType = 1 } } } + #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] } + #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } } + #{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } } + #{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test node.description = audiotestsrc node.param.Props = { live = false }} } + #{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } } + + # A default dummy driver. This handles nodes marked with the "node.always-process" + # 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 + node.sync-group = sync.dummy + priority.driver = 200000 + #clock.id = monotonic # realtime | tai | monotonic-raw | boottime + #clock.name = "clock.system.monotonic" + } + condition = [ { factory.dummy-driver = !false } ] + } + { factory = spa-node-factory + args = { + factory.name = support.node.driver + node.name = Freewheel-Driver + priority.driver = 190000 + node.group = pipewire.freewheel + node.sync-group = sync.dummy + node.freewheel = true + #freewheel.wait = 10 + } + condition = [ { factory.freewheel-driver = !false } ] + } + + # 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" + # monitor.passthrough = 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 = "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" + # } + #} + + # Use the metadata factory to create metadata and some default values. + #{ factory = metadata + # args = { + # metadata.name = my-metadata + # metadata.values = [ + # { key = default.audio.sink value = { name = somesink } } + # { key = default.audio.source value = { name = somesource } } + # ] + # } + #} +] + +context.exec = [ + #{ path = + # ( args = "" | [ ... ] ) + # ( condition = [ { = ... } ... ] ) + #} + # + # Execute the given program with arguments. + # If condition is given, the program is executed only when the context + # properties all match the match rules. + # + # 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@" + @sm_comment@ condition = [ { exec.session-manager = !false } ] } + # + # You can optionally start the pulseaudio-server here as well + # but it is better to start it as a systemd service. + # 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" ] + @pulse_comment@ condition = [ { exec.pipewire-pulse = !false } ] } +] 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..0cc1767 --- /dev/null +++ b/src/daemon/systemd/system/meson.build @@ -0,0 +1,21 @@ +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', 'pipewire-manager.socket', 'pipewire-pulse.socket' ], + install_dir : systemd_system_services_dir) + +systemd_config = configuration_data() +systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire') +systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse') + +configure_file(input : 'pipewire.service.in', + output : 'pipewire.service', + configuration : systemd_config, + install_dir : systemd_system_services_dir) + +configure_file(input : 'pipewire-pulse.service.in', + output : 'pipewire-pulse.service', + configuration : systemd_config, + install_dir : systemd_system_services_dir) diff --git a/src/daemon/systemd/system/pipewire-manager.socket b/src/daemon/systemd/system/pipewire-manager.socket new file mode 100644 index 0000000..235f3db --- /dev/null +++ b/src/daemon/systemd/system/pipewire-manager.socket @@ -0,0 +1,13 @@ +[Unit] +Description=PipeWire Multimedia System Manager Socket + +[Socket] +Service=pipewire.service +Priority=6 +ListenStream=%t/pipewire/pipewire-0-manager +SocketUser=pipewire +SocketGroup=pipewire +SocketMode=0600 + +[Install] +WantedBy=sockets.target diff --git a/src/daemon/systemd/system/pipewire-pulse.service.in b/src/daemon/systemd/system/pipewire-pulse.service.in new file mode 100644 index 0000000..8752f78 --- /dev/null +++ b/src/daemon/systemd/system/pipewire-pulse.service.in @@ -0,0 +1,24 @@ +[Unit] +Description=PipeWire PulseAudio Service +Requires=pipewire-pulse.socket +Wants=pipewire.service pipewire-session-manager.service +After=pipewire.service pipewire-session-manager.service + +[Service] +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service +Type=simple +AmbientCapabilities=CAP_SYS_NICE +ExecStart=@PW_PULSE_BINARY@ +Restart=on-failure +User=pipewire +Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire +Environment=PULSE_RUNTIME_PATH=%t/pulse + +[Install] +Also=pipewire-pulse.socket +WantedBy=pipewire.service + diff --git a/src/daemon/systemd/system/pipewire-pulse.socket b/src/daemon/systemd/system/pipewire-pulse.socket new file mode 100644 index 0000000..0a69294 --- /dev/null +++ b/src/daemon/systemd/system/pipewire-pulse.socket @@ -0,0 +1,12 @@ +[Unit] +Description=PipeWire PulseAudio System Socket + +[Socket] +Priority=6 +ListenStream=%t/pulse/native +SocketUser=pipewire +SocketGroup=pipewire +SocketMode=0660 + +[Install] +WantedBy=sockets.target diff --git a/src/daemon/systemd/system/pipewire.service.in b/src/daemon/systemd/system/pipewire.service.in new file mode 100644 index 0000000..dc8db3f --- /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 pipewire-manager.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..a96409f --- /dev/null +++ b/src/daemon/systemd/user/meson.build @@ -0,0 +1,33 @@ +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') + +pw_service_reqs = '' +if get_option('dbus').enabled() + pw_service_reqs += 'dbus.service ' +endif +systemd_config.set('PW_SERVICE_REQS', pw_service_reqs) + +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..f0c8d9c --- /dev/null +++ b/src/daemon/systemd/user/pipewire.service.in @@ -0,0 +1,33 @@ +[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 @PW_SERVICE_REQS@ +ConditionUser=!root + +[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..890342a --- /dev/null +++ b/src/daemon/systemd/user/pipewire.socket @@ -0,0 +1,11 @@ +[Unit] +Description=PipeWire Multimedia System Sockets +ConditionUser=!root + +[Socket] +Priority=6 +ListenStream=%t/pipewire-0 +ListenStream=%t/pipewire-0-manager + +[Install] +WantedBy=sockets.target diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c new file mode 100644 index 0000000..c44f905 --- /dev/null +++ b/src/examples/audio-capture.c @@ -0,0 +1,189 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio capture using \ref pw_stream "pw_stream". + [title] + */ + +#include +#include +#include +#include + +#include + +#include + +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 = (uint32_t)SPA_CLAMPF(max * 30, 0.f, 39.f); + + 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..f5bda85 --- /dev/null +++ b/src/examples/audio-dsp-filter.c @@ -0,0 +1,160 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio filter using \ref pw_filter "pw_filter". + [title] + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include + +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..135d2e2 --- /dev/null +++ b/src/examples/audio-dsp-src.c @@ -0,0 +1,145 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio source using \ref pw_filter "pw_filter" + [title] + */ + +#include +#include +#include +#include + +#include +#include + +#define M_PI_M2f (float)(M_PI+M_PI) + +#define DEFAULT_RATE 44100 +#define DEFAULT_FREQ 440 +#define DEFAULT_VOLUME 0.7f + +struct data; + +struct port { + struct data *data; + float 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_M2f * DEFAULT_FREQ / DEFAULT_RATE; + if (out_port->accumulator >= M_PI_M2f) + out_port->accumulator -= M_PI_M2f; + + *out++ = sinf(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-ring.c b/src/examples/audio-src-ring.c new file mode 100644 index 0000000..b96e26f --- /dev/null +++ b/src/examples/audio-src-ring.c @@ -0,0 +1,226 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio source using \ref pw_stream "pw_stream" and ringbuffer. + [title] + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#define M_PI_M2f (float)(M_PI+M_PI) + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_VOLUME 0.7f + +#define BUFFER_SIZE (16*1024) + +struct data { + struct pw_main_loop *main_loop; + struct pw_loop *loop; + struct pw_stream *stream; + + float accumulator; + + struct spa_source *refill_event; + + struct spa_ringbuffer ring; + float buffer[BUFFER_SIZE * DEFAULT_CHANNELS]; +}; + +static void fill_f32(struct data *d, uint32_t offset, int n_frames) +{ + float val; + int i, c; + + for (i = 0; i < n_frames; i++) { + d->accumulator += M_PI_M2f * 440 / DEFAULT_RATE; + if (d->accumulator >= M_PI_M2f) + d->accumulator -= M_PI_M2f; + + val = sinf(d->accumulator) * DEFAULT_VOLUME; + for (c = 0; c < DEFAULT_CHANNELS; c++) + d->buffer[((offset + i) % BUFFER_SIZE) * DEFAULT_CHANNELS + c] = val; + } +} + +/* this is called from the main-thread when we need to fill up the ringbuffer + * with more data */ +static void do_refill(void *userdata, uint64_t count) +{ + struct data *data = userdata; + int32_t filled; + uint32_t index, avail; + + filled = spa_ringbuffer_get_write_index(&data->ring, &index); + /* we xrun, this can not happen because we never read more + * than what there is in the ringbuffer and we never write more than + * what is left */ + spa_assert(filled >= 0); + spa_assert(filled <= BUFFER_SIZE); + + /* this is how much samples we can write */ + avail = BUFFER_SIZE - filled; + + /* write new samples to the ringbuffer from the given index */ + fill_f32(data, index, avail); + + /* and advance the ringbuffer */ + spa_ringbuffer_write_update(&data->ring, index + avail); +} + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * b = pw_stream_dequeue_buffer(stream); + * + * .. generate stuff in the buffer ... + * In this case we read samples from a ringbuffer. The ringbuffer is + * filled up by another thread. + * + * pw_stream_queue_buffer(stream, b); + */ +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + uint8_t *p; + uint32_t index, to_read, to_silence; + int32_t avail, n_frames, stride; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((p = buf->datas[0].data) == NULL) + return; + + /* the amount of space in the ringbuffer and the read index */ + avail = spa_ringbuffer_get_read_index(&data->ring, &index); + + stride = sizeof(float) * DEFAULT_CHANNELS; + n_frames = buf->datas[0].maxsize / stride; + if (b->requested) + n_frames = SPA_MIN((int32_t)b->requested, n_frames); + + /* we can read if there is something available */ + to_read = avail > 0 ? SPA_MIN(avail, n_frames) : 0; + /* and fill the remainder with silence */ + to_silence = n_frames - to_read; + + if (to_read > 0) { + /* read data into the buffer */ + spa_ringbuffer_read_data(&data->ring, + data->buffer, BUFFER_SIZE * stride, + (index % BUFFER_SIZE) * stride, + p, to_read * stride); + /* update the read pointer */ + spa_ringbuffer_read_update(&data->ring, index + to_read); + } + if (to_silence > 0) + /* set the rest of the buffer to silence */ + memset(SPA_PTROFF(p, to_read * stride, void), 0, to_silence * stride); + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = n_frames * stride; + + pw_stream_queue_buffer(data->stream, b); + + /* signal the main thread to fill the ringbuffer, we can only do this, for + * example when the available ringbuffer space falls below a certain + * level. */ + pw_loop_signal_event(data->loop, data->refill_event); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->main_loop); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct pw_properties *props; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw_init(&argc, &argv); + + data.main_loop = pw_main_loop_new(NULL); + data.loop = pw_main_loop_get_loop(data.main_loop); + + pw_loop_add_signal(data.loop, SIGINT, do_quit, &data); + pw_loop_add_signal(data.loop, SIGTERM, do_quit, &data); + + /* we're going to refill a ringbuffer from the main loop. Make an + * event for this. */ + spa_ringbuffer_init(&data.ring); + data.refill_event = pw_loop_add_event(data.loop, do_refill, &data); + /* prefill the ringbuffer */ + do_refill(&data, 0); + + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL); + if (argc > 1) + /* Set stream target if given on command line */ + pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]); + + data.stream = pw_stream_new_simple( + data.loop, + "audio-src-ring", + props, + &stream_events, + &data); + + /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat + * id means that this is a format enumeration (of 1 value). */ + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .channels = DEFAULT_CHANNELS, + .rate = DEFAULT_RATE )); + + /* Now connect this stream. We ask that our process function is + * called in a realtime thread. */ + pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + + /* and wait while we let things run */ + pw_main_loop_run(data.main_loop); + + pw_stream_destroy(data.stream); + pw_loop_destroy_source(data.loop, data.refill_event); + pw_main_loop_destroy(data.main_loop); + pw_deinit(); + + return 0; +} diff --git a/src/examples/audio-src-ring2.c b/src/examples/audio-src-ring2.c new file mode 100644 index 0000000..19f8eb4 --- /dev/null +++ b/src/examples/audio-src-ring2.c @@ -0,0 +1,269 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio source using \ref pw_stream "pw_stream" and ringbuffer. + + This one uses a thread-loop and does a blocking push into a + ringbuffer. + [title] + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#define M_PI_M2f (float)(M_PI+M_PI) + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_VOLUME 0.7f + +#define BUFFER_SIZE (16*1024) + +#define MIN_SIZE 256 +#define MAX_SIZE BUFFER_SIZE + +static float samples[BUFFER_SIZE * DEFAULT_CHANNELS]; + +struct data { + struct pw_thread_loop *thread_loop; + struct pw_loop *loop; + struct pw_stream *stream; + int eventfd; + bool running; + + float accumulator; + + struct spa_ringbuffer ring; + float buffer[BUFFER_SIZE * DEFAULT_CHANNELS]; +}; + +static void fill_f32(struct data *d, float *samples, int n_frames) +{ + float val; + int i, c; + + for (i = 0; i < n_frames; i++) { + d->accumulator += M_PI_M2f * 440 / DEFAULT_RATE; + if (d->accumulator >= M_PI_M2f) + d->accumulator -= M_PI_M2f; + + val = sinf(d->accumulator) * DEFAULT_VOLUME; + for (c = 0; c < DEFAULT_CHANNELS; c++) + samples[i * DEFAULT_CHANNELS + c] = val; + } +} + +/* this can be called from any thread with a block of samples to write into + * the ringbuffer. It will block until all data has been written */ +static void push_samples(void *userdata, float *samples, uint32_t n_samples) +{ + struct data *data = userdata; + int32_t filled; + uint32_t index, avail, stride = sizeof(float) * DEFAULT_CHANNELS; + uint64_t count; + float *s = samples; + + while (n_samples > 0) { + while (true) { + filled = spa_ringbuffer_get_write_index(&data->ring, &index); + /* we xrun, this can not happen because we never read more + * than what there is in the ringbuffer and we never write more than + * what is left */ + spa_assert(filled >= 0); + spa_assert(filled <= BUFFER_SIZE); + + /* this is how much samples we can write */ + avail = BUFFER_SIZE - filled; + if (avail > 0) + break; + + /* no space.. block and wait for free space */ + spa_system_eventfd_read(data->loop->system, data->eventfd, &count); + } + if (avail > n_samples) + avail = n_samples; + + spa_ringbuffer_write_data(&data->ring, + data->buffer, BUFFER_SIZE * stride, + (index % BUFFER_SIZE) * stride, + s, avail * stride); + + s += avail * DEFAULT_CHANNELS; + n_samples -= avail; + + /* and advance the ringbuffer */ + spa_ringbuffer_write_update(&data->ring, index + avail); + } + +} + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * b = pw_stream_dequeue_buffer(stream); + * + * .. generate stuff in the buffer ... + * In this case we read samples from a ringbuffer. The ringbuffer is + * filled up by another thread. + * + * pw_stream_queue_buffer(stream, b); + */ +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + uint8_t *p; + uint32_t index, to_read, to_silence; + int32_t avail, n_frames, stride; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((p = buf->datas[0].data) == NULL) + return; + + /* the amount of space in the ringbuffer and the read index */ + avail = spa_ringbuffer_get_read_index(&data->ring, &index); + + stride = sizeof(float) * DEFAULT_CHANNELS; + n_frames = buf->datas[0].maxsize / stride; + if (b->requested) + n_frames = SPA_MIN((int32_t)b->requested, n_frames); + + /* we can read if there is something available */ + to_read = avail > 0 ? SPA_MIN(avail, n_frames) : 0; + /* and fill the remainder with silence */ + to_silence = n_frames - to_read; + + if (to_read > 0) { + /* read data into the buffer */ + spa_ringbuffer_read_data(&data->ring, + data->buffer, BUFFER_SIZE * stride, + (index % BUFFER_SIZE) * stride, + p, to_read * stride); + /* update the read pointer */ + spa_ringbuffer_read_update(&data->ring, index + to_read); + } + if (to_silence > 0) + /* set the rest of the buffer to silence */ + memset(SPA_PTROFF(p, to_read * stride, void), 0, to_silence * stride); + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = n_frames * stride; + + pw_stream_queue_buffer(data->stream, b); + + /* signal the main thread to fill the ringbuffer, we can only do this, for + * example when the available ringbuffer space falls below a certain + * level. */ + spa_system_eventfd_write(data->loop->system, data->eventfd, 1); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + data->running = false; +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct pw_properties *props; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw_init(&argc, &argv); + + data.thread_loop = pw_thread_loop_new("audio-src", NULL); + data.loop = pw_thread_loop_get_loop(data.thread_loop); + data.running = true; + + pw_thread_loop_lock(data.thread_loop); + pw_loop_add_signal(data.loop, SIGINT, do_quit, &data); + pw_loop_add_signal(data.loop, SIGTERM, do_quit, &data); + + spa_ringbuffer_init(&data.ring); + if ((data.eventfd = spa_system_eventfd_create(data.loop->system, SPA_FD_CLOEXEC)) < 0) + return data.eventfd; + + pw_thread_loop_start(data.thread_loop); + + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL); + if (argc > 1) + /* Set stream target if given on command line */ + pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]); + + data.stream = pw_stream_new_simple( + data.loop, + "audio-src-ring", + props, + &stream_events, + &data); + + /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat + * id means that this is a format enumeration (of 1 value). */ + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .channels = DEFAULT_CHANNELS, + .rate = DEFAULT_RATE )); + + /* Now connect this stream. We ask that our process function is + * called in a realtime thread. */ + pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + + /* prefill the ringbuffer */ + fill_f32(&data, samples, BUFFER_SIZE); + push_samples(&data, samples, BUFFER_SIZE); + + srand(time(NULL)); + + pw_thread_loop_start(data.thread_loop); + pw_thread_loop_unlock(data.thread_loop); + + while (data.running) { + uint32_t size = rand() % ((MAX_SIZE - MIN_SIZE + 1) + MIN_SIZE); + /* make new random sized block of samples and push */ + fill_f32(&data, samples, size); + push_samples(&data, samples, size); + } + + pw_thread_loop_lock(data.thread_loop); + pw_stream_destroy(data.stream); + pw_thread_loop_unlock(data.thread_loop); + pw_thread_loop_destroy(data.thread_loop); + close(data.eventfd); + pw_deinit(); + + return 0; +} diff --git a/src/examples/audio-src.c b/src/examples/audio-src.c new file mode 100644 index 0000000..08c9342 --- /dev/null +++ b/src/examples/audio-src.c @@ -0,0 +1,168 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio source using \ref pw_stream "pw_stream". + [title] + */ + +#include +#include +#include +#include + +#include + +#include + +#define M_PI_M2f (float)(M_PI+M_PI) + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_VOLUME 0.7f + +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + + float 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_M2f * 440 / DEFAULT_RATE; + if (d->accumulator >= M_PI_M2f) + d->accumulator -= M_PI_M2f; + + val = sinf(d->accumulator) * DEFAULT_VOLUME; + for (c = 0; c < DEFAULT_CHANNELS; c++) + *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 = buf->datas[0].maxsize / stride; + if (b->requested) + n_frames = SPA_MIN((int)b->requested, n_frames); + + 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..81f9926 --- /dev/null +++ b/src/examples/bluez-session.c @@ -0,0 +1,381 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Using the \ref spa_device "SPA Device API", among other things. + [title] + */ + +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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; + struct pw_properties *props; +}; + +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); + pw_unload_spa_handle(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, &impl->props->dict); + 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); + + 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; + } + + if ((impl.props = pw_properties_new(NULL, NULL)) == NULL) { + 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..0a5644f --- /dev/null +++ b/src/examples/export-sink.c @@ -0,0 +1,564 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Exporting and implementing a video sink SPA node, using \ref api_pw_core. + [title] + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define WIDTH 640 +#define HEIGHT 480 +#define BPP 3 + +#include "sdl.h" + +#define M_PI_M2f (float)(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; + float 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, (sinf(data->param_accum) * 127.0f) + 127.0f); + spa_pod_builder_pop(&b, &f[1]); + spa_pod_builder_pop(&b, &f[0]); + + data->param_accum += M_PI_M2f / 30.0f; + if (data->param_accum >= M_PI_M2f) + data->param_accum -= M_PI_M2f; +} + +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, PROT_READ, + MAP_PRIVATE, buf->datas[0].fd, buf->datas[0].mapoffset); + sdata = map; + } 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); + + 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..85af5b7 --- /dev/null +++ b/src/examples/export-source.c @@ -0,0 +1,545 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Exporting and implementing a video source SPA node, using \ref api_pw_core. + [title] + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define M_PI_M2f (float)(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; + + float accumulator; + float 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, (sinf(data->volume_accum) / 2.0f) + 0.5f); + spa_pod_builder_pop(&b, &f[1]); + spa_pod_builder_pop(&b, &f[0]); + + data->volume_accum += M_PI_M2f / 1000.0f; + if (data->volume_accum >= M_PI_M2f) + data->volume_accum -= M_PI_M2f; +} + +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, PROT_WRITE, + MAP_SHARED, datas[0].fd, datas[0].mapoffset); + if (b->ptr == MAP_FAILED) { + pw_log_error("failed to buffer mem"); + return -errno; + + } + 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_M2f * 440 / d->format.rate; + if (d->accumulator >= M_PI_M2f) + d->accumulator -= M_PI_M2f; + + val = sinf(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_M2f * 440 / d->format.rate; + if (d->accumulator >= M_PI_M2f) + d->accumulator -= M_PI_M2f; + + val = (int16_t) (sinf(d->accumulator) * 32767.0f); + + 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..60d8dd6 --- /dev/null +++ b/src/examples/export-spa-device.c @@ -0,0 +1,124 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Exporting and loading a SPA device, using \ref api_pw_core. + [title] + */ + +#include +#include +#include + +#include +#include +#include + +#include + +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 \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..db72169 --- /dev/null +++ b/src/examples/export-spa.c @@ -0,0 +1,163 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Exporting and loading a SPA node, using \ref api_pw_core. + [title] + */ + +#include +#include +#include + +#include +#include +#include + +#include + +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_props(void *_data, uint32_t global_id, const struct spa_dict *props) +{ + 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_props = proxy_event_bound_props, +}; + +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 [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/gmain.c b/src/examples/gmain.c new file mode 100644 index 0000000..6a13b03 --- /dev/null +++ b/src/examples/gmain.c @@ -0,0 +1,101 @@ + +#include + +#include +#include + +typedef struct _PipeWireSource +{ + GSource base; + + struct pw_loop *loop; +} PipeWireSource; + +static gboolean +pipewire_loop_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + PipeWireSource *s = (PipeWireSource *) source; + int result; + + result = pw_loop_iterate (s->loop, 0); + if (result < 0) + g_warning ("pipewire_loop_iterate failed: %s", spa_strerror (result)); + + return TRUE; +} + +static GSourceFuncs pipewire_source_funcs = +{ + .dispatch = pipewire_loop_source_dispatch, +}; + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + printf("object: id:%u type:%s/%d\n", id, type, version); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +int main(int argc, char *argv[]) +{ + GMainLoop *main_loop; + PipeWireSource *source; + struct pw_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook registry_listener; + + main_loop = g_main_loop_new (NULL, FALSE); + + pw_init(&argc, &argv); + + loop = pw_loop_new(NULL /* properties */); + /* wrap */ + source = (PipeWireSource *) g_source_new (&pipewire_source_funcs, + sizeof (PipeWireSource)); + source->loop = loop; + g_source_add_unix_fd (&source->base, + pw_loop_get_fd (loop), + G_IO_IN | G_IO_ERR); + g_source_attach (&source->base, NULL); + g_source_unref (&source->base); + + context = pw_context_new(loop, + NULL /* properties */, + 0 /* user_data size */); + + core = pw_context_connect(context, + NULL /* properties */, + 0 /* user_data size */); + + registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, + 0 /* user_data size */); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, + ®istry_events, NULL); + + /* enter and leave must be called from the same thread that runs + * the mainloop */ + pw_loop_enter(loop); + g_main_loop_run(main_loop); + pw_loop_leave(loop); + + pw_proxy_destroy((struct pw_proxy*)registry); + pw_core_disconnect(core); + pw_context_destroy(context); + pw_loop_destroy(loop); + + g_main_loop_unref(main_loop); + + return 0; +} +/* [code] */ diff --git a/src/examples/internal.c b/src/examples/internal.c new file mode 100644 index 0000000..67d39b5 --- /dev/null +++ b/src/examples/internal.c @@ -0,0 +1,128 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + In process pipewire graph + [title] + */ + +#include +#include +#include +#include + +#include + +#include +#include + +struct data { + struct pw_main_loop *loop; + + struct pw_context *context; + struct pw_core *core; + + struct pw_proxy *source; + struct pw_proxy *sink; + struct pw_proxy *link; + + int res; +}; + +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, }; + struct pw_loop *loop; + struct pw_properties *props; + const char *dev = "hw:0"; + + pw_init(&argc, &argv); + + data.loop = pw_main_loop_new(NULL); + + loop = pw_main_loop_get_loop(data.loop); + + if (argc > 1) + dev = argv[1]; + + pw_loop_add_signal(loop, SIGINT, do_quit, &data); + pw_loop_add_signal(loop, SIGTERM, do_quit, &data); + + data.context = pw_context_new(loop, NULL, 0); + + pw_context_load_module(data.context, "libpipewire-module-spa-node-factory", NULL, NULL); + pw_context_load_module(data.context, "libpipewire-module-link-factory", NULL, NULL); + + data.core = pw_context_connect_self(data.context, NULL, 0); + if (data.core == NULL) { + fprintf(stderr, "can't connect: %m\n"); + data.res = -errno; + goto cleanup; + } + + props = pw_properties_new( + SPA_KEY_LIBRARY_NAME, "audiotestsrc/libspa-audiotestsrc", + SPA_KEY_FACTORY_NAME, "audiotestsrc", + PW_KEY_NODE_NAME, "test_source", + "node.param.Props", "{ live = false }", + NULL); + data.source = pw_core_create_object(data.core, + "spa-node-factory", + PW_TYPE_INTERFACE_Node, + PW_VERSION_NODE, + &props->dict, 0); + pw_properties_free(props); + + props = pw_properties_new( + SPA_KEY_LIBRARY_NAME, "alsa/libspa-alsa", + SPA_KEY_FACTORY_NAME, SPA_NAME_API_ALSA_PCM_SINK, + PW_KEY_NODE_NAME, "alsa_sink", + "api.alsa.path", dev, + "priority.driver", "1000", + NULL); + data.sink = pw_core_create_object(data.core, + "spa-node-factory", + PW_TYPE_INTERFACE_Node, + PW_VERSION_NODE, + &props->dict, 0); + + pw_loop_enter(loop); + while (true) { + if (pw_proxy_get_bound_id(data.source) != SPA_ID_INVALID && + pw_proxy_get_bound_id(data.sink) != SPA_ID_INVALID) + break; + + pw_loop_iterate(loop, -1); + } + pw_loop_leave(loop); + + pw_properties_clear(props); + pw_properties_setf(props, + PW_KEY_LINK_OUTPUT_NODE, "%d", pw_proxy_get_bound_id(data.source)); + pw_properties_setf(props, + PW_KEY_LINK_INPUT_NODE, "%d", pw_proxy_get_bound_id(data.sink)); + + data.link = pw_core_create_object(data.core, + "link-factory", + PW_TYPE_INTERFACE_Link, + PW_VERSION_LINK, + &props->dict, 0); + pw_properties_free(props); + + 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/examples/local-v4l2.c b/src/examples/local-v4l2.c new file mode 100644 index 0000000..2093a9b --- /dev/null +++ b/src/examples/local-v4l2.c @@ -0,0 +1,453 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Using libspa-v4l2 + [title] + */ + +#include +#include + +#define WIDTH 640 +#define HEIGHT 480 +#define BPP 3 +#define MAX_BUFFERS 32 + +#include "sdl.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +struct data { + SDL_Renderer *renderer; + SDL_Window *window; + SDL_Texture *texture; + + struct pw_main_loop *main_loop; + struct pw_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->main_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, PROT_READ, + MAP_PRIVATE, buf->datas[0].fd, buf->datas[0].mapoffset); + sdata = map; + } 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); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct data *d = object; + int res; + + if ((res = pw_loop_invoke(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); + + + pw_loop_enter(data->loop); + while (true) { + + if (pw_proxy_get_bound_id(data->out) != SPA_ID_INVALID && + pw_proxy_get_bound_id(data->in) != SPA_ID_INVALID) + break; + + pw_loop_iterate(data->loop, -1); + } + pw_loop_leave(data->loop); + + 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.main_loop = pw_main_loop_new(NULL); + data.loop = pw_main_loop_get_loop(data.main_loop); + data.context = pw_context_new( + 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.main_loop); + + pw_proxy_destroy(data.link); + pw_proxy_destroy(data.in); + pw_proxy_destroy(data.out); + pw_context_destroy(data.context); + pw_main_loop_destroy(data.main_loop); + pw_deinit(); + + return 0; +} diff --git a/src/examples/meson.build b/src/examples/meson.build new file mode 100644 index 0000000..7d45a34 --- /dev/null +++ b/src/examples/meson.build @@ -0,0 +1,62 @@ +# Examples, in order from simple to complicated +examples = [ + 'audio-src', + 'audio-src-ring', + 'audio-src-ring2', + 'audio-dsp-src', + 'audio-dsp-filter', + 'audio-capture', + 'video-play', + 'video-src', + 'video-dsp-play', + 'video-dsp-src', + 'video-play-pull', + 'video-play-reneg', + 'video-src-alloc', + 'video-src-reneg', + 'video-src-fixate', + 'video-play-fixate', + 'midi-src', + 'internal', + 'export-sink', + 'export-source', + 'export-spa', + 'export-spa-device', + 'bluez-session', + 'local-v4l2', + 'gmain', +] + +if not get_option('examples').allowed() + subdir_done() +endif + +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], + 'gmain': [glib2_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/midi-src.c b/src/examples/midi-src.c new file mode 100644 index 0000000..edcaa0f --- /dev/null +++ b/src/examples/midi-src.c @@ -0,0 +1,264 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2024 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + MIDI source using \ref pw_filter "pw_filter". + [title] + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + + +#define PERIOD_NSEC (SPA_NSEC_PER_SEC/8) + +struct port { +}; + +struct data { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct port *port; + uint32_t clock_id; + int64_t offset; + uint64_t position; +}; + +static void on_process(void *userdata, struct spa_io_position *position) +{ + struct data *data = userdata; + struct port *port = data->port; + struct pw_buffer *buf; + struct spa_data *d; + struct spa_pod_builder builder; + struct spa_pod_frame frame; + uint64_t sample_offset, sample_period, sample_position, cycle; + + /* + * Use the clock sample position. + * + * If the playback switches to using a different clock, we reset + * playback as the sample position can then be discontinuous. + */ + if (data->clock_id != position->clock.id) { + pw_log_info("switch to clock %u", position->clock.id); + data->offset = position->clock.position - data->position; + data->clock_id = position->clock.id; + } + + sample_position = position->clock.position - data->offset; + data->position = sample_position + position->clock.duration; + + /* + * Produce note on/off every `PERIOD_NSEC` nanoseconds (rounded down to + * samples, for simplicity). + * + * We want to place the notes on the playback timeline, so we use sample + * positions (not real time!). + */ + + sample_period = PERIOD_NSEC * position->clock.rate.denom + / position->clock.rate.num / SPA_NSEC_PER_SEC; + + cycle = sample_position / sample_period; + if (sample_position % sample_period != 0) + ++cycle; + + sample_offset = cycle*sample_period - sample_position; + + if (sample_offset >= position->clock.duration) + return; /* don't need to produce anything yet */ + + /* Get output buffer */ + if ((buf = pw_filter_dequeue_buffer(port)) == NULL) + return; + + /* Midi buffers always have exactly one data block */ + spa_assert(buf->buffer->n_datas == 1); + + d = &buf->buffer->datas[0]; + d->chunk->offset = 0; + d->chunk->size = 0; + d->chunk->stride = 1; + d->chunk->flags = 0; + + /* + * MIDI buffers contain a SPA POD with a sequence of + * control messages and their raw MIDI data. + */ + spa_pod_builder_init(&builder, d->data, d->maxsize); + spa_pod_builder_push_sequence(&builder, &frame, 0); + + while (sample_offset < position->clock.duration) { + if (cycle % 2 == 0) { + /* MIDI note on, channel 0, middle C, max velocity */ + uint32_t event = 0x20903c7f; + + /* The time position of the message in the graph cycle + * is given as offset from the cycle start, in + * samples. The cycle has duration of `clock.duration` + * samples, and the sample offset should satisfy + * 0 <= sample_offset < position->clock.duration. + */ + spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_UMP); + + /* Raw MIDI data for the message */ + spa_pod_builder_bytes(&builder, &event, sizeof(event)); + + pw_log_info("note on at %"PRIu64, sample_position + sample_offset); + } else { + /* MIDI note off, channel 0, middle C, max velocity */ + uint32_t event = 0x20803c7f; + + spa_pod_builder_control(&builder, sample_offset, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&builder, &event, sizeof(event)); + + pw_log_info("note off at %"PRIu64, sample_position + sample_offset); + } + + sample_offset += sample_period; + ++cycle; + } + + /* + * Finish the sequence and queue buffer to output. + */ + spa_pod_builder_pop(&builder, &frame); + d->chunk->size = builder.state.offset; + + pw_log_trace("produced %u/%u bytes", d->chunk->size, d->maxsize); + + pw_filter_queue_buffer(port, buf); +} + +static void state_changed(void *userdata, enum pw_filter_state old, + enum pw_filter_state state, const char *error) +{ + struct data *data = userdata; + + switch (state) { + case PW_FILTER_STATE_STREAMING: + /* reset playback position */ + pw_log_info("start playback"); + data->clock_id = SPA_ID_INVALID; + data->offset = 0; + data->position = 0; + break; + default: + break; + } +} + +static const struct pw_filter_events filter_events = { + PW_VERSION_FILTER_EVENTS, + .process = on_process, + .state_changed = state_changed, +}; + +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 = {}; + uint8_t buffer[1024]; + struct spa_pod_builder builder; + struct spa_pod *params[1]; + + 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), + "midi-src", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Midi", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_CLASS, "Midi/Source", + NULL), + &filter_events, + &data); + + /* Make a midi output port */ + data.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 raw UMP", + PW_KEY_PORT_NAME, "output", + NULL), + NULL, 0); + + /* Update SPA_PARAM_Buffers to request a specific sizes and counts. + * This is not mandatory: if you skip this, you'll get default sized + * buffers, usually 4k or 32k bytes or so. + * + * We'll here ask for 4096 bytes as that's enough. + */ + spa_pod_builder_init(&builder, buffer, sizeof(buffer)); + + params[0] = spa_pod_builder_add_object(&builder, + /* POD Object for the buffer parameter */ + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + /* Default 1 buffer, minimum of 1, max of 32 buffers. + * We can do with 1 buffer as we dequeue and queue in the same + * cycle. + */ + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, 32), + /* MIDI buffers always have 1 data block */ + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + /* Buffer size: request default 4096 bytes, min 4096, no maximum */ + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(4096, 4096, INT32_MAX), + /* MIDI buffers have stride 1 */ + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)); + + pw_filter_update_params(data.filter, data.port, + (const struct spa_pod **)params, SPA_N_ELEMENTS(params)); + + /* 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/sdl.h b/src/examples/sdl.h new file mode 100644 index 0000000..f96ed26 --- /dev/null +++ b/src/examples/sdl.h @@ -0,0 +1,178 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + SDL2 video format conversions + [title] + */ + +#include + +#include +#include +#include +#include + +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..ad645d7 --- /dev/null +++ b/src/examples/video-dsp-play.c @@ -0,0 +1,295 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_filter "pw_filter". + [title] + */ + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#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((uint8_t)(p[j].r * 255.0f), 0, 255); + dst[j * 4 + 1] = SPA_CLAMP((uint8_t)(p[j].g * 255.0f), 0, 255); + dst[j * 4 + 2] = SPA_CLAMP((uint8_t)(p[j].b * 255.0f), 0, 255); + dst[j * 4 + 3] = SPA_CLAMP((uint8_t)(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-dsp-src.c b/src/examples/video-dsp-src.c new file mode 100644 index 0000000..5992088 --- /dev/null +++ b/src/examples/video-dsp-src.c @@ -0,0 +1,374 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2023 Columbarius */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video source using \ref pw_stream. + [title] + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define BPP 16 +#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 pixel { + float r, g, b, a; +}; + +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_dsp format; + int32_t stride; + struct spa_io_position *position; + + 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 inline float map_value(int value) +{ + return (value%256)/255.0f; +} + +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 pixel *px; + 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 + h->pts = pw_stream_get_nsec(data->stream); +#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->position->video.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 = (int32_t)data->crop; + mc->region.position.y = (int32_t)data->crop; + mc->region.size.width = data->position->video.size.width - (int32_t)(data->crop*2); + mc->region.size.height = data->position->video.size.height - (int32_t)(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 = (int32_t)((sin(data->accumulator) + 1.0) * 160.0 + 80); + mcs->position.y = (int32_t)((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 = (uint32_t)((cos(data->accumulator) + 1.0) * (1 << 23)); + color |= 0xff000000; + + draw_elipse(bitmap, mb->size.width, mb->size.height, color); + } + + for (i = 0; i < data->position->video.size.height; i++) { + px = (struct pixel *)p; + for (j = 0; j < data->position->video.size.width; j++) { + px[j] = (struct pixel){map_value(data->counter + j * i), map_value(data->counter + j * (i + 1)), map_value(data->counter + j * (i + 2)), 1.0f}; + } + 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->position->video.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_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; + if (data->position) + pw_log_info("Position: %ux%u", data->position->video.size.width, data->position->video.size.height); + 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_Tag) { + spa_debug_pod(0, NULL, param); + return; + } + if (param == NULL || id != SPA_PARAM_Format) + return; + + spa_format_video_dsp_parse(param, &data->format); + + data->stride = SPA_ROUND_UP_N(data->position->video.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->position->video.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, + .io_changed = on_stream_io_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[2]; + 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_dsp), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32)); + + { + struct spa_pod_frame f; + struct spa_dict_item items[1]; + /* send a tag, output tags travel downstream */ + spa_tag_build_start(&b, &f, SPA_PARAM_Tag, SPA_DIRECTION_OUTPUT); + items[0] = SPA_DICT_ITEM_INIT("my-tag-key", "my-special-tag-value"); + spa_tag_build_add_dict(&b, &SPA_DICT_INIT(items, 1)); + params[1] = spa_tag_build_end(&b, &f); + } + + 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, 2); + + 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/examples/video-play-fixate.c b/src/examples/video-play-fixate.c new file mode 100644 index 0000000..b59be31 --- /dev/null +++ b/src/examples/video-play-fixate.c @@ -0,0 +1,496 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_stream "pw_stream", with format fixation. + [title] + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#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<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..f9492ed --- /dev/null +++ b/src/examples/video-play-pull.c @@ -0,0 +1,571 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_stream_trigger_process, for pull mode. + [title] + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#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((uint8_t)(p[j].r * 255.0f), 0u, 255u); + dst[j * 4 + 1] = SPA_CLAMP((uint8_t)(p[j].g * 255.0f), 0u, 255u); + dst[j * 4 + 2] = SPA_CLAMP((uint8_t)(p[j].b * 255.0f), 0u, 255u); + dst[j * 4 + 3] = SPA_CLAMP((uint8_t)(p[j].a * 255.0f), 0u, 255u); + } + 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: + printf("driving:%d lazy:%d\n", + pw_stream_is_driving(data->stream), + pw_stream_is_lazy(data->stream)); + if (pw_stream_is_driving(data->stream) != pw_stream_is_lazy(data->stream)) + 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; + 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: + pw_log_trace("%p trigger", data); + 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<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_NODE_SUPPORTS_LAZY, "1", + PW_KEY_NODE_SUPPORTS_REQUEST, "1", + 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..c93ed24 --- /dev/null +++ b/src/examples/video-play-reneg.c @@ -0,0 +1,418 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_stream "pw_stream", with format renegotiation. + [title] + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#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<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..53041f2 --- /dev/null +++ b/src/examples/video-play.c @@ -0,0 +1,539 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_stream "pw_stream". + [title] + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#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; + struct spa_meta_header *h; + 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; + + if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) { + uint64_t now = pw_stream_get_nsec(stream); + pw_log_debug("now:%"PRIu64" pts:%"PRIu64" diff:%"PRIi64, + now, h->pts, now - h->pts); + } + + /* 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((uint8_t)(p[j].r * 255.0f), 0u, 255u); + dst[j * 4 + 1] = SPA_CLAMP((uint8_t)(p[j].g * 255.0f), 0u, 255u); + dst[j * 4 + 2] = SPA_CLAMP((uint8_t)(p[j].b * 255.0f), 0u, 255u); + dst[j * 4 + 3] = SPA_CLAMP((uint8_t)(p[j].a * 255.0f), 0u, 255u); + } + 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; + + if (param != NULL && id == SPA_PARAM_Tag) { + spa_debug_pod(0, NULL, param); + return; + } + if (param != NULL && id == SPA_PARAM_Latency) { + struct spa_latency_info info; + if (spa_latency_parse(param, &info) >= 0) + fprintf(stderr, "got latency: %"PRIu64"\n", (info.min_ns + info.max_ns) / 2); + return; + } + /* 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<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[3]; + 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); + + { + struct spa_pod_frame f; + struct spa_dict_item items[1]; + /* send a tag, input tags travel upstream */ + spa_tag_build_start(&b, &f, SPA_PARAM_Tag, SPA_DIRECTION_INPUT); + items[0] = SPA_DICT_ITEM_INIT("my-tag-other-key", "my-special-other-tag-value"); + spa_tag_build_add_dict(&b, &SPA_DICT_INIT(items, 1)); + params[n_params++] = spa_tag_build_end(&b, &f); + } + + /* 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..1165183 --- /dev/null +++ b/src/examples/video-src-alloc.c @@ -0,0 +1,442 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Allocating buffer memory and sending fds to the server. + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#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 + h->pts = pw_stream_get_nsec(data->stream)); +#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 = (int32_t)data->crop; + mc->region.position.y = (int32_t)data->crop; + mc->region.size.width = data->format.size.width - (int32_t)(data->crop*2); + mc->region.size.height = data->format.size.height - (int32_t)(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 = (int32_t)((sin(data->accumulator) + 1.0) * 160.0 + 80); + mcs->position.y = (int32_t)((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 = (uint32_t)((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<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<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..724974a --- /dev/null +++ b/src/examples/video-src-fixate.c @@ -0,0 +1,580 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Fixating negotiated modifiers. + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#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 + h->pts = pw_stream_get_nsec(data->stream); +#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 = (int32_t)data->crop; + mc->region.position.y = (int32_t)data->crop; + mc->region.size.width = data->format.size.width - (int32_t)(data->crop*2); + mc->region.size.height = data->format.size.height - (int32_t)(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 = (int32_t)((sin(data->accumulator) + 1.0) * 160.0 + 80); + mcs->position.y = (int32_t)((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 = (uint32_t)((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< 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<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<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<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<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..d9f6abf --- /dev/null +++ b/src/examples/video-src-reneg.c @@ -0,0 +1,487 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Renegotiating video producer and consumer formats with \ref pw_stream + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#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 + h->pts = pw_stream_get_nsec(data->stream); +#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 = (int32_t)data->crop; + mc->region.position.y = (int32_t)data->crop; + mc->region.size.width = data->format.size.width - (int32_t)(data->crop*2); + mc->region.size.height = data->format.size.height - (int32_t)(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 = (int32_t)((sin(data->accumulator) + 1.0) * 160.0 + 80); + mcs->position.y = (int32_t)((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 = (uint32_t)((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<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<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..f439f16 --- /dev/null +++ b/src/examples/video-src.c @@ -0,0 +1,357 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video source using \ref pw_stream. + [title] + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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 + h->pts = pw_stream_get_nsec(data->stream); +#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 = (int32_t)data->crop; + mc->region.position.y = (int32_t)data->crop; + mc->region.size.width = data->format.size.width - (int32_t)(data->crop*2); + mc->region.size.height = data->format.size.height - (int32_t)(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 = (int32_t)((sin(data->accumulator) + 1.0) * 160.0 + 80); + mcs->position.y = (int32_t)((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 = (uint32_t)((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; + + printf("driving:%d lazy:%d\n", + pw_stream_is_driving(data->stream), + pw_stream_is_lazy(data->stream)); + + if (pw_stream_is_driving(data->stream) != pw_stream_is_lazy(data->stream)) + 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_Tag) { + spa_debug_pod(0, NULL, param); + return; + } + 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[2]; + 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", + PW_KEY_NODE_SUPPORTS_REQUEST, "1", + 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))); + + { + struct spa_pod_frame f; + /* send a tag, output tags travel downstream */ + spa_tag_build_start(&b, &f, SPA_PARAM_Tag, SPA_DIRECTION_OUTPUT); + spa_tag_build_add_dict(&b, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM("my-tag-key", "my-special-tag-value"))); + params[1] = spa_tag_build_end(&b, &f); + } + + 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, 2); + + 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..5761a09 --- /dev/null +++ b/src/gst/gstpipewire.c @@ -0,0 +1,49 @@ +/* PipeWire GStreamer Elements */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/** + * SECTION:element-pipewiresrc + * + * + * Example launch line + * |[ + * gst-launch -v pipewiresrc ! ximagesink + * ]| Shows PipeWire output in an X window. + * + */ + +#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..701bb6f --- /dev/null +++ b/src/gst/gstpipewireclock.c @@ -0,0 +1,114 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#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 (GstPipeWireStream *stream, GstClockTime last_time) +{ + GstPipeWireClock *clock; + + clock = g_object_new (GST_TYPE_PIPEWIRE_CLOCK, NULL); + g_weak_ref_set (&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; + g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&pclock->stream); + GstClockTime result; + uint64_t now; + + if (G_UNLIKELY (!s)) + return pclock->last_time; + + now = pw_stream_get_nsec(s->pwstream); +#if 1 + struct pw_time t; + if (s->pwstream == NULL || + pw_stream_get_time_n (s->pwstream, &t, sizeof(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 += now - t.now; + + result += pclock->time_offset; + pclock->last_time = result; + + GST_DEBUG ("%"PRId64", %d/%d %"PRId64" %"PRId64" %"PRId64, + t.ticks, t.rate.num, t.rate.denom, t.now, result, now); +#else + result = now + 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_weak_ref_set (&clock->stream, NULL); + + 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) +{ +#if 0 + 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)); +#endif +} diff --git a/src/gst/gstpipewireclock.h b/src/gst/gstpipewireclock.h new file mode 100644 index 0000000..8b41598 --- /dev/null +++ b/src/gst/gstpipewireclock.h @@ -0,0 +1,35 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_CLOCK_H__ +#define __GST_PIPEWIRE_CLOCK_H__ + +#include "config.h" +#include "gstpipewirestream.h" + +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_PIPEWIRE_CLOCK (gst_pipewire_clock_get_type()) +G_DECLARE_FINAL_TYPE (GstPipeWireClock, gst_pipewire_clock, GST, PIPEWIRE_CLOCK, GstSystemClock) + +struct _GstPipeWireClock { + GstSystemClock parent; + + GWeakRef stream; + + GstClockTime last_time; + GstClockTimeDiff time_offset; +}; + +GstClock * gst_pipewire_clock_new (GstPipeWireStream *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..dcb45ef --- /dev/null +++ b/src/gst/gstpipewirecore.c @@ -0,0 +1,200 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" +#include +#include +#include + +#include + +#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); + if (core->loop == NULL) + goto loop_failed; + core->context = pw_context_new (pw_thread_loop_get_loop(core->loop), NULL, 0); + if (core->context == NULL) + goto context_failed; + 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; + +loop_failed: + { + GST_ERROR ("error creating threadloop"); + g_free (core); + return NULL; + } +context_failed: + { + GST_ERROR ("error creating context"); + pw_thread_loop_destroy (core->loop); + g_free (core); + return NULL; + } +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: %s", strerror (errno)); + 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..486b153 --- /dev/null +++ b/src/gst/gstpipewirecore.h @@ -0,0 +1,40 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_CORE_H__ +#define __GST_PIPEWIRE_CORE_H__ + +#include + +#include + +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..5bdd09b --- /dev/null +++ b/src/gst/gstpipewiredeviceprovider.c @@ -0,0 +1,973 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#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 spa_list ports; +}; + +struct port_data { + struct spa_list link; + 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 GstPipeWireDevice * +gst_pipewire_device_new (int fd, uint32_t id, uint64_t serial, + GstPipeWireDeviceType type, const gchar * element, int priority, + const gchar * klass, const gchar * display_name, const GstCaps * caps, + const GstStructure * props) +{ + GstPipeWireDevice *gstdev; + + gstdev = + g_object_new (GST_TYPE_PIPEWIRE_DEVICE, "display-name", display_name, + "caps", caps, "device-class", klass, "id", id, "serial", serial, "fd", fd, + "properties", props, NULL); + + gstdev->id = id; + gstdev->serial = serial; + gstdev->type = type; + gstdev->element = element; + gstdev->priority = priority; + + return gstdev; +} + +static GstDevice * +new_node (GstPipeWireDeviceProvider *self, struct node_data *data) +{ + GstStructure *props; + const gchar *klass = NULL, *name = NULL; + GstPipeWireDeviceType type; + const struct pw_node_info *info = data->info; + const gchar *element = NULL; + GstPipeWireDevice *gstdev; + int priority = 0; + + 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 ("pipewire-proplist", "is-default", G_TYPE_BOOLEAN, FALSE, NULL); + if (info->props) { + const struct spa_dict_item *item; + const char *str; + + klass = spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS); + name = spa_dict_lookup(info->props, PW_KEY_NODE_DESCRIPTION); + + spa_dict_for_each (item, info->props) { + gst_structure_set (props, item->key, G_TYPE_STRING, item->value, NULL); + if (spa_streq(item->key, "node.name") && klass) { + if (spa_streq(klass, "Audio/Source") && spa_streq(item->value, self->default_audio_source_name)) + gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL); + else if (spa_streq(klass, "Audio/Sink") && + spa_streq(item->value, self->default_audio_sink_name)) + gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL); + else if (spa_streq(klass, "Video/Source") && + spa_streq(item->value, self->default_video_source_name)) + gst_structure_set(props, "is-default", G_TYPE_BOOLEAN, TRUE, NULL); + } + } + + if ((str = spa_dict_lookup(info->props, PW_KEY_PRIORITY_SESSION))) + priority = atoi(str); + } + if (klass == NULL) + klass = "unknown/unknown"; + if (name == NULL) + name = "unknown"; + + gstdev = gst_pipewire_device_new (self->fd, data->id, data->serial, type, + element, priority, klass, name, data->caps, props); + if (props) + gst_structure_free (props); + + return GST_DEVICE (gstdev); +} + +static int +compare_device_session_priority (const void *a, + const void *b) +{ + const GstPipeWireDevice *dev_a = a; + const GstPipeWireDevice *dev_b = b; + + if (dev_a->priority < dev_b->priority) + return 1; + else if (dev_a->priority > dev_b->priority) + return -1; + else + return 0; +} + +static void do_add_nodes(GstPipeWireDeviceProvider *self) +{ + struct node_data *nd; + g_autoptr (GList) new_devices = NULL; + GList *l; + + 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) + new_devices = g_list_prepend (new_devices, nd->dev); + } + if (!new_devices) + return; + + new_devices = g_list_sort (new_devices, + compare_device_session_priority); + for (l = new_devices; l != NULL; l = l->next) { + GstDevice *device = l->data; + + if(self->list_only) { + self->devices = g_list_insert_sorted (self->devices, + gst_object_ref_sink (device), + compare_device_session_priority); + } else { + gst_object_ref (device); + gst_device_provider_device_add (GST_DEVICE_PROVIDER (self), device); + } + } +} + +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 (node_data == NULL) + return; + + 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; + + if (node_data == NULL) + return; + + 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; + struct port_data *pd; + GstPipeWireDeviceProvider *self = nd->self; + GstDeviceProvider *provider = GST_DEVICE_PROVIDER (self); + + pw_log_debug("destroy %p", nd); + + spa_list_consume(pd, &nd->ports, link) { + spa_list_remove(&pd->link); + pd->node_data = NULL; + } + + if (nd->dev != NULL) { + gst_device_provider_device_remove (provider, nd->dev); + gst_clear_object (&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); + if (pd->node_data != NULL) { + spa_list_remove(&pd->link); + pd->node_data = NULL; + } +} + +static const struct pw_proxy_events proxy_port_events = { + PW_VERSION_PROXY_EVENTS, + .removed = removed_port, + .destroy = destroy_port, +}; + +static gboolean +is_default_device_name (GstPipeWireDeviceProvider * self, + const gchar * name, const gchar * klass, GstPipeWireDeviceType type) +{ + gboolean ret = FALSE; + + GST_OBJECT_LOCK (self); + switch (type) { + case GST_PIPEWIRE_DEVICE_TYPE_SINK: + if (g_str_has_prefix (klass, "Audio")) + ret = !g_strcmp0 (name, self->default_audio_sink_name); + break; + case GST_PIPEWIRE_DEVICE_TYPE_SOURCE: + if (g_str_has_prefix (klass, "Audio")) + ret = !g_strcmp0 (name, self->default_audio_source_name); + else if (g_str_has_prefix (klass, "Video")) + ret = !g_strcmp0 (name, self->default_video_source_name); + break; + default: + GST_ERROR_OBJECT (self, "Unknown pipewire device type!"); + break; + } + GST_OBJECT_UNLOCK (self); + + return ret; +} + +static void +sync_default_devices (GstPipeWireDeviceProvider * self) +{ + GList *tmp, *devices = NULL; + + for (tmp = GST_DEVICE_PROVIDER_CAST (self)->devices; tmp; tmp = tmp->next) + devices = g_list_prepend (devices, gst_object_ref (tmp->data)); + + for (tmp = devices; tmp; tmp = tmp->next) { + GstPipeWireDevice *dev = tmp->data; + GstStructure *props = gst_device_get_properties (GST_DEVICE_CAST (dev)); + gboolean was_default = FALSE, is_default = FALSE; + const gchar *name; + gchar *klass = gst_device_get_device_class (GST_DEVICE_CAST (dev)); + + g_assert (props); + gst_structure_get_boolean (props, "is-default", &was_default); + name = gst_structure_get_string (props, "node.name"); + + switch (dev->type) { + case GST_PIPEWIRE_DEVICE_TYPE_SINK: + is_default = + is_default_device_name (self, name, klass, dev->type); + break; + case GST_PIPEWIRE_DEVICE_TYPE_SOURCE: + is_default = + is_default_device_name (self, name, klass, dev->type); + break; + case GST_PIPEWIRE_DEVICE_TYPE_UNKNOWN: + break; + } + + if (was_default != is_default) { + GstPipeWireDevice *updated_device; + gchar *display_name = gst_device_get_display_name (GST_DEVICE_CAST (dev)); + GstCaps *caps = gst_device_get_caps (GST_DEVICE_CAST (dev)); + + gst_structure_set (props, "is-default", G_TYPE_BOOLEAN, is_default, NULL); + updated_device = + gst_pipewire_device_new (self->fd, dev->id, dev->serial, dev->type, + dev->element, dev->priority, klass, display_name, caps, props); + + gst_device_provider_device_changed (GST_DEVICE_PROVIDER_CAST (self), + GST_DEVICE_CAST (updated_device), GST_DEVICE_CAST (dev)); + + g_free (display_name); + gst_caps_unref (caps); + } + gst_structure_free (props); + g_free (klass); + } + g_list_free_full (devices, gst_object_unref); +} + +static int metadata_property(void *data, uint32_t id, const char *key, + const char *type, const char *value) { + GstPipeWireDeviceProvider *self = data; + char name[1024]; + + if (value == NULL) + return 0; + + if (spa_streq(key, "default.audio.source")) { + if (!spa_streq(type, "Spa:String:JSON")) + return 0; + + g_free(self->default_audio_source_name); + if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0) + self->default_audio_source_name = g_strdup(name); + goto sync_devices; + } + if (spa_streq(key, "default.audio.sink")) { + if (!spa_streq(type, "Spa:String:JSON")) + return 0; + + g_free(self->default_audio_sink_name); + if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0) + self->default_audio_sink_name = g_strdup(name); + goto sync_devices; + } + if (spa_streq(key, "default.video.source")) { + if (!spa_streq(type, "Spa:String:JSON")) + return 0; + + g_free(self->default_video_source_name); + if (spa_json_str_object_find(value, strlen(value), "name", name, sizeof(name)) >= 0) + self->default_video_source_name = g_strdup(name); + goto sync_devices; + } + + return 0; + + sync_devices: + sync_default_devices (self); + return 0; +} + +static const struct pw_metadata_events metadata_events = { + PW_VERSION_METADATA_EVENTS, .property = metadata_property}; + +static void registry_event_global(void *data, uint32_t id, uint32_t permissions, + const char *type, uint32_t version, + const struct spa_dict *props) +{ + 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_init(&nd->ports); + 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; + spa_list_append(&nd->ports, &pd->link); + pw_port_add_listener(port, &pd->port_listener, &port_events, pd); + pw_proxy_add_listener((struct pw_proxy*)port, &pd->proxy_listener, &proxy_port_events, pd); + resync(self); + } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata) && props) { + const char *name; + + name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); + if (name == NULL) + return; + + if (!spa_streq(name, "default")) + return; + + self->metadata = + pw_registry_bind(self->registry, id, type, PW_VERSION_METADATA, 0); + pw_metadata_add_listener(self->metadata, &self->metadata_listener, + &metadata_events, self); + } + + return; + +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); + 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, ®istry_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); + 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, ®istry_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); + + /* core might be NULL if we failed to connect in _start. */ + if (self->core != NULL) { + pw_thread_loop_lock (self->core->loop); + } + GST_DEBUG_OBJECT (self, "stopping provider"); + + if (self->metadata) { + spa_hook_remove(&self->metadata_listener); + pw_proxy_destroy((struct pw_proxy *)self->metadata); + self->metadata = NULL; + } + + g_clear_pointer ((struct pw_proxy**)&self->registry, pw_proxy_destroy); + if (self->core != NULL) { + pw_thread_loop_unlock (self->core->loop); + } + 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_free (self->default_audio_source_name); + g_free (self->default_audio_sink_name); + g_free (self->default_video_source_name); + + G_OBJECT_CLASS (gst_pipewire_device_provider_parent_class)->finalize (object); +} + +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 "); +} + +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..82b2a8a --- /dev/null +++ b/src/gst/gstpipewiredeviceprovider.h @@ -0,0 +1,72 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_DEVICE_PROVIDER_H__ +#define __GST_PIPEWIRE_DEVICE_PROVIDER_H__ + +#include "config.h" + +#include + +#include +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_PIPEWIRE_DEVICE (gst_pipewire_device_get_type()) +#define GST_PIPEWIRE_DEVICE_CAST(obj) ((GstPipeWireDevice *)(obj)) +G_DECLARE_FINAL_TYPE (GstPipeWireDevice, gst_pipewire_device, GST, PIPEWIRE_DEVICE, GstDevice) + +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; + int priority; +}; + +#define GST_TYPE_PIPEWIRE_DEVICE_PROVIDER (gst_pipewire_device_provider_get_type()) +#define GST_PIPEWIRE_DEVICE_PROVIDER_CAST(obj) ((GstPipeWireDeviceProvider *)(obj)) +G_DECLARE_FINAL_TYPE (GstPipeWireDeviceProvider, gst_pipewire_device_provider, GST, PIPEWIRE_DEVICE_PROVIDER, GstDeviceProvider) + +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 pw_metadata *metadata; + struct spa_hook metadata_listener; + + gchar *default_audio_source_name; + gchar *default_audio_sink_name; + gchar *default_video_source_name; + + struct spa_list nodes; + int seq; + + int error; + gboolean end; + gboolean list_only; + GList *devices; +}; + +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..903d216 --- /dev/null +++ b/src/gst/gstpipewireformat.c @@ -0,0 +1,1326 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "gstpipewireformat.h" + +#ifndef DRM_FORMAT_INVALID +#define DRM_FORMAT_INVALID 0 +#endif + +#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, +}; + +static const uint32_t interlace_mode_map[] = { + SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE, + SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, + SPA_VIDEO_INTERLACE_MODE_MIXED, + SPA_VIDEO_INTERLACE_MODE_FIELDS, +}; + +#if __BYTE_ORDER == __BIG_ENDIAN +#define _FORMAT_LE(fmt) SPA_AUDIO_FORMAT_ ## fmt ## _OE +#define _FORMAT_BE(fmt) SPA_AUDIO_FORMAT_ ## fmt +#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 { + const struct media_type *type; + 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 void +add_limits (struct spa_pod_dynamic_builder *b, ConvertData *d) +{ + struct spa_pod_choice *choice; + struct spa_pod_frame f; + const GValue *value, *value2; + int i; + + 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 (&b->b, SPA_FORMAT_VIDEO_size, 0); + spa_pod_builder_push_choice(&b->b, &f, get_range_type2 (value, value2), 0); + } + + spa_pod_builder_rectangle (&b->b, v.width, v.height); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b->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 (&b->b, SPA_FORMAT_VIDEO_framerate, 0); + spa_pod_builder_push_choice(&b->b, &f, get_range_type (value), 0); + } + + spa_pod_builder_fraction (&b->b, v.num, v.denom); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b->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 (&b->b, SPA_FORMAT_VIDEO_maxFramerate, 0); + spa_pod_builder_push_choice(&b->b, &f, get_range_type (value), 0); + } + + spa_pod_builder_fraction (&b->b, v.num, v.denom); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b->b, &f); + if (i == 1) + choice->body.type = SPA_CHOICE_None; + } + } +} + +static void +add_video_format (gpointer format_ptr, + gpointer modifiers_ptr, + gpointer user_data) +{ + uint32_t format = GPOINTER_TO_UINT (format_ptr); + GHashTable *modifiers = modifiers_ptr; + ConvertData *d = user_data; + struct spa_pod_dynamic_builder b; + struct spa_pod_frame f; + int n_mods; + + spa_pod_dynamic_builder_init (&b, NULL, 0, 1024); + + spa_pod_builder_push_object (&b.b, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaType, 0); + spa_pod_builder_id(&b.b, d->type->media_type); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaSubtype, 0); + spa_pod_builder_id(&b.b, d->type->media_subtype); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id (&b.b, format); + + n_mods = g_hash_table_size (modifiers); + if (n_mods > 0) { + struct spa_pod_frame f2; + GHashTableIter iter; + gpointer key, value; + uint32_t flags, choice_type; + + flags = SPA_POD_PROP_FLAG_MANDATORY; + if (n_mods > 1) { + choice_type = SPA_CHOICE_Enum; + flags |= SPA_POD_PROP_FLAG_DONT_FIXATE; + } else { + choice_type = SPA_CHOICE_None; + } + + spa_pod_builder_prop (&b.b, SPA_FORMAT_VIDEO_modifier, flags); + spa_pod_builder_push_choice (&b.b, &f2, choice_type, 0); + + g_hash_table_iter_init (&iter, modifiers); + g_hash_table_iter_next (&iter, &key, &value); + spa_pod_builder_long (&b.b, (uint64_t) key); + + if (n_mods > 1) { + do { + spa_pod_builder_long (&b.b, (uint64_t) key); + } while (g_hash_table_iter_next (&iter, &key, &value)); + } + + spa_pod_builder_pop (&b.b, &f2); + } + + add_limits (&b, d); + + g_ptr_array_add (d->array, spa_pod_builder_pop (&b.b, &f)); +} + +static void +handle_video_fields (ConvertData *d) +{ + g_autoptr (GHashTable) formats = NULL; + const GValue *value; + gboolean dmabuf_caps; + int i; + + formats = g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify) g_hash_table_unref); + dmabuf_caps = (d->cf && + gst_caps_features_contains (d->cf, + GST_CAPS_FEATURE_MEMORY_DMABUF)); + + value = gst_structure_get_value (d->cs, "format"); + if (value) { + const char *v; + + for (i = 0; (v = get_nth_string (value, i)); i++) { + int idx; + + idx = gst_video_format_from_string (v); +#ifdef HAVE_GSTREAMER_DMA_DRM + if (dmabuf_caps && idx == GST_VIDEO_FORMAT_DMA_DRM) { + const GValue *value2; + + value2 = gst_structure_get_value (d->cs, "drm-format"); + if (value2) { + const char *v2; + int j; + + for (j = 0; (v2 = get_nth_string (value2, j)); j++) { + uint32_t fourcc; + uint64_t mod; + int idx2; + + fourcc = gst_video_dma_drm_fourcc_from_string (v2, &mod); + idx2 = gst_video_dma_drm_fourcc_to_format (fourcc); + + if (idx2 != GST_VIDEO_FORMAT_UNKNOWN && + idx2 < (int)SPA_N_ELEMENTS (video_format_map)) { + GHashTable *modifiers = + g_hash_table_lookup (formats, + GINT_TO_POINTER (video_format_map[idx2])); + if (!modifiers) { + modifiers = g_hash_table_new (NULL, NULL); + g_hash_table_insert (formats, + GINT_TO_POINTER (video_format_map[idx2]), + modifiers); + } + + g_hash_table_add (modifiers, GINT_TO_POINTER (mod)); + } + } + } + } else +#endif + if (idx != GST_VIDEO_FORMAT_UNKNOWN && + idx < (int)SPA_N_ELEMENTS (video_format_map)) { + GHashTable *modifiers = + g_hash_table_lookup (formats, + GINT_TO_POINTER (video_format_map[idx])); + if (!modifiers) { + modifiers = g_hash_table_new (NULL, NULL); + g_hash_table_insert (formats, + GINT_TO_POINTER (video_format_map[idx]), + modifiers); + } + + if (dmabuf_caps) { + g_hash_table_add (modifiers, GINT_TO_POINTER (DRM_FORMAT_MOD_LINEAR)); + g_hash_table_add (modifiers, GINT_TO_POINTER (DRM_FORMAT_MOD_INVALID)); + } + } + } + } + + if (g_hash_table_size (formats) > 0) { + g_hash_table_foreach (formats, add_video_format, d); + } else if (!dmabuf_caps) { + struct spa_pod_dynamic_builder b; + struct spa_pod_frame f; + + spa_pod_dynamic_builder_init (&b, NULL, 0, 1024); + + spa_pod_builder_push_object (&b.b, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaType, 0); + spa_pod_builder_id(&b.b, d->type->media_type); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaSubtype, 0); + spa_pod_builder_id(&b.b, d->type->media_subtype); + + add_limits (&b, d); + + g_ptr_array_add (d->array, spa_pod_builder_pop (&b.b, &f)); + } +} + +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 void +handle_audio_fields (ConvertData *d) +{ + const GValue *value; + struct spa_pod_dynamic_builder b; + struct spa_pod_choice *choice; + struct spa_pod_frame f, f0; + int i = 0; + + spa_pod_dynamic_builder_init (&b, NULL, 0, 1024); + + spa_pod_builder_push_object (&b.b, &f0, SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaType, 0); + spa_pod_builder_id(&b.b, d->type->media_type); + + spa_pod_builder_prop (&b.b, SPA_FORMAT_mediaSubtype, 0); + spa_pod_builder_id(&b.b, d->type->media_subtype); + + 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 (&b.b, SPA_FORMAT_AUDIO_format, 0); + spa_pod_builder_push_choice(&b.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 (&b.b, audio_format_map[idx]); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b.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 (&b.b, SPA_FORMAT_AUDIO_format, 0); + spa_pod_builder_id (&b.b, SPA_AUDIO_FORMAT_ULAW); + } else if (strcmp(d->type->name, "audio/x-alaw") == 0) { + spa_pod_builder_prop (&b.b, SPA_FORMAT_AUDIO_format, 0); + spa_pod_builder_id (&b.b, SPA_AUDIO_FORMAT_ALAW); + } else if (strcmp(d->type->name, "audio/mpeg") == 0) { + spa_pod_builder_prop (&b.b, SPA_FORMAT_AUDIO_format, 0); + spa_pod_builder_id (&b.b, SPA_AUDIO_FORMAT_ENCODED); + } else if (strcmp(d->type->name, "audio/x-flac") == 0) { + spa_pod_builder_prop (&b.b, SPA_FORMAT_AUDIO_format, 0); + spa_pod_builder_id (&b.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 (&b.b, SPA_FORMAT_AUDIO_layout, 0); + spa_pod_builder_push_choice(&b.b, &f, get_range_type (value), 0); + } + + spa_pod_builder_id (&b.b, layout); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b.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 (&b.b, SPA_FORMAT_AUDIO_rate, 0); + spa_pod_builder_push_choice(&b.b, &f, get_range_type (value), 0); + } + + spa_pod_builder_int (&b.b, v); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b.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 (&b.b, SPA_FORMAT_AUDIO_channels, 0); + spa_pod_builder_push_choice(&b.b, &f, get_range_type (value), 0); + } + + spa_pod_builder_int (&b.b, v); + } + if (i > 0) { + choice = spa_pod_builder_pop(&b.b, &f); + if (i == 1) { + choice->body.type = SPA_CHOICE_None; + set_default_channels (&b.b, v); + } + } + } + + g_ptr_array_add (d->array, spa_pod_builder_pop (&b.b, &f0)); +} + +static void +handle_fields (ConvertData *d) +{ + if (!(d->type = find_media_types (gst_structure_get_name (d->cs)))) + return; + + 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); +} + +static gboolean +foreach_func_dmabuf (GstCapsFeatures *features, + GstStructure *structure, + ConvertData *d) +{ + if (!features || !gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_DMABUF)) + return TRUE; + + d->cf = features; + d->cs = structure; + + handle_fields (d); + + return TRUE; +} + +static gboolean +foreach_func_no_dmabuf (GstCapsFeatures *features, + GstStructure *structure, + ConvertData *d) +{ + if (features && gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_DMABUF)) + return TRUE; + + d->cf = features; + d->cs = structure; + + handle_fields (d); + + return TRUE; +} + + +GPtrArray * +gst_caps_to_format_all (GstCaps *caps) +{ + ConvertData d; + + d.array = g_ptr_array_new_full (gst_caps_get_size (caps), (GDestroyNotify)g_free); + + gst_caps_foreach (caps, (GstCapsForeachFunc) foreach_func_dmabuf, &d); + gst_caps_foreach (caps, (GstCapsForeachFunc) foreach_func_no_dmabuf, &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); +} + +#ifdef HAVE_GSTREAMER_DMA_DRM +static char *video_id_to_dma_drm_fourcc(uint32_t id, uint64_t mod) +{ + int idx; + guint32 fourcc; + if ((idx = find_index(video_format_map, SPA_N_ELEMENTS(video_format_map), id)) == -1) + return NULL; + fourcc = gst_video_dma_drm_fourcc_from_format(idx); + return gst_video_dma_drm_fourcc_to_string(fourcc, mod); +} +#endif + +static const char *interlace_mode_id_to_string(uint32_t id) +{ + int idx; + if ((idx = find_index(interlace_mode_map, SPA_N_ELEMENTS(interlace_mode_map), id)) == -1) + return NULL; + return gst_video_interlace_mode_to_string(idx); +} + +static const char *audio_id_to_string(uint32_t id) +{ + int idx; + 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 || n_items == 0) + 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_dmabuf_prop (const struct spa_pod_prop *prop, + const struct spa_pod_prop *prop_modifier, GstCaps *res) +{ + g_autoptr (GPtrArray) fmt_array = NULL; + g_autoptr (GPtrArray) drm_fmt_array = NULL; + const struct spa_pod *pod_modifier; + struct spa_pod *val; + uint32_t *id, n_fmts, n_mods, choice, i, j; + uint64_t *mods; + + val = spa_pod_get_values (&prop->value, &n_fmts, &choice); + if (val->type != SPA_TYPE_Id || n_fmts == 0) + return; + if (choice != SPA_CHOICE_None && choice != SPA_CHOICE_Enum) + return; + + id = SPA_POD_BODY (val); + if (n_fmts > 1) { + n_fmts--; + id++; + } + + pod_modifier = spa_pod_get_values (&prop_modifier->value, &n_mods, &choice); + if (pod_modifier->type != SPA_TYPE_Long || n_mods == 0) + return; + if (choice != SPA_CHOICE_None && choice != SPA_CHOICE_Enum) + return; + + mods = SPA_POD_BODY (pod_modifier); + if (n_mods > 1) { + n_mods--; + mods++; + } + + fmt_array = g_ptr_array_new_with_free_func (g_free); + drm_fmt_array = g_ptr_array_new_with_free_func (g_free); + + for (i = 0; i < n_fmts; i++) { + for (j = 0; j < n_mods; j++) { + const char *fmt_str; + + if ((mods[j] == DRM_FORMAT_MOD_LINEAR || + mods[j] == DRM_FORMAT_MOD_INVALID) && + (fmt_str = video_id_to_string(id[i]))) + g_ptr_array_add(fmt_array, g_strdup_printf ("%s", fmt_str)); + +#ifdef HAVE_GSTREAMER_DMA_DRM + if (mods[j] != DRM_FORMAT_MOD_INVALID) { + char *drm_str; + + if ((drm_str = video_id_to_dma_drm_fourcc(id[i], mods[j]))) + g_ptr_array_add(drm_fmt_array, drm_str); + } +#endif + } + } + +#ifdef HAVE_GSTREAMER_DMA_DRM + if (drm_fmt_array->len > 0) { + g_ptr_array_add (fmt_array, g_strdup_printf ("DMA_DRM")); + + if (drm_fmt_array->len == 1) { + gst_caps_set_simple (res, "drm-format", G_TYPE_STRING, + g_ptr_array_index (drm_fmt_array, 0), NULL); + } else { + GValue list = { 0 }; + + g_value_init (&list, GST_TYPE_LIST); + for (i = 0; i < drm_fmt_array->len; i++) { + GValue v = { 0 }; + + g_value_init (&v, G_TYPE_STRING); + g_value_set_string (&v, g_ptr_array_index (drm_fmt_array, i)); + gst_value_list_append_and_take_value (&list, &v); + } + + gst_caps_set_value (res, "drm-format", &list); + g_value_unset (&list); + } + } +#endif + + if (fmt_array->len > 0) { + gst_caps_set_features_simple (res, + gst_caps_features_from_string (GST_CAPS_FEATURE_MEMORY_DMABUF)); + + if (fmt_array->len == 1) { + gst_caps_set_simple (res, "format", G_TYPE_STRING, + g_ptr_array_index (fmt_array, 0), NULL); + } else { + GValue list = { 0 }; + + g_value_init (&list, GST_TYPE_LIST); + for (i = 0; i < fmt_array->len; i++) { + GValue v = { 0 }; + + g_value_init (&v, G_TYPE_STRING); + g_value_set_string (&v, g_ptr_array_index (fmt_array, i)); + gst_value_list_append_and_take_value (&list, &v); + } + + gst_caps_set_value (res, "format", &list); + g_value_unset (&list); + } + } +} + +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 || n_items == 0) + 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 || n_items == 0) + 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; + + if (rect[1].width == rect[2].width && + rect[1].height == rect[2].height) { + gst_caps_set_simple (res, + width, G_TYPE_INT, rect[1].width, + height, G_TYPE_INT, rect[1].height, + NULL); + } else { + 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 || n_items == 0) + 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; + + if (fract[1].num == fract[2].num && + fract[1].denom == fract[2].denom) { + gst_caps_set_simple (res, key, GST_TYPE_FRACTION, + fract[1].num, fract[1].denom, NULL); + } else { + 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) { + const struct spa_pod_prop *prop_modifier; + + res = gst_caps_new_empty_simple ("video/x-raw"); + + if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_format)) && + (prop_modifier = spa_pod_object_find_prop (obj, NULL, SPA_FORMAT_VIDEO_modifier))) { + handle_dmabuf_prop (prop, prop_modifier, res); + } else { + if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_format))) { + handle_id_prop (prop, "format", video_id_to_string, res); + } + } + if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_interlaceMode))) { + handle_id_prop (prop, "interlace-mode", interlace_mode_id_to_string, res); + } else { + gst_caps_set_simple(res, "interlace-mode", G_TYPE_STRING, "progressive", NULL); + } + } + else if (media_subtype == SPA_MEDIA_SUBTYPE_mjpg) { + res = gst_caps_new_empty_simple ("image/jpeg"); + } + 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; +} + +static gboolean +filter_dmabuf_caps (GstCapsFeatures *features, + GstStructure *structure, + gpointer user_data) +{ + const GValue *value; + const char *v; + + if (!gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_DMABUF)) + return TRUE; + + if (!(value = gst_structure_get_value (structure, "format")) || + !(v = get_nth_string (value, 0))) + return FALSE; + +#ifdef HAVE_GSTREAMER_DMA_DRM + { + int idx; + + idx = gst_video_format_from_string (v); + if (idx == GST_VIDEO_FORMAT_UNKNOWN) + return FALSE; + + if (idx == GST_VIDEO_FORMAT_DMA_DRM && + !gst_structure_get_value (structure, "drm-format")) + return FALSE; + } +#endif + + return TRUE; +} + +void +gst_caps_sanitize (GstCaps **caps) +{ + g_return_if_fail (GST_IS_CAPS (*caps)); + + *caps = gst_caps_make_writable (*caps); + gst_caps_filter_and_map_in_place (*caps, filter_dmabuf_caps, NULL); +} + +void +gst_caps_maybe_fixate_dma_format (GstCaps *caps) +{ +#ifdef HAVE_GSTREAMER_DMA_DRM + GstCapsFeatures *features; + GstStructure *structure; + const GValue *format_value; + const GValue *drm_format_value; + const char *format_string; + const char *drm_format_string; + uint32_t fourcc; + uint64_t mod; + int drm_idx; + int i; + + g_return_if_fail (GST_IS_CAPS (caps)); + + if (gst_caps_is_fixed (caps) || gst_caps_get_size(caps) != 1) + return; + + features = gst_caps_get_features (caps, 0); + if (!gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_DMABUF)) + return; + + structure = gst_caps_get_structure (caps, 0); + if (!gst_structure_has_field (structure, "format") || + !gst_structure_has_field (structure, "drm-format")) + return; + + format_value = gst_structure_get_value (structure, "format"); + drm_format_value = gst_structure_get_value (structure, "drm-format"); + if (G_VALUE_TYPE (format_value) != GST_TYPE_LIST || + ((GArray *) g_value_peek_pointer (format_value))->len != 2 || + G_VALUE_TYPE (drm_format_value) != G_TYPE_STRING) + return; + + drm_format_string = g_value_get_string (drm_format_value); + fourcc = gst_video_dma_drm_fourcc_from_string (drm_format_string, &mod); + drm_idx = gst_video_dma_drm_fourcc_to_format (fourcc); + if (drm_idx == GST_VIDEO_FORMAT_UNKNOWN || mod != DRM_FORMAT_MOD_LINEAR) + return; + + for (i = 0; (format_string = get_nth_string (format_value, i)); i++) { + int idx; + + idx = gst_video_format_from_string (format_string); + if (idx != GST_VIDEO_FORMAT_DMA_DRM && idx != drm_idx) + return; + } + + gst_caps_set_simple (caps, "format", G_TYPE_STRING, "DMA_DRM", NULL); + g_warn_if_fail (gst_caps_is_fixed (caps)); +#endif +} diff --git a/src/gst/gstpipewireformat.h b/src/gst/gstpipewireformat.h new file mode 100644 index 0000000..2ecfedd --- /dev/null +++ b/src/gst/gstpipewireformat.h @@ -0,0 +1,24 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef _GST_PIPEWIRE_FORMAT_H_ +#define _GST_PIPEWIRE_FORMAT_H_ + +#include + +#include + +G_BEGIN_DECLS + +GPtrArray * gst_caps_to_format_all (GstCaps *caps); + +GstCaps * gst_caps_from_format (const struct spa_pod *format); + +void gst_caps_sanitize (GstCaps **caps); + +void gst_caps_maybe_fixate_dma_format (GstCaps *caps); + +G_END_DECLS + +#endif diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c new file mode 100644 index 0000000..78869e1 --- /dev/null +++ b/src/gst/gstpipewirepool.c @@ -0,0 +1,359 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include + +#include + +#include +#include + +#include + +#include "gstpipewirepool.h" + +#include +#include + + +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 (GstPipeWireStream *stream) +{ + GstPipeWirePool *pool; + + pool = g_object_new (GST_TYPE_PIPEWIRE_POOL, NULL); + g_weak_ref_set (&pool->stream, stream); + + 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_DEBUG_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_DEBUG_OBJECT (pool, "wrap data (%s %d) %d %d", + spa_debug_type_find_short_name(spa_type_data_type, d->type), d->type, + d->mapoffset, d->maxsize); + if (d->type == SPA_DATA_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) { + 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) { + gmem = gst_memory_new_wrapped (0, d->data, d->maxsize, 0, + d->maxsize, NULL, NULL); + } + if (gmem) + gst_buffer_insert_memory (buf, i, gmem); + } + + if (pool->add_metavideo) { + gst_buffer_add_video_meta_full (buf, GST_VIDEO_FRAME_FLAG_NONE, + GST_VIDEO_INFO_FORMAT (&pool->video_info), + GST_VIDEO_INFO_WIDTH (&pool->video_info), + GST_VIDEO_INFO_HEIGHT (&pool->video_info), + GST_VIDEO_INFO_N_PLANES (&pool->video_info), + pool->video_info.offset, + pool->video_info.stride); + } + + 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; + + pool->n_buffers++; +} + +void gst_pipewire_pool_remove_buffer (GstPipeWirePool *pool, struct pw_buffer *b) +{ + GstPipeWirePoolData *data = b->user_data; + + data->b = NULL; + data->header = NULL; + data->crop = NULL; + data->videotransform = NULL; + + gst_buffer_remove_all_memory (data->buf); + + /* this will also destroy the pool data, if this is the last reference */ + gst_clear_buffer (&data->buf); + + pool->n_buffers--; +} + +GstPipeWirePoolData *gst_pipewire_pool_get_data (GstBuffer *buffer) +{ + return gst_mini_object_get_qdata (GST_MINI_OBJECT_CAST (buffer), pool_data_quark); +} + +static GstFlowReturn +acquire_buffer (GstBufferPool * pool, GstBuffer ** buffer, + GstBufferPoolAcquireParams * params) +{ + GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); + g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&p->stream); + GstPipeWirePoolData *data; + struct pw_buffer *b; + + if (G_UNLIKELY (!s)) + return GST_FLOW_ERROR; + + GST_OBJECT_LOCK (pool); + while (TRUE) { + if (G_UNLIKELY (GST_BUFFER_POOL_IS_FLUSHING (pool))) + goto flushing; + + if ((b = pw_stream_dequeue_buffer(s->pwstream))) { + GST_LOG_OBJECT (pool, "dequeued buffer %p", b); + break; + } + + if (params) { + if (params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_DONTWAIT) + goto no_more_buffers; + + if ((params->flags & GST_BUFFER_POOL_ACQUIRE_FLAG_LAST) && + p->paused) + goto paused; + } + + GST_WARNING_OBJECT (pool, "failed to dequeue buffer: %s", strerror(errno)); + g_cond_wait (&p->cond, GST_OBJECT_GET_LOCK (pool)); + } + + data = b->user_data; + data->queued = FALSE; + + *buffer = data->buf; + + GST_OBJECT_UNLOCK (pool); + GST_LOG_OBJECT (pool, "acquired gstbuffer %p", *buffer); + + return GST_FLOW_OK; + +flushing: + { + GST_OBJECT_UNLOCK (pool); + return GST_FLOW_FLUSHING; + } +paused: + { + GST_OBJECT_UNLOCK (pool); + return GST_FLOW_CUSTOM_ERROR_1; + } +no_more_buffers: + { + GST_LOG_OBJECT (pool, "no more buffers"); + GST_OBJECT_UNLOCK (pool); + return GST_FLOW_EOS; + } +} + +static const gchar ** +get_options (GstBufferPool * pool) +{ + static const gchar *options[] = { GST_BUFFER_POOL_OPTION_VIDEO_META, NULL }; + return options; +} + +static gboolean +set_config (GstBufferPool * pool, GstStructure * config) +{ + GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); + GstCaps *caps; + GstStructure *structure; + guint size, min_buffers, max_buffers; + gboolean has_video; + + if (!gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers)) { + GST_WARNING_OBJECT (pool, "invalid config"); + return FALSE; + } + + if (caps == NULL) { + GST_WARNING_OBJECT (pool, "no caps in config"); + return FALSE; + } + + structure = gst_caps_get_structure (caps, 0); + if (g_str_has_prefix (gst_structure_get_name (structure), "video/") || + g_str_has_prefix (gst_structure_get_name (structure), "image/")) { + has_video = TRUE; + gst_video_info_from_caps (&p->video_info, caps); + } else { + has_video = FALSE; + } + + p->add_metavideo = has_video && gst_buffer_pool_config_has_option (config, + GST_BUFFER_POOL_OPTION_VIDEO_META); + + if (p->video_info.size != 0) + size = p->video_info.size; + + gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers); + + return GST_BUFFER_POOL_CLASS (gst_pipewire_pool_parent_class)->set_config (pool, config); +} + + +void gst_pipewire_pool_set_paused (GstPipeWirePool *pool, gboolean paused) +{ + GST_DEBUG_OBJECT (pool, "pause: %u", paused); + GST_OBJECT_LOCK (pool); + pool->paused = paused; + g_cond_signal (&pool->cond); + GST_OBJECT_UNLOCK (pool); +} + +static void +flush_start (GstBufferPool * pool) +{ + GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); + + GST_DEBUG_OBJECT (pool, "flush start"); + GST_OBJECT_LOCK (pool); + g_cond_signal (&p->cond); + GST_OBJECT_UNLOCK (pool); +} + +static void +release_buffer (GstBufferPool * pool, GstBuffer *buffer) +{ + GST_LOG_OBJECT (pool, "release buffer %p", buffer); + + GstPipeWirePoolData *data = gst_pipewire_pool_get_data(buffer); + + GST_OBJECT_LOCK (pool); + + if (!data->queued && data->b != NULL) + { + GstPipeWirePool *p = GST_PIPEWIRE_POOL (pool); + g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&p->stream); + int res; + + pw_thread_loop_lock (s->core->loop); + + if ((res = pw_stream_return_buffer (s->pwstream, data->b)) < 0) { + GST_ERROR_OBJECT (pool,"can't return buffer %p; gstbuffer : %p, %s",data->b, buffer, spa_strerror(res)); + } else { + data->queued = TRUE; + GST_DEBUG_OBJECT (pool, "returned buffer %p; gstbuffer:%p", data->b, buffer); + } + + pw_thread_loop_unlock (s->core->loop); + } + GST_OBJECT_UNLOCK (pool); +} + +static gboolean +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_weak_ref_set (&pool->stream, NULL); + 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->get_options = get_options; + bufferpool_class->set_config = set_config; + 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..fb00a10 --- /dev/null +++ b/src/gst/gstpipewirepool.h @@ -0,0 +1,73 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_POOL_H__ +#define __GST_PIPEWIRE_POOL_H__ + +#include "gstpipewirestream.h" + +#include + +#include + +#include + +G_BEGIN_DECLS + +#define GST_TYPE_PIPEWIRE_POOL (gst_pipewire_pool_get_type()) +G_DECLARE_FINAL_TYPE (GstPipeWirePool, gst_pipewire_pool, GST, PIPEWIRE_POOL, GstBufferPool) + +typedef struct _GstPipeWirePoolData GstPipeWirePoolData; +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; + + GWeakRef stream; + guint n_buffers; + + gboolean add_metavideo; + GstVideoInfo video_info; + + GstAllocator *fd_allocator; + GstAllocator *dmabuf_allocator; + + GCond cond; + gboolean paused; +}; + +enum GstPipeWirePoolMode { + USE_BUFFERPOOL_NO = 0, + USE_BUFFERPOOL_AUTO, + USE_BUFFERPOOL_YES +}; + +GstPipeWirePool * gst_pipewire_pool_new (GstPipeWireStream *stream); + +void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *buffer); +void gst_pipewire_pool_remove_buffer (GstPipeWirePool *pool, struct pw_buffer *buffer); + +static inline gboolean +gst_pipewire_pool_has_buffers (GstPipeWirePool *pool) +{ + return pool->n_buffers > 0; +} + +GstPipeWirePoolData *gst_pipewire_pool_get_data (GstBuffer *buffer); + +void gst_pipewire_pool_set_paused (GstPipeWirePool *pool, gboolean paused); + +G_END_DECLS + +#endif /* __GST_PIPEWIRE_POOL_H__ */ diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c new file mode 100644 index 0000000..b79ae6a --- /dev/null +++ b/src/gst/gstpipewiresink.c @@ -0,0 +1,1119 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/** + * SECTION:element-pipewiresink + * + * + * Example launch line + * |[ + * gst-launch -v videotestsrc ! pipewiresink + * ]| Sends a test video source to PipeWire + * + */ + +#define PW_ENABLE_DEPRECATED + +#include "config.h" +#include "gstpipewiresink.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "gstpipewireclock.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 DEFAULT_PROP_SLAVE_METHOD GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE +#define DEFAULT_PROP_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO + +#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, + PROP_SLAVE_METHOD, + PROP_USE_BUFFERPOOL, +}; + +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; +} + +GType +gst_pipewire_sink_slave_method_get_type (void) +{ + static gsize method_type = 0; + static const GEnumValue method[] = { + {GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE, "GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE", "none"}, + {GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE, "GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE", "resample"}, + {0, NULL, NULL}, + }; + + if (g_once_init_enter (&method_type)) { + GType tmp = + g_enum_register_static ("GstPipeWireSinkSlaveMethod", method); + g_once_init_leave (&method_type, tmp); + } + + return (GType) method_type; +} + + + +static GstStaticPadTemplate gst_pipewire_sink_template = +GST_STATIC_PAD_TEMPLATE ("sink", + 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_event (GstBaseSink *sink, GstEvent *event); + +static GstClock * +gst_pipewire_sink_provide_clock (GstElement * elem) +{ + GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (elem); + GstClock *clock; + + GST_OBJECT_LOCK (pwsink); + if (!GST_OBJECT_FLAG_IS_SET (pwsink, GST_ELEMENT_FLAG_PROVIDE_CLOCK)) + goto clock_disabled; + + if (pwsink->stream->clock) + clock = GST_CLOCK_CAST (gst_object_ref (pwsink->stream->clock)); + else + clock = NULL; + GST_OBJECT_UNLOCK (pwsink); + + return clock; + + /* ERRORS */ +clock_disabled: + { + GST_DEBUG_OBJECT (pwsink, "clock provide disabled"); + GST_OBJECT_UNLOCK (pwsink); + return NULL; + } +} + +static void +gst_pipewire_sink_finalize (GObject * object) +{ + GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (object); + + gst_clear_object (&pwsink->stream); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +gst_pipewire_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query) +{ + GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (bsink); + + if (pwsink->use_bufferpool != USE_BUFFERPOOL_NO) + gst_query_add_allocation_pool (query, GST_BUFFER_POOL_CAST (pwsink->stream->pool), 0, 0, 0); + + gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL); + return TRUE; +} + +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)); + + g_object_class_install_property (gobject_class, + PROP_SLAVE_METHOD, + g_param_spec_enum ("slave-method", + "Slave Method", + "Algorithm used to match the rate of the masterclock", + GST_TYPE_PIPEWIRE_SINK_SLAVE_METHOD, + DEFAULT_PROP_SLAVE_METHOD, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_USE_BUFFERPOOL, + g_param_spec_boolean ("use-bufferpool", + "Use bufferpool", + "Use bufferpool (default: true for video, false for audio)", + DEFAULT_PROP_USE_BUFFERPOOL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + gstelement_class->provide_clock = gst_pipewire_sink_provide_clock; + gstelement_class->change_state = gst_pipewire_sink_change_state; + + gst_element_class_set_static_metadata (gstelement_class, + "PipeWire sink", "Sink/Audio/Video", + "Send audio/video to PipeWire", "Wim Taymans "); + + 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->render = gst_pipewire_sink_render; + gstbasesink_class->event = gst_pipewire_sink_event; + + GST_DEBUG_CATEGORY_INIT (pipewire_sink_debug, "pipewiresink", 0, + "PipeWire Sink"); +} + +static void +gst_pipewire_sink_update_params (GstPipeWireSink *sink) +{ + GstPipeWirePool *pool = sink->stream->pool; + 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; + guint n_params = 0; + + 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); + 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<is_video) { + port_params[n_params++] = 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))); + } + + pw_thread_loop_lock (sink->stream->core->loop); + pw_stream_update_params (sink->stream->pwstream, port_params, n_params); + pw_thread_loop_unlock (sink->stream->core->loop); +} + +static void +pool_activated (GstPipeWirePool *pool, GstPipeWireSink *sink) +{ + GST_DEBUG_OBJECT (pool, "activated"); + g_cond_signal (&sink->stream->pool->cond); +} + +static void +gst_pipewire_sink_init (GstPipeWireSink * sink) +{ + sink->stream = gst_pipewire_stream_new (GST_ELEMENT (sink)); + + sink->mode = DEFAULT_PROP_MODE; + sink->use_bufferpool = DEFAULT_PROP_USE_BUFFERPOOL; + sink->is_video = false; + + GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + + g_signal_connect (sink->stream->pool, "activated", G_CALLBACK (pool_activated), sink); +} + +static GstCaps * +gst_pipewire_sink_sink_fixate (GstBaseSink * bsink, GstCaps * caps) +{ + GstStructure *structure; + GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK(bsink); + + caps = gst_caps_make_writable (caps); + + structure = gst_caps_get_structure (caps, 0); + + if (gst_structure_has_name (structure, "video/x-raw")) { + pwsink->is_video = true; + gst_structure_fixate_field_nearest_int (structure, "width", 320); + gst_structure_fixate_field_nearest_int (structure, "height", 240); + gst_structure_fixate_field_nearest_fraction (structure, "framerate", 30, 1); + + 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->stream->path); + pwsink->stream->path = g_value_dup_string (value); + break; + + case PROP_TARGET_OBJECT: + g_free (pwsink->stream->target_object); + pwsink->stream->target_object = g_value_dup_string (value); + break; + + case PROP_CLIENT_NAME: + g_free (pwsink->stream->client_name); + pwsink->stream->client_name = g_value_dup_string (value); + break; + + case PROP_CLIENT_PROPERTIES: + if (pwsink->stream->client_properties) + gst_structure_free (pwsink->stream->client_properties); + pwsink->stream->client_properties = + gst_structure_copy (gst_value_get_structure (value)); + break; + + case PROP_STREAM_PROPERTIES: + if (pwsink->stream->stream_properties) + gst_structure_free (pwsink->stream->stream_properties); + pwsink->stream->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->stream->fd = g_value_get_int (value); + break; + + case PROP_SLAVE_METHOD: + pwsink->slave_method = g_value_get_enum (value); + break; + + case PROP_USE_BUFFERPOOL: + if(g_value_get_boolean (value)) + pwsink->use_bufferpool = USE_BUFFERPOOL_YES; + else + pwsink->use_bufferpool = USE_BUFFERPOOL_NO; + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +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->stream->path); + break; + + case PROP_TARGET_OBJECT: + g_value_set_string (value, pwsink->stream->target_object); + break; + + case PROP_CLIENT_NAME: + g_value_set_string (value, pwsink->stream->client_name); + break; + + case PROP_CLIENT_PROPERTIES: + gst_value_set_structure (value, pwsink->stream->client_properties); + break; + + case PROP_STREAM_PROPERTIES: + gst_value_set_structure (value, pwsink->stream->stream_properties); + break; + + case PROP_MODE: + g_value_set_enum (value, pwsink->mode); + break; + + case PROP_FD: + g_value_set_int (value, pwsink->stream->fd); + break; + + case PROP_SLAVE_METHOD: + g_value_set_enum (value, pwsink->slave_method); + break; + + case PROP_USE_BUFFERPOOL: + g_value_set_boolean (value, !!pwsink->use_bufferpool); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void rate_match_resample(GstPipeWireSink *pwsink) +{ + GstPipeWireStream *stream = pwsink->stream; + double err, corr; + struct pw_time ts; + guint64 queued, now, elapsed, target; + + if (!pwsink->rate_match) + return; + + pw_stream_get_time_n(stream->pwstream, &ts, sizeof(ts)); + now = pw_stream_get_nsec(stream->pwstream); + if (ts.now != 0) + elapsed = gst_util_uint64_scale_int (now - ts.now, ts.rate.denom, GST_SECOND * ts.rate.num); + else + elapsed = 0; + + queued = ts.queued - ts.size; + target = elapsed; + err = ((gint64)queued - ((gint64)target)); + + corr = spa_dll_update(&stream->dll, SPA_CLAMPD(err, -128.0, 128.0)); + + stream->err_wdw = (double)ts.rate.denom/ts.size; + + double avg = (stream->err_avg * stream->err_wdw + (err - stream->err_avg)) / (stream->err_wdw + 1.0); + stream->err_var = (stream->err_var * stream->err_wdw + + (err - stream->err_avg) * (err - avg)) / (stream->err_wdw + 1.0); + stream->err_avg = avg; + + if (stream->last_ts == 0 || stream->last_ts + SPA_NSEC_PER_SEC < now) { + double bw; + + stream->last_ts = now; + + if (stream->err_var == 0.0) + bw = 0.0; + else + bw = fabs(stream->err_avg) / sqrt(fabs(stream->err_var)); + + spa_dll_set_bw(&stream->dll, SPA_CLAMPD(bw, 0.001, SPA_DLL_BW_MAX), ts.size, ts.rate.denom); + + GST_INFO_OBJECT (pwsink, "q:%"PRIi64"/%"PRIi64" e:%"PRIu64" err:%+03f corr:%f %f %f %f", + ts.queued, ts.size, elapsed, err, corr, + stream->err_avg, stream->err_var, stream->dll.bw); + } + + pw_stream_set_rate (stream->pwstream, corr); +} + +static void +on_add_buffer (void *_data, struct pw_buffer *b) +{ + GstPipeWireSink *pwsink = _data; + GST_DEBUG_OBJECT (pwsink, "add pw_buffer %p", b); + gst_pipewire_pool_wrap_buffer (pwsink->stream->pool, b); +} + +static void +on_remove_buffer (void *_data, struct pw_buffer *b) +{ + GstPipeWireSink *pwsink = _data; + GST_DEBUG_OBJECT (pwsink, "remove pw_buffer %p", b); + gst_pipewire_pool_remove_buffer (pwsink->stream->pool, b); + + if (!gst_pipewire_pool_has_buffers (pwsink->stream->pool) && + !GST_BUFFER_POOL_IS_FLUSHING (GST_BUFFER_POOL_CAST (pwsink->stream->pool))) { + GST_ELEMENT_ERROR (pwsink, RESOURCE, NOT_FOUND, + ("all buffers have been removed"), + ("PipeWire link to remote node was destroyed")); + } +} + +static void +do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer) +{ + GstPipeWirePoolData *data; + GstPipeWireStream *stream = pwsink->stream; + gboolean res; + guint i; + struct spa_buffer *b; + + data = gst_pipewire_pool_get_data(buffer); + + b = data->b->buffer; + + if (data->header) { + data->header->seq = GST_BUFFER_OFFSET (buffer); + data->header->pts = GST_BUFFER_PTS (buffer); + if (GST_BUFFER_DTS(buffer) != GST_CLOCK_TIME_NONE) + data->header->dts_offset = GST_BUFFER_DTS (buffer) - GST_BUFFER_PTS (buffer); + else + data->header->dts_offset = 0; + } + 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; + } + } + data->b->size = 0; + for (i = 0; i < b->n_datas; i++) { + struct spa_data *d = &b->datas[i]; + GstMemory *mem = gst_buffer_peek_memory (buffer, i); + d->chunk->offset = mem->offset; + d->chunk->size = mem->size; + d->chunk->stride = stream->pool->video_info.stride[i]; + + data->b->size += mem->size / 4; + } + + GstVideoMeta *meta = gst_buffer_get_video_meta (buffer); + if (meta) { + if (meta->n_planes == b->n_datas) { + gsize video_size = 0; + for (i = 0; i < meta->n_planes; i++) { + struct spa_data *d = &b->datas[i]; + d->chunk->offset += meta->offset[i] - video_size; + d->chunk->stride = meta->stride[i]; + + video_size += d->chunk->size; + } + } else { + GST_ERROR_OBJECT (pwsink, "plane num not matching, meta:%u buffer:%u", meta->n_planes, b->n_datas); + } + } + + if ((res = pw_stream_queue_buffer (stream->pwstream, data->b)) < 0) { + GST_WARNING_OBJECT (pwsink, "can't send buffer %s", spa_strerror(res)); + } else { + data->queued = TRUE; + GST_LOG_OBJECT(pwsink, "queued pwbuffer: %p; gstbuffer %p ",data->b, buffer); + } + + switch (pwsink->slave_method) { + case GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE: + break; + case GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE: + rate_match_resample(pwsink); + break; + } +} + + +static void +on_process (void *data) +{ + GstPipeWireSink *pwsink = data; + GST_LOG_OBJECT (pwsink, "signal"); + g_cond_signal (&pwsink->stream->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_OBJECT (pwsink, "got stream state \"%s\" (%d)", + pw_stream_state_as_string(state), 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->pwstream)) + pw_stream_trigger_process (pwsink->stream->pwstream); + break; + case PW_STREAM_STATE_ERROR: + /* make the error permanent, if it is not already; + pw_stream_set_error() will recursively call us again */ + if (pw_stream_get_state (pwsink->stream->pwstream, NULL) != PW_STREAM_STATE_ERROR) + pw_stream_set_error (pwsink->stream->pwstream, -EPIPE, "%s", error); + else + GST_ELEMENT_ERROR (pwsink, RESOURCE, FAILED, + ("stream error: %s", error), (NULL)); + break; + } + pw_thread_loop_signal (pwsink->stream->core->loop, FALSE); +} + +static void +on_param_changed (void *data, uint32_t id, const struct spa_pod *param) +{ + GstPipeWireSink *pwsink = data; + GstPipeWirePool *pool = pwsink->stream->pool; + + if (param == NULL || id != SPA_PARAM_Format) + return; + + GST_OBJECT_LOCK (pool); + while (!gst_buffer_pool_is_active (GST_BUFFER_POOL (pool))) { + GST_DEBUG_OBJECT (pool, "waiting for pool to become active"); + g_cond_wait(&pool->cond, GST_OBJECT_GET_LOCK (pool)); + } + GST_OBJECT_UNLOCK (pool); + + gst_pipewire_sink_update_params (pwsink); +} + +static gboolean +gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps) +{ + GstPipeWireSink *pwsink; + g_autoptr(GPtrArray) possible = NULL; + enum pw_stream_state state; + const char *error = NULL; + GstStructure *config, *s; + guint size; + guint min_buffers; + guint max_buffers; + struct timespec abstime; + gint rate; + + pwsink = GST_PIPEWIRE_SINK (bsink); + + s = gst_caps_get_structure (caps, 0); + if (gst_structure_has_name (s, "audio/x-raw")) { + gst_structure_get_int (s, "rate", &rate); + pwsink->rate = rate; + pwsink->rate_match = true; + + /* Don't provide bufferpool for audio if not requested by the application/user */ + if (pwsink->use_bufferpool != USE_BUFFERPOOL_YES) + pwsink->use_bufferpool = USE_BUFFERPOOL_NO; + } else { + pwsink->rate = rate = 0; + pwsink->rate_match = false; + pwsink->is_video = true; + } + + spa_dll_set_bw(&pwsink->stream->dll, SPA_DLL_BW_MIN, 4096, rate); + + possible = gst_caps_to_format_all (caps); + + pw_thread_loop_lock (pwsink->stream->core->loop); + state = pw_stream_get_state (pwsink->stream->pwstream, &error); + + if (state == PW_STREAM_STATE_ERROR) + goto start_error; + + if (state == PW_STREAM_STATE_UNCONNECTED) { + enum pw_stream_flags flags; + uint32_t target_id; + struct spa_dict_item items[3]; + uint32_t n_items = 0; + char buf[64]; + + flags = PW_STREAM_FLAG_ASYNC; + flags |= PW_STREAM_FLAG_EARLY_PROCESS; + if (pwsink->mode != GST_PIPEWIRE_SINK_MODE_PROVIDE) + flags |= PW_STREAM_FLAG_AUTOCONNECT; + else + flags |= PW_STREAM_FLAG_DRIVER; + + target_id = pwsink->stream->path ? (uint32_t)atoi(pwsink->stream->path) : PW_ID_ANY; + + if (pwsink->stream->target_object) { + uint64_t serial; + + items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_TARGET_OBJECT, pwsink->stream->target_object); + + /* If target.object is a name, set it also to node.target */ + if (!spa_atou64(pwsink->stream->target_object, &serial, 0)) { + target_id = PW_ID_ANY; + /* XXX deprecated but the portal and some example apps only + * provide the object id */ + items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_TARGET, pwsink->stream->target_object); + } + } + if (rate != 0) { + snprintf(buf, sizeof(buf), "1/%u", rate); + items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, buf); + } + if (n_items > 0) + pw_stream_update_properties (pwsink->stream->pwstream, &SPA_DICT_INIT(items, n_items)); + + pw_stream_connect (pwsink->stream->pwstream, + PW_DIRECTION_OUTPUT, + target_id, + flags, + (const struct spa_pod **) possible->pdata, + possible->len); + + pw_thread_loop_get_time (pwsink->stream->core->loop, &abstime, + GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC); + + while (TRUE) { + state = pw_stream_get_state (pwsink->stream->pwstream, &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->stream->core->loop, &abstime) < 0) { + error = "timeout"; + goto start_error; + } + } + } + + gst_pipewire_clock_reset (GST_PIPEWIRE_CLOCK (pwsink->stream->clock), 0); + + config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool)); + gst_buffer_pool_config_get_params (config, NULL, &size, &min_buffers, &max_buffers); + gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers); + if(pwsink->is_video) + gst_buffer_pool_config_add_option(config, GST_BUFFER_POOL_OPTION_VIDEO_META); + gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool), config); + + pw_thread_loop_unlock (pwsink->stream->core->loop); + + pwsink->negotiated = TRUE; + + return TRUE; + +start_error: + { + GST_ERROR_OBJECT (pwsink, "could not start stream: %s", error); + pw_thread_loop_unlock (pwsink->stream->core->loop); + return FALSE; + } +} + +static GstFlowReturn +gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer) +{ + GstPipeWireSink *pwsink; + GstFlowReturn res = GST_FLOW_OK; + const char *error = NULL; + + pwsink = GST_PIPEWIRE_SINK (bsink); + + if (!pwsink->negotiated) + goto not_negotiated; + + if (buffer->pool != GST_BUFFER_POOL_CAST (pwsink->stream->pool) && + !gst_buffer_pool_is_active (GST_BUFFER_POOL_CAST (pwsink->stream->pool))) { + GstStructure *config; + GstCaps *caps; + guint size, min_buffers, max_buffers; + + config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool)); + gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers); + + if (size == 0) { + gsize maxsize; + gst_buffer_get_sizes (buffer, NULL, &maxsize); + size = maxsize; + } + + gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers); + gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool), config); + + gst_buffer_pool_set_active (GST_BUFFER_POOL_CAST (pwsink->stream->pool), TRUE); + } + + pw_thread_loop_lock (pwsink->stream->core->loop); + if (pw_stream_get_state (pwsink->stream->pwstream, &error) != PW_STREAM_STATE_STREAMING) + goto done_unlock; + + if (buffer->pool != GST_BUFFER_POOL_CAST (pwsink->stream->pool)) { + gsize offset = 0; + gsize buf_size = gst_buffer_get_size (buffer); + + GST_TRACE_OBJECT(pwsink, "Buffer is not from pipewirepool, copying into our pool"); + + /* For some streams, the buffer size is changed and may exceed the acquired + * buffer size which is acquired from the pool of pipewiresink. Need split + * the buffer and send them in turn for this case */ + while (buf_size) { + GstBuffer *b = NULL; + GstMapInfo info = { 0, }; + GstBufferPoolAcquireParams params = { 0, }; + + pw_thread_loop_unlock (pwsink->stream->core->loop); + + params.flags = GST_BUFFER_POOL_ACQUIRE_FLAG_LAST; + res = gst_buffer_pool_acquire_buffer (GST_BUFFER_POOL_CAST (pwsink->stream->pool), + &b, ¶ms); + if (res == GST_FLOW_CUSTOM_ERROR_1) { + res = gst_base_sink_wait_preroll (bsink); + if (res != GST_FLOW_OK) + goto done; + continue; + } + if (res != GST_FLOW_OK) + goto done; + + if (pwsink->is_video) { + GstVideoFrame src, dst; + gboolean copied = FALSE; + buf_size = 0; // to break from the loop + + /* + splitting of buffers in the case of video might break the frame layout + and that seems to be causing issues while retrieving the buffers on the receiver + side. Hence use the video_frame_map to copy the buffer of bigger size into the + pipewirepool's buffer + */ + + if (!gst_video_frame_map (&dst, &pwsink->stream->pool->video_info, b, + GST_MAP_WRITE)) { + GST_ERROR_OBJECT(pwsink, "Failed to map dest buffer"); + return GST_FLOW_ERROR; + } + + if (!gst_video_frame_map (&src, &pwsink->stream->pool->video_info, buffer, GST_MAP_READ)) { + gst_video_frame_unmap (&dst); + GST_ERROR_OBJECT(pwsink, "Failed to map src buffer"); + return GST_FLOW_ERROR; + } + + copied = gst_video_frame_copy (&dst, &src); + + gst_video_frame_unmap (&src); + gst_video_frame_unmap (&dst); + + if (!copied) { + GST_ERROR_OBJECT(pwsink, "Failed to copy the frame"); + return GST_FLOW_ERROR; + } + + gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1); + } else { + gst_buffer_map (b, &info, GST_MAP_WRITE); + gsize extract_size = (buf_size <= info.maxsize) ? buf_size: info.maxsize; + gst_buffer_extract (buffer, offset, info.data, info.maxsize); + gst_buffer_unmap (b, &info); + gst_buffer_resize (b, 0, extract_size); + gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1); + buf_size -= extract_size; + offset += extract_size; + } + + pw_thread_loop_lock (pwsink->stream->core->loop); + if (pw_stream_get_state (pwsink->stream->pwstream, &error) != PW_STREAM_STATE_STREAMING) { + gst_buffer_unref (b); + goto done_unlock; + } + + do_send_buffer (pwsink, b); + gst_buffer_unref (b); + + if (pw_stream_is_driving (pwsink->stream->pwstream)) + pw_stream_trigger_process (pwsink->stream->pwstream); + } + } else { + GST_TRACE_OBJECT(pwsink, "Buffer is from pipewirepool"); + + do_send_buffer (pwsink, buffer); + + if (pw_stream_is_driving (pwsink->stream->pwstream)) + pw_stream_trigger_process (pwsink->stream->pwstream); + } + +done_unlock: + pw_thread_loop_unlock (pwsink->stream->core->loop); +done: + return res; + +not_negotiated: + { + return GST_FLOW_NOT_NEGOTIATED; + } +} + +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 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_stream_open (this->stream, &stream_events)) + goto open_failed; + break; + case GST_STATE_CHANGE_READY_TO_PAUSED: + /* the initial stream state is active, which is needed for linking and + * negotiation to happen and the bufferpool to be set up. We don't know + * if we'll go to plaing, so we deactivate the stream until that + * transition happens. This is janky, but because of how bins propagate + * state changes one transition at a time, there may not be a better way + * to do this. PAUSED -> READY -> PAUSED transitions, this is a noop */ + pw_thread_loop_lock (this->stream->core->loop); + pw_stream_set_active(this->stream->pwstream, false); + pw_thread_loop_unlock (this->stream->core->loop); + gst_pipewire_pool_set_paused(this->stream->pool, TRUE); + break; + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + /* stop play ASAP by corking */ + gst_pipewire_pool_set_paused(this->stream->pool, TRUE); + pw_thread_loop_lock (this->stream->core->loop); + pw_stream_set_active(this->stream->pwstream, false); + pw_thread_loop_unlock (this->stream->core->loop); + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_PLAYING: + /* For some cases, the param_changed event is earlier than the state switch + * from paused state to playing state which will wait until buffer pool is ready. + * Guarantee to finish preoll if needed to active buffer pool before uncorking and + * starting play */ + gst_pipewire_pool_set_paused(this->stream->pool, FALSE); + pw_thread_loop_lock (this->stream->core->loop); + pw_stream_set_active(this->stream->pwstream, true); + pw_thread_loop_unlock (this->stream->core->loop); + break; + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + break; + case GST_STATE_CHANGE_PAUSED_TO_READY: + gst_buffer_pool_set_active(GST_BUFFER_POOL_CAST(this->stream->pool), FALSE); + this->negotiated = FALSE; + break; + case GST_STATE_CHANGE_READY_TO_NULL: + gst_pipewire_stream_close (this->stream); + break; + default: + break; + } + return ret; + + /* ERRORS */ +open_failed: + { + return GST_STATE_CHANGE_FAILURE; + } +} + +static gboolean gst_pipewire_sink_event (GstBaseSink *sink, GstEvent *event) { + GstPipeWireSink *pw_sink = GST_PIPEWIRE_SINK(sink); + GstState current_state = GST_ELEMENT(sink)->current_state; + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_FLUSH_START: + { + GST_DEBUG_OBJECT (pw_sink, "flush-start"); + pw_thread_loop_lock (pw_sink->stream->core->loop); + + /* The stream would be already inactive if the sink is not PLAYING */ + if (current_state == GST_STATE_PLAYING) + pw_stream_set_active(pw_sink->stream->pwstream, false); + + gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(pw_sink->stream->pool), TRUE); + pw_stream_flush(pw_sink->stream->pwstream, false); + pw_thread_loop_unlock (pw_sink->stream->core->loop); + break; + } + case GST_EVENT_FLUSH_STOP: + { + GST_DEBUG_OBJECT (pw_sink, "flush-stop"); + pw_thread_loop_lock (pw_sink->stream->core->loop); + + /* The stream needs to remain inactive if the sink is not PLAYING */ + if (current_state == GST_STATE_PLAYING) + pw_stream_set_active(pw_sink->stream->pwstream, true); + + gst_buffer_pool_set_flushing(GST_BUFFER_POOL_CAST(pw_sink->stream->pool), FALSE); + pw_thread_loop_unlock (pw_sink->stream->core->loop); + break; + } + default: + break; + } + + return GST_BASE_SINK_CLASS (parent_class)->event (sink, event); +} diff --git a/src/gst/gstpipewiresink.h b/src/gst/gstpipewiresink.h new file mode 100644 index 0000000..60eb3b7 --- /dev/null +++ b/src/gst/gstpipewiresink.h @@ -0,0 +1,82 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_SINK_H__ +#define __GST_PIPEWIRE_SINK_H__ + +#include "gstpipewirestream.h" + +#include +#include + +#include +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_PIPEWIRE_SINK (gst_pipewire_sink_get_type()) +#define GST_PIPEWIRE_SINK_CAST(obj) ((GstPipeWireSink *) (obj)) +G_DECLARE_FINAL_TYPE (GstPipeWireSink, gst_pipewire_sink, GST, PIPEWIRE_SINK, GstBaseSink) + +/** + * 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 ()) + + +/** + * GstPipeWireSinkSlaveMethod: + * @GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE: no clock and timestamp slaving + * @GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE: resample audio + * + * Different clock slaving methods + */ +typedef enum +{ + GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE, + GST_PIPEWIRE_SINK_SLAVE_METHOD_RESAMPLE, +} GstPipeWireSinkSlaveMethod; + +#define GST_TYPE_PIPEWIRE_SINK_SLAVE_METHOD (gst_pipewire_sink_slave_method_get_type ()) + +/** + * GstPipeWireSink: + * + * Opaque data structure. + */ +struct _GstPipeWireSink { + GstBaseSink element; + + /*< private >*/ + GstPipeWireStream *stream; + gboolean use_bufferpool; + + /* video state */ + gboolean negotiated; + gboolean rate_match; + gint rate; + gboolean is_video; + + GstPipeWireSinkMode mode; + GstPipeWireSinkSlaveMethod slave_method; +}; + +GType gst_pipewire_sink_mode_get_type (void); + +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..694cbea --- /dev/null +++ b/src/gst/gstpipewiresrc.c @@ -0,0 +1,1573 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/** + * SECTION:element-pipewiresrc + * + * + * Example launch line + * |[ + * gst-launch -v pipewiresrc ! videoconvert ! ximagesink + * ]| Shows pipewire output in an X window. + * + */ + +#define PW_ENABLE_DEPRECATED + +#include "gstpipewiresrc.h" +#include "gstpipewireformat.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#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 +#define DEFAULT_AUTOCONNECT true +#define DEFAULT_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO + +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, + PROP_AUTOCONNECT, + PROP_USE_BUFFERPOOL, +}; + + +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_get_times (GstBaseSrc * basesrc, GstBuffer * buffer, + GstClockTime * start, GstClockTime * end); + +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->stream->path); + pwsrc->stream->path = g_value_dup_string (value); + break; + + case PROP_TARGET_OBJECT: + g_free (pwsrc->stream->target_object); + pwsrc->stream->target_object = g_value_dup_string (value); + break; + + case PROP_CLIENT_NAME: + g_free (pwsrc->stream->client_name); + pwsrc->stream->client_name = g_value_dup_string (value); + break; + + case PROP_CLIENT_PROPERTIES: + if (pwsrc->stream->client_properties) + gst_structure_free (pwsrc->stream->client_properties); + pwsrc->stream->client_properties = + gst_structure_copy (gst_value_get_structure (value)); + break; + + case PROP_STREAM_PROPERTIES: + if (pwsrc->stream->stream_properties) + gst_structure_free (pwsrc->stream->stream_properties); + pwsrc->stream->stream_properties = + gst_structure_copy (gst_value_get_structure (value)); + break; + + case PROP_ALWAYS_COPY: + /* don't provide buffer if always copy*/ + if (g_value_get_boolean (value)) + pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; + else + pwsrc->use_bufferpool = USE_BUFFERPOOL_YES; + break; + + case PROP_MIN_BUFFERS: + 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->stream->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; + + case PROP_AUTOCONNECT: + pwsrc->autoconnect = g_value_get_boolean (value); + break; + + case PROP_USE_BUFFERPOOL: + if(g_value_get_boolean (value)) + pwsrc->use_bufferpool = USE_BUFFERPOOL_YES; + else + pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +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->stream->path); + break; + + case PROP_TARGET_OBJECT: + g_value_set_string (value, pwsrc->stream->target_object); + break; + + case PROP_CLIENT_NAME: + g_value_set_string (value, pwsrc->stream->client_name); + break; + + case PROP_CLIENT_PROPERTIES: + gst_value_set_structure (value, pwsrc->stream->client_properties); + break; + + case PROP_STREAM_PROPERTIES: + gst_value_set_structure (value, pwsrc->stream->stream_properties); + break; + + case PROP_ALWAYS_COPY: + g_value_set_boolean (value, !pwsrc->use_bufferpool); + 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->stream->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; + + case PROP_AUTOCONNECT: + g_value_set_boolean (value, pwsrc->autoconnect); + break; + + case PROP_USE_BUFFERPOOL: + g_value_set_boolean (value, !!pwsrc->use_bufferpool); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +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->stream->clock && pwsrc->is_live) + clock = GST_CLOCK_CAST (gst_object_ref (pwsrc->stream->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); + + gst_clear_object (&pwsrc->stream); + + 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_PARAM_DEPRECATED)); + + 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)); + + g_object_class_install_property (gobject_class, + PROP_AUTOCONNECT, + g_param_spec_boolean ("autoconnect", + "Connect automatically", + "Attempt to find a peer to connect to", + DEFAULT_AUTOCONNECT, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_USE_BUFFERPOOL, + g_param_spec_boolean ("use-bufferpool", + "Use bufferpool", + "Use bufferpool (default: true for video, false for audio)", + DEFAULT_USE_BUFFERPOOL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + gstelement_class->provide_clock = gst_pipewire_src_provide_clock; + gstelement_class->change_state = gst_pipewire_src_change_state; + gstelement_class->send_event = gst_pipewire_src_send_event; + + gst_element_class_set_static_metadata (gstelement_class, + "PipeWire source", "Source/Audio/Video", + "Uses PipeWire to create audio/video", "Wim Taymans "); + + 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; + gstbasesrc_class->get_times = gst_pipewire_src_get_times; + 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->stream = gst_pipewire_stream_new (GST_ELEMENT (src)); + + src->use_bufferpool = DEFAULT_USE_BUFFERPOOL; + src->min_buffers = DEFAULT_MIN_BUFFERS; + src->max_buffers = DEFAULT_MAX_BUFFERS; + src->resend_last = DEFAULT_RESEND_LAST; + src->keepalive_time = DEFAULT_KEEPALIVE_TIME; + src->autoconnect = DEFAULT_AUTOCONNECT; + src->min_latency = 0; + src->max_latency = GST_CLOCK_TIME_NONE; + + src->transform_value = UINT32_MAX; +} + +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->stream->core->loop); + if (!obj->dispose) { + pw_thread_loop_unlock (src->stream->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->pwstream, 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->stream->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->stream->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->pwstream, 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; + enum spa_meta_videotransform_value transform_value; + struct pw_time time; + guint i; + + b = pw_stream_dequeue_buffer (pwsrc->stream->pwstream); + 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; + } + + pw_stream_get_time_n(pwsrc->stream->pwstream, &time, sizeof(time)); + + if (pwsrc->delay != time.delay && time.rate.denom != 0) { + pwsrc->min_latency = time.delay * GST_SECOND * time.rate.num / time.rate.denom; + GST_LOG_OBJECT (pwsrc, "latency changed %"PRIi64" -> %"PRIi64" %"PRIu64, + pwsrc->delay, time.delay, pwsrc->min_latency); + pwsrc->delay = time.delay; + gst_element_post_message (GST_ELEMENT_CAST (pwsrc), + gst_message_new_latency (GST_OBJECT_CAST (pwsrc))); + } + + 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; + 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; + } else { + GST_BUFFER_PTS (buf) = b->time - pwsrc->delay; + GST_BUFFER_DTS (buf) = b->time - pwsrc->delay; + } + + if (pwsrc->is_video) { + if (pwsrc->video_info.fps_n) { + GST_BUFFER_DURATION (buf) = gst_util_uint64_scale (GST_SECOND, + pwsrc->video_info.fps_d, pwsrc->video_info.fps_n); + } + } else { + GST_BUFFER_DURATION (buf) = gst_util_uint64_scale (GST_SECOND, + time.size * time.rate.num, time.rate.denom); + } + + 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; + } + } + + transform_value = data->videotransform ? data->videotransform->transform : + SPA_META_TRANSFORMATION_None; + if (transform_value != pwsrc->transform_value) { + GstEvent *tag_event; + const char* tag_string; + + tag_string = spa_transform_value_to_gst_image_orientation(transform_value); + + GST_LOG_OBJECT (pwsrc, "got new videotransform: %u / %s", + transform_value, 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 = transform_value; + } + + if (pwsrc->is_video) { + gsize video_size = 0; + GstVideoInfo *info = &pwsrc->video_info; + GstVideoMeta *meta = gst_buffer_add_video_meta_full (buf, GST_VIDEO_FRAME_FLAG_NONE, + GST_VIDEO_INFO_FORMAT (info), + GST_VIDEO_INFO_WIDTH (info), + GST_VIDEO_INFO_HEIGHT (info), + GST_VIDEO_INFO_N_PLANES (info), + info->offset, + info->stride); + + for (i = 0; i < MIN (b->buffer->n_datas, GST_VIDEO_MAX_PLANES); i++) { + struct spa_data *d = &b->buffer->datas[i]; + meta->offset[i] = video_size; + meta->stride[i] = d->chunk->stride; + + video_size += d->chunk->size; + } + } + + for (i = 0; i < b->buffer->n_datas; i++) { + struct spa_data *d = &b->buffer->datas[i]; + + if (d->chunk->size == 0) { + // Skip the 0 sized chunk, not adding to the buffer + GST_DEBUG_OBJECT(pwsrc, "Chunk size is 0, skipping"); + continue; + } + + GstMemory *pmem = gst_buffer_peek_memory (data->buf, i); + if (pmem) { + GstMemory *mem; + if (pwsrc->use_bufferpool != USE_BUFFERPOOL_NO) + mem = gst_memory_share (pmem, d->chunk->offset, d->chunk->size); + else + mem = gst_memory_copy (pmem, d->chunk->offset, d->chunk->size); + gst_buffer_insert_memory (buf, i, mem); + } + if (d->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED) { + GST_DEBUG_OBJECT(pwsrc, "Buffer corrupted"); + GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_CORRUPTED); + } + } + if (pwsrc->use_bufferpool != USE_BUFFERPOOL_NO) + gst_buffer_add_parent_buffer_meta (buf, data->buf); + gst_buffer_unref (data->buf); + + if (gst_buffer_get_size(buf) == 0) + { + GST_ERROR_OBJECT(pwsrc, "Buffer is empty, dropping this"); + gst_buffer_unref(buf); + buf = NULL; + } + + return buf; +} + +static void +on_process (void *_data) +{ + GstPipeWireSrc *pwsrc = _data; + pw_thread_loop_signal (pwsrc->stream->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: + /* make the error permanent, if it is not already; + pw_stream_set_error() will recursively call us again */ + if (pw_stream_get_state (pwsrc->stream->pwstream, NULL) != PW_STREAM_STATE_ERROR) + pw_stream_set_error (pwsrc->stream->pwstream, -EPIPE, "%s", error); + else + GST_ELEMENT_ERROR (pwsrc, RESOURCE, FAILED, + ("stream error: %s", error), (NULL)); + break; + } + pw_thread_loop_signal (pwsrc->stream->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; + 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->stream->core->loop); + GST_DEBUG_OBJECT (pwsrc, "doing stream start"); + + pw_thread_loop_get_time (pwsrc->stream->core->loop, &abstime, + GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC); + + while (TRUE) { + enum pw_stream_state state = pw_stream_get_state (pwsrc->stream->pwstream, &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->stream->core->loop, &abstime) < 0) { + error = "timeout"; + goto start_error; + } + } + + parse_stream_properties (pwsrc, pw_stream_get_properties (pwsrc->stream->pwstream)); + GST_DEBUG_OBJECT (pwsrc, "signal started"); + pwsrc->started = TRUE; + pw_thread_loop_signal (pwsrc->stream->core->loop, FALSE); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + return TRUE; + +start_error: + { + GST_DEBUG_OBJECT (pwsrc, "error starting stream: %s", error); + pwsrc->started = FALSE; + pw_thread_loop_signal (pwsrc->stream->core->loop, FALSE); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return FALSE; + } +} + +static enum pw_stream_state +wait_started (GstPipeWireSrc *this) +{ + enum pw_stream_state state, prev_state = PW_STREAM_STATE_UNCONNECTED; + const char *error = NULL; + struct timespec abstime; + gboolean restart = FALSE; + + pw_thread_loop_lock (this->stream->core->loop); + + pw_thread_loop_get_time (this->stream->core->loop, &abstime, + GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC); + + /* when started already is true then expects a re-start, so allow prev_state + * degrade until turned around. */ + if (this->started) { + GST_DEBUG_OBJECT (this, "restart in progress"); + restart = TRUE; + this->started = FALSE; + } + + while (TRUE) { + state = pw_stream_get_state (this->stream->pwstream, &error); + + GST_DEBUG_OBJECT (this, "waiting for started signal, state now %s", + pw_stream_state_as_string (state)); + + if (state == PW_STREAM_STATE_ERROR || + (state == PW_STREAM_STATE_UNCONNECTED && prev_state > PW_STREAM_STATE_UNCONNECTED && !restart) || + this->flushing) { + state = PW_STREAM_STATE_ERROR; + break; + } + + if (this->started) + break; + + if (this->autoconnect) { + if (pw_thread_loop_timed_wait_full (this->stream->core->loop, &abstime) < 0) { + state = PW_STREAM_STATE_ERROR; + break; + } + } else { + pw_thread_loop_wait (this->stream->core->loop); + } + + if (restart) + restart = state != PW_STREAM_STATE_UNCONNECTED; + prev_state = state; + } + GST_DEBUG_OBJECT (this, "got started signal: %s", + pw_stream_state_as_string (state)); + pw_thread_loop_unlock (this->stream->core->loop); + + return state; +} + +static gboolean +gst_pipewire_src_negotiate (GstBaseSrc * basesrc) +{ + GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc); + g_autoptr (GstCaps) thiscaps = NULL; + g_autoptr (GstCaps) possible_caps = NULL; + g_autoptr (GstCaps) negotiated_caps = NULL; + g_autoptr (GstCaps) peercaps = NULL; + g_autoptr (GPtrArray) possible = NULL; + gboolean result = FALSE; + 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 */ + possible_caps = g_steal_pointer (&peercaps); + } else { + /* no peer, work with our own caps then */ + possible_caps = g_steal_pointer (&thiscaps); + } + + GST_DEBUG_OBJECT (basesrc, "have common caps: %" GST_PTR_FORMAT, possible_caps); + gst_caps_sanitize (&possible_caps); + + if (gst_caps_is_empty (possible_caps)) + goto no_common_caps; + + GST_DEBUG_OBJECT (basesrc, "have common caps (sanitized): %" GST_PTR_FORMAT, possible_caps); + + if (pw_stream_get_state(pwsrc->stream->pwstream, NULL) == PW_STREAM_STATE_STREAMING) { + g_autoptr (GstCaps) current_caps = NULL; + g_autoptr (GstCaps) preferred_new_caps = NULL; + + current_caps = gst_pad_get_current_caps (GST_BASE_SRC_PAD (pwsrc)); + preferred_new_caps = gst_caps_copy_nth (possible_caps, 0); + + if (current_caps && gst_caps_is_equal (current_caps, preferred_new_caps)) { + GST_DEBUG_OBJECT (pwsrc, + "Stream running and new caps equal current ones. " + "Skipping renegotiation."); + goto no_nego_needed; + } + } + + /* open a connection with these caps */ + possible = gst_caps_to_format_all (possible_caps); + + /* first disconnect */ + pw_thread_loop_lock (pwsrc->stream->core->loop); + if (pw_stream_get_state(pwsrc->stream->pwstream, &error) != PW_STREAM_STATE_UNCONNECTED) { + GST_DEBUG_OBJECT (basesrc, "disconnect capture"); + pw_stream_disconnect (pwsrc->stream->pwstream); + while (TRUE) { + enum pw_stream_state state = pw_stream_get_state (pwsrc->stream->pwstream, &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) + goto connect_error; + + pw_thread_loop_wait (pwsrc->stream->core->loop); + } + } + + target_id = pwsrc->stream->path ? (uint32_t)atoi(pwsrc->stream->path) : PW_ID_ANY; + + if (pwsrc->stream->target_object) { + struct spa_dict_item items[2] = { + SPA_DICT_ITEM_INIT(PW_KEY_TARGET_OBJECT, pwsrc->stream->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->stream->target_object, &serial, 0)) { + dict.n_items = 1; + } else { + target_id = PW_ID_ANY; + items[1].value = pwsrc->stream->target_object; + } + + pw_stream_update_properties (pwsrc->stream->pwstream, &dict); + } + + GST_DEBUG_OBJECT (basesrc, "connect capture with path %s, target-object %s", + pwsrc->stream->path, pwsrc->stream->target_object); + + pwsrc->possible_caps = possible_caps; + pwsrc->negotiated = FALSE; + + enum pw_stream_flags flags; + flags = PW_STREAM_FLAG_DONT_RECONNECT | + PW_STREAM_FLAG_ASYNC; + if (pwsrc->autoconnect) + flags |= PW_STREAM_FLAG_AUTOCONNECT; + pw_stream_connect (pwsrc->stream->pwstream, + PW_DIRECTION_INPUT, + target_id, + flags, + (const struct spa_pod **)possible->pdata, + possible->len); + + pw_thread_loop_get_time (pwsrc->stream->core->loop, &abstime, + GST_PIPEWIRE_DEFAULT_TIMEOUT * SPA_NSEC_PER_SEC); + + while (TRUE) { + enum pw_stream_state state = pw_stream_get_state (pwsrc->stream->pwstream, &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 (pwsrc->autoconnect) { + if (pw_thread_loop_timed_wait_full (pwsrc->stream->core->loop, &abstime) < 0) + goto connect_error; + } else { + pw_thread_loop_wait (pwsrc->stream->core->loop); + } + } + + negotiated_caps = g_steal_pointer (&pwsrc->caps); + pwsrc->possible_caps = NULL; + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + if (negotiated_caps == NULL) + goto no_caps; + + gst_pipewire_clock_reset (GST_PIPEWIRE_CLOCK (pwsrc->stream->clock), 0); + + GST_DEBUG_OBJECT (pwsrc, "set format %" GST_PTR_FORMAT, negotiated_caps); + result = gst_base_src_set_caps (GST_BASE_SRC (pwsrc), negotiated_caps); + if (!result) + goto no_caps; + + result = gst_pipewire_src_stream_start (pwsrc); + + return result; + +no_nego_needed: + { + GST_DEBUG_OBJECT (basesrc, "no negotiation needed"); + return TRUE; + } +no_caps: + { + const gchar * error_string = "No supported formats found"; + + GST_ELEMENT_ERROR (basesrc, STREAM, FORMAT, + ("%s", error_string), + ("This element did not produce valid caps")); + pw_thread_loop_lock (pwsrc->stream->core->loop); + pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "%s", error_string); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return FALSE; + } +no_common_caps: + { + const gchar * error_string = "No supported formats found"; + + GST_ELEMENT_ERROR (basesrc, STREAM, FORMAT, + ("%s", error_string), + ("This element does not have formats in common with the peer")); + pw_thread_loop_lock (pwsrc->stream->core->loop); + pw_stream_set_error (pwsrc->stream->pwstream, -EPIPE, "%s", error_string); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return FALSE; + } +connect_error: + { + g_clear_pointer (&pwsrc->caps, gst_caps_unref); + pwsrc->possible_caps = NULL; + GST_DEBUG_OBJECT (basesrc, "connect error"); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return FALSE; + } +} + +static void +handle_format_change (GstPipeWireSrc *pwsrc, + const struct spa_pod *param) +{ + GstStructure *structure; + g_autoptr (GstCaps) pw_peer_caps = NULL; + + g_clear_pointer (&pwsrc->caps, gst_caps_unref); + if (param == NULL) { + GST_DEBUG_OBJECT (pwsrc, "clear format"); + pwsrc->negotiated = FALSE; + pwsrc->is_video = FALSE; + return; + } + + pw_peer_caps = gst_caps_from_format (param); + + if (pw_peer_caps && pwsrc->possible_caps) { + GST_DEBUG_OBJECT (pwsrc, "peer caps %" GST_PTR_FORMAT, pw_peer_caps); + GST_DEBUG_OBJECT (pwsrc, "possible caps %" GST_PTR_FORMAT, pwsrc->possible_caps); + + pwsrc->caps = gst_caps_intersect_full (pw_peer_caps, + pwsrc->possible_caps, + GST_CAPS_INTERSECT_FIRST); + + /* + * We expect pw_peer_caps to be fixed caps as we receive that from + * PipeWire. See pw_context_find_format() and SPA_PARAM_Format. + * possible_caps can be non-fixated caps based on what is downstream + * in the pipeline. + * + * The intersection result above might give us non-fixated caps. A + * possible scenario for this is the below pipeline. + * pipewiresrc ! audioconvert ! audio/x-raw,rate=44100,channels=2 ! .. + * + * So we fixate the caps explicitly here. + */ + pwsrc->caps = gst_caps_fixate (pwsrc->caps); + gst_caps_maybe_fixate_dma_format (pwsrc->caps); + } + + if (pwsrc->caps) { + g_return_if_fail (gst_caps_is_fixed (pwsrc->caps)); + + pwsrc->negotiated = TRUE; + + structure = gst_caps_get_structure (pwsrc->caps, 0); + if (g_str_has_prefix (gst_structure_get_name (structure), "video/") || + g_str_has_prefix (gst_structure_get_name (structure), "image/")) { + pwsrc->is_video = TRUE; + +#ifdef HAVE_GSTREAMER_DMA_DRM + if (gst_video_is_dma_drm_caps (pwsrc->caps)) { + if (!gst_video_info_dma_drm_from_caps (&pwsrc->drm_info, pwsrc->caps)) { + GST_WARNING_OBJECT (pwsrc, "Can't create drm video info from caps"); + pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "internal error"); + return; + } + + if (!gst_video_info_dma_drm_to_video_info (&pwsrc->drm_info, + &pwsrc->video_info)) { + GST_WARNING_OBJECT (pwsrc, "Can't create video info from drm video info"); + pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "internal error"); + return; + } + } else { + gst_video_info_dma_drm_init (&pwsrc->drm_info); +#endif + gst_video_info_from_caps (&pwsrc->video_info, pwsrc->caps); +#ifdef HAVE_GSTREAMER_DMA_DRM + } +#endif + } else { + /* Don't provide bufferpool for audio if not requested by the + * application/user */ + if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES) + pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; + } + } else { + pwsrc->negotiated = FALSE; + pwsrc->is_video = FALSE; + } + + if (pwsrc->caps) { + 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<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, 1, 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->pwstream, params, SPA_N_ELEMENTS(params)); + } else { + GST_WARNING_OBJECT (pwsrc, "finish format with error"); + pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "unhandled format"); + } + pw_thread_loop_signal (pwsrc->stream->core->loop, FALSE); +} + +static void +on_param_changed (void *data, uint32_t id, + const struct spa_pod *param) +{ + GstPipeWireSrc *pwsrc = data; + switch (id) { + case SPA_PARAM_Format: + handle_format_change(pwsrc, param); + break; + } +} + +static gboolean +gst_pipewire_src_unlock (GstBaseSrc * basesrc) +{ + GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc); + + pw_thread_loop_lock (pwsrc->stream->core->loop); + GST_DEBUG_OBJECT (pwsrc, "setting flushing"); + pwsrc->flushing = TRUE; + pw_thread_loop_signal (pwsrc->stream->core->loop, FALSE); + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + return TRUE; +} + +static gboolean +gst_pipewire_src_unlock_stop (GstBaseSrc * basesrc) +{ + GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc); + + pw_thread_loop_lock (pwsrc->stream->core->loop); + GST_DEBUG_OBJECT (pwsrc, "unsetting flushing"); + pwsrc->flushing = FALSE; + pw_thread_loop_unlock (pwsrc->stream->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); + 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 void +gst_pipewire_src_get_times (GstBaseSrc * basesrc, GstBuffer * buffer, + GstClockTime * start, GstClockTime * end) +{ + GstPipeWireSrc *pwsrc = GST_PIPEWIRE_SRC (basesrc); + + /* for live sources, sync on the timestamp of the buffer */ + if (gst_base_src_is_live (basesrc)) { + GstClockTime timestamp = GST_BUFFER_PTS (buffer); + + if (GST_CLOCK_TIME_IS_VALID (timestamp)) { + /* get duration to calculate end time */ + GstClockTime duration = GST_BUFFER_DURATION (buffer); + + if (GST_CLOCK_TIME_IS_VALID (duration)) { + *end = timestamp + duration; + } + *start = timestamp; + } + } else { + *start = GST_CLOCK_TIME_NONE; + *end = GST_CLOCK_TIME_NONE; + } + + GST_LOG_OBJECT (pwsrc, "start %" GST_TIME_FORMAT " (%" G_GUINT64_FORMAT + "), end %" GST_TIME_FORMAT " (%" G_GUINT64_FORMAT ")", + GST_TIME_ARGS (*start), *start, GST_TIME_ARGS (*end), *end); +} + +static GstFlowReturn +gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) +{ + GstPipeWireSrc *pwsrc; + const char *error = NULL; + GstBuffer *buf; + gboolean update_time = FALSE, timeout = FALSE; + GstCaps *caps = NULL; + + pwsrc = GST_PIPEWIRE_SRC (psrc); + + pw_thread_loop_lock (pwsrc->stream->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->pwstream, &error); + if (state == PW_STREAM_STATE_ERROR) + goto streaming_error; + + if (state == PW_STREAM_STATE_UNCONNECTED) + goto streaming_stopped; + + if ((caps = pwsrc->caps) != NULL) { + pwsrc->caps = NULL; + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + GST_DEBUG_OBJECT (pwsrc, "set format %" GST_PTR_FORMAT, caps); + gst_base_src_set_caps (GST_BASE_SRC (pwsrc), caps); + gst_caps_unref (caps); + + pw_thread_loop_lock (pwsrc->stream->core->loop); + continue; + } + + 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->stream->core->loop, &abstime, + pwsrc->keepalive_time * SPA_NSEC_PER_MSEC); + if (pw_thread_loop_timed_wait_full (pwsrc->stream->core->loop, &abstime) == -ETIMEDOUT) + timeout = TRUE; + } else { + pw_thread_loop_wait (pwsrc->stream->core->loop); + } + } + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + *buffer = buf; + + if (update_time) { + GstClock *clock; + GstClockTime pts, dts; + + 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; + } + + GST_BUFFER_PTS (*buffer) = pts; + GST_BUFFER_DTS (*buffer) = dts; + + GST_LOG_OBJECT (pwsrc, "Sending keepalive buffer pts/dts: %" GST_TIME_FORMAT + " (%" G_GUINT64_FORMAT ")", GST_TIME_ARGS (pts), pts); + } + + return GST_FLOW_OK; + +not_negotiated: + { + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return GST_FLOW_NOT_NEGOTIATED; + } +streaming_eos: + { + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return GST_FLOW_EOS; + } +streaming_error: + { + pw_thread_loop_unlock (pwsrc->stream->core->loop); + return GST_FLOW_ERROR; + } +streaming_stopped: + { + pw_thread_loop_unlock (pwsrc->stream->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->stream->core->loop); + pwsrc->eos = false; + gst_buffer_replace (&pwsrc->last_buffer, NULL); + gst_caps_replace(&pwsrc->caps, NULL); + pwsrc->transform_value = UINT32_MAX; + pw_thread_loop_unlock (pwsrc->stream->core->loop); + + 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_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->stream->core->loop); + this->eos = true; + pw_thread_loop_signal (this->stream->core->loop, FALSE); + pw_thread_loop_unlock (this->stream->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_stream_open (this->stream, &stream_events)) + 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->stream->core->loop); + pw_stream_set_active (this->stream->pwstream, true); + pw_thread_loop_unlock (this->stream->core->loop); + break; + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + /* stop recording ASAP by corking */ + pw_thread_loop_lock (this->stream->core->loop); + pw_stream_set_active (this->stream->pwstream, false); + pw_thread_loop_unlock (this->stream->core->loop); + break; + 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; + + pw_thread_loop_lock (this->stream->core->loop); + /* the initial stream state is active, which is needed for linking and + * negotiation to happen and the bufferpool to be set up. We don't know + * if we'll go to playing, so we deactivate the stream until that + * transition happens. This is janky, but because of how bins propagate + * state changes one transition at a time, there may not be a better way + * to do this. */ + pw_stream_set_active (this->stream->pwstream, false); + pw_thread_loop_unlock (this->stream->core->loop); + + 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->stream->core->loop); + this->negotiated = FALSE; + pw_thread_loop_unlock (this->stream->core->loop); + break; + case GST_STATE_CHANGE_READY_TO_NULL: + gst_pipewire_stream_close (this->stream); + 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..d5728cd --- /dev/null +++ b/src/gst/gstpipewiresrc.h @@ -0,0 +1,72 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_SRC_H__ +#define __GST_PIPEWIRE_SRC_H__ + +#include "config.h" + +#include "gstpipewirestream.h" + +#include +#include + +#include + +#include +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_PIPEWIRE_SRC (gst_pipewire_src_get_type()) +#define GST_PIPEWIRE_SRC_CAST(obj) ((GstPipeWireSrc *) (obj)) +G_DECLARE_FINAL_TYPE (GstPipeWireSrc, gst_pipewire_src, GST, PIPEWIRE_SRC, GstPushSrc) + + +/** + * GstPipeWireSrc: + * + * Opaque data structure. + */ +struct _GstPipeWireSrc { + GstPushSrc element; + + GstPipeWireStream *stream; + + /*< private >*/ + gint use_bufferpool; + gint min_buffers; + gint max_buffers; + gboolean resend_last; + gint keepalive_time; + gboolean autoconnect; + + GstCaps *caps; + GstCaps *possible_caps; + + gboolean is_video; + GstVideoInfo video_info; +#ifdef HAVE_GSTREAMER_DMA_DRM + GstVideoInfoDmaDrm drm_info; +#endif + + gboolean negotiated; + gboolean flushing; + gboolean started; + gboolean eos; + + gboolean is_live; + int64_t delay; + GstClockTime min_latency; + GstClockTime max_latency; + + GstBuffer *last_buffer; + + enum spa_meta_videotransform_value transform_value; +}; + +G_END_DECLS + +#endif /* __GST_PIPEWIRE_SRC_H__ */ diff --git a/src/gst/gstpipewirestream.c b/src/gst/gstpipewirestream.c new file mode 100644 index 0000000..68cb9be --- /dev/null +++ b/src/gst/gstpipewirestream.c @@ -0,0 +1,172 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#include "gstpipewirestream.h" + +#include "gstpipewirepool.h" +#include "gstpipewireclock.h" + +GST_DEBUG_CATEGORY_STATIC (pipewire_stream_debug); +#define GST_CAT_DEFAULT pipewire_stream_debug + +G_DEFINE_TYPE (GstPipeWireStream, gst_pipewire_stream, GST_TYPE_OBJECT) + +static void +gst_pipewire_stream_init (GstPipeWireStream * self) +{ + self->fd = -1; + self->client_name = g_strdup (pw_get_client_name()); + self->pool = gst_pipewire_pool_new (self); + spa_dll_init(&self->dll); +} + +static void +gst_pipewire_stream_finalize (GObject * object) +{ + GstPipeWireStream * self = GST_PIPEWIRE_STREAM (object); + + g_clear_object (&self->pool); + g_free (self->path); + g_free (self->target_object); + g_free (self->client_name); + gst_clear_structure (&self->client_properties); + gst_clear_structure (&self->stream_properties); + + G_OBJECT_CLASS(gst_pipewire_stream_parent_class)->finalize (object); +} + +void +gst_pipewire_stream_class_init (GstPipeWireStreamClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gst_pipewire_stream_finalize; + + GST_DEBUG_CATEGORY_INIT (pipewire_stream_debug, "pipewirestream", 0, + "PipeWire Stream"); +} + +GstPipeWireStream * +gst_pipewire_stream_new (GstElement * element) +{ + GstPipeWireStream *stream; + + stream = g_object_new (GST_TYPE_PIPEWIRE_STREAM, NULL); + stream->element = element; + + return stream; +} + +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; +} + +gboolean +gst_pipewire_stream_open (GstPipeWireStream * self, + const struct pw_stream_events * pwstream_events) +{ + struct pw_properties *props; + + g_return_val_if_fail (self->core == NULL, FALSE); + + GST_DEBUG_OBJECT (self, "open"); + + /* acquire the core */ + self->core = gst_pipewire_core_get (self->fd); + if (self->core == NULL) + goto connect_error; + + pw_thread_loop_lock (self->core->loop); + + /* update the client properties */ + if (self->client_properties) { + props = pw_properties_new (NULL, NULL); + gst_structure_foreach (self->client_properties, copy_properties, props); + pw_core_update_properties (self->core->core, &props->dict); + pw_properties_free (props); + } + + /* create stream */ + props = pw_properties_new (NULL, NULL); + if (self->client_name) { + pw_properties_set (props, PW_KEY_NODE_NAME, self->client_name); + pw_properties_set (props, PW_KEY_NODE_DESCRIPTION, self->client_name); + } + if (self->stream_properties) { + gst_structure_foreach (self->stream_properties, copy_properties, props); + } + + if ((self->pwstream = pw_stream_new (self->core->core, + self->client_name, props)) == NULL) + goto no_stream; + + pw_stream_add_listener(self->pwstream, + &self->pwstream_listener, + pwstream_events, + self->element); + + /* create clock */ + self->clock = gst_pipewire_clock_new (self, 0); + + pw_thread_loop_unlock (self->core->loop); + + return TRUE; + + /* ERRORS */ +connect_error: + { + GST_ELEMENT_ERROR (self->element, RESOURCE, FAILED, + ("Failed to connect"), (NULL)); + return FALSE; + } +no_stream: + { + GST_ELEMENT_ERROR (self->element, RESOURCE, FAILED, + ("can't create stream"), (NULL)); + pw_thread_loop_unlock (self->core->loop); + return FALSE; + } +} + +void +gst_pipewire_stream_close (GstPipeWireStream * self) +{ + GST_DEBUG_OBJECT (self, "close"); + + /* destroy the clock */ + gst_element_post_message (GST_ELEMENT (self->element), + gst_message_new_clock_lost (GST_OBJECT_CAST (self->element), self->clock)); + g_weak_ref_set (&GST_PIPEWIRE_CLOCK (self->clock)->stream, NULL); + g_clear_object (&self->clock); + + /* destroy the pw stream */ + pw_thread_loop_lock (self->core->loop); + if (self->pwstream) { + /* Do not use g_clear_pointer() here as pw_stream_destroy() may chain up to + * code requiring the pointer to still be around */ + pw_stream_destroy (self->pwstream); + self->pwstream = NULL; + } + pw_thread_loop_unlock (self->core->loop); + + /* release the core */ + g_clear_pointer (&self->core, gst_pipewire_core_release); +} diff --git a/src/gst/gstpipewirestream.h b/src/gst/gstpipewirestream.h new file mode 100644 index 0000000..a301375 --- /dev/null +++ b/src/gst/gstpipewirestream.h @@ -0,0 +1,61 @@ +/* GStreamer */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2024 Collabora Ltd. */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __GST_PIPEWIRE_STREAM_H__ +#define __GST_PIPEWIRE_STREAM_H__ + +#include "config.h" + +#include "gstpipewirecore.h" + +#include +#include +#include + +G_BEGIN_DECLS + +typedef struct _GstPipeWirePool GstPipeWirePool; + +#define GST_TYPE_PIPEWIRE_STREAM (gst_pipewire_stream_get_type()) +G_DECLARE_FINAL_TYPE(GstPipeWireStream, gst_pipewire_stream, GST, PIPEWIRE_STREAM, GstObject) + +struct _GstPipeWireStream { + GstObject parent; + + /* relatives */ + GstElement *element; + GstPipeWireCore *core; + GstPipeWirePool *pool; + GstClock *clock; + + guint64 position; + struct spa_dll dll; + double err_avg, err_var, err_wdw; + guint64 last_ts; + guint64 base_buffer_ts; + guint64 base_ts; + + /* the actual pw stream */ + struct pw_stream *pwstream; + struct spa_hook pwstream_listener; + + /* common properties */ + int fd; + gchar *path; + gchar *target_object; + gchar *client_name; + GstStructure *client_properties; + GstStructure *stream_properties; +}; + +GstPipeWireStream * gst_pipewire_stream_new (GstElement * element); + +gboolean gst_pipewire_stream_open (GstPipeWireStream * self, + const struct pw_stream_events * pwstream_events); +void gst_pipewire_stream_close (GstPipeWireStream * self); + +G_END_DECLS + +#endif /* __GST_PIPEWIRE_STREAM_H__ */ diff --git a/src/gst/meson.build b/src/gst/meson.build new file mode 100644 index 0000000..1e39bcf --- /dev/null +++ b/src/gst/meson.build @@ -0,0 +1,35 @@ +pipewire_gst_sources = [ + 'gstpipewire.c', + 'gstpipewirecore.c', + 'gstpipewireclock.c', + 'gstpipewireformat.c', + 'gstpipewirepool.c', + 'gstpipewiresink.c', + 'gstpipewiresrc.c', + 'gstpipewirestream.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', + 'gstpipewirestream.h', +] + +pipewire_gst = shared_library('gstpipewire', + pipewire_gst_sources, + include_directories : [ configinc ], + dependencies : [ spa_dep, gst_dep, pipewire_dep, mathlib ], + 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..5e5ecc4 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,13 @@ + +subdir('pipewire') +subdir('daemon') +subdir('tools') +subdir('modules') +subdir('examples') +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..9b839e2 --- /dev/null +++ b/src/modules/flatpak-utils.h @@ -0,0 +1,140 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef FLATPAK_UTILS_H +#define FLATPAK_UTILS_H + +#include "config.h" + +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_VFS_H +#include +#endif + +#include "config.h" + +#ifdef HAVE_GLIB2 +#include +#endif + +#include +#include +#include + +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]; + struct stat stat_buf; + int res; + + if (app_id) + *app_id = NULL; + if (devices) + *devices = NULL; + + snprintf(root_path, sizeof(root_path), "/proc/%d/root", (int)pid); + + spa_autoclose int root_fd = openat(AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); + if (root_fd < 0) { + res = -errno; + pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res)); + + if (res == -EACCES) { + /* If we can't access the root filesystem, consider not sandboxed. + * This should not happen but for now it is a workaround for selinux + * where we can't access the gnome-shell root when it connects for + * screen sharing. + */ + 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. */ + return res; + } + + spa_autoclose int info_fd = openat(root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY); + if (info_fd < 0) { + 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)); + } + + 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..be9912d --- /dev/null +++ b/src/modules/meson.build @@ -0,0 +1,759 @@ +subdir('module-rt') + +# 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-filter.c', + 'module-example-sink.c', + 'module-example-source.c', + 'module-fallback-sink.c', + 'module-ffado-driver.c', + 'module-filter-chain.c', + 'module-jack-tunnel.c', + 'module-jackdbus-detect.c', + 'module-link-factory.c', + 'module-loopback.c', + 'module-metadata.c', + 'module-netjack2-driver.c', + 'module-netjack2-manager.c', + 'module-parametric-equalizer.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-sap.c', + 'module-rtp-session.c', + 'module-rtp-source.c', + 'module-rtp-sink.c', + 'module-spa-node.c', + 'module-spa-node-factory.c', + 'module-spa-device.c', + 'module-spa-device-factory.c', + 'module-snapcast-discover.c', + 'module-vban-recv.c', + 'module-vban-send.c', + '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], +) + +pipewire_module_filter_chain = shared_library('pipewire-module-filter-chain', + [ 'module-filter-chain.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_combine_stream = shared_library('pipewire-module-combine-stream', + [ 'module-combine-stream.c' ], + include_directories : [configinc], + install : true, + 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', + [ 'module-echo-cancel.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, pipewire_dep, plugin_dependencies], +) + +build_module_jack_tunnel = jack_dep.found() +if build_module_jack_tunnel + pipewire_module_jack_tunnel = shared_library('pipewire-module-jack-tunnel', + [ 'module-jack-tunnel.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, pipewire_dep], + ) + build_module_jackdbus_detect = dbus_dep.found() + if build_module_jackdbus_detect + pipewire_module_jack_tunnel = shared_library('pipewire-module-jackdbus-detect', + [ 'module-jackdbus-detect.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, pipewire_dep, dbus_dep], + ) + endif +endif + +summary({'jack-tunnel': build_module_jack_tunnel}, bool_yn: true, section: 'Optional Modules') + +build_module_ffado_driver = libffado_dep.found() +if build_module_ffado_driver + pipewire_module_jack_tunnel = shared_library('pipewire-module-ffado-driver', + [ 'module-ffado-driver.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, pipewire_dep, libffado_dep], + ) +endif + +summary({'ffado-driver': build_module_ffado_driver}, bool_yn: true, section: 'Optional Modules') + +opus_custom_h = cc.has_header('opus/opus_custom.h', dependencies: opus_dep) +opus_custom_lib = cc.has_function('opus_custom_encoder_ctl', dependencies: opus_dep) + +# One would imagine that opus_dep is a requirement but for some reason it's not, so we need to manually check that +if opus_dep.found() and opus_custom_h and opus_custom_lib + opus_custom_dep = declare_dependency(compile_args: ['-DHAVE_OPUS_CUSTOM'], dependencies: opus_dep) +else + opus_custom_dep = dependency('', required: false) +endif +summary({'Opus with custom modes for NetJack2': opus_custom_dep}, bool_yn: true, section: 'Streaming between daemons') + +pipewire_module_netjack2_driver = shared_library('pipewire-module-netjack2-driver', + [ 'module-netjack2-driver.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, opus_custom_dep], +) + +pipewire_module_netjack2_manager = shared_library('pipewire-module-netjack2-manager', + [ 'module-netjack2-manager.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep, opus_custom_dep], +) + +pipewire_module_parametric_equalizer = shared_library('pipewire-module-parametric-equalizer', + [ 'module-parametric-equalizer.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_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 + 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') + +pipewire_module_spa_node = shared_library('pipewire-module-spa-node', + [ 'module-spa-node.c', 'spa/spa-node.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], +) +pipewire_module_spa_node_factory = shared_library('pipewire-module-spa-node-factory', + [ 'module-spa-node-factory.c', 'spa/spa-node.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], +) +pipewire_module_spa_device = shared_library('pipewire-module-spa-device', + [ 'module-spa-device.c', 'spa/spa-device.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], +) +pipewire_module_spa_device_factory = shared_library('pipewire-module-spa-device-factory', + [ 'module-spa-device-factory.c', 'spa/spa-device.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], +) + +build_module_portal = dbus_dep.found() +if build_module_portal + pipewire_module_portal = shared_library('pipewire-module-portal', [ 'module-portal.c' ], + 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 + +if selinux_dep.found() + pipewire_module_protocol_deps += selinux_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/security-context.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/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-alsa-sink.c', + 'module-protocol-pulse/modules/module-alsa-source.c', + 'module-protocol-pulse/modules/module-always-sink.c', + 'module-protocol-pulse/modules/module-combine-sink.c', + 'module-protocol-pulse/modules/module-device-manager.c', + 'module-protocol-pulse/modules/module-device-restore.c', + 'module-protocol-pulse/modules/module-echo-cancel.c', + 'module-protocol-pulse/modules/module-jackdbus-detect.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-stream-restore.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-virtual-sink.c', + 'module-protocol-pulse/modules/module-virtual-source.c', + 'module-protocol-pulse/modules/module-x11-bell.c', + 'module-protocol-pulse/modules/module-zeroconf-discover.c', +] + +if snap_dep.found() and glib2_snap_dep.found() and gio2_snap_dep.found() and apparmor_snap_dep.found() + pipewire_module_protocol_pulse_sources += [ + 'module-protocol-pulse/snap-policy.c', + ] + pipewire_module_protocol_pulse_deps += snap_deps +endif + +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 gsettings_gio_dep.found() + pipewire_module_protocol_pulse_sources += [ + 'module-protocol-pulse/modules/module-gsettings.c', + ] + pipewire_module_protocol_pulse_deps += gsettings_gio_dep + cdata.set('HAVE_GIO', true) + if get_option('gsettings-pulse-schema').enabled() + install_data(['module-protocol-pulse/modules/org.freedesktop.pulseaudio.gschema.xml'], + install_dir: pipewire_datadir / 'glib-2.0' / 'schemas' + ) + gnome = import('gnome') + gnome.post_install( + glib_compile_schemas: true + ) + endif +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_filter = shared_library('pipewire-module-example-filter', + [ 'module-example-filter.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-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_snapcast_discover = avahi_dep.found() +if build_module_snapcast_discover + pipewire_module_snapcast_discover = shared_library('pipewire-module-snapcast-discover', + [ 'module-snapcast-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({'snapcast-discover (needs Avahi)': build_module_snapcast_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', + 'module-rtp/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, opus_dep, openssl_lib], + ) +endif +summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') + +roc_dep = dependency('roc', version: '>= 0.4.0', required: get_option('roc')) +summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons') + +pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source', + [ 'module-rtp-source.c', + 'module-rtp/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, opus_dep], +) + +pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink', + [ 'module-rtp-sink.c', + 'module-rtp/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, opus_dep], +) + +build_module_rtp_session = avahi_dep.found() +if build_module_rtp_session + pipewire_module_rtp_session = shared_library('pipewire-module-rtp-session', + [ 'module-rtp/stream.c', + 'module-zeroconf-discover/avahi-poll.c', + 'module-rtp-session.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, opus_dep], + ) +endif + +pipewire_module_rtp_sap = shared_library('pipewire-module-rtp-sap', + [ 'module-rtp-sap.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_vban_send = shared_library('pipewire-module-vban-send', + [ 'module-vban-send.c', + 'module-vban/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], +) + +pipewire_module_vban_recv = shared_library('pipewire-module-vban-recv', + [ 'module-vban-recv.c', + 'module-vban/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], +) + +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..2e246ae --- /dev/null +++ b/src/modules/module-access.c @@ -0,0 +1,405 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#ifdef HAVE_SYS_VFS_H +#include +#endif +#ifdef HAVE_SYS_MOUNT_H +#include +#endif + +#include +#include +#include +#include + +#include + +#include "flatpak-utils.h" + +/** \page page_module_access 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 below. Permission management beyond unrestricted access + * is delegated to an external agent, usually the session manager. + * + * This module sets the \ref PW_KEY_ACCESS as follows: + * + * - If `access.legacy` module option is not enabled: + + * The value defined for the socket in `access.socket` module option, or + * `"default"` if no value is defined. + * + * - If `access.legacy` is enabled, the value is: + * + * - `"flatpak"`: if client is a Flatpak client + * - `"unrestricted"`: if \ref PW_KEY_CLIENT_ACCESS client property is set to `"allowed"` + * - Value of \ref PW_KEY_CLIENT_ACCESS client property, if set + * - `"unrestricted"`: otherwise + * + * If the resulting \ref PW_KEY_ACCESS value is `"unrestricted"`, this module + * will give the client all permissions to access all resources. Otherwise, the + * client will be forced to wait until an external actor, such as the session + * manager, updates the client permissions. + * + * For connections from applications running inside Flatpak, and not mediated by + * other clients (eg. portal or pipewire-pulse), the + * `pipewire.access.portal.app_id` property is to the Flatpak application ID, if + * found. In addition, `pipewire.sec.flatpak` is set to `true`. + * + * ## Module Name + * + * `libpipewire-module-access` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * - `access.socket = { "socket-name" = "access-value", ... }`: + * + * Socket-specific access permissions. Has the default value + * `{ "CORENAME-manager": "unrestricted" }` + * where `CORENAME` is the name of the PipeWire core, usually `pipewire-0`. + * + * - `access.legacy = true`: enable backward-compatible access mode. Cannot be + * enabled when using socket-based permissions. + * + * If `access.socket` is not specified, has the default value `true` + * otherwise `false`. + * + * \warning The legacy mode is deprecated. The default value is subject to + * change and the legacy mode may be removed in future PipeWire + * releases. + * + * ## General options + * + * Options with well-known behavior: + * + * - \ref PW_KEY_ACCESS + * - \ref PW_KEY_CLIENT_ACCESS + * + * ## Config override + * + * A `module.access.args` config section can be added + * to override the module arguments. + * + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-access-args.conf + * + * module.access.args = { + * access.socket = { + * pipewire-0 = "default", + * pipewire-0-manager = "unrestricted", + * } + * } + *\endcode + * + * ## Example configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-access + * args = { + * # Use separate socket for session manager applications, + * # and pipewire-0 for usual applications. + * access.socket = { + * pipewire-0 = "default", + * pipewire-0-manager = "unrestricted", + * } + * } + * } + *] + *\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.socket={ =, ... } ) " \ + "( access.legacy=true ) " + +#define ACCESS_UNRESTRICTED "unrestricted" +#define ACCESS_FLATPAK "flatpak" +#define ACCESS_DEFAULT "default" + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { 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 *socket_access; + + struct spa_hook context_listener; + struct spa_hook module_listener; + + unsigned int legacy:1; +}; + +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[3]; + const struct pw_properties *props; + const char *str; + const char *access; + const char *socket; + spa_autofree char *flatpak_app_id = NULL; + int nitems = 0; + bool sandbox_flatpak; + int pid, res; + + /* Get client properties */ + + pid = -EINVAL; + socket = NULL; + sandbox_flatpak = false; + + 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); + socket = pw_properties_get(props, PW_KEY_SEC_SOCKET); + } + + if (pid < 0) { + pw_log_info("client %p: no trusted pid found, assuming not sandboxed", client); + } else { + pw_log_info("client %p has trusted pid %d", client, pid); + + res = pw_check_flatpak(pid, &flatpak_app_id, NULL); + if (res != 0) { + if (res < 0) + pw_log_warn("%p: client %p flatpak check failed: %s", + impl, client, spa_strerror(res)); + + pw_log_info("client %p is from flatpak", client); + sandbox_flatpak = true; + } + } + + /* Apply rules */ + + if (!impl->legacy) { + if ((str = pw_properties_get(impl->socket_access, socket)) != NULL) + access = str; + else + access = ACCESS_DEFAULT; + } else { + if (sandbox_flatpak) { + access = ACCESS_FLATPAK; + } else if ((str = pw_properties_get(props, PW_KEY_CLIENT_ACCESS)) != NULL) { + if (spa_streq(str, "allowed")) + access = ACCESS_UNRESTRICTED; + else + access = str; + } else { + access = ACCESS_UNRESTRICTED; + } + } + + /* Handle resolution */ + + if (sandbox_flatpak) { + items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.app_id", + flatpak_app_id); + items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.sec.flatpak", "true"); + } + + if (spa_streq(access, ACCESS_UNRESTRICTED)) { + 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); + } else { + 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)); + } +} + +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; + + if (impl->context) { + spa_hook_remove(&impl->context_listener); + spa_hook_remove(&impl->module_listener); + } + + pw_properties_free(impl->socket_access); + + free(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +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 int parse_socket_args(struct impl *impl, const char *str) +{ + struct spa_json it[1]; + char socket[PATH_MAX]; + const char *val; + int len; + + if (spa_json_begin_object(&it[0], str, strlen(str)) <= 0) + return -EINVAL; + + while ((len = spa_json_object_next(&it[0], socket, sizeof(socket), &val)) > 0) { + char value[256]; + + if (spa_json_parse_stringn(val, len, value, sizeof(value)) <= 0) + return -EINVAL; + + pw_properties_set(impl->socket_access, socket, value); + } + + return 0; +} + +static int parse_args(struct impl *impl, const struct pw_properties *props, const struct pw_properties *args) +{ + const char *str; + int res; + + if ((str = pw_properties_get(args, "access.legacy")) != NULL) { + impl->legacy = spa_atob(str); + } else if (pw_properties_get(args, "access.socket")) { + impl->legacy = false; + } else { + /* When time comes, we should change this to false */ + impl->legacy = true; + } + + if (pw_properties_get(args, "access.force") || + pw_properties_get(args, "access.allowed") || + pw_properties_get(args, "access.rejected") || + pw_properties_get(args, "access.restricted")) { + pw_log_warn("access.force/allowed/rejected/restricted are deprecated and ignored " + "but imply legacy access mode"); + impl->legacy = true; + } + + if ((str = pw_properties_get(args, "access.socket")) != NULL) { + if (impl->legacy) { + pw_log_error("access.socket and legacy mode cannot be both enabled"); + return -EINVAL; + } + + if ((res = parse_socket_args(impl, str)) < 0) { + pw_log_error("invalid access.socket value"); + return res; + } + } else { + char def[PATH_MAX]; + + spa_scnprintf(def, sizeof(def), "%s-manager", get_server_name(&props->dict)); + pw_properties_set(impl->socket_access, def, ACCESS_UNRESTRICTED); + } + + if (impl->legacy) + pw_log_info("Using backward-compatible legacy access mode."); + + return 0; +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args_str) +{ + struct pw_context *context = pw_impl_module_get_context(module); + const struct pw_properties *props = pw_context_get_properties(context); + spa_autoptr(pw_properties) args = NULL; + struct impl *impl; + int res; + + 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_str); + + if (args_str) + args = pw_properties_new_string(args_str); + else + args = pw_properties_new(NULL, NULL); + + if (!args) { + res = -errno; + goto error; + } + + pw_context_conf_update_props(context, "module."NAME".args", args); + + impl->socket_access = pw_properties_new(NULL, NULL); + + if ((res = parse_args(impl, props, args)) < 0) + goto error; + + impl->context = context; + + 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; + +error: + module_destroy(impl); + return res; +} diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c new file mode 100644 index 0000000..ea913eb --- /dev/null +++ b/src/modules/module-adapter.c @@ -0,0 +1,400 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include + +#include + +#include "modules/spa/spa-node.h" +#include "module-adapter/adapter.h" + +/** \page page_module_adapter Adapter + * + * ## Module Name + * + * `libpipewire-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"= " \ + "("SPA_KEY_LIBRARY_NAME"=) " \ + ADAPTER_USAGE + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { 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, +}; + +struct match { + struct pw_properties *props; + int count; +}; +#define MATCH_INIT(p) ((struct match){ .props = (p) }) + +static int execute_match(void *data, const char *location, const char *action, + const char *val, size_t len) +{ + struct match *match = data; + if (spa_streq(action, "update-props")) { + match->count += pw_properties_update_string(match->props, val, len); + } + return 1; +} + +static void *create_object(void *_data, + struct pw_resource *resource, + const char *type, + 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; + struct match match; + + factory_name = pw_properties_get(properties, SPA_KEY_FACTORY_NAME); + if (factory_name == NULL) + goto error_properties; + + match = MATCH_INIT(properties); + pw_context_conf_section_match_rules(d->context, "node.rules", + &properties->dict, execute_match, &match); + + handle = pw_context_load_spa_handle(d->context, + factory_name, + &properties->dict); + 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..93ba0d3 --- /dev/null +++ b/src/modules/module-adapter/adapter.c @@ -0,0 +1,242 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 const struct pw_impl_node_events node_events = { + PW_VERSION_IMPL_NODE_EVENTS, + .free = node_free, +}; + +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; +} + +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; + 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, PW_KEY_PORT_GROUP) == NULL) + pw_properties_setf(props, PW_KEY_PORT_GROUP, "stream.0"); + + 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; + } + + node = pw_spa_node_load(context, + factory_name, + PW_SPA_NODE_FLAG_ACTIVATE | PW_SPA_NODE_FLAG_NO_REGISTER, + pw_properties_copy(props), 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); + + 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..b63d720 --- /dev/null +++ b/src/modules/module-adapter/adapter.h @@ -0,0 +1,28 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_ADAPTER_H +#define PIPEWIRE_ADAPTER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ADAPTER_USAGE PW_KEY_NODE_NAME"= " + +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..b730b17 --- /dev/null +++ b/src/modules/module-avb.c @@ -0,0 +1,113 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include + +#include +#include + +#include "module-avb/avb.h" + +/** \page page_module_avb AVB + * + * ## Module Name + * + * `libpipewire-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 " }, + { 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..da61dc4 --- /dev/null +++ b/src/modules/module-avb/aaf.h @@ -0,0 +1,82 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..951538b --- /dev/null +++ b/src/modules/module-avb/acmp.c @@ -0,0 +1,456 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +#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..bdd0d7e --- /dev/null +++ b/src/modules/module-avb/acmp.h @@ -0,0 +1,80 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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); +void avb_acmp_unregister(struct avb_acmp *acmp); + +#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..2275b7b --- /dev/null +++ b/src/modules/module-avb/adp.c @@ -0,0 +1,357 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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\": }] : trigger discover\\n" + "\" }"); + return 0; +} + +static int do_discover(struct adp *adp, const char *args, FILE *out) +{ + struct spa_json it[1]; + char key[128]; + uint64_t entity_id = 0ULL; + int len; + const char *value; + + if (spa_json_begin_object(&it[0], args, strlen(args)) <= 0) + return -EINVAL; + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &value)) > 0) { + uint64_t id_val; + + 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..408a488 --- /dev/null +++ b/src/modules/module-avb/adp.h @@ -0,0 +1,86 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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); +void avb_adp_unregister(struct avb_adp *adp); + +#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..9969e62 --- /dev/null +++ b/src/modules/module-avb/aecp-aem-descriptors.h @@ -0,0 +1,227 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..640add3 --- /dev/null +++ b/src/modules/module-avb/aecp-aem.c @@ -0,0 +1,265 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..5048584 --- /dev/null +++ b/src/modules/module-avb/aecp-aem.h @@ -0,0 +1,325 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..6e6a1ba --- /dev/null +++ b/src/modules/module-avb/aecp.c @@ -0,0 +1,148 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include + +#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..59276ab --- /dev/null +++ b/src/modules/module-avb/aecp.h @@ -0,0 +1,40 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..7bfa85e --- /dev/null +++ b/src/modules/module-avb/avb.c @@ -0,0 +1,89 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "avb.h" +#include "internal.h" + +#include + +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) { + pw_log_warn("vm.overrides in avb.properties are deprecated, " + "use avb.properties.rules instead"); + 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..a401faf --- /dev/null +++ b/src/modules/module-avb/avb.h @@ -0,0 +1,26 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_AVB_H +#define PIPEWIRE_AVB_H + +#include + +#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..fd80ec8 --- /dev/null +++ b/src/modules/module-avb/avdecc.c @@ -0,0 +1,317 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#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; + const char *str; + int res = 0; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + spa_list_append(&impl->servers, &server->link); + str = spa_dict_lookup(props, "ifname"); + server->ifname = str ? strdup(str) : NULL; + 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->timer); + 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..6ec5678 --- /dev/null +++ b/src/modules/module-avb/descriptors.h @@ -0,0 +1,255 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "adp.h" +#include "aecp-aem.h" +#include "aecp-aem-descriptors.h" +#include "internal.h" + +static inline 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..98f735e --- /dev/null +++ b/src/modules/module-avb/iec61883.h @@ -0,0 +1,90 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..d1e72c8 --- /dev/null +++ b/src/modules/module-avb/internal.h @@ -0,0 +1,146 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_INTERNAL_H +#define AVB_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +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..2ba40cd --- /dev/null +++ b/src/modules/module-avb/maap.c @@ -0,0 +1,441 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include + +#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) (uint64_t)(((n) + (MAAP_PROBE_INTERVAL_MS + \ + drand48() * MAAP_PROBE_INTERVAL_VAR_MS) * SPA_NSEC_PER_MSEC)) +#define ANNOUNCE_TIMEOUT(n) (uint64_t)(((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[2]; + bool have_offset = false; + int count = 0, offset = 0, len; + const char *val; + + snprintf(key, sizeof(key), "maap.%s", maap->server->ifname); + pw_conf_load_state("module-avb", key, maap->props); + + if ((str = pw_properties_get(maap->props, "maap.addresses")) == NULL) + return 0; + + if (spa_json_begin_array(&it[0], str, strlen(str)) <= 0) + return 0; + + if (spa_json_enter_object(&it[0], &it[1]) <= 0) + return 0; + + while ((len = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "start")) { + uint8_t addr[6]; + if (avb_utils_parse_addr(val, len, addr) >= 0 && + 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); + + pw_random(maap->xsubi, sizeof(maap->xsubi)); + + 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..a8827b4 --- /dev/null +++ b/src/modules/module-avb/maap.h @@ -0,0 +1,50 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..f36de4d --- /dev/null +++ b/src/modules/module-avb/mmrp.c @@ -0,0 +1,213 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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..054161a --- /dev/null +++ b/src/modules/module-avb/mmrp.h @@ -0,0 +1,48 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..1df176b --- /dev/null +++ b/src/modules/module-avb/mrp.c @@ -0,0 +1,592 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#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..3f16abb --- /dev/null +++ b/src/modules/module-avb/mrp.h @@ -0,0 +1,161 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..4ddc377 --- /dev/null +++ b/src/modules/module-avb/msrp.c @@ -0,0 +1,439 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include + +#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..932fb9b --- /dev/null +++ b/src/modules/module-avb/msrp.h @@ -0,0 +1,114 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..b62d9eb --- /dev/null +++ b/src/modules/module-avb/mvrp.c @@ -0,0 +1,277 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#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..cc6ea96 --- /dev/null +++ b/src/modules/module-avb/mvrp.h @@ -0,0 +1,42 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..cb23752 --- /dev/null +++ b/src/modules/module-avb/packets.h @@ -0,0 +1,81 @@ +/* Spa AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_PACKETS_H +#define AVB_PACKETS_H + +#include + +#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..b2fe4d6 --- /dev/null +++ b/src/modules/module-avb/srp.c @@ -0,0 +1,39 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#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..455090e --- /dev/null +++ b/src/modules/module-avb/srp.h @@ -0,0 +1,12 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#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..dd8ecca --- /dev/null +++ b/src/modules/module-avb/stream.c @@ -0,0 +1,569 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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..64de0a9 --- /dev/null +++ b/src/modules/module-avb/stream.h @@ -0,0 +1,84 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_STREAM_H +#define AVB_STREAM_H + +#include +#include +#include +#include + +#include +#include + +#include + +#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..c7663c4 --- /dev/null +++ b/src/modules/module-avb/utils.h @@ -0,0 +1,66 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_UTILS_H +#define AVB_UTILS_H + +#include + +#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..84b8456 --- /dev/null +++ b/src/modules/module-client-device.c @@ -0,0 +1,269 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include "config.h" + +#include + +#include + +#include "module-client-device/client-device.h" + +/** \page page_module_client_device Client Device + * + * Allow clients to export devices to the PipeWire daemon. + * + * This module creates an export type for the \ref SPA_TYPE_INTERFACE_Device + * interface. + * + * With \ref pw_core_export(), objects of this type can be exported to the + * PipeWire server. All actions performed on the device locally will be visible + * to connecteced clients. + * + * In some cases, it is possible to use this factory directly. + * With \ref pw_core_create_object() on the `client-device` + * factory will result in a \ref SPA_TYPE_INTERFACE_Device proxy that can be + * used to control the server side created \ref pw_impl_device. + * + * Schematically, the client side \ref spa_device is wrapped in the ClientDevice + * proxy and unwrapped by the server side resource so that all actions on the client + * side device are reflected on the server side device and server side actions are + * reflected in the client. + * + *\code{.unparsed} + * + * client side proxy server side resource + * .------------------------------. .----------------------------------. + * | SPA_TYPE_INTERFACE_Device | | PW_TYPE_INTERFACE_Device | + * | | IPC |.--------------------------------.| + * | | -----> || SPA_TYPE_INTERFACE_Device || + * | | |'--------------------------------'| + * '------------------------------' '----------------------------------' + *\endcode + * + * ## Module Name + * + * `libpipewire-module-client-device` + * + * ## Module Options + * + * This module has no options. + * + * ## Properties for the create_object call + * + * All properties are passed directly to the \ref pw_context_create_device() call. + * + * ## Example configuration + * + * The module is usually added to the config file of the main PipeWire daemon and the + * clients. + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-client-device } + * ] + *\endcode + * + * ## See also + * + * - `module-spa-device-factory`: make nodes from a factory + */ + +#define NAME "client-device" + +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 " }, + { 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..c15b98e --- /dev/null +++ b/src/modules/module-client-device/client-device.h @@ -0,0 +1,24 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_CLIENT_DEVICE_H +#define PIPEWIRE_CLIENT_DEVICE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLIENT_DEVICE_USAGE "["PW_KEY_DEVICE_NAME"=]" + +struct pw_impl_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..59d16e3 --- /dev/null +++ b/src/modules/module-client-device/protocol-native.c @@ -0,0 +1,536 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include + +#include + +#include + +#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(¶m)) < 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(¶ms.id), + SPA_POD_Int(¶ms.index), + SPA_POD_Int(¶ms.next), + SPA_POD_PodObject(¶ms.param), + NULL) < 0) + return -EINVAL; + + result = ¶ms; + 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..15cf26a --- /dev/null +++ b/src/modules/module-client-device/proxy-device.c @@ -0,0 +1,70 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include + +#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..ca00bfe --- /dev/null +++ b/src/modules/module-client-device/resource-device.c @@ -0,0 +1,137 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "client-device.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..dc97808 --- /dev/null +++ b/src/modules/module-client-node.c @@ -0,0 +1,281 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include "config.h" + +#include + +#include + +#define PW_API_CLIENT_NODE_IMPL SPA_EXPORT +#include "module-client-node/v0/client-node.h" +#include "module-client-node/client-node.h" + +/** \page page_module_client_node Client Node + * + * Allow clients to export processing nodes to the PipeWire daemon. + * + * This module creates 2 export types, one for the \ref PW_TYPE_INTERFACE_Node and + * another for the \ref SPA_TYPE_INTERFACE_Node interfaces. + * + * With \ref pw_core_export(), objects of these types can be exported to the + * PipeWire server. All actions performed on the node locally will be visible + * to connecteced clients and scheduling of the Node will be performed. + * + * Objects of the \ref PW_TYPE_INTERFACE_Node interface can be made with + * \ref pw_context_create_node(), for example. You would manually need to create + * and add an object of the \ref SPA_TYPE_INTERFACE_Node interface. Exporting a + * \ref SPA_TYPE_INTERFACE_Node directly will first wrap it in a + * \ref PW_TYPE_INTERFACE_Node interface. + * + * Usually this module is not used directly but through the \ref pw_stream and + * \ref pw_filter APIs, which provides API to implement the \ref SPA_TYPE_INTERFACE_Node + * interface. + * + * In some cases, it is possible to use this factory directly (the PipeWire JACK + * implementation does this). With \ref pw_core_create_object() on the `client-node` + * factory will result in a \ref PW_TYPE_INTERFACE_ClientNode proxy that can be + * used to control the server side created \ref pw_impl_node. + * + * Schematically, the client side \ref pw_impl_node is wrapped in the ClientNode + * proxy and unwrapped by the server side resource so that all actions on the client + * side node are reflected on the server side node and server side actions are + * reflected in the client. + * + *\code{.unparsed} + * + * client side proxy server side resource + * .------------------------------. .----------------------------------. + * | PW_TYPE_INTERFACE_ClientNode | | PW_TYPE_INTERFACE_Node | + * |.----------------------------.| IPC |.--------------------------------.| + * || PW_TYPE_INTERFACE_Node || -----> || SPA_TYPE_INTERFACE_Node || + * ||.--------------------------.|| ||.------------------------------.|| + * ||| SPA_TYPE_INTERFACE_Node ||| ||| PW_TYPE_INTERFACE_ClientNode ||| + * ||| ||| ||| ||| + * ||'--------------------------'|| ||'------------------------------'|| + * |'----------------------------'| |'--------------------------------'| + * '------------------------------' '----------------------------------' + *\endcode + * + * ## Module Name + * + * `libpipewire-module-client-node` + * + * ## Module Options + * + * This module has no options. + * + * ## Properties for the create_object call + * + * All properties are passed directly to the \ref pw_context_create_node() call. + * + * ## Example configuration + * + * The module is usually added to the config file of the main PipeWire daemon and the + * clients. + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-client-node } + * ] + *\endcode + * + * ## See also + * + * - `module-spa-node-factory`: make nodes from a factory + */ + +#define NAME "client-node" + +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 " }, + { 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..3c6d6e4 --- /dev/null +++ b/src/modules/module-client-node/client-node.c @@ -0,0 +1,1846 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#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 AREA_SLOT (sizeof(struct spa_io_async_buffers)) +#define AREA_SIZE (4096u / AREA_SLOT) +#define MAX_AREAS 32 + +#define CHECK_FREE_PORT(impl,d,p) (p <= pw_map_get_size(&impl->ports[d]) && !CHECK_PORT(impl,d,p)) +#define CHECK_PORT(impl,d,p) (pw_map_lookup(&impl->ports[d], p) != NULL) +#define GET_PORT(impl,d,p) (pw_map_lookup(&impl->ports[d], p)) + +#define CHECK_PORT_BUFFER(impl,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 { + uint32_t mix_id; + struct port *port; + uint32_t peer_id; + uint32_t n_buffers; + uint32_t impl_mix_id; + struct buffer buffers[MAX_BUFFERS]; +}; + +struct params { + uint32_t n_params; + struct spa_pod **params; +}; + +struct port { + struct pw_impl_port *port; + struct impl *impl; + + enum spa_direction direction; + uint32_t id; + + struct spa_node mix_node; + struct spa_hook_list mix_hooks; + + struct spa_port_info info; + struct pw_properties *properties; + + struct params params; + + unsigned int removed:1; + unsigned int destroyed:1; + + struct pw_map mix; +}; + +struct impl { + struct pw_impl_client_node this; + + struct pw_context *context; + struct pw_mempool *context_pool; + + 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; + + struct pw_resource *resource; + struct pw_impl_client *client; + struct pw_mempool *client_pool; + + struct spa_source data_source; + + struct pw_map ports[2]; + + struct port dummy; + + struct params params; + + struct pw_map io_map; + struct pw_array 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; +}; + +#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) +{ + if (mix_id == SPA_ID_INVALID) + mix_id = 0; + else + mix_id++; + + return pw_map_lookup(&p->mix, mix_id); +} + +static struct mix *create_mix(struct port *p, uint32_t mix_id) +{ + struct mix *mix = NULL; + size_t len; + int res; + + if (mix_id == SPA_ID_INVALID) + mix_id = 0; + else + mix_id++; + + if (pw_map_lookup(&p->mix, mix_id) != NULL) { + errno = EEXIST; + return NULL; + } + + /* pad map size */ + for (len = pw_map_get_size(&p->mix); len < mix_id; ++len) + if ((res = pw_map_insert_at(&p->mix, len, NULL)) < 0) + goto fail; + + mix = calloc(1, sizeof(struct mix)); + if (mix == NULL) + return NULL; + if ((res = pw_map_insert_at(&p->mix, mix_id, mix)) < 0) + goto fail; + + mix->mix_id = mix_id; + mix->port = p; + mix->n_buffers = 0; + mix->impl_mix_id = SPA_ID_INVALID; + return mix; + +fail: + free(mix); + errno = -res; + return NULL; +} + +static void clear_data(struct impl *impl, struct spa_data *d) +{ + 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(impl->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 void clear_buffer(struct impl *impl, struct spa_buffer *b) +{ + uint32_t i; + for (i = 0; i < b->n_datas; i++) + clear_data(impl, &b->datas[i]); +} + +static int clear_buffers(struct impl *impl, struct mix *mix) +{ + uint32_t i; + for (i = 0; i < mix->n_buffers; i++) { + struct buffer *b = &mix->buffers[i]; + + spa_log_debug(impl->log, "%p: clear buffer %d", impl, i); + clear_buffer(impl, &b->buffer); + pw_memblock_unref(b->mem); + } + mix->n_buffers = 0; + return 0; +} + +static void free_mix(struct port *p, struct mix *mix) +{ + struct impl *impl = p->impl; + + if (mix == NULL) + return; + + if (mix->n_buffers) { + /* this shouldn't happen */ + spa_log_warn(impl->log, "%p: mix port-id:%u freeing leaked buffers", impl, mix->mix_id - 1u); + } + + clear_buffers(impl, mix); + + /* never realloc so it's safe to call from pw_map_foreach */ + if (mix->mix_id < pw_map_get_size(&p->mix)) + pw_map_insert_at(&p->mix, mix->mix_id, NULL); + + free(mix); +} + +static void mix_clear(struct impl *impl, struct mix *mix) +{ + struct port *port = mix->port; + + do_port_use_buffers(impl, port->direction, port->id, + mix->mix_id, 0, NULL, 0); + + free_mix(port, mix); +} + +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 = 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(impl != 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 >= impl->params.n_params) + break; + + param = impl->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", impl, seq, result.index); + spa_node_emit_result(&impl->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 impl *impl = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + if (impl->resource == NULL) + return param == NULL ? 0 : -EIO; + + return pw_client_node_resource_set_param(impl->resource, id, flags, param); +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *impl = object; + 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(impl->client_pool, tag, sizeof(tag)); + + if (data) { + mm = pw_mempool_import_map(impl->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 (impl->resource == NULL) + return data == NULL ? 0 : -EIO; + + return pw_client_node_resource_set_io(impl->resource, + id, + memid, + mem_offset, mem_size); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *impl = object; + uint32_t id; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + id = SPA_NODE_COMMAND_ID(command); + pw_log_debug("%p: send command %d (%s)", impl, id, + spa_debug_type_find_name(spa_type_node_command_id, id)); + + if (impl->resource == NULL) + return -EIO; + + return pw_client_node_resource_command(impl->resource, command); +} + + +static void emit_port_info(struct impl *impl, struct port *port) +{ + spa_node_emit_port_info(&impl->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 impl *impl = object; + struct spa_hook_list save; + union pw_map_item *item; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); + + pw_array_for_each(item, &impl->ports[SPA_DIRECTION_INPUT].items) { + if (item->data) + emit_port_info(impl, item->data); + } + pw_array_for_each(item, &impl->ports[SPA_DIRECTION_OUTPUT].items) { + if (item->data) + emit_port_info(impl, item->data); + } + 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 = 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 = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + pw_log_debug("%p: sync", impl); + + if (impl->resource == NULL) + return -EIO; + + return pw_resource_ping(impl->resource, seq); +} + +static void +do_update_port(struct impl *impl, + 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(impl->log, "%p: port %u update %d params", impl, 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(&impl->hooks, port->direction, port->id, info); + } + } +} + +static int mix_clear_cb(void *item, void *data) +{ + if (item) + mix_clear(data, item); + return 0; +} + +static void +clear_port(struct impl *impl, struct port *port) +{ + spa_log_debug(impl->log, "%p: clear port %p", impl, port); + + do_update_port(impl, port, + PW_CLIENT_NODE_PORT_UPDATE_PARAMS | + PW_CLIENT_NODE_PORT_UPDATE_INFO, 0, NULL, NULL); + + pw_map_for_each(&port->mix, mix_clear_cb, impl); + pw_map_clear(&port->mix); + pw_map_init(&port->mix, 0, 2); + + pw_map_insert_at(&impl->ports[port->direction], port->id, NULL); + + if (!port->removed) + spa_node_emit_port_info(&impl->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 impl *impl = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_FREE_PORT(impl, direction, port_id), -EINVAL); + + if (impl->resource == NULL) + return -EIO; + + return pw_client_node_resource_add_port(impl->resource, direction, port_id, props); +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *impl = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); + + if (impl->resource == NULL) + return -EIO; + + return pw_client_node_resource_remove_port(impl->resource, direction, port_id); +} + +static int +node_port_enum_params(struct impl *impl, 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 spa_hook_list *hooks) +{ + 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(impl != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + port = GET_PORT(impl, 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", + impl, 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", impl, seq, result.index); + spa_node_emit_result(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_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 = object; + return node_port_enum_params(impl, seq, direction, port_id, id, + start, num, filter, &impl->hooks); +} + +static int clear_buffers_cb(void *item, void *data) +{ + if (item) + clear_buffers(data, item); + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *impl = object; + struct port *port; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + port = GET_PORT(impl, direction, port_id); + if(port == NULL) + return param == NULL ? 0 : -EINVAL; + + pw_log_debug("%p: port %d.%d set param %s %d", impl, + direction, port_id, + spa_debug_type_find_name(spa_type_param, id), id); + + if (id == SPA_PARAM_Format) + pw_map_for_each(&port->mix, clear_buffers_cb, impl); + + if (impl->resource == NULL) + return param == NULL ? 0 : -EIO; + + return pw_client_node_resource_port_set_param(impl->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) +{ + 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", impl, + direction == SPA_DIRECTION_INPUT ? "input" : "output", + port_id, mix_id, data, size); + + old = pw_mempool_find_tag(impl->client_pool, tag, sizeof(tag)); + + port = GET_PORT(impl, direction, port_id); + if (port == NULL) { + pw_memmap_free(old); + return data == NULL ? 0 : -EINVAL; + } + + if ((mix = find_mix(port, mix_id)) == NULL) { + pw_memmap_free(old); + return -EINVAL; + } + + if (data) { + mm = pw_mempool_import_map(impl->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 (impl->resource == NULL) + return data == NULL ? 0 : -EIO; + + return pw_client_node_resource_port_set_io(impl->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 port *p; + struct mix *mix; + uint32_t i, j; + struct pw_client_node_buffer *mb; + + p = GET_PORT(impl, direction, port_id); + if (p == NULL) + return n_buffers == 0 ? 0 : -EINVAL; + + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + spa_log_debug(impl->log, "%p: %s port %d.%d use buffers %p %u flags:%08x", impl, + direction == SPA_DIRECTION_INPUT ? "input" : "output", + port_id, mix_id, buffers, n_buffers, flags); + + if (direction == SPA_DIRECTION_OUTPUT) + mix_id = SPA_ID_INVALID; + + if ((mix = find_mix(p, mix_id)) == NULL) + return -EINVAL; + + clear_buffers(impl, mix); + + if (n_buffers > 0) { + mb = alloca(n_buffers * sizeof(struct pw_client_node_buffer)); + } else { + mb = NULL; + } + + if (impl->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(impl->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(impl->log, "%p: buffer %d %d %d %d", impl, 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: + case SPA_DATA_SyncObj: + { + uint32_t flags = PW_MEMBLOCK_FLAG_DONT_CLOSE; + + if (!(d->flags & SPA_DATA_FLAG_MAPPABLE)) + flags |= PW_MEMBLOCK_FLAG_UNMAPPABLE; + 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(impl->log, "mem %d type:%d fd:%d", j, d->type, (int)d->fd); + m = pw_mempool_import(impl->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(impl->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(impl->log, "invalid memory type %d", d->type); + break; + } + } + } + mix->n_buffers = n_buffers; + + return pw_client_node_resource_port_use_buffers(impl->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 impl *impl = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + 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 impl *impl = object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + spa_log_trace_fp(impl->log, "reuse buffer %d", buffer_id); + + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *impl = object; + struct pw_impl_node *n = impl->this.node; + struct timespec ts; + + /* this should not be called, we call the exported node + * directly */ + spa_log_warn(impl->log, "exported node activation"); + spa_system_clock_gettime(impl->data_system, CLOCK_MONOTONIC, &ts); + n->rt.target.activation->status = PW_NODE_ACTIVATION_TRIGGERED; + n->rt.target.activation->signal_time = SPA_TIMESPEC_TO_NSEC(&ts); + + if (SPA_UNLIKELY(spa_system_eventfd_write(n->rt.target.system, n->rt.target.fd, 1) < 0)) + pw_log_warn("%p: write failed %m", impl); + + 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; + uint32_t new_id = user_data_size; + + pw_log_debug("%p: bind %u/%u", impl, new_id, version); + + impl->bind_node_version = version; + impl->bind_node_id = new_id; + pw_map_insert_at(&impl->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; + + if (change_mask & PW_CLIENT_NODE_UPDATE_PARAMS) { + pw_log_debug("%p: update %d params", impl, n_params); + update_params(&impl->params, n_params, params); + } + if (change_mask & PW_CLIENT_NODE_UPDATE_INFO) { + spa_node_emit_info(&impl->hooks, info); + } + pw_log_debug("%p: got node update", impl); + 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 port *port; + bool remove; + + spa_log_debug(impl->log, "%p: got port update change:%08x params:%d", + impl, change_mask, n_params); + + remove = (change_mask == 0); + + port = GET_PORT(impl, direction, port_id); + + if (remove) { + if (port == NULL) + return 0; + port->destroyed = true; + clear_port(impl, port); + } else { + struct port *target; + + if (port == NULL) { + if (!CHECK_FREE_PORT(impl, direction, port_id)) + return -EINVAL; + + target = &impl->dummy; + spa_zero(impl->dummy); + target->direction = direction; + target->id = port_id; + } else + target = port; + + do_update_port(impl, + target, + change_mask, + n_params, params, + info); + } + return 0; +} + +static int client_node_set_active(void *data, bool active) +{ + struct impl *impl = data; + spa_log_debug(impl->log, "%p: active:%d", impl, 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; + spa_node_emit_event(&impl->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 port *p; + struct mix *mix; + uint32_t i, j; + + spa_log_debug(impl->log, "%p: %s port %d.%d buffers %p %u", impl, + direction == SPA_DIRECTION_INPUT ? "input" : "output", + port_id, mix_id, buffers, n_buffers); + + p = GET_PORT(impl, 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) + goto invalid; + + if (mix->n_buffers != n_buffers) + goto invalid; + + for (i = 0; i < n_buffers; i++) { + if (mix->buffers[i].outbuf->n_datas != buffers[i]->n_datas) + goto invalid; + } + + for (i = 0; i < n_buffers; i++) { + struct spa_buffer *oldbuf, *newbuf; + struct buffer *b = &mix->buffers[i]; + + oldbuf = b->outbuf; + newbuf = buffers[i]; + + spa_log_debug(impl->log, "buffer %d n_datas:%d", i, newbuf->n_datas); + + for (j = 0; j < b->buffer.n_datas; j++) { + struct spa_chunk *oldchunk = oldbuf->datas[j].chunk; + struct spa_data *d = &newbuf->datas[j]; + uint32_t flags = d->flags; + + if (d->type == SPA_DATA_MemFd && + !SPA_FLAG_IS_SET(flags, SPA_DATA_FLAG_MAPPABLE)) { + spa_log_debug(impl->log, "buffer:%d data:%d has non mappable MemFd, " + "fixing to ensure backwards compatibility.", + i, j); + flags |= SPA_DATA_FLAG_MAPPABLE; + } + + /* overwrite everything except the chunk */ + oldbuf->datas[j] = *d; + oldbuf->datas[j].flags = flags; + oldbuf->datas[j].chunk = oldchunk; + + b->datas[j].type = d->type; + b->datas[j].flags = flags; + b->datas[j].fd = d->fd; + + spa_log_debug(impl->log, " data %d type:%d fl:%08x fd:%d, offs:%d max:%d", + j, d->type, flags, (int) d->fd, d->mapoffset, + d->maxsize); + } + } + return 0; +invalid: + for (i = 0; i < n_buffers; i++) + clear_buffer(impl, buffers[i]); + return -EINVAL; +} + +static const struct pw_client_node_methods client_node_methods = { + 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 impl *impl = source->data; + + if (SPA_UNLIKELY(source->rmask & (SPA_IO_ERR | SPA_IO_HUP))) { + spa_log_warn(impl->log, "%p: got error", impl); + return; + } + if (SPA_LIKELY(source->rmask & SPA_IO_IN)) { + uint64_t cmd; + struct pw_impl_node *node = impl->this.node; + + if (SPA_UNLIKELY(spa_system_eventfd_read(impl->data_system, + impl->data_source.fd, &cmd) < 0)) + pw_log_warn("%p: read failed %m", impl); + else if (SPA_UNLIKELY(cmd > 1)) + pw_log_info("(%s-%u) client missed %"PRIu64" wakeups", + node->name, node->info.id, cmd - 1); + + if (impl->resource && impl->resource->version < 5) { + struct pw_node_activation *a = node->rt.target.activation; + int status = a->state[0].status; + spa_log_trace_fp(impl->log, "%p: got ready %d", impl, status); + spa_node_call_ready(&impl->callbacks, status); + } else { + spa_log_trace_fp(impl->log, "%p: got complete", impl); + pw_impl_node_rt_emit_complete(node); + } + } +} + +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_init(struct impl *impl, + struct spa_dict *info) +{ + impl->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, impl); + spa_hook_list_init(&impl->hooks); + + impl->data_source.func = node_on_data_fd_events; + impl->data_source.data = impl; + impl->data_source.fd = -1; + impl->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP; + impl->data_source.rmask = 0; + + return 0; +} + +static int impl_clear(struct impl *impl) +{ + update_params(&impl->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; + + pw_log_debug("%p: destroy", impl); + + impl->resource = this->resource = NULL; + spa_hook_remove(&impl->resource_listener); + spa_hook_remove(&impl->object_listener); + + if (impl->data_source.fd != -1) { + spa_loop_invoke(impl->data_loop, + do_remove_source, + SPA_ID_INVALID, + NULL, + 0, + true, + &impl->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 spa_result_node_error result; + + pw_log_error("%p: error seq:%d %d (%s)", impl, seq, res, message); + result.message = message; + spa_node_emit_result(&impl->hooks, seq, res, SPA_RESULT_TYPE_NODE_ERROR, &result); +} + +static void client_node_resource_pong(void *data, int seq) +{ + struct impl *impl = data; + pw_log_debug("%p: got pong, emit result %d", impl, seq); + spa_node_emit_result(&impl->hooks, seq, 0, 0, NULL); +} + +static void node_peer_added(void *data, struct pw_impl_node *peer) +{ + struct impl *impl = data; + struct pw_memblock *m; + + m = pw_mempool_import_block(impl->client_pool, peer->activation); + if (m == NULL) { + pw_log_warn("%p: can't ensure mem: %m", impl); + return; + } + + pw_log_debug("%p: peer %p/%p id:%u added mem_id:%u %p %d", impl, peer, + impl->this.node, peer->info.id, m->id, m, m->ref); + + if (impl->resource == NULL) + return; + + pw_client_node_resource_set_activation(impl->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 pw_memblock *m; + + m = pw_mempool_find_fd(impl->client_pool, peer->activation->fd); + if (m == NULL) { + pw_log_warn("%p: unknown peer %p fd:%d", impl, peer, + peer->source.fd); + return; + } + + pw_log_debug("%p: peer %p/%p id:%u removed mem_id:%u", impl, peer, + impl->this.node, peer->info.id, m->id); + + if (impl->resource != NULL) { + pw_client_node_resource_set_activation(impl->resource, + peer->info.id, + -1, + SPA_ID_INVALID, + 0, + 0); + } + pw_memblock_unref(m); +} + +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->client; + uint32_t node_id = global->id; + + pw_log_debug("%p: %d", &impl->node, node_id); + + impl->activation = pw_mempool_import_block(impl->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 (impl->resource == NULL) + return; + + pw_resource_set_bound_id(impl->resource, node_id); + + pw_client_node_resource_transport(impl->resource, + this->node->source.fd, + impl->data_source.fd, + 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 int add_area(struct impl *impl) +{ + size_t size; + struct pw_memblock *area; + + size = AREA_SLOT * AREA_SIZE; + + area = pw_mempool_alloc(impl->context_pool, + PW_MEMBLOCK_FLAG_READWRITE | + PW_MEMBLOCK_FLAG_SEAL | + PW_MEMBLOCK_FLAG_MAP, + SPA_DATA_MemFd, size); + if (area == NULL) + return -errno; + + pw_log_debug("%p: io area %u %p", impl, + (unsigned)pw_array_get_len(&impl->io_areas, struct pw_memblock*), + area->map->ptr); + + pw_array_add_ptr(&impl->io_areas, area); + return 0; +} + +static void node_initialized(void *data) +{ + struct impl *impl = data; + struct pw_impl_client_node *this = &impl->this; + struct pw_global *global; + struct spa_system *data_system = impl->data_system; + + impl->data_source.fd = spa_system_eventfd_create(data_system, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + spa_loop_add_source(impl->data_loop, &impl->data_source); + pw_log_debug("%p: transport read-fd:%d write-fd:%d", impl, + impl->data_source.fd, this->node->source.fd); + + if (add_area(impl) < 0) + return; + + 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 spa_system *data_system = impl->data_system; + uint32_t tag[5] = { impl->node_id, }; + struct pw_memmap *mm; + struct pw_memblock **area; + + this->node = NULL; + + pw_log_debug("%p: free", impl); + impl_clear(impl); + + spa_hook_remove(&impl->node_listener); + + while ((mm = pw_mempool_find_tag(impl->client_pool, tag, sizeof(uint32_t))) != NULL) + pw_memmap_free(mm); + + if (impl->activation) + pw_memblock_free(impl->activation); + + pw_array_for_each(area, &impl->io_areas) { + if (*area) + pw_memblock_unref(*area); + } + pw_array_clear(&impl->io_areas); + + if (impl->resource) + pw_resource_destroy(impl->resource); + + pw_map_clear(&impl->ports[0]); + pw_map_clear(&impl->ports[1]); + pw_map_clear(&impl->io_map); + + if (impl->data_source.fd != -1) + spa_system_close(data_system, impl->data_source.fd); + 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; + uint32_t idx, pos, len; + struct pw_memblock *area; + struct spa_io_async_buffers *ab; + + if ((m = create_mix(port, mix->port.port_id)) == NULL) + return -ENOMEM; + + mix->id = pw_map_insert_new(&impl->io_map, NULL); + if (mix->id == SPA_ID_INVALID) { + free_mix(port, m); + return -errno; + } + + idx = mix->id / AREA_SIZE; + pos = mix->id % AREA_SIZE; + + len = pw_array_get_len(&impl->io_areas, struct pw_memblock *); + if (idx > len) + goto no_mem; + if (idx == len) { + pw_log_debug("%p: extend area idx:%u pos:%u", impl, idx, pos); + if (add_area(impl) < 0) + goto no_mem; + } + area = *pw_array_get_unchecked(&impl->io_areas, idx, struct pw_memblock*); + + ab = SPA_PTROFF(area->map->ptr, pos * AREA_SLOT, void); + mix->io_data = ab; + mix->io[0] = &ab->buffers[0]; + mix->io[1] = &ab->buffers[1]; + *mix->io[0] = SPA_IO_BUFFERS_INIT; + *mix->io[1] = SPA_IO_BUFFERS_INIT; + + m->peer_id = mix->peer_id; + m->impl_mix_id = mix->id; + + if (impl->resource && impl->resource->version >= 4) + pw_client_node_resource_port_set_mix_info(impl->resource, + mix->port.direction, mix->p->port_id, + mix->port.port_id, mix->peer_id, NULL); + + pw_log_debug("%p: init mix id:%d io:%p/%p base:%p", impl, + mix->id, mix->io[0], mix->io[1], area->map->ptr); + + return 0; +no_mem: + pw_map_remove(&impl->io_map, mix->id); + free_mix(port, m); + return -ENOMEM; +} + +static int port_release_mix(void *data, struct pw_impl_port_mix *mix) +{ + struct port *port = data; + struct impl *impl = port->impl; + struct mix *m; + + pw_log_debug("%p: remove mix id:%d io:%p", + impl, mix->id, mix->io); + + if (!pw_map_has_item(&impl->io_map, mix->id)) + return -EINVAL; + + if (impl->resource && impl->resource->version >= 4 && !port->destroyed) + pw_client_node_resource_port_set_mix_info(impl->resource, + mix->port.direction, mix->p->port_id, + mix->port.port_id, SPA_ID_INVALID, NULL); + + pw_map_remove(&impl->io_map, mix->id); + + m = find_mix(port, mix->port.port_id); + if (m && m->impl_mix_id == mix->id) + free_mix(port, m); + else + pw_log_debug("%p: already cleared mix id:%d port-id:%d", + impl, mix->id, mix->port.port_id); + + 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_add_listener(void *object, struct spa_hook *listener, + const struct spa_node_events *events, void *data) +{ + struct port *port = object; + spa_hook_list_append(&port->mix_hooks, listener, events, data); + return 0; +} + +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 node_port_enum_params(port->impl, seq, direction, port->id, + id, start, num, filter, &port->mix_hooks); +} + +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 pw_impl_port_mix *mix; + + mix = pw_map_lookup(&port->mix_port_map, mix_id); + if (mix == NULL) + return -EINVAL; + + switch (id) { + case SPA_IO_Buffers: + if (data && size >= sizeof(struct spa_io_buffers)) + mix->io[0] = mix->io[1] = data; + else + mix->io[0] = mix->io[1] = NULL; + break; + case SPA_IO_AsyncBuffers: + if (data && size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = data; + mix->io[0] = &ab->buffers[0]; + mix->io[1] = &ab->buffers[1]; + } + else + mix->io[0] = mix->io[1] = NULL; + break; + default: + break; + } + 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->impl, 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, + .add_listener = impl_mix_add_listener, + .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); + + pw_log_debug("%p: port %p init", impl, port); + + *p = impl->dummy; + p->port = port; + p->impl = impl; + p->direction = port->direction; + p->id = port->port_id; + p->impl = impl; + pw_map_init(&p->mix, 2, 2); + spa_hook_list_init(&p->mix_hooks); + p->mix_node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_port_mix, p); + create_mix(p, SPA_ID_INVALID); + + pw_map_insert_at(&impl->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 port *p = pw_impl_port_get_user_data(port); + + pw_log_debug("%p: port %p remove", impl, port); + + p->removed = true; + clear_port(impl, p); +} + +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, +}; + +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); + 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->context_pool = pw_context_get_mempool(context); + impl->data_source.fd = -1; + pw_log_debug("%p: new", &impl->node); + + impl_init(impl, NULL); + impl->log = pw_log_get(); + impl->resource = resource; + impl->client = client; + impl->client_pool = pw_impl_client_get_mempool(client); + this->flags = do_register ? 0 : 1; + + pw_map_init(&impl->ports[0], 64, 64); + pw_map_init(&impl->ports[1], 64, 64); + pw_map_init(&impl->io_map, 64, 64); + pw_array_init(&impl->io_areas, 64 * sizeof(struct pw_memblock*)); + + 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, + NULL, + properties, 0); + + if (this->node == NULL) + goto error_no_node; + + if (this->node->data_loop == NULL) { + errno = EIO; + goto error_no_node; + } + + impl->data_loop = this->node->data_loop->loop; + impl->data_system = this->node->data_loop->system; + + this->node->remote = true; + this->flags = 0; + if (resource->version < PW_VERSION_CLIENT_NODE) { + pw_log_warn("detected old client version %d", resource->version); + if (resource->version < 6) + this->node->rt.target.activation->client_version = 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; + impl_clear(impl); + 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..5683da5 --- /dev/null +++ b/src/modules/module-client-node/client-node.h @@ -0,0 +1,40 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_CLIENT_NODE_H +#define PIPEWIRE_CLIENT_NODE_H + +#include +#include + +#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..3051703 --- /dev/null +++ b/src/modules/module-client-node/protocol-native.c @@ -0,0 +1,1248 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include + +#include + +#include +#include + +#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(¶ms[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(¶m)) < 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; + + if (event == NULL) + 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; + + if (command == NULL) + 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(¶m)) < 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; + + if (event == NULL) + 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..8b5c7da --- /dev/null +++ b/src/modules/module-client-node/remote-node.c @@ -0,0 +1,1295 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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; + struct pw_impl_port_mix mix; + struct pw_array buffers; +}; + +struct node_data { + struct pw_context *context; + + struct pw_loop *data_loop; + struct spa_system *data_system; + + 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; + struct spa_hook node_rt_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; +}; + +/** \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->target.id == node_id) + return l; + } + return NULL; +} + +static void clear_link(struct node_data *data, struct link *link) +{ + pw_log_debug("link %p", link); + pw_impl_node_remove_target(data->node, &link->target); + pw_memmap_free(link->map); + spa_system_close(link->target.system, link->target.fd); + 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_impl_node *node = data->node; + 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(node->node, mm->tag[2], NULL, 0); + + pw_memmap_free(mm); + } + + pw_memmap_free(data->activation); + node->rt.target.activation = node->activation->map->ptr; + + spa_system_close(data->data_system, data->rtwritefd); + data->have_transport = false; +} + +static void mix_init(struct mix *mix, struct pw_impl_port *port, + uint32_t mix_id, uint32_t peer_id) +{ + pw_log_debug("port %p: mix init %d.%d", port, port->port_id, mix_id); + mix->port = port; + mix->mix.id = mix_id; + mix->mix.peer_id = peer_id; + if (mix_id != SPA_ID_INVALID) + pw_impl_port_init_mix(port, &mix->mix); + pw_array_init(&mix->buffers, 32); + pw_array_ensure_size(&mix->buffers, sizeof(struct buffer) * 64); +} + +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 *create_mix(struct node_data *data, struct pw_impl_port *port, + uint32_t mix_id, uint32_t peer_id) +{ + struct mix *mix; + + 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, peer_id); + spa_list_append(&data->mix[port->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_impl_node *node = data->node; + 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; + } + + node->rt.target.activation = data->activation->ptr; + + pw_impl_node_set_io(node, SPA_IO_Clock, + &node->rt.target.activation->position.clock, + sizeof(struct spa_io_clock)); + pw_impl_node_set_io(node, SPA_IO_Position, + &node->rt.target.activation->position, + sizeof(struct spa_io_position)); + + 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(node->rt.target.system, node->source.fd); + node->rt.target.fd = node->source.fd = readfd; + + node->rt.target.activation->client_version = PW_VERSION_NODE_ACTIVATION; + + data->have_transport = true; + + if (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, ¶m, &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;;) { + struct spa_node *qnode; + uint32_t qport; + + spa_pod_dynamic_builder_init(&b, buf, sizeof(buf), 4096); + + switch (id) { + case SPA_PARAM_IO: + qnode = port->mix; + qport = SPA_ID_INVALID; + break; + default: + qnode = port->node->node; + qport = port->port_id; + break; + } + res = spa_node_port_enum_params_sync(qnode, + port->direction, qport, + id, &idx, NULL, ¶m, &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_impl_node *node = data->node; + 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 = pw_impl_node_set_io(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) +{ + uint32_t id = SPA_NODE_EVENT_ID(event); + pw_log_warn("unhandled node event %d (%s)", id, + spa_debug_type_find_name(spa_type_node_event_id, id)); + return -ENOTSUP; +} + +static int client_node_command(void *_data, const struct spa_command *command) +{ + struct node_data *data = _data; + struct pw_impl_node *node = data->node; + struct pw_proxy *proxy = (struct pw_proxy*)data->client_node; + int res; + uint32_t id = SPA_NODE_COMMAND_ID(command); + + pw_log_debug("%p: got command %d (%s)", proxy, id, + spa_debug_type_find_name(spa_type_node_command_id, id)); + + switch (id) { + case SPA_NODE_COMMAND_Pause: + if ((res = pw_impl_node_set_state(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: + if ((res = pw_impl_node_set_state(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: + if ((res = pw_impl_node_set_state(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(node, command); + break; + default: + pw_log_warn("unhandled node command %d (%s)", id, + spa_debug_type_find_name(spa_type_node_command_id, id)); + res = -ENOTSUP; + pw_proxy_errorf(proxy, res, "command %d (%s) not supported", id, + spa_debug_type_find_name(spa_type_node_command_id, id)); + } + 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 = find_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 flags:%08x", + j, bm->id, bm->fd, d->maxsize, d->flags); + } 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 flags:%08x", + j, bid->id, d->data, offs, d->maxsize, d->flags); + } 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(%u:%u:%d): %d %s", mix, + direction, port_id, mix_id, res, spa_strerror(res)); + pw_proxy_errorf(proxy, res, "port_use_buffers(%u:%u:%d) error: %s", + direction, port_id, mix_id, 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 = find_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 ((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; + } +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 +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 (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; + } + if (data->remote_id == node_id) { + pw_log_debug("node %p: our activation %u: %u %p %u %u", node, node_id, + memid, ptr, offset, size); + } else { + pw_log_debug("node %p: set activation %u: %u %p %u %u", node, node_id, + memid, ptr, offset, size); + } + + if (ptr) { + link = calloc(1, sizeof(struct link)); + if (link == NULL) { + res = -errno; + goto error_exit; + } + link->data = data; + link->map = mm; + link->target.id = node_id; + link->target.activation = ptr; + link->target.system = data->data_system; + link->target.fd = signalfd; + link->target.trigger = link->target.activation->server_version < 1 ? + trigger_target_v0 : trigger_target_v1; + spa_list_append(&data->links, &link->link); + + pw_impl_node_add_target(node, &link->target); + + pw_log_debug("node %p: add link %p: memid:%u fd:%d id:%u state:%p pending:%d/%d", + node, link, memid, signalfd, node_id, + &link->target.activation->state[0], + link->target.activation->state[0].pending, + link->target.activation->state[0].required); + } else { + link = find_activation(&data->links, node_id); + if (link == NULL) { + res = -ENOENT; + goto error_exit; + } + pw_log_debug("node %p: remove link %p: id:%u state:%p pending:%d/%d", + node, link, node_id, + &link->target.activation->state[0], + link->target.activation->state[0].pending, + link->target.activation->state[0].required); + 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 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); + + if (mix->mix.id != SPA_ID_INVALID) + spa_node_port_set_io(mix->port->mix, mix->mix.port.direction, + mix->mix.port.port_id, SPA_IO_Buffers, NULL, 0); + + spa_list_remove(&mix->link); + + clear_buffers(data, mix); + pw_array_clear(&mix->buffers); + + spa_list_append(&data->free_mix, &mix->link); + if (mix->mix.id != SPA_ID_INVALID) + pw_impl_port_release_mix(mix->port, &mix->mix); +} + +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 node_data *data = _data; + struct mix *mix; + + pw_log_debug("%p: %d:%d:%d peer:%d", data, direction, port_id, mix_id, peer_id); + + mix = find_mix(data, direction, port_id, mix_id); + + if (peer_id == SPA_ID_INVALID) { + if (mix == NULL) + return -EINVAL; + clear_mix(data, mix); + } else { + struct pw_impl_port *port; + if (mix != NULL) + return -EEXIST; + port = pw_impl_node_find_port(data->node, direction, port_id); + if (port == NULL) + return -ENOENT; + mix = create_mix(data, port, mix_id, peer_id); + if (mix == NULL) + return -errno; + } + return 0; +} + +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, +}; + +static void do_node_init(struct node_data *data) +{ + struct pw_impl_node *node = data->node; + struct pw_impl_port *port; + struct mix *mix; + + pw_log_debug("%p: node %p init", 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, &node->input_ports, link) { + mix = create_mix(data, port, SPA_ID_INVALID, SPA_ID_INVALID); + if (mix == NULL) + pw_log_error("%p: failed to create port mix: %m", node); + add_port_update(data, port, + PW_CLIENT_NODE_PORT_UPDATE_PARAMS | + PW_CLIENT_NODE_PORT_UPDATE_INFO); + } + spa_list_for_each(port, &node->output_ports, link) { + mix = create_mix(data, port, SPA_ID_INVALID, SPA_ID_INVALID); + if (mix == NULL) + pw_log_error("%p: failed to create port mix: %m", node); + add_port_update(data, port, + PW_CLIENT_NODE_PORT_UPDATE_PARAMS | + PW_CLIENT_NODE_PORT_UPDATE_INFO); + } +} + +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_added(void *data, struct pw_impl_port *port) +{ + struct node_data *d = data; + struct mix *mix; + + pw_log_debug("added %p", d); + + if (d->client_node == NULL) + return; + + mix = create_mix(d, port, SPA_ID_INVALID, SPA_ID_INVALID); + if (mix == NULL) + pw_log_error("%p: failed to create port mix: %m", d->node); +} + +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_added = node_port_added, + .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; + struct pw_impl_node *node = data->node; + pw_log_debug("%p: removed", data); + + spa_hook_remove(&data->proxy_client_node_listener); + spa_hook_remove(&data->client_node_listener); + + if (node) { + spa_hook_remove(&data->node_listener); + pw_impl_node_remove_rt_listener(node, + &data->node_rt_listener); + pw_impl_node_set_state(node, PW_NODE_STATE_SUSPENDED); + + clean_node(data); + + if (data->do_free) + pw_impl_node_destroy(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_props(void *_data, uint32_t global_id, const struct spa_dict *props) +{ + struct node_data *data = _data; + pw_log_debug("%p: bound %u", data, global_id); + data->remote_id = global_id; + if (props) + pw_properties_update(data->node->properties, props); +} + +static const struct pw_proxy_events proxy_client_node_events = { + PW_VERSION_PROXY_EVENTS, + .removed = client_node_removed, + .destroy = client_node_destroy, + .bound_props = client_node_bound_props, +}; + +static void node_rt_complete(void *data) +{ + struct node_data *d = data; + struct pw_impl_node *node = d->node; + struct spa_system *data_system = d->data_system; + + if (!node->driving || + !SPA_FLAG_IS_SET(node->rt.target.activation->flags, PW_NODE_ACTIVATION_FLAG_PROFILER)) + return; + + if (SPA_UNLIKELY(spa_system_eventfd_write(data_system, d->rtwritefd, 1) < 0)) + pw_log_warn("node %p: write failed %m", node); +} + +static const struct pw_impl_node_rt_events node_rt_events = { + PW_VERSION_IMPL_NODE_RT_EVENTS, + .complete = node_rt_complete, +}; + +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; + + if (node->data_loop == NULL) + goto error; + + pw_log_debug("%p: export node %p", core, object); + + 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->data_loop = node->data_loop; + data->data_system = data->data_loop->system; + data->client_node = (struct pw_client_node *)client_node; + data->remote_id = SPA_ID_INVALID; + + /* the node might have been registered and added to a driver. When we export, + * we will be assigned a new driver target from the server and we can forget our + * local ones. */ + pw_node_peer_unref(spa_steal_ptr(node->from_driver_peer)); + pw_node_peer_unref(spa_steal_ptr(node->to_driver_peer)); + + 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); + + pw_impl_node_add_listener(node, &data->node_listener, &node_events, data); + pw_impl_node_add_rt_listener(node, &data->node_rt_listener, + &node_rt_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..771e166 --- /dev/null +++ b/src/modules/module-client-node/v0/client-node.c @@ -0,0 +1,1429 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2015 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define PW_ENABLE_DEPRECATED + +#include "pipewire/pipewire.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 pw_mempool *context_pool; + + 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 = pw_resource_get_client(this->resource); + 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(pw_resource_get_client(this->resource), 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(pw_impl_node_get_implementation(n)); +} + +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", pw_global_get_id(pw_impl_client_get_global(client))); + + impl->context = context; + impl->context_pool = pw_context_get_mempool(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..5c27406 --- /dev/null +++ b/src/modules/module-client-node/v0/client-node.h @@ -0,0 +1,91 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2015 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_CLIENT_NODE0_H +#define PIPEWIRE_CLIENT_NODE0_H + +#include + +#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; +}; + +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, +}; + +#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..bd2f69b --- /dev/null +++ b/src/modules/module-client-node/v0/ext-client-node.h @@ -0,0 +1,394 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __PIPEWIRE_EXT_CLIENT_NODE0_H__ +#define __PIPEWIRE_EXT_CLIENT_NODE0_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include + +#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..a577c6f --- /dev/null +++ b/src/modules/module-client-node/v0/protocol-native.c @@ -0,0 +1,514 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2017 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include +#include + +#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", ¶ms[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", ¶ms[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..d62f23c --- /dev/null +++ b/src/modules/module-client-node/v0/transport.c @@ -0,0 +1,241 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include + +#include +#include + +#include + +#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(pw_context_get_mempool(context), + PW_MEMBLOCK_FLAG_READWRITE | + PW_MEMBLOCK_FLAG_SEAL | + PW_MEMBLOCK_FLAG_MAP, + 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..46072a2 --- /dev/null +++ b/src/modules/module-client-node/v0/transport.h @@ -0,0 +1,39 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ +#define __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +#include + +/** 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..4afcb8f --- /dev/null +++ b/src/modules/module-combine-stream.c @@ -0,0 +1,1655 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \page page_module_combine_stream 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 + * + * The sources and sink that need to be combined can be selected using generic match + * rules. This makes it possible to combine static nodes or nodes based on certain + * properties. + * + * ## Module Name + * + * `libpipewire-module-combine-stream` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `node.description`: a human readable name for the stream + * - `combine.mode` = capture | playback | sink | source, default sink + * - `combine.latency-compensate`: use delay buffers to match stream latencies + * - `combine.on-demand-streams`: use metadata to create streams on demand + * - `combine.props = {}`: properties to be passed to the sink/source + * - `stream.props = {}`: properties to be passed to the streams + * - `stream.rules = {}`: rules for matching streams, use create-stream actions + * + * ## 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} + * # ~/.config/pipewire/pipewire.conf.d/my-combine-stream-1.conf + * + * context.modules = [ + * { name = libpipewire-module-combine-stream + * args = { + * combine.mode = sink + * node.name = "combine_sink" + * node.description = "My Combine Sink" + * combine.latency-compensate = false + * 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 emitted. + * { + * # all keys must match the value. ! negates. ~ 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} + * # ~/.config/pipewire/pipewire.conf.d/my-combine-stream-2.conf + * + * 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.latency-compensate = false + * 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} + * # ~/.config/pipewire/pipewire.conf.d/my-combine-stream-3.conf + * + * 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= ) " \ + "( combine.mode=, default:sink ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( combine.props= ) " \ + "( stream.props= ) " \ + "( stream.rules= ) " + +#define DELAYBUF_MAX_SIZE (20 * sizeof(float) * 96000) + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { 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_loop *main_loop; + struct pw_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_metadata *metadata; + struct spa_hook metadata_listener; + uint32_t metadata_id; + + struct spa_source *update_delay_event; + + 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_latency_info latency; + + int64_t latency_offset; + + struct spa_audio_info_raw info; + + unsigned int do_disconnect:1; + unsigned int latency_compensate:1; + unsigned int on_demand_streams:1; + + struct spa_list streams; + uint32_t n_streams; +}; + +struct ringbuffer { + void *buf; + uint32_t idx; + uint32_t size; +}; + +struct stream { + uint32_t id; + char *on_demand_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_latency_info latency; + + struct spa_audio_info_raw info; + uint32_t remap[SPA_AUDIO_MAX_CHANNELS]; + + void *delaybuf; + struct ringbuffer delay[SPA_AUDIO_MAX_CHANNELS]; + + int64_t delay_samples; /* for main loop */ + int64_t data_delay_samples; /* for data loop */ + + unsigned int ready:1; + unsigned int added:1; + unsigned int have_latency:1; +}; + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static void ringbuffer_init(struct ringbuffer *r, void *buf, uint32_t size) +{ + r->buf = buf; + r->idx = 0; + r->size = size; +} + +static void ringbuffer_memcpy(struct ringbuffer *r, void *dst, void *src, uint32_t size) +{ + uint32_t avail; + + avail = SPA_MIN(size, r->size); + + /* buf to dst */ + if (dst && avail > 0) { + spa_ringbuffer_read_data(NULL, r->buf, r->size, r->idx, dst, avail); + dst = SPA_PTROFF(dst, avail, void); + } + + /* src to dst */ + if (size > avail) { + if (dst) + memcpy(dst, src, size - avail); + src = SPA_PTROFF(src, size - avail, void); + } + + /* src to buf */ + if (avail > 0) { + spa_ringbuffer_write_data(NULL, r->buf, r->size, r->idx, src, avail); + r->idx = (r->idx + avail) % r->size; + } +} + +static void mix_f32(float *dst, float *src, uint32_t size) +{ + uint32_t i, s = size / sizeof(float); + for (i = 0; i < s; i++) + dst[i] += src[i]; +} + +static void ringbuffer_mix(struct ringbuffer *r, void *dst, void *src, uint32_t size) +{ + uint32_t avail; + + avail = SPA_MIN(size, r->size); + + /* buf to dst */ + if (dst && avail > 0) { + uint32_t l0 = SPA_MIN(avail, r->size - r->idx), l1 = avail - l0; + mix_f32(dst, SPA_PTROFF(r->buf, r->idx, void), l0); + if (SPA_UNLIKELY(l1 > 0)) + mix_f32(SPA_PTROFF(dst, l0, void), r->buf, l1); + dst = SPA_PTROFF(dst, avail, void); + } + /* src to dst */ + if (size > avail) { + if (dst) + mix_f32(dst, src, size - avail); + src = SPA_PTROFF(src, size - avail, void); + } + /* src to buf */ + if (avail > 0) { + spa_ringbuffer_write_data(NULL, r->buf, r->size, r->idx, src, avail); + r->idx = (r->idx + avail) % r->size; + } +} + +static void ringbuffer_copy(struct ringbuffer *dst, struct ringbuffer *src) +{ + uint32_t l0, l1; + + if (dst->size == 0 || src->size == 0) + return; + + l0 = src->size - src->idx; + l1 = src->idx; + + ringbuffer_memcpy(dst, NULL, SPA_PTROFF(src->buf, src->idx, void), l0); + ringbuffer_memcpy(dst, NULL, src->buf, l1); +} + +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 struct stream *find_on_demand_stream(struct impl *impl, const char *on_demand_id) +{ + struct stream *s; + spa_list_for_each(s, &impl->streams, link) + if (spa_streq(s->on_demand_id, on_demand_id)) + return s; + return NULL; +} + +static enum pw_direction get_combine_direction(struct impl *impl) +{ + if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) + return PW_DIRECTION_INPUT; + else + return PW_DIRECTION_OUTPUT; +} + +static void apply_latency_offset(struct spa_latency_info *latency, int64_t offset) +{ + latency->min_ns += SPA_MAX(offset, -(int64_t)latency->min_ns); + latency->max_ns += SPA_MAX(offset, -(int64_t)latency->max_ns); +} + +static int64_t get_stream_delay(struct stream *s) +{ + struct pw_time t; + + if (pw_stream_get_time_n(s->stream, &t, sizeof(t)) < 0) + return INT64_MIN; + + return t.delay; /* samples at graph rate */ +} + +static void update_latency(struct impl *impl) +{ + struct spa_latency_info latency; + struct stream *s; + + if (impl->combine == NULL) + return; + + if (!impl->latency_compensate) { + spa_latency_info_combine_start(&latency, get_combine_direction(impl)); + + spa_list_for_each(s, &impl->streams, link) + if (s->have_latency) + spa_latency_info_combine(&latency, &s->latency); + + spa_latency_info_combine_finish(&latency); + } else { + int64_t max_delay = INT64_MIN; + + latency = SPA_LATENCY_INFO(get_combine_direction(impl)); + + spa_list_for_each(s, &impl->streams, link) { + int64_t delay = get_stream_delay(s); + + if (delay > max_delay && s->have_latency) { + latency = s->latency; + max_delay = delay; + } + } + } + + apply_latency_offset(&latency, impl->latency_offset); + + if (spa_latency_info_compare(&latency, &impl->latency) != 0) { + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + const struct spa_pod *param; + + impl->latency = latency; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + pw_stream_update_params(impl->combine, ¶m, 1); + } +} + +struct replace_delay_info { + struct stream *stream; + void *buf; + struct ringbuffer delay[SPA_AUDIO_MAX_CHANNELS]; +}; + +static int do_replace_delay(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct replace_delay_info *info = user_data; + unsigned int i; + + for (i = 0; i < SPA_N_ELEMENTS(info->stream->delay); ++i) { + ringbuffer_copy(&info->delay[i], &info->stream->delay[i]); + info->stream->delay[i] = info->delay[i]; + } + + SPA_SWAP(info->stream->delaybuf, info->buf); + return 0; +} + +static void resize_delay(struct stream *stream, uint32_t size) +{ + struct replace_delay_info info; + uint32_t channels = stream->info.channels; + unsigned int i; + + size = SPA_MIN(size, DELAYBUF_MAX_SIZE); + + for (i = 0; i < channels; ++i) + if (stream->delay[i].size != size) + break; + if (i == channels) + return; + + pw_log_info("stream %d latency compensation samples:%u", stream->id, + (unsigned int)(size / sizeof(float))); + + spa_zero(info); + info.stream = stream; + if (size > 0) + info.buf = calloc(channels, size); + if (!info.buf) + size = 0; + + for (i = 0; i < channels; ++i) + ringbuffer_init(&info.delay[i], SPA_PTROFF(info.buf, i*size, void), size); + + pw_loop_invoke(stream->impl->data_loop, do_replace_delay, 0, NULL, 0, true, &info); + + free(info.buf); +} + +static void update_delay(struct impl *impl) +{ + struct stream *s; + int64_t max_delay = INT64_MIN; + + if (!impl->latency_compensate) + return; + + spa_list_for_each(s, &impl->streams, link) { + int64_t delay = get_stream_delay(s); + + if (delay != s->delay_samples && delay != INT64_MIN) + pw_log_debug("stream %d delay:%"PRIi64" samples", s->id, delay); + + max_delay = SPA_MAX(max_delay, delay); + s->delay_samples = delay; + } + + spa_list_for_each(s, &impl->streams, link) { + uint32_t size = 0; + + if (s->delay_samples != INT64_MIN) { + int64_t delay = max_delay - s->delay_samples; + size = delay * sizeof(float); + } + + resize_delay(s, size); + } + + update_latency(impl); +} + +static void update_delay_event(void *data, uint64_t count) +{ + struct impl *impl = data; + + /* in main loop */ + update_delay(impl); +} + +static int do_clear_delaybuf(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct impl *impl = user_data; + struct stream *s; + unsigned int i; + + spa_list_for_each(s, &impl->streams, link) { + for (i = 0; i < SPA_N_ELEMENTS(s->delay); ++i) + if (s->delay[i].size) + memset(s->delay[i].buf, 0, s->delay[i].size); + } + + return 0; +} + +static void clear_delaybuf(struct impl *impl) +{ + pw_loop_invoke(impl->data_loop, do_clear_delaybuf, 0, NULL, 0, true, impl); +} + +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 void param_tag_changed(struct impl *impl, const struct spa_pod *param) +{ + if (param == NULL) + return; + + pw_log_debug("tag update"); + struct stream *s; + struct spa_tag_info tag; + const struct spa_pod *params[1] = { param }; + void *state = NULL; + + if (spa_tag_parse(param, &tag, &state) < 0) + return; + spa_list_for_each(s, &impl->streams, link) { + if (s->stream == NULL) + continue; + pw_log_debug("updating stream %d", s->id); + pw_stream_update_params(s->stream, params, 1); + } +} + +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 remove_stream(struct stream *s, bool destroy) +{ + pw_log_debug("destroy stream %d", s->id); + + pw_loop_invoke(s->impl->data_loop, do_remove_stream, 0, NULL, 0, true, s); + + if (destroy && s->stream) { + spa_hook_remove(&s->stream_listener); + pw_stream_destroy(s->stream); + } + + free(s->on_demand_id); + free(s->delaybuf); + free(s); +} + +static void destroy_stream(struct stream *s) +{ + remove_stream(s, true); +} + +static void destroy_all_on_demand_streams(struct impl *impl) +{ + struct stream *s, *tmp; + spa_list_for_each_safe(s, tmp, &impl->streams, link) + if (s->on_demand_id) + destroy_stream(s); +} + +static void stream_destroy(void *d) +{ + struct stream *s = d; + spa_hook_remove(&s->stream_listener); + remove_stream(s, false); +} + +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 void stream_param_changed(void *d, uint32_t id, const struct spa_pod *param) +{ + struct stream *s = d; + struct spa_latency_info latency; + + switch (id) { + case SPA_PARAM_Format: + update_delay(s->impl); + break; + case SPA_PARAM_Latency: + if (param == NULL) { + s->have_latency = false; + } else if (spa_latency_parse(param, &latency) == 0 && + latency.direction == get_combine_direction(s->impl)) { + s->have_latency = true; + s->latency = latency; + } + update_latency(s->impl); + update_delay(s->impl); + break; + default: + break; + } +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .param_changed = stream_param_changed, +}; + +struct stream_info { + struct impl *impl; + uint32_t id; + const char *on_demand_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, *dir_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; + + if (info->on_demand_id) { + node_name = info->on_demand_id; + pw_log_info("create on demand stream: %s", node_name); + } else { + node_name = spa_dict_lookup(info->props, PW_KEY_NODE_NAME); + if (node_name == NULL) + node_name = spa_dict_lookup(info->props, PW_KEY_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->stream_events = stream_events; + + flags = PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS | + PW_STREAM_FLAG_ASYNC; + + if (impl->mode == MODE_SINK || impl->mode == MODE_CAPTURE) { + direction = PW_DIRECTION_OUTPUT; + flags |= PW_STREAM_FLAG_TRIGGER; + dir_name = "output"; + } else { + direction = PW_DIRECTION_INPUT; + s->stream_events.process = stream_input_process; + dir_name = "input"; + } + + s->info = impl->info; + if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL) + spa_audio_parse_position(str, strlen(str), s->info.position, &s->info.channels); + if (s->info.channels == 0) + s->info = impl->info; + + spa_zero(remap_info); + if ((str = pw_properties_get(info->stream_props, "combine.audio.position")) != NULL) + spa_audio_parse_position(str, strlen(str), remap_info.position, &remap_info.channels); + 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 %s", str, dir_name); + if (pw_properties_get(info->stream_props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_setf(info->stream_props, PW_KEY_NODE_DESCRIPTION, + "%s %s", str, dir_name); + + 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, + "%s.%s_%s", dir_name, str, node_name); + + if (info->on_demand_id) { + s->on_demand_id = strdup(info->on_demand_id); + pw_properties_set(info->stream_props, "combine.on-demand-id", s->on_demand_id); + } else { + 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; + + 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_loop_invoke(impl->data_loop, do_add_stream, 0, NULL, 0, true, s); + update_delay(impl); + 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 int metadata_property(void *data, uint32_t id, + const char *key, const char *type, const char *value) +{ + struct impl *impl = data; + const char *on_demand_id; + struct stream *s; + + if (id != impl->combine_id) + return 0; + + if (!key) { + destroy_all_on_demand_streams(impl); + goto out; + } + + if (!spa_strstartswith(key, "combine.on-demand-stream.")) + return 0; + + on_demand_id = key + strlen("combine.on-demand-stream."); + if (*on_demand_id == '\0') + return 0; + + if (value) { + struct stream_info info; + + s = find_on_demand_stream(impl, on_demand_id); + if (s) + destroy_stream(s); + + spa_zero(info); + info.impl = impl; + info.id = SPA_ID_INVALID; + info.on_demand_id = on_demand_id; + info.stream_props = pw_properties_copy(impl->stream_props); + + pw_properties_update_string(info.stream_props, value, strlen(value)); + + create_stream(&info); + + pw_properties_free(info.stream_props); + } else { + s = find_on_demand_stream(impl, on_demand_id); + if (s) + destroy_stream(s); + } + +out: + update_delay(impl); + 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 impl *impl = data; + const char *str; + struct stream_info info; + + if (impl->on_demand_streams && spa_streq(type, PW_TYPE_INTERFACE_Metadata)) { + if (!props) + return; + + if (!spa_streq(spa_dict_lookup(props, "metadata.name"), "default")) + return; + + impl->metadata = pw_registry_bind(impl->registry, + id, type, PW_VERSION_METADATA, 0); + pw_metadata_add_listener(impl->metadata, + &impl->metadata_listener, + &metadata_events, impl); + impl->metadata_id = id; + return; + } + + 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 + 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; + + if (impl->metadata && id == impl->metadata_id) { + destroy_all_on_demand_streams(impl); + update_delay(impl); + spa_hook_remove(&impl->metadata_listener); + pw_proxy_destroy((struct pw_proxy*)impl->metadata); + impl->metadata = NULL; + return; + } + + s = find_stream(impl, id); + if (s == NULL) + return; + + destroy_stream(s); + update_delay(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 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: + clear_delaybuf(impl); + 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 bool check_stream_delay(struct stream *s) +{ + int64_t delay; + + if (!s->impl->latency_compensate) + return false; + + delay = get_stream_delay(s); + if (delay == INT64_MIN || delay == s->data_delay_samples) + return false; + + s->data_delay_samples = delay; + return true; +} + +static void combine_input_process(void *d) +{ + struct impl *impl = d; + struct pw_buffer *in, *out; + struct stream *s; + bool delay_changed = false; + + in = NULL; + while (true) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(impl->combine)) == NULL) + break; + if (in) + pw_stream_queue_buffer(impl->combine, in); + in = t; + } + if (in == NULL) { + pw_log_debug("%p: out of input buffers: %m", impl); + return; + } + + spa_list_for_each(s, &impl->streams, link) { + uint32_t j; + + if (s->stream == NULL) + continue; + + if (check_stream_delay(s)) + delay_changed = true; + + if ((out = pw_stream_dequeue_buffer(s->stream)) == NULL) { + pw_log_warn("%p: out of playback buffers: %m", s); + 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); + + ringbuffer_memcpy(&s->delay[j], + 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); + + /* Update delay if quantum etc. has changed. + * This should be rare enough so that doing it via main loop doesn't matter. + */ + if (impl->latency_compensate && delay_changed) + pw_loop_signal_event(impl->main_loop, impl->update_delay_event); +} + +static void combine_output_process(void *d) +{ + struct impl *impl = d; + struct pw_buffer *in, *out; + struct stream *s; + bool delay_changed = false; + bool mix[SPA_AUDIO_MAX_CHANNELS]; + + if ((out = pw_stream_dequeue_buffer(impl->combine)) == NULL) { + pw_log_debug("%p: out of output buffers: %m", impl); + return; + } + for (uint32_t i = 0; i < out->buffer->n_datas; i++) + mix[i] = false; + + spa_list_for_each(s, &impl->streams, link) { + uint32_t j; + + if (s->stream == NULL) + continue; + + if (check_stream_delay(s)) + delay_changed = true; + + in = NULL; + while (true) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(s->stream)) == NULL) + break; + if (in) + pw_stream_queue_buffer(s->stream, in); + in = t; + } + if (in == NULL) { + pw_log_debug("%p: out of input 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]; + + 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); + + if (mix[remap]) { + ringbuffer_mix(&s->delay[j], + dd->data, SPA_PTROFF(ds->data, offs, void), size); + } else { + ringbuffer_memcpy(&s->delay[j], + dd->data, SPA_PTROFF(ds->data, offs, void), size); + mix[remap] = true; + } + + outsize = SPA_MAX(outsize, size); + stride = SPA_MAX(stride, ds->chunk->stride); + + 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); + + if (impl->latency_compensate && delay_changed) + pw_loop_signal_event(impl->main_loop, impl->update_delay_event); +} + +static void combine_param_changed(void *d, uint32_t id, const struct spa_pod *param) +{ + struct impl *impl = d; + + switch (id) { + case SPA_PARAM_Props: { + int64_t latency_offset; + uint8_t buffer[1024]; + struct spa_pod_builder b; + const struct spa_pod *p; + + if (!param) + latency_offset = 0; + else if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(&latency_offset)) < 0) + break; + + if (latency_offset == impl->latency_offset) + break; + + impl->latency_offset = latency_offset; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + p = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(impl->latency_offset)); + pw_stream_update_params(impl->combine, &p, 1); + + update_latency(impl); + break; + } + case SPA_PARAM_Tag: { + param_tag_changed(impl, param); + break; + } + default: + break; + } +} + +static const struct pw_stream_events combine_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = combine_destroy, + .state_changed = combine_state_changed, + .param_changed = combine_param_changed, +}; + +static int create_combine(struct impl *impl) +{ + int res; + uint32_t n_params; + const struct spa_pod *params[3]; + 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); + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + 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)); + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(impl->latency_offset)); + + 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; + } + if (impl->metadata) { + spa_hook_remove(&impl->metadata_listener); + pw_proxy_destroy((struct pw_proxy*)impl->metadata); + impl->metadata = NULL; + } + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + PW_VERSION_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->update_delay_event) + pw_loop_destroy_source(impl->main_loop, impl->update_delay_event); + + if (impl->metadata) { + spa_hook_remove(&impl->metadata_listener); + pw_proxy_destroy((struct pw_proxy*)impl->metadata); + impl->metadata = NULL; + } + 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; + } + if (impl->data_loop) + pw_context_release_loop(impl->context, impl->data_loop); + + 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; + struct spa_error_location loc = {}; + + 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->module = module; + impl->context = context; + + spa_list_init(&impl->streams); + + if (args == NULL) + args = ""; + + props = pw_properties_new_string_checked(args, strlen(args), &loc); + if (props == NULL) { + res = -errno; + if (loc.reason) + spa_debug_log_error_location(pw_log_get(), SPA_LOG_LEVEL_ERROR, &loc, + "invalid module arguments: %s", loc.reason); + else + pw_log_error("can't create properties: %m"); + goto error; + } + impl->props = props; + + impl->main_loop = pw_context_get_main_loop(context); + impl->data_loop = pw_context_acquire_loop(context, &props->dict); + + 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"; + } + + if ((str = pw_properties_get(props, "combine.latency-compensate")) != NULL) + impl->latency_compensate = spa_atob(str); + if ((str = pw_properties_get(props, "combine.on-demand-streams")) != NULL) + impl->on_demand_streams = spa_atob(str); + + 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; + } + + pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); + + 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, "resample.disable") == NULL) + pw_properties_set(props, "resample.disable", "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_NODE_LOOP_NAME); + 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"); + copy_props(props, impl->combine_props, "resample.disable"); + + parse_audio_info(impl->combine_props, &impl->info); + + copy_props(props, impl->stream_props, PW_KEY_NODE_LOOP_NAME); + 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"); + copy_props(props, impl->stream_props, "resample.disable"); + + 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"); + + if (impl->latency_compensate) { + impl->update_delay_event = pw_loop_add_event(impl->main_loop, + update_delay_event, impl); + if (impl->update_delay_event == NULL) { + res = -errno; + pw_log_error("can't create event source: %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_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, + ®istry_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..4f08927 --- /dev/null +++ b/src/modules/module-echo-cancel.c @@ -0,0 +1,1539 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2021 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +/** \page page_module_echo_cancel Echo Cancel + * + * The `echo-cancel` module performs echo cancellation. The module creates + * virtual `echo-cancel-capture` source and `echo-cancel-playback` sink + * nodes and the associated streams. + * + * The echo-cancel module is mostly used in video or audio conference + * applications. When the other participants talk and the audio is going out to + * the speakers, the signal will be picked up again by the microphone and sent + * back to the other participants (along with your talking), resulting in an + * echo. This is annoying because the other participants will hear their own + * echo from you. + * + * Conceptually the echo-canceler is composed of 4 streams: + * + *\code{.unparsed} + * .--------. .---------. .--------. .----------. .-------. + * | mic | --> | capture | --> | | --> | source | --> | app | + * '--------' '---------' | echo | '----------' '-------' + * | cancel | + * .--------. .---------. | | .----------. .---------. + * | app | --> | sink | --> | | --> | playback | --> | speaker | + * '--------' '---------' '--------' '----------' '---------' + *\endcode + + * - A capture stream that captures audio from a microphone. + * - A Sink that takes the signal containing the data that should be canceled + * out from the capture stream. This is where the application (video conference + * application) send the audio to and it contains the signal from the other + * participants that are speaking and that needs to be cancelled out. + * - A playback stream that just passes the signal from the Sink to the speaker. + * This is so that you can hear the other participants. It is also the signal + * that gets picked up by the microphone and that eventually needs to be + * removed again. + * - A Source that exposes the echo-canceled data captured from the capture + * stream. The data from the sink stream and capture stream are correlated and + * the signal from the sink stream is removed from the capture stream data. + * This data then goes into the application (the conference application) and + * does not contain the echo from the other participants anymore. + * + * ## Module Name + * + * `libpipewire-module-echo-cancel` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * - `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 = `: the echo cancellation library Currently supported: + * `aec/libspa-aec-webrtc`. Leave unset to use the default method (`aec/libspa-aec-webrtc`). + * - `aec.args = `: 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} + * # ~/.config/pipewire/pipewire.conf.d/my-echo-cancel.conf + * + * 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 + * + */ + +/** + */ +#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_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 " }, + { PW_KEY_MODULE_DESCRIPTION, "Echo Cancellation" }, + { PW_KEY_MODULE_USAGE, " ( remote.name= ) " + "( node.latency= ) " + "( audio.rate= ) " + "( audio.channels= ) " + "( audio.position= ) " + "( buffer.max_size= ) " + "( buffer.play_delay= ) " + "( library.name = ) " + "( aec.args= ) " + "( capture.props= ) " + "( source.props= ) " + "( sink.props= ) " + "( playback.props= ) " }, + { 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 rec_info; + struct spa_audio_info_raw out_info; + struct spa_audio_info_raw play_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; + + char wav_path[512]; + struct wav_file *wav_file; +}; + +static inline void aec_run(struct impl *impl, const float *rec[], const float *play[], + float *out[], uint32_t n_samples) +{ + spa_audio_aec_run(impl->aec, rec, play, out, n_samples); + +#ifdef HAVE_SPA_PLUGINS + if (SPA_UNLIKELY(impl->wav_path[0])) { + if (impl->wav_file == NULL) { + struct wav_file_info info; + + spa_zero(info); + info.info.media_type = SPA_MEDIA_TYPE_audio; + info.info.media_subtype = SPA_MEDIA_SUBTYPE_raw; + info.info.info.raw.format = SPA_AUDIO_FORMAT_F32P; + info.info.info.raw.rate = impl->rec_info.rate; + info.info.info.raw.channels = impl->play_info.channels + + impl->rec_info.channels + impl->out_info.channels; + + impl->wav_file = wav_file_open(impl->wav_path, + "w", &info); + if (impl->wav_file == NULL) + pw_log_warn("can't open wav path '%s': %m", + impl->wav_path); + } + if (impl->wav_file) { + uint32_t i, n, c = impl->play_info.channels + + impl->rec_info.channels + impl->out_info.channels; + const float *data[c]; + + for (i = n = 0; i < impl->play_info.channels; i++) + data[n++] = play[i]; + for (i = 0; i < impl->rec_info.channels; i++) + data[n++] = rec[i]; + for (i = 0; i < impl->out_info.channels; i++) + data[n++] = out[i]; + + wav_file_write(impl->wav_file, (void*)data, n_samples); + } else { + spa_zero(impl->wav_path); + } + } else if (impl->wav_file != NULL) { + wav_file_close(impl->wav_file); + impl->wav_file = NULL; + } +#endif +} + +static void process(struct impl *impl) +{ + struct pw_buffer *cout; + struct pw_buffer *pout = NULL; + float rec_buf[impl->rec_info.channels][impl->aec_blocksize / sizeof(float)]; + float play_buf[impl->play_info.channels][impl->aec_blocksize / sizeof(float)]; + float play_delayed_buf[impl->play_info.channels][impl->aec_blocksize / sizeof(float)]; + float out_buf[impl->out_info.channels][impl->aec_blocksize / sizeof(float)]; + const float *rec[impl->rec_info.channels]; + const float *play[impl->play_info.channels]; + const float *play_delayed[impl->play_info.channels]; + float *out[impl->out_info.channels]; + struct spa_data *dd; + uint32_t i, size; + uint32_t rindex, pindex, oindex, pdindex, avail; + + 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); + + for (i = 0; i < impl->rec_info.channels; i++) { + /* captured samples, with echo from sink */ + rec[i] = &rec_buf[i][0]; + + spa_ringbuffer_read_data(&impl->rec_ring, impl->rec_buffer[i], + impl->rec_ringsize, rindex % impl->rec_ringsize, + (void*)rec[i], size); + + } + spa_ringbuffer_read_update(&impl->rec_ring, rindex + size); + + for (i = 0; i < impl->out_info.channels; i++) { + /* filtered samples, without echo from sink */ + out[i] = &out_buf[i][0]; + } + + spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + + for (i = 0; i < impl->play_info.channels; i++) { + /* echo from sink */ + play[i] = &play_buf[i][0]; + /* echo from sink delayed */ + play_delayed[i] = &play_delayed_buf[i][0]; + + spa_ringbuffer_read_data(&impl->play_ring, impl->play_buffer[i], + impl->play_ringsize, pindex % impl->play_ringsize, + (void *)play[i], size); + + 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 = sizeof(float); + } + } + 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->out_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->play_info.channels]; + float *o[impl->out_info.channels]; + + for (i = 0; i < impl->play_info.channels; i++) + pd[i] = play_delayed[i] + delay_left; + for (i = 0; i < impl->out_info.channels; i++) + o[i] = out[i] + delay_left; + + aec_run(impl, rec, pd, o, size / sizeof(float) - delay_left); + } + } else { + /* run the canceller */ + aec_run(impl, 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->out_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->out_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 = sizeof(float); + } + + 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->rec_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; + 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) { + if (pw_stream_get_state(impl->sink, NULL) != 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: + if (pw_stream_get_state(impl->sink, NULL) == 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: capture unconnected", impl); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_ERROR: + pw_log_info("%p: capture 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; + + 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: source unconnected", impl); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_ERROR: + pw_log_info("%p: source error: %s", impl, error); + break; + default: + break; + } +} + +static void reset_buffers(struct impl *impl) +{ + uint32_t index, i; + + 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); + + for (i = 0; i < impl->rec_info.channels; i++) + memset(impl->rec_buffer[i], 0, impl->rec_ringsize); + for (i = 0; i < impl->play_info.channels; i++) + memset(impl->play_buffer[i], 0, impl->play_ringsize); + for (i = 0; i < impl->out_info.channels; i++) + memset(impl->out_buffer[i], 0, impl->out_ringsize); + + 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))); +} + +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 (param == NULL || 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) +{ + 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_pod_builder_string(b, "debug.aec.wav-path"); + spa_pod_builder_string(b, impl->wav_path); + + if (spa_audio_aec_get_params(impl->aec, NULL) > 0) + spa_audio_aec_get_params(impl->aec, b); + + spa_pod_builder_pop(b, &f[1]); + return spa_pod_builder_pop(b, &f[0]); +} + +static int set_params(struct impl* impl, const struct spa_pod *params) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + + spa_pod_parser_pod(&prs, params); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + struct spa_pod *pod; + char value[512]; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + + if (spa_pod_parser_get_pod(&prs, &pod) < 0) + break; + + if (spa_pod_is_string(pod)) { + spa_pod_copy_string(pod, sizeof(value), value); + } else if (spa_pod_is_none(pod)) { + spa_zero(value); + } else + continue; + + pw_log_info("key:'%s' val:'%s'", name, value); + + if (spa_streq(name, "debug.aec.wav-path")) { + spa_scnprintf(impl->wav_path, + sizeof(impl->wav_path), "%s", value); + } + } + spa_audio_aec_set_params(impl->aec, params); + return 1; +} + +static void props_changed(struct impl* impl, const struct spa_pod *param) +{ + uint8_t buffer[1024]; + struct spa_pod_dynamic_builder b; + const struct spa_pod* params[1]; + const struct spa_pod_prop* prop; + struct spa_pod_object* obj = (struct spa_pod_object*)param; + + if (param == NULL) + return; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + if (prop->key == SPA_PROP_params) + set_params(impl, &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); + if (impl->playback != NULL) + pw_stream_update_params(impl->playback, params, 1); + } + spa_pod_dynamic_builder_clean(&b); +} + +static void input_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) + reset_buffers(impl); + break; + case SPA_PARAM_Latency: + input_param_latency_changed(impl, param); + break; + case SPA_PARAM_Props: + props_changed(impl, param); + 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 playback_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: playback unconnected", impl); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_ERROR: + pw_log_info("%p: playback error: %s", impl, error); + break; + default: + break; + } +} + +static void sink_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->sink, false); + if (impl->playback != NULL) + pw_stream_flush(impl->playback, false); + if (old == PW_STREAM_STATE_STREAMING) { + impl->current_delay = 0; + + if (pw_stream_get_state(impl->capture, NULL) != 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: + if (pw_stream_get_state(impl->capture, NULL) == 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: sink unconnected", impl); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_ERROR: + pw_log_info("%p: sink 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 (param == NULL || 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 impl *impl = data; + + switch (id) { + case SPA_PARAM_Format: + if (param == NULL) + reset_buffers(impl); + break; + case SPA_PARAM_Latency: + output_param_latency_changed(impl, param); + break; + case SPA_PARAM_Props: + props_changed(impl, param); + 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->play_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 = playback_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 = sink_state_changed, + .param_changed = output_param_changed +}; + +#define MAX_PARAMS 512u + +static int setup_streams(struct impl *impl) +{ + int res; + uint32_t n_params, i; + uint32_t offsets[MAX_PARAMS]; + const struct spa_pod *params[MAX_PARAMS]; + struct spa_pod_dynamic_builder b; + + 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); + + if (n_params < MAX_PARAMS) { + 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); + for (int i = 0; i < nbr_of_external_props; i++) { + if (n_params < MAX_PARAMS) { + offsets[n_params++] = b.b.state.offset; + spa_audio_aec_enum_props(impl->aec, i, &b.b); + } + } + if (n_params < MAX_PARAMS) { + offsets[n_params++] = b.b.state.offset; + spa_pod_builder_add_object(&b.b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("debug.aec.wav-path"), + SPA_PROP_INFO_description, SPA_POD_String("Path to WAV file"), + SPA_PROP_INFO_type, SPA_POD_String(impl->wav_path), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + } + if (n_params < MAX_PARAMS) { + offsets[n_params++] = b.b.state.offset; + 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 | + PW_STREAM_FLAG_ASYNC, + 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 | + PW_STREAM_FLAG_ASYNC, + 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->rec_info.rate / 1000; + impl->play_ringsize = sizeof(float) * ((impl->max_buffer_size * impl->play_info.rate / 1000) + impl->buffer_delay); + impl->out_ringsize = sizeof(float) * impl->max_buffer_size * impl->out_info.rate / 1000; + for (i = 0; i < impl->rec_info.channels; i++) + impl->rec_buffer[i] = malloc(impl->rec_ringsize); + for (i = 0; i < impl->play_info.channels; i++) + impl->play_buffer[i] = malloc(impl->play_ringsize); + for (i = 0; i < impl->out_info.channels; i++) + impl->out_buffer[i] = malloc(impl->out_ringsize); + + reset_buffers(impl); + + 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->rec_info.channels; i++) + free(impl->rec_buffer[i]); + for (i = 0; i < impl->play_info.channels; i++) + free(impl->play_buffer[i]); + for (i = 0; i < impl->out_info.channels; 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 void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_RATE, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + 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 spa_audio_info_raw info; + 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, &info); + + impl->capture_info = info; + impl->source_info = info; + impl->sink_info = info; + impl->playback_info = 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"); + } + + 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, SPA_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, "resample.prefill"); + + 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 = (info.rate * req_num) / req_denom; + } else { + impl->buffer_delay = DELAY_MS * info.rate / 1000; + pw_log_warn("Sample rate for buffer.play_delay is 0 using default"); + } + } else { + impl->buffer_delay = DELAY_MS * info.rate / 1000; + pw_log_warn("Wrong value/format for buffer.play_delay using default"); + } + } else { + impl->buffer_delay = DELAY_MS * info.rate / 1000; + } + + if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_POSITION)) != NULL) { + spa_audio_parse_position(str, strlen(str), + impl->capture_info.position, &impl->capture_info.channels); + } + if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL) { + spa_audio_parse_position(str, strlen(str), + impl->source_info.position, &impl->source_info.channels); + } + if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL) { + spa_audio_parse_position(str, strlen(str), + impl->sink_info.position, &impl->sink_info.channels); + impl->playback_info = impl->sink_info; + } + if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_POSITION)) != NULL) { + spa_audio_parse_position(str, strlen(str), + impl->playback_info.position, &impl->playback_info.channels); + if (impl->playback_info.channels != impl->sink_info.channels) + impl->playback_info = impl->sink_info; + } + + 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 dict_items[] = { + { SPA_KEY_LIBRARY_NAME, path }, + }; + struct spa_dict dict = SPA_DICT_INIT_ARRAY(dict_items); + + handle = spa_plugin_loader_load(impl->loader, SPA_NAME_AEC, &dict); + 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); + + + if (spa_interface_callback_check(&impl->aec->iface, struct spa_audio_aec_methods, init2, 3)) { + impl->rec_info = impl->capture_info; + impl->out_info = impl->source_info; + impl->play_info = impl->sink_info; + + res = spa_audio_aec_init2(impl->aec, &aec_props->dict, + &impl->rec_info, &impl->out_info, &impl->play_info); + + if (impl->sink_info.channels != impl->play_info.channels) + impl->sink_info = impl->play_info; + if (impl->playback_info.channels != impl->play_info.channels) + impl->playback_info = impl->play_info; + if (impl->capture_info.channels != impl->rec_info.channels) + impl->capture_info = impl->rec_info; + if (impl->source_info.channels != impl->out_info.channels) + impl->source_info = impl->out_info; + } else { + if (impl->source_info.channels != impl->sink_info.channels) + impl->source_info = impl->sink_info; + if (impl->capture_info.channels != impl->source_info.channels) + impl->capture_info = impl->source_info; + if (impl->playback_info.channels != impl->sink_info.channels) + impl->playback_info = impl->sink_info; + + info = impl->playback_info; + + res = spa_audio_aec_init(impl->aec, &aec_props->dict, &info); + + impl->rec_info = info; + impl->out_info = info; + impl->play_info = 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) * 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) * info.rate * num / denom * factor; + } + } else { + /* Implementation doesn't care about the block size */ + impl->aec_blocksize = 0; + } + + copy_props(impl, props, PW_KEY_NODE_LATENCY); + + 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-example-filter.c b/src/modules/module-example-filter.c new file mode 100644 index 0000000..5f6268d --- /dev/null +++ b/src/modules/module-example-filter.c @@ -0,0 +1,629 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \page page_module_example_filter Example Filter + * + * The example filter is a good starting point for writing a custom + * filter. We refer to the source code for more information. + * + * ## Module Name + * + * `libpipewire-module-example-filter` + * + * ## Module Options + * + * - `node.description`: a human readable name for the filter streams + * - `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 + * 'filter-PID-MODULEID'. + * + * 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 + * + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-example-filter.conf + * + * context.modules = [ + * { name = libpipewire-module-example-filter + * args = { + * node.description = "Example Filter" + * capture.props = { + * audio.position = [ FL FR ] + * node.passive = true + * } + * playback.props = { + * node.name = "Example Filter" + * media.class = "Audio/Source" + * audio.position = [ FL FR ] + * } + * } + * } + * ] + *\endcode + * + *\code{.unparsed} + * pw-cli -m lm libpipewire-module-example-filter '{ audio.position=[FL FR] }' + *\endcode + * + */ + +#define NAME "example-filter" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_POSITION "[ FL FR ]" + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Create example filter streams" }, + { PW_KEY_MODULE_USAGE, " ( remote.name= ) " + "( node.latency= ) " + "( node.description= ) " + "( audio.rate= ) " + "( audio.channels= ) " + "( audio.position= ) " + "( capture.props= ) " + "( playback.props= ) " }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +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; +}; + +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; + int res; + if ((res = pw_stream_trigger_process(impl->playback)) < 0) { + while (true) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) + break; + pw_stream_queue_buffer(impl->capture, t); + } + } +} + +static void playback_process(void *d) +{ + struct impl *impl = d; + struct pw_buffer *in, *out; + uint32_t i; + + in = NULL; + while (true) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) + break; + if (in) + pw_stream_queue_buffer(impl->capture, in); + in = t; + } + if (in == 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) { + uint32_t outsize = UINT32_MAX; + int32_t stride = 0; + struct spa_data *d; + const void *src[in->buffer->n_datas]; + void *dst[out->buffer->n_datas]; + + 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); + } + for (i = 0; i < out->buffer->n_datas; i++) { + d = &out->buffer->datas[i]; + + outsize = SPA_MIN(outsize, d->maxsize); + dst[i] = d->data; + + if (i < in->buffer->n_datas) { + /* do filtering here, samples are a single + * channel float */ + memcpy(dst[i], src[i], outsize); + } else { + memset(dst[i], 0, outsize); + } + d->chunk->offset = 0; + d->chunk->size = outsize; + d->chunk->stride = stride; + } + } + + 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 (param == NULL || 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); +} + +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); + 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 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; + 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, + "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); + + /* 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 | + PW_STREAM_FLAG_ASYNC, + 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 void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_RATE, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + 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, "filter-%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-%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)); + + 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, + "filter-%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 (!impl->capture_info.rate && !impl->playback_info.rate) { + if (pw_properties_get(impl->playback_props, "resample.disable") == NULL) + pw_properties_set(impl->playback_props, "resample.disable", "true"); + if (pw_properties_get(impl->capture_props, "resample.disable") == NULL) + pw_properties_set(impl->capture_props, "resample.disable", "true"); + } else 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 (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-example-sink.c b/src/modules/module-example-sink.c new file mode 100644 index 0000000..8014f61 --- /dev/null +++ b/src/modules/module-example-sink.c @@ -0,0 +1,442 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \page page_module_example_sink Example Sink + * + * The example sink is a good starting point for writing a custom + * sink. We refer to the source code for more information. + * + * ## Module Name + * + * `libpipewire-module-example-sink` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `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} + * # ~/.config/pipewire/pipewire.conf.d/my-example-sink.conf + * + * 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= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ] " \ + "( stream.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { 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 void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_FORMAT, + SPA_KEY_AUDIO_RATE, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + 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..47a0618 --- /dev/null +++ b/src/modules/module-example-source.c @@ -0,0 +1,448 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \page page_module_example_source Example Source + * + * The example source is a good starting point for writing a custom + * source. We refer to the source code for more information. + * + * ## Module Name + * + * `libpipewire-module-example-source` + * + * ## Module Options + * + * - `node.name`: a unique name for the stream + * - `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} + * # ~/.config/pipewire/pipewire.conf.d/my-example-source.conf + * + * 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= ) " \ + "( node.name= ) " \ + "( node.description= ) " \ + "( audio.format= ) " \ + "( audio.rate= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ + "( stream.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { 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 void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_FORMAT, + SPA_KEY_AUDIO_RATE, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static int calc_frame_size(const struct spa_audio_info_raw *info) +{ + 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..86982e6 --- /dev/null +++ b/src/modules/module-fallback-sink.c @@ -0,0 +1,462 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include + +#include +#include + +/** \page page_module_fallback_sink Fallback Sink + * + * Fallback sink, which appear dynamically when no other sinks are + * present. This is only useful for Pulseaudio compatibility. + * + * ## Module Name + * + * `libpipewire-module-fallback-sink` + * + * ## Module Options + * + * - `sink.name`: sink name + * - `sink.description`: sink description + */ + +#define NAME "fallback-sink" + +#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= ) " \ + "( sink.description= ) ") + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Pauli Virtanen " }, + { 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_props(void *data, uint32_t id, const struct spa_dict *props) +{ + 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_props = sink_proxy_bound_props, + .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, + ®istry_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-ffado-driver.c b/src/modules/module-ffado-driver.c new file mode 100644 index 0000000..4345e5a --- /dev/null +++ b/src/modules/module-ffado-driver.c @@ -0,0 +1,1616 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +/** \page page_module_ffado_driver FFADO firewire audio driver + * + * The ffado-driver module provides a source or sink using the libffado library for + * reading and writing to firewire audio devices. + * + * ## Module Name + * + * `libpipewire-module-ffado-driver` + * + * ## Module Options + * + * - `driver.mode`: the driver mode, sink|source|duplex, default duplex + * - `ffado.devices`: array of devices to open, default "hw:0" + * - `ffado.period-size`: period size,default 1024. A value of 0 will use the graph duration. + * - `ffado.period-num`: period number,default 3 + * - `ffado.sample-rate`: sample-rate, default 48000. A value of 0 will use the graph rate. + * - `ffado.slave-mode`: slave mode + * - `ffado.snoop-mode`: snoop mode + * - `ffado.verbose`: ffado verbose level + * - `ffado.rtprio`: ffado realtime priority, this is by default the PipeWire server + * priority + 5 + * - `ffado.realtime`: ffado realtime mode. this requires correctly configured rlimits + * to acquire FIFO scheduling at the ffado.rtprio priority + * - `latency.internal.input`: extra input latency in frames + * - `latency.internal.output`: extra output latency in frames + * - `source.props`: Extra properties for the source filter + * - `sink.props`: Extra properties for the sink filter + * + * ## General options + * + * Options with well-known behavior. + * + * - \ref PW_KEY_REMOTE_NAME + * - \ref SPA_KEY_AUDIO_POSITION + * - \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 duplex sink/source + * + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-ffado-driver.conf + * + * context.modules = [ + * { name = libpipewire-module-ffado-driver + * args = { + * #driver.mode = duplex + * #ffado.devices = [ "hw:0" ] + * #ffado.period-size = 1024 + * #ffado.period-num = 3 + * #ffado.sample-rate = 48000 + * #ffado.slave-mode = false + * #ffado.snoop-mode = false + * #ffado.verbose = 0 + * #ffado.rtprio = 65 + * #ffado.realtime = true + * #latency.internal.input = 0 + * #latency.internal.output = 0 + * #audio.position = [ FL FR ] + * source.props = { + * # extra sink properties + * } + * sink.props = { + * # extra sink properties + * } + * } + * } + * ] + *\endcode + */ + +#define NAME "ffado-driver" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define MAX_PORTS 128 +#define FFADO_RT_PRIORITY_PACKETIZER_RELATIVE 5 + +#define DEFAULT_DEVICES "[ \"hw:0\" ]" +#define DEFAULT_PERIOD_SIZE 1024 +#define DEFAULT_PERIOD_NUM 3 +#define DEFAULT_SAMPLE_RATE 48000 +#define DEFAULT_SLAVE_MODE false +#define DEFAULT_SNOOP_MODE false +#define DEFAULT_VERBOSE 0 +#define DEFAULT_RTPRIO (RTPRIO_SERVER + FFADO_RT_PRIORITY_PACKETIZER_RELATIVE) +#define DEFAULT_REALTIME true + +#define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_MIDI_PORTS 1 + +#define MODULE_USAGE "( remote.name= ) " \ + "( driver.mode= ) " \ + "( ffado.devices= ) " \ + "( ffado.period-size= ) " \ + "( ffado.period-num= ) " \ + "( ffado.sample-rate= ) " \ + "( ffado.slave-mode= ) " \ + "( ffado.snoop-mode= ) " \ + "( ffado.verbose= ) " \ + "( ffado.rtprio= ) " \ + "( ffado.realtime= ) " \ + "( audio.position= ) " \ + "( source.props= ) " \ + "( sink.props= ) " + + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Create an FFADO based driver" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct port_data { + struct port *port; +}; + +struct port { + enum spa_direction direction; + ffado_streaming_stream_type stream_type; + char name[280]; + + struct spa_latency_info latency[2]; + bool latency_changed[2]; + unsigned int is_midi:1; + unsigned int cleared:1; + void *buffer; + + uint8_t event_byte; + uint8_t event_type; + uint32_t event_time; + uint8_t event_buffer[512]; + uint32_t event_pos; + int event_pending; + + struct port_data *data; +}; + +struct volume { + bool mute; + uint32_t n_volumes; + float volumes[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct stream { + struct impl *impl; + + enum spa_direction direction; + struct pw_properties *props; + struct pw_filter *filter; + struct spa_hook listener; + struct spa_audio_info_raw info; + uint32_t n_ports; + struct port *ports[MAX_PORTS]; + struct volume volume; + + unsigned int ready:1; + unsigned int running:1; + + struct { + unsigned int transfered:1; + } rt; +}; + +struct impl { + struct pw_context *context; + struct pw_loop *main_loop; + struct pw_loop *data_loop; + struct spa_system *system; + struct spa_source *ffado_timer; + + ffado_device_info_t device_info; + ffado_options_t device_options; + ffado_device_t *dev; + +#define MODE_SINK (1<<0) +#define MODE_SOURCE (1<<1) +#define MODE_DUPLEX (MODE_SINK|MODE_SOURCE) + 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 reset_work_id; + + struct spa_io_position *position; + + uint32_t latency[2]; + + struct stream source; + struct stream sink; + + char *devices[FFADO_MAX_SPECSTRINGS]; + uint32_t n_devices; + int32_t sample_rate; + int32_t period_size; + int32_t n_periods; + bool slave_mode; + bool snoop_mode; + uint32_t verbose; + int32_t rtprio; + bool realtime; + + uint32_t input_latency; + uint32_t output_latency; + uint32_t quantum_limit; + + uint32_t frame_time; + + unsigned int do_disconnect:1; + unsigned int fix_midi:1; + unsigned int started:1; + unsigned int freewheel:1; + + pthread_t thread; + + struct { + unsigned int done:1; + unsigned int triggered:1; + unsigned int new_xrun:1; + uint32_t pw_xrun; + uint32_t ffado_xrun; + } rt; +}; + +static int stop_ffado_device(struct impl *impl); +static int start_ffado_device(struct impl *impl); +static void schedule_reset_ffado_device(struct impl *impl); + +static void reset_volume(struct volume *vol, uint32_t n_volumes) +{ + uint32_t i; + vol->mute = false; + vol->n_volumes = n_volumes; + for (i = 0; i < n_volumes; i++) + vol->volumes[i] = 1.0f; +} + +static inline void do_volume(float *dst, const float *src, struct volume *vol, uint32_t ch, uint32_t n_samples) +{ + float v = vol->mute ? 0.0f : vol->volumes[ch]; + + if (v == 0.0f || src == NULL) + memset(dst, 0, n_samples * sizeof(float)); + else if (v == 1.0f) + memcpy(dst, src, n_samples * sizeof(float)); + else { + uint32_t i; + for (i = 0; i < n_samples; i++) + dst[i] = src[i] * v; + } +} + +static void clear_port_buffer(struct port *p, uint32_t n_samples) +{ + if (!p->cleared) { + if (p->buffer) + memset(p->buffer, 0, n_samples * sizeof(uint32_t)); + p->cleared = true; + } +} + +static inline void fix_midi_event(uint8_t *data, size_t size) +{ + /* fixup NoteOn with vel 0 */ + if (size > 2 && (data[0] & 0xF0) == 0x90 && data[2] == 0x00) { + data[0] = 0x80 + (data[0] & 0x0F); + data[2] = 0x40; + } +} + +static void midi_to_ffado(struct port *p, float *src, uint32_t n_samples) +{ + struct spa_pod *pod; + struct spa_pod_sequence *seq; + struct spa_pod_control *c; + uint32_t i, index = 0, unhandled = 0; + uint32_t *dst = p->buffer; + + if (src == NULL) + return; + + if ((pod = spa_pod_from_data(src, n_samples * sizeof(float), 0, n_samples * sizeof(float))) == NULL) + return; + if (!spa_pod_is_sequence(pod)) + return; + + seq = (struct spa_pod_sequence*)pod; + + clear_port_buffer(p, n_samples); + + /* first leftovers from previous cycle, always start at offset 0 */ + for (i = 0; i < p->event_pos; i++) { + dst[index] = 0x01000000 | (uint32_t) p->event_buffer[i]; + index += 8; + } + p->event_pos = 0; + + SPA_POD_SEQUENCE_FOREACH(seq, c) { + uint8_t data[16]; + int j, size; + + if (c->type != SPA_CONTROL_UMP) + continue; + + size = spa_ump_to_midi(SPA_POD_BODY(&c->value), + SPA_POD_BODY_SIZE(&c->value), data, sizeof(data)); + if (size <= 0) + continue; + + if (index < c->offset) + index = SPA_ROUND_UP_N(c->offset, 8); + for (j = 0; j < size; j++) { + if (index >= n_samples) { + /* keep events that don't fit for the next cycle */ + if (p->event_pos < sizeof(p->event_buffer)) + p->event_buffer[p->event_pos++] = data[j]; + else + unhandled++; + } + else + dst[index] = 0x01000000 | (uint32_t) data[j]; + index += 8; + } + } + if (unhandled > 0) + pw_log_warn("%u MIDI events dropped (index %d)", unhandled, index); + else if (p->event_pos > 0) + pw_log_debug("%u MIDI events saved (index %d)", p->event_pos, index); +} + +static int take_bytes(struct port *p, uint32_t *frame, uint8_t **bytes, size_t *size) +{ + if (p->event_pos == 0) + return 0; + *frame = p->event_time; + *bytes = p->event_buffer; + *size = p->event_pos; + return 1; +} + +static const int status_len[] = { + 2, /* noteoff */ + 2, /* noteon */ + 2, /* keypress */ + 2, /* controller */ + 1, /* pgmchange */ + 1, /* chanpress */ + 2, /* pitchbend */ + -1, /* invalid */ + 1, /* sysex 0xf0 */ + 1, /* qframe 0xf1 */ + 2, /* songpos 0xf2 */ + 1, /* songsel 0xf3 */ + -1, /* none 0xf4 */ + -1, /* none 0xf5 */ + 0, /* tune request 0xf6 */ + -1, /* none 0xf7 */ + 0, /* clock 0xf8 */ + -1, /* none 0xf9 */ + 0, /* start 0xfa */ + 0, /* continue 0xfb */ + 0, /* stop 0xfc */ + -1, /* none 0xfd */ + 0, /* sensing 0xfe */ + 0, /* reset 0xff */ +}; + +static int process_byte(struct port *p, uint32_t time, uint8_t byte, + uint32_t *frame, uint8_t **bytes, size_t *size) +{ + int res = 0; + if (byte >= 0xf8) { + if (byte == 0xfd) { + pw_log_warn("dropping invalid MIDI status bytes %08x", byte); + return false; + } + p->event_byte = byte; + *frame = time; + *bytes = &p->event_byte; + *size = 1; + return 1; + } + if ((byte & 0x80) && (byte != 0xf7 || p->event_type != 8)) { + if (p->event_pending > 0) + pw_log_warn("incomplete MIDI message %02x dropped %u time:%u", + p->event_type, p->event_pending, time); + /* new command */ + p->event_buffer[0] = byte; + p->event_time = time; + if ((byte & 0xf0) == 0xf0) /* system message */ + p->event_type = (byte & 0x0f) + 8; + else + p->event_type = (byte >> 4) & 0x07; + p->event_pos = 1; + p->event_pending = status_len[p->event_type]; + } else { + if (p->event_pending > 0) { + /* rest of command */ + if (p->event_pos < sizeof(p->event_buffer)) + p->event_buffer[p->event_pos++] = byte; + if (p->event_type != 8) + p->event_pending--; + } else { + /* running status */ + p->event_buffer[1] = byte; + p->event_time = time; + p->event_pending = status_len[p->event_type] - 1; + p->event_pos = 2; + } + } + if (p->event_pending == 0) { + res = take_bytes(p, frame, bytes, size); + if (p->event_type >= 8) + p->event_type = 7; + } else if (p->event_type == 8) { + if (byte == 0xf7 || p->event_pos >= sizeof(p->event_buffer)) { + res = take_bytes(p, frame, bytes, size); + p->event_pos = 0; + if (byte == 0xf7) { + p->event_pending = 0; + p->event_type = 7; + } + } + } + return res; +} + +static void ffado_to_midi(struct port *p, float *dst, uint32_t *src, uint32_t size) +{ + struct spa_pod_builder b = { 0, }; + uint32_t i, count; + struct spa_pod_frame f; + + count = src ? size : 0; + + spa_pod_builder_init(&b, dst, size); + spa_pod_builder_push_sequence(&b, &f, 0); + for (i = 0; i < count; i++) { + uint32_t data = src[i], frame; + uint8_t *bytes; + size_t size; + + if ((data & 0xff000000) == 0) + continue; + + if (process_byte(p, i, data & 0xff, &frame, &bytes, &size)) { + uint64_t state = 0; + while (size > 0) { + uint32_t ev[4]; + int ev_size = spa_ump_from_midi(&bytes, &size, ev, sizeof(ev), 0, &state); + if (ev_size <= 0) + break; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ev, ev_size); + } + } + } + spa_pod_builder_pop(&b, &f); + if (p->event_pending > 0) + /* make sure the rest of the MIDI message is sent first in the next cycle */ + p->event_time = 0; +} + +static inline uint64_t get_time_ns(struct impl *impl) +{ + uint64_t nsec; + if (impl->sink.filter) + nsec = pw_filter_get_nsec(impl->sink.filter); + else if (impl->source.filter) + nsec = pw_filter_get_nsec(impl->source.filter); + else + nsec = 0; + return nsec; +} + +static int set_timeout(struct impl *impl, uint64_t time) +{ + struct timespec timeout, interval; + timeout.tv_sec = time / SPA_NSEC_PER_SEC; + timeout.tv_nsec = time % SPA_NSEC_PER_SEC; + interval.tv_sec = 0; + interval.tv_nsec = 0; + pw_loop_update_timer(impl->data_loop, + impl->ffado_timer, &timeout, &interval, true); + return 0; +} + +static void stream_destroy(void *d) +{ + struct stream *s = d; + uint32_t i; + for (i = 0; i < s->n_ports; i++) { + struct port *p = s->ports[i]; + if (p != NULL) { + s->ports[i] = NULL; + free(p->buffer); + free(p); + } + } + s->n_ports = 0; + spa_hook_remove(&s->listener); + s->filter = NULL; + s->ready = false; + s->running = false; +} + +static void check_start(struct impl *impl) +{ + if ((!(impl->mode & MODE_SINK) || (impl->sink.ready && impl->sink.running)) && + (!(impl->mode & MODE_SOURCE) || (impl->source.ready && impl->source.running))) + start_ffado_device(impl); +} + +static void stream_state_changed(void *d, enum pw_filter_state old, + enum pw_filter_state state, const char *error) +{ + struct stream *s = d; + struct impl *impl = s->impl; + switch (state) { + case PW_FILTER_STATE_ERROR: + pw_log_warn("filter state %d error: %s", state, error); + break; + case PW_FILTER_STATE_UNCONNECTED: + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_FILTER_STATE_PAUSED: + s->running = false; + if (!impl->sink.running && !impl->source.running) + stop_ffado_device(impl); + break; + case PW_FILTER_STATE_STREAMING: + s->running = true; + check_start(impl); + break; + default: + break; + } +} + +static void sink_process(void *d, struct spa_io_position *position) +{ + struct stream *s = d; + struct impl *impl = s->impl; + uint32_t i, n_samples = position->clock.duration; + + pw_log_trace_fp("process %d", impl->rt.triggered); + if (impl->mode == MODE_SINK && impl->rt.triggered) { + impl->rt.triggered = false; + return; + } + + for (i = 0; i < s->n_ports; i++) { + struct port *p = s->ports[i]; + float *src; + if (p == NULL || p->data == NULL) + continue; + + src = pw_filter_get_dsp_buffer(p->data, n_samples); + if (src == NULL) { + clear_port_buffer(p, n_samples); + continue; + } + + if (SPA_UNLIKELY(p->is_midi)) + midi_to_ffado(p, src, n_samples); + else + do_volume(p->buffer, src, &s->volume, i, n_samples); + + p->cleared = false; + } + ffado_streaming_transfer_playback_buffers(impl->dev); + s->rt.transfered = true; + + if (impl->mode == MODE_SINK) { + pw_log_trace_fp("done %u", impl->frame_time); + impl->rt.done = true; + set_timeout(impl, position->clock.nsec); + } +} + +static void silence_playback(struct impl *impl) +{ + uint32_t i; + struct stream *s = &impl->sink; + + for (i = 0; i < s->n_ports; i++) { + struct port *p = s->ports[i]; + if (p != NULL) + clear_port_buffer(p, impl->device_options.period_size); + } + ffado_streaming_transfer_playback_buffers(impl->dev); + s->rt.transfered = true; +} + +static void source_process(void *d, struct spa_io_position *position) +{ + struct stream *s = d; + struct impl *impl = s->impl; + uint32_t i, n_samples = position->clock.duration; + + pw_log_trace_fp("process %d", impl->rt.triggered); + + if (SPA_FLAG_IS_SET(impl->position->clock.flags, SPA_IO_CLOCK_FLAG_XRUN_RECOVER)) + return; + + if (!impl->rt.triggered) { + pw_log_trace_fp("done %u", impl->frame_time); + impl->rt.done = true; + if (!impl->sink.rt.transfered) + silence_playback(impl); + set_timeout(impl, position->clock.nsec); + return; + } + + impl->rt.triggered = false; + + ffado_streaming_transfer_capture_buffers(impl->dev); + s->rt.transfered = true; + + for (i = 0; i < s->n_ports; i++) { + struct port *p = s->ports[i]; + float *dst; + + if (p == NULL || p->data == NULL || p->buffer == NULL) + continue; + + dst = pw_filter_get_dsp_buffer(p->data, n_samples); + if (dst == NULL) + continue; + + if (SPA_UNLIKELY(p->is_midi)) + ffado_to_midi(p, dst, p->buffer, n_samples); + else + do_volume(dst, p->buffer, &s->volume, i, n_samples); + } +} + +static void stream_io_changed(void *data, void *port_data, uint32_t id, void *area, uint32_t size) +{ + struct stream *s = data; + struct impl *impl = s->impl; + bool freewheel; + + if (port_data == NULL) { + switch (id) { + case SPA_IO_Position: + impl->position = area; + freewheel = impl->position != NULL && + SPA_FLAG_IS_SET(impl->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL); + if (impl->freewheel != freewheel) { + pw_log_info("freewheel: %d -> %d", impl->freewheel, freewheel); + impl->freewheel = freewheel; + if (impl->started) { + if (freewheel) { + set_timeout(impl, 0); + ffado_streaming_stop(impl->dev); + } else { + ffado_streaming_start(impl->dev); + impl->rt.done = true; + set_timeout(impl, get_time_ns(impl)); + } + } + } + break; + default: + break; + } + } +} + +static void param_latency_changed(struct stream *s, const struct spa_pod *param, + struct port_data *data) +{ + struct port *port = data->port; + struct spa_latency_info latency; + bool update = false; + enum spa_direction direction = port->direction; + + if (param == NULL || spa_latency_parse(param, &latency) < 0) + return; + + if (spa_latency_info_compare(&port->latency[direction], &latency)) { + port->latency[direction] = latency; + port->latency_changed[direction] = update = true; + } +} + +static int make_stream_ports(struct stream *s) +{ + struct impl *impl = s->impl; + struct pw_properties *props; + uint8_t buffer[1024]; + struct spa_pod_builder b; + struct spa_latency_info latency; + const struct spa_pod *params[2]; + uint32_t i, n_params = 0, n_channels = 0; + bool is_midi; + + for (i = 0; i < s->n_ports; i++) { + struct port *port = s->ports[i]; + if (port->data != NULL) { + free(port->buffer); + pw_filter_remove_port(port->data); + port->data = NULL; + } + } + for (i = 0; i < s->n_ports; i++) { + struct port *port = s->ports[i]; + char channel[32]; + + snprintf(channel, sizeof(channel), "AUX%u", n_channels % SPA_AUDIO_MAX_CHANNELS); + + switch (port->stream_type) { + case ffado_stream_type_audio: + props = pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_PHYSICAL, "true", + PW_KEY_PORT_TERMINAL, "true", + PW_KEY_PORT_NAME, port->name, + PW_KEY_AUDIO_CHANNEL, channel, + NULL); + is_midi = false; + n_channels++; + break; + case ffado_stream_type_midi: + props = pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_PORT_NAME, port->name, + PW_KEY_PORT_PHYSICAL, "true", + PW_KEY_PORT_TERMINAL, "true", + PW_KEY_PORT_CONTROL, "true", + NULL); + + is_midi = true; + break; + default: + pw_log_info("not registering unknown stream %d %s (type %d)", i, + port->name, port->stream_type); + continue; + + } + latency = SPA_LATENCY_INFO(s->direction, + .min_quantum = 1, + .max_quantum = 1, + .min_rate = impl->latency[s->direction], + .max_rate = impl->latency[s->direction]); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + n_params = 0; + params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + + port->data = pw_filter_add_port(s->filter, + s->direction, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port_data), props, params, n_params); + if (port->data == NULL) { + pw_log_error("Can't create port: %m"); + return -errno; + } + port->data->port = port; + + port->latency[s->direction] = latency; + port->is_midi = is_midi; + port->buffer = calloc(impl->quantum_limit, sizeof(float)); + if (port->buffer == NULL) { + pw_log_error("Can't create port buffer: %m"); + return -errno; + } + } + return 0; +} + +static void setup_stream_ports(struct stream *s) +{ + struct impl *impl = s->impl; + uint32_t i; + for (i = 0; i < s->n_ports; i++) { + struct port *port = s->ports[i]; + if (s->direction == PW_DIRECTION_INPUT) { + if (ffado_streaming_set_playback_stream_buffer(impl->dev, i, port->buffer)) + pw_log_error("cannot configure port buffer for %s", port->name); + + if (ffado_streaming_playback_stream_onoff(impl->dev, i, 1)) + pw_log_error("cannot enable port %s", port->name); + } else { + if (ffado_streaming_set_capture_stream_buffer(impl->dev, i, port->buffer)) + pw_log_error("cannot configure port buffer for %s", port->name); + + if (ffado_streaming_capture_stream_onoff(impl->dev, i, 1)) + pw_log_error("cannot enable port %s", port->name); + } + } +} + +static struct spa_pod *make_props_param(struct spa_pod_builder *b, + struct volume *vol) +{ + return spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_mute, SPA_POD_Bool(vol->mute), + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, vol->n_volumes, vol->volumes)); +} + +static void parse_props(struct stream *s, const struct spa_pod *param) +{ + struct spa_pod_object *obj = (struct spa_pod_object *) param; + struct spa_pod_prop *prop; + uint8_t buffer[1024]; + struct spa_pod_builder b; + const struct spa_pod *params[1]; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_mute: + { + bool mute; + if (spa_pod_get_bool(&prop->value, &mute) == 0) + s->volume.mute = mute; + break; + } + case SPA_PROP_channelVolumes: + { + uint32_t n; + float vols[SPA_AUDIO_MAX_CHANNELS]; + if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + s->volume.n_volumes = n; + for (n = 0; n < s->volume.n_volumes; n++) + s->volume.volumes[n] = vols[n]; + } + break; + } + default: + break; + } + } + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[0] = make_props_param(&b, &s->volume); + + pw_filter_update_params(s->filter, NULL, params, 1); +} + +static void stream_param_changed(void *data, void *port_data, uint32_t id, + const struct spa_pod *param) +{ + struct stream *s = data; + + if (port_data != NULL) { + switch (id) { + case SPA_PARAM_Latency: + param_latency_changed(s, param, port_data); + break; + } + } else { + switch (id) { + case SPA_PARAM_PortConfig: + pw_log_debug("PortConfig"); + if (make_stream_ports(s) >= 0) { + s->ready = true; + check_start(s->impl); + } + break; + case SPA_PARAM_Props: + pw_log_debug("Props"); + parse_props(s, param); + break; + } + } +} + +static const struct pw_filter_events sink_events = { + PW_VERSION_FILTER_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .param_changed = stream_param_changed, + .io_changed = stream_io_changed, + .process = sink_process +}; + +static const struct pw_filter_events source_events = { + PW_VERSION_FILTER_EVENTS, + .destroy = stream_destroy, + .state_changed = stream_state_changed, + .param_changed = stream_param_changed, + .io_changed = stream_io_changed, + .process = source_process, +}; + +static int update_stream_format(struct stream *s, uint32_t samplerate) +{ + uint8_t buffer[1024]; + struct spa_pod_builder b; + uint32_t n_params; + const struct spa_pod *params[2]; + + if (s->info.rate == samplerate) + return 0; + + s->info.rate = samplerate; + + if (s->filter == NULL) + return 0; + + 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); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_Format, &s->info); + + return pw_filter_update_params(s->filter, NULL, params, n_params); +} + +static int make_stream(struct stream *s, const char *name) +{ + struct impl *impl = s->impl; + uint32_t n_params; + const struct spa_pod *params[4]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + s->filter = pw_filter_new(impl->core, name, pw_properties_copy(s->props)); + if (s->filter == NULL) + return -errno; + + spa_zero(s->listener); + if (s->direction == PW_DIRECTION_INPUT) { + pw_filter_add_listener(s->filter, &s->listener, + &sink_events, s); + } else { + pw_filter_add_listener(s->filter, &s->listener, + &source_events, s); + } + + reset_volume(&s->volume, s->info.channels); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + n_params = 0; + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_EnumFormat, &s->info); + params[n_params++] = spa_format_audio_raw_build(&b, + SPA_PARAM_Format, &s->info); + params[n_params++] = make_props_param(&b, &s->volume); + + return pw_filter_connect(s->filter, + PW_FILTER_FLAG_DRIVER | + PW_FILTER_FLAG_RT_PROCESS | + PW_FILTER_FLAG_CUSTOM_LATENCY, + params, n_params); +} + +static void destroy_stream(struct stream *s) +{ + if (s->filter) + pw_filter_destroy(s->filter); +} + +static void on_ffado_timeout(void *data, uint64_t expirations) +{ + struct impl *impl = data; + bool source_running, sink_running; + uint64_t nsec; + ffado_wait_response response; + + pw_log_trace_fp("wakeup %d", impl->rt.done); + + if (impl->freewheel) + return; + + if (!impl->rt.done) { + impl->rt.pw_xrun++; + impl->rt.new_xrun = true; + ffado_streaming_reset(impl->dev); + } +again: + pw_log_trace_fp("FFADO wait"); + response = ffado_streaming_wait(impl->dev); + nsec = get_time_ns(impl); + + switch (response) { + case ffado_wait_ok: + break; + case ffado_wait_xrun: + pw_log_debug("FFADO xrun"); + impl->rt.ffado_xrun++; + impl->rt.new_xrun = true; + goto again; + case ffado_wait_shutdown: + pw_log_info("FFADO shutdown"); + return; + case ffado_wait_error: + default: + pw_log_error("FFADO error"); + return; + } + source_running = impl->source.running && impl->sink.ready; + sink_running = impl->sink.running && impl->source.ready; + + impl->source.rt.transfered = false; + impl->sink.rt.transfered = false; + + if (!source_running) { + ffado_streaming_transfer_capture_buffers(impl->dev); + impl->source.rt.transfered = true; + } + if (!sink_running) + silence_playback(impl); + + pw_log_trace_fp("process %d %u %u %p %d %"PRIu64, + impl->device_options.period_size, source_running, + sink_running, impl->position, impl->frame_time, nsec); + + if (impl->rt.new_xrun) { + pw_log_warn("Xrun FFADO:%u PipeWire:%u source:%d sink:%d", + impl->rt.ffado_xrun, impl->rt.pw_xrun, source_running, sink_running); + impl->rt.new_xrun = false; + } + + if (impl->position) { + struct spa_io_clock *c = &impl->position->clock; + +#if 0 + if (c->target_duration != (uint64_t) impl->device_options.period_size) { + ffado_streaming_transfer_capture_buffers(impl->dev); + silence_playback(impl); + + if (ffado_streaming_set_period_size(impl->dev, c->target_duration) != 0) { + pw_log_warn("can't change period size"); + } else { + sleep(1); + impl->device_options.period_size = c->target_duration; + } + goto again; + } +#endif + + c->nsec = nsec; + c->rate = SPA_FRACTION(1, impl->device_options.sample_rate); + c->position += impl->device_options.period_size; + c->duration = impl->device_options.period_size; + c->delay = 0; + c->rate_diff = 1.0; + c->next_nsec = nsec + (c->duration * SPA_NSEC_PER_SEC) / impl->device_options.sample_rate; + + c->target_rate = c->rate; + c->target_duration = c->duration; + } + if (impl->mode & MODE_SOURCE && source_running) { + impl->rt.done = false; + impl->rt.triggered = true; + set_timeout(impl, nsec + SPA_NSEC_PER_SEC); + pw_filter_trigger_process(impl->source.filter); + } else if (impl->mode == MODE_SINK && sink_running) { + impl->rt.done = false; + impl->rt.triggered = true; + set_timeout(impl, nsec + SPA_NSEC_PER_SEC); + pw_filter_trigger_process(impl->sink.filter); + } else { + impl->rt.done = true; + set_timeout(impl, nsec); + } +} + +static void close_ffado_device(struct impl *impl) +{ + if (impl->dev == NULL) + return; + + stop_ffado_device(impl); + ffado_streaming_finish(impl->dev); + impl->dev = NULL; + + pw_log_info("closed FFADO device %s", impl->devices[0]); +} + +static int open_ffado_device(struct impl *impl) +{ + int32_t target_rate, target_period; + + if (impl->dev != NULL) + return 0; + + target_rate = impl->sample_rate; + target_period = impl->period_size; + + if (impl->position) { + struct spa_io_clock *c = &impl->position->clock; + if (target_rate == 0) + target_rate = c->target_rate.denom; + if (target_period == 0) + target_period = c->target_duration; + } + if (target_rate == 0) + target_rate = DEFAULT_SAMPLE_RATE; + if (target_period == 0) + target_period = DEFAULT_PERIOD_SIZE; + + spa_zero(impl->device_info); + impl->device_info.device_spec_strings = impl->devices; + impl->device_info.nb_device_spec_strings = impl->n_devices; + + spa_zero(impl->device_options); + impl->device_options.sample_rate = target_rate; + impl->device_options.period_size = target_period; + impl->device_options.nb_buffers = impl->n_periods; + impl->device_options.realtime = impl->realtime; + impl->device_options.packetizer_priority = impl->rtprio; + impl->device_options.verbose = impl->verbose; + impl->device_options.slave_mode = impl->slave_mode; + impl->device_options.snoop_mode = impl->snoop_mode; + + impl->dev = ffado_streaming_init(impl->device_info, impl->device_options); + if (impl->dev == NULL) { + pw_log_error("can't open FFADO device %s", impl->devices[0]); + return -EIO; + } + + if (impl->device_options.realtime) { + pw_log_info("Streaming thread running with Realtime scheduling, priority %d", + impl->device_options.packetizer_priority); + } else { + pw_log_info("Streaming thread running without Realtime scheduling"); + } + + ffado_streaming_set_audio_datatype(impl->dev, ffado_audio_datatype_float); + + impl->source.n_ports = ffado_streaming_get_nb_capture_streams(impl->dev); + impl->sink.n_ports = ffado_streaming_get_nb_playback_streams(impl->dev); + + if (impl->source.n_ports == 0 && impl->sink.n_ports == 0) { + close_ffado_device(impl); + return -EIO; + } + + update_stream_format(&impl->source, impl->device_options.sample_rate); + update_stream_format(&impl->sink, impl->device_options.sample_rate); + + pw_log_info("opened FFADO device %s source:%d sink:%d rate:%d period:%d %p", + impl->devices[0], impl->source.n_ports, impl->sink.n_ports, + impl->device_options.sample_rate, + impl->device_options.period_size, impl->position); + + return 0; +} + +static int probe_ffado_device(struct impl *impl) +{ + int res; + uint32_t i, n_channels; + struct port *port; + char name[256]; + + if ((res = open_ffado_device(impl)) < 0) + return res; + + n_channels = 0; + for (i = 0; i < impl->source.n_ports; i++) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + + port->direction = impl->source.direction; + port->stream_type = ffado_streaming_get_capture_stream_type(impl->dev, i); + ffado_streaming_get_capture_stream_name(impl->dev, i, name, sizeof(name)); + snprintf(port->name, sizeof(port->name), "%s_out", name); + + switch (port->stream_type) { + case ffado_stream_type_audio: + n_channels++; + break; + default: + break; + } + impl->source.ports[i] = port; + } + if (impl->source.info.channels != n_channels) { + impl->source.info.channels = n_channels; + for (i = 0; i < SPA_MIN(impl->source.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } + + n_channels = 0; + for (i = 0; i < impl->sink.n_ports; i++) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + + port->direction = impl->sink.direction; + port->stream_type = ffado_streaming_get_playback_stream_type(impl->dev, i); + ffado_streaming_get_playback_stream_name(impl->dev, i, name, sizeof(name)); + snprintf(port->name, sizeof(port->name), "%s_in", name); + + switch (port->stream_type) { + case ffado_stream_type_audio: + n_channels++; + break; + default: + break; + } + impl->sink.ports[i] = port; + } + if (impl->sink.info.channels != n_channels) { + impl->sink.info.channels = n_channels; + for (i = 0; i < SPA_MIN(impl->sink.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + impl->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } + + if (impl->mode & MODE_SINK) { + if ((res = make_stream(&impl->sink, "FFADO Sink")) < 0) + goto exit; + } + if (impl->mode & MODE_SOURCE) { + if ((res = make_stream(&impl->source, "FFADO Source")) < 0) + goto exit; + } +exit: + close_ffado_device(impl); + + return res; +} + + +static int start_ffado_device(struct impl *impl) +{ + int res; + + if (impl->started) + return 0; + + if ((res = open_ffado_device(impl)) < 0) + return res; + + setup_stream_ports(&impl->source); + setup_stream_ports(&impl->sink); + + if (ffado_streaming_prepare(impl->dev)) { + pw_log_error("Could not prepare streaming"); + schedule_reset_ffado_device(impl); + return -EIO; + } + + if (ffado_streaming_start(impl->dev)) { + pw_log_warn("Could not start FFADO streaming, try reset"); + schedule_reset_ffado_device(impl); + return -EIO; + } + pw_log_info("FFADO started streaming"); + + impl->started = true; + impl->rt.done = true; + set_timeout(impl, get_time_ns(impl)); + return 0; +} + +static int stop_ffado_device(struct impl *impl) +{ + if (!impl->started) + return 0; + + impl->started = false; + set_timeout(impl, 0); + if (ffado_streaming_stop(impl->dev)) + pw_log_error("Could not stop FFADO streaming"); + else + pw_log_info("FFADO stopped streaming"); + + close_ffado_device(impl); + + return 0; +} + + +static void do_reset_ffado(void *obj, void *data, int res, uint32_t id) +{ + struct impl *impl = obj; + + impl->reset_work_id = SPA_ID_INVALID; + close_ffado_device(impl); + open_ffado_device(impl); +} + +static void schedule_reset_ffado_device(struct impl *impl) +{ + if (impl->reset_work_id != SPA_ID_INVALID) + return; + + impl->reset_work_id = pw_work_queue_add(pw_context_get_work_queue(impl->context), + impl, 0, do_reset_ffado, NULL); +} + +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) +{ + uint32_t i; + + if (impl->reset_work_id != SPA_ID_INVALID) + pw_work_queue_cancel(pw_context_get_work_queue(impl->context), + impl, SPA_ID_INVALID); + + close_ffado_device(impl); + + destroy_stream(&impl->source); + destroy_stream(&impl->sink); + + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + if (impl->ffado_timer) + pw_loop_destroy_source(impl->data_loop, impl->ffado_timer); + + if (impl->data_loop) + pw_context_release_loop(impl->context, impl->data_loop); + + pw_properties_free(impl->sink.props); + pw_properties_free(impl->source.props); + pw_properties_free(impl->props); + + for (i = 0; i < impl->n_devices; i++) + free(impl->devices[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 void parse_devices(struct impl *impl, const char *val, size_t len) +{ + struct spa_json it[1]; + char v[FFADO_MAX_SPECSTRING_LENGTH]; + + if (spa_json_begin_array_relax(&it[0], val, len) <= 0) + return; + + impl->n_devices = 0; + while (spa_json_get_string(&it[0], v, sizeof(v)) > 0 && + impl->n_devices < FFADO_MAX_SPECSTRINGS) { + impl->devices[impl->n_devices++] = strdup(v); + } +} + +static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +{ + spa_audio_info_raw_init_dict_keys(info, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), + SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + &props->dict, + SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_POSITION, NULL); +} + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->sink.props, key) == NULL) + pw_properties_set(impl->sink.props, key, str); + if (pw_properties_get(impl->source.props, key) == NULL) + pw_properties_set(impl->source.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; + str = pw_properties_get(props, "ffado.devices"); + if (str == NULL) + str = DEFAULT_DEVICES; + parse_devices(impl, str, strlen(str)); + + impl->period_size = pw_properties_get_int32(props, + "ffado.period-size", DEFAULT_PERIOD_SIZE); + impl->n_periods = pw_properties_get_int32(props, + "ffado.period-num", DEFAULT_PERIOD_NUM); + impl->sample_rate = pw_properties_get_int32(props, + "ffado.sample-rate", DEFAULT_SAMPLE_RATE); + impl->slave_mode = pw_properties_get_bool(props, + "ffado.slave-mode", DEFAULT_SLAVE_MODE); + impl->snoop_mode = pw_properties_get_bool(props, + "ffado.snoop-mode", DEFAULT_SNOOP_MODE); + impl->verbose = pw_properties_get_uint32(props, + "ffado.verbose", DEFAULT_VERBOSE); + impl->rtprio = pw_properties_get_uint32(props, + "ffado.rtprio", DEFAULT_RTPRIO); + impl->realtime = pw_properties_get_bool(props, + "ffado.realtime", DEFAULT_REALTIME); + impl->input_latency = pw_properties_get_uint32(props, + "latency.internal.input", 0); + impl->output_latency = pw_properties_get_uint32(props, + "latency.internal.output", 0); + + impl->quantum_limit = pw_properties_get_uint32( + pw_context_get_properties(context), + "default.clock.quantum-limit", 8192u); + + impl->sink.props = pw_properties_new(NULL, NULL); + impl->source.props = pw_properties_new(NULL, NULL); + if (impl->source.props == NULL || impl->sink.props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto error; + } + + impl->module = module; + impl->context = context; + impl->main_loop = pw_context_get_main_loop(context); + impl->data_loop = pw_context_acquire_loop(context, &props->dict); + impl->system = impl->main_loop->system; + impl->reset_work_id = SPA_ID_INVALID; + + impl->source.impl = impl; + impl->source.direction = PW_DIRECTION_OUTPUT; + impl->sink.impl = impl; + impl->sink.direction = PW_DIRECTION_INPUT; + + impl->mode = MODE_DUPLEX; + if ((str = pw_properties_get(props, "driver.mode")) != NULL) { + if (spa_streq(str, "source")) { + impl->mode = MODE_SOURCE; + } else if (spa_streq(str, "sink")) { + impl->mode = MODE_SINK; + } else if (spa_streq(str, "duplex")) { + impl->mode = MODE_DUPLEX; + } else { + pw_log_error("invalid driver.mode '%s'", str); + res = -EINVAL; + goto error; + } + } + impl->ffado_timer = pw_loop_add_timer(impl->data_loop, on_ffado_timeout, impl); + if (impl->ffado_timer == NULL) { + pw_log_error("can't create ffado timer: %m"); + res = -errno; + goto error; + } + + pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); + 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_GROUP) == NULL) + pw_properties_set(props, PW_KEY_NODE_GROUP, "ffado-group"); + if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL) + pw_properties_set(props, PW_KEY_NODE_LINK_GROUP, "ffado-group"); + if (pw_properties_get(props, PW_KEY_NODE_PAUSE_ON_IDLE) == NULL) + pw_properties_set(props, PW_KEY_NODE_PAUSE_ON_IDLE, "false"); + + pw_properties_set(impl->sink.props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); + pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_DRIVER, "35000"); + pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_SESSION, "2000"); + pw_properties_set(impl->sink.props, PW_KEY_NODE_NAME, "ffado_sink"); + pw_properties_set(impl->sink.props, PW_KEY_NODE_DESCRIPTION, "FFADO Sink"); + + pw_properties_set(impl->source.props, PW_KEY_MEDIA_CLASS, "Audio/Source"); + pw_properties_set(impl->source.props, PW_KEY_PRIORITY_DRIVER, "35001"); + pw_properties_set(impl->source.props, PW_KEY_PRIORITY_SESSION, "2001"); + pw_properties_set(impl->source.props, PW_KEY_NODE_NAME, "ffado_source"); + pw_properties_set(impl->source.props, PW_KEY_NODE_DESCRIPTION, "FFADO Source"); + + 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, "source.props")) != NULL) + pw_properties_update_string(impl->source.props, str, strlen(str)); + + copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); + copy_props(impl, props, PW_KEY_NODE_LINK_GROUP); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_NODE_PAUSE_ON_IDLE); + + parse_audio_info(impl->source.props, &impl->source.info); + parse_audio_info(impl->sink.props, &impl->sink.info); + + 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 = probe_ffado_device(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-filter-chain.c b/src/modules/module-filter-chain.c new file mode 100644 index 0000000..1446cc4 --- /dev/null +++ b/src/modules/module-filter-chain.c @@ -0,0 +1,1531 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include +#include +#include +#include + +#include + +#define NAME "filter-chain" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +extern struct spa_handle_factory spa_filter_graph_factory; + +/** + * \page page_module_filter_chain Filter-Chain + * + * 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 Name + * + * `libpipewire-module-filter-chain` + * + * ## 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 = + * name = + * plugin = + * label =