From 7a46c07230b8d8108c0e8e80df4522d0ac116538 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:28:17 +0200 Subject: Adding upstream version 0.3.65. Signed-off-by: Daniel Baumann --- spa/examples/adapter-control.c | 771 +++ spa/examples/example-control.c | 560 ++ spa/examples/local-libcamera.c | 562 ++ spa/examples/local-v4l2.c | 553 ++ spa/examples/meson.build | 32 + spa/include/meson.build | 18 + spa/include/spa/buffer/alloc.h | 347 ++ spa/include/spa/buffer/buffer.h | 131 + spa/include/spa/buffer/meta.h | 192 + spa/include/spa/buffer/type-info.h | 94 + spa/include/spa/control/control.h | 62 + spa/include/spa/control/type-info.h | 61 + spa/include/spa/debug/buffer.h | 133 + spa/include/spa/debug/context.h | 62 + spa/include/spa/debug/dict.h | 62 + spa/include/spa/debug/format.h | 234 + spa/include/spa/debug/log.h | 103 + spa/include/spa/debug/mem.h | 71 + spa/include/spa/debug/node.h | 67 + spa/include/spa/debug/pod.h | 227 + spa/include/spa/debug/types.h | 127 + spa/include/spa/graph/graph.h | 365 ++ spa/include/spa/interfaces/audio/aec.h | 110 + spa/include/spa/monitor/device.h | 307 ++ spa/include/spa/monitor/event.h | 63 + spa/include/spa/monitor/type-info.h | 67 + spa/include/spa/monitor/utils.h | 106 + spa/include/spa/node/command.h | 73 + spa/include/spa/node/event.h | 64 + spa/include/spa/node/io.h | 304 ++ spa/include/spa/node/keys.h | 64 + spa/include/spa/node/node.h | 680 +++ spa/include/spa/node/type-info.h | 107 + spa/include/spa/node/utils.h | 158 + spa/include/spa/param/audio/aac-types.h | 58 + spa/include/spa/param/audio/aac-utils.h | 89 + spa/include/spa/param/audio/aac.h | 71 + spa/include/spa/param/audio/alac-utils.h | 80 + spa/include/spa/param/audio/alac.h | 49 + spa/include/spa/param/audio/amr-types.h | 52 + spa/include/spa/param/audio/amr-utils.h | 84 + spa/include/spa/param/audio/amr.h | 56 + spa/include/spa/param/audio/ape-utils.h | 80 + spa/include/spa/param/audio/ape.h | 49 + spa/include/spa/param/audio/compressed.h | 39 + spa/include/spa/param/audio/dsd-utils.h | 100 + spa/include/spa/param/audio/dsd.h | 81 + spa/include/spa/param/audio/dsp-utils.h | 75 + spa/include/spa/param/audio/dsp.h | 48 + spa/include/spa/param/audio/flac-utils.h | 80 + spa/include/spa/param/audio/flac.h | 49 + spa/include/spa/param/audio/format-utils.h | 141 + spa/include/spa/param/audio/format.h | 80 + spa/include/spa/param/audio/iec958-types.h | 59 + spa/include/spa/param/audio/iec958-utils.h | 79 + spa/include/spa/param/audio/iec958.h | 69 + spa/include/spa/param/audio/layout.h | 192 + spa/include/spa/param/audio/mp3-types.h | 54 + spa/include/spa/param/audio/mp3-utils.h | 80 + spa/include/spa/param/audio/mp3.h | 57 + spa/include/spa/param/audio/ra-utils.h | 80 + spa/include/spa/param/audio/ra.h | 49 + spa/include/spa/param/audio/raw-types.h | 278 + spa/include/spa/param/audio/raw-utils.h | 96 + spa/include/spa/param/audio/raw.h | 317 ++ spa/include/spa/param/audio/type-info.h | 35 + spa/include/spa/param/audio/vorbis-utils.h | 80 + spa/include/spa/param/audio/vorbis.h | 49 + spa/include/spa/param/audio/wma-types.h | 57 + spa/include/spa/param/audio/wma-utils.h | 93 + spa/include/spa/param/audio/wma.h | 67 + spa/include/spa/param/bluetooth/audio.h | 74 + spa/include/spa/param/bluetooth/type-info.h | 78 + spa/include/spa/param/buffers-types.h | 90 + spa/include/spa/param/buffers.h | 72 + spa/include/spa/param/format-types.h | 191 + spa/include/spa/param/format-utils.h | 58 + spa/include/spa/param/format.h | 176 + spa/include/spa/param/latency-types.h | 75 + spa/include/spa/param/latency-utils.h | 177 + spa/include/spa/param/latency.h | 89 + spa/include/spa/param/param-types.h | 115 + spa/include/spa/param/param.h | 107 + spa/include/spa/param/port-config-types.h | 73 + spa/include/spa/param/port-config.h | 66 + spa/include/spa/param/profile-types.h | 65 + spa/include/spa/param/profile.h | 72 + spa/include/spa/param/profiler-types.h | 60 + spa/include/spa/param/profiler.h | 97 + spa/include/spa/param/props-types.h | 119 + spa/include/spa/param/props.h | 132 + spa/include/spa/param/route-types.h | 71 + spa/include/spa/param/route.h | 69 + spa/include/spa/param/type-info.h | 38 + spa/include/spa/param/video/chroma.h | 64 + spa/include/spa/param/video/color.h | 125 + spa/include/spa/param/video/dsp-utils.h | 83 + spa/include/spa/param/video/dsp.h | 55 + spa/include/spa/param/video/encoded.h | 31 + spa/include/spa/param/video/format-utils.h | 84 + spa/include/spa/param/video/format.h | 61 + spa/include/spa/param/video/h264-utils.h | 90 + spa/include/spa/param/video/h264.h | 68 + spa/include/spa/param/video/mjpg-utils.h | 82 + spa/include/spa/param/video/mjpg.h | 53 + spa/include/spa/param/video/multiview.h | 134 + spa/include/spa/param/video/raw-types.h | 164 + spa/include/spa/param/video/raw-utils.h | 135 + spa/include/spa/param/video/raw.h | 221 + spa/include/spa/param/video/type-info.h | 30 + spa/include/spa/pod/builder.h | 701 +++ spa/include/spa/pod/command.h | 69 + spa/include/spa/pod/compare.h | 190 + spa/include/spa/pod/dynamic.h | 81 + spa/include/spa/pod/event.h | 68 + spa/include/spa/pod/filter.h | 481 ++ spa/include/spa/pod/iter.h | 475 ++ spa/include/spa/pod/parser.h | 594 +++ spa/include/spa/pod/pod.h | 246 + spa/include/spa/pod/vararg.h | 113 + spa/include/spa/support/cpu.h | 168 + spa/include/spa/support/dbus.h | 167 + spa/include/spa/support/i18n.h | 107 + spa/include/spa/support/log-impl.h | 143 + spa/include/spa/support/log.h | 321 ++ spa/include/spa/support/loop.h | 327 ++ spa/include/spa/support/plugin-loader.h | 101 + spa/include/spa/support/plugin.h | 229 + spa/include/spa/support/system.h | 165 + spa/include/spa/support/thread.h | 149 + spa/include/spa/utils/ansi.h | 114 + spa/include/spa/utils/defs.h | 399 ++ spa/include/spa/utils/dict.h | 120 + spa/include/spa/utils/dll.h | 71 + spa/include/spa/utils/enum-types.h | 65 + spa/include/spa/utils/hook.h | 472 ++ spa/include/spa/utils/json-pod.h | 177 + spa/include/spa/utils/json.h | 485 ++ spa/include/spa/utils/keys.h | 151 + spa/include/spa/utils/list.h | 166 + spa/include/spa/utils/names.h | 164 + spa/include/spa/utils/result.h | 72 + spa/include/spa/utils/ringbuffer.h | 188 + spa/include/spa/utils/string.h | 414 ++ spa/include/spa/utils/type-info.h | 118 + spa/include/spa/utils/type.h | 153 + spa/meson.build | 101 + spa/plugins/aec/aec-null.c | 175 + spa/plugins/aec/aec-webrtc.cpp | 276 + spa/plugins/aec/meson.build | 16 + spa/plugins/alsa/90-pipewire-alsa.rules | 214 + spa/plugins/alsa/acp-tool.c | 786 +++ spa/plugins/alsa/acp/acp.c | 1983 +++++++ spa/plugins/alsa/acp/acp.h | 308 ++ spa/plugins/alsa/acp/alsa-mixer.c | 5398 ++++++++++++++++++++ spa/plugins/alsa/acp/alsa-mixer.h | 459 ++ spa/plugins/alsa/acp/alsa-ucm.c | 2420 +++++++++ spa/plugins/alsa/acp/alsa-ucm.h | 301 ++ spa/plugins/alsa/acp/alsa-util.c | 1904 +++++++ spa/plugins/alsa/acp/alsa-util.h | 176 + spa/plugins/alsa/acp/array.h | 150 + spa/plugins/alsa/acp/card.h | 75 + spa/plugins/alsa/acp/channelmap.h | 476 ++ spa/plugins/alsa/acp/compat.c | 210 + spa/plugins/alsa/acp/compat.h | 656 +++ 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 | 159 + spa/plugins/alsa/acp/hashmap.h | 219 + spa/plugins/alsa/acp/idxset.h | 200 + spa/plugins/alsa/acp/llist.h | 109 + spa/plugins/alsa/acp/meson.build | 22 + spa/plugins/alsa/acp/proplist.h | 211 + spa/plugins/alsa/acp/volume.h | 207 + spa/plugins/alsa/alsa-acp-device.c | 1133 ++++ spa/plugins/alsa/alsa-compress-offload-sink.c | 1143 +++++ spa/plugins/alsa/alsa-pcm-device.c | 581 +++ spa/plugins/alsa/alsa-pcm-sink.c | 1014 ++++ spa/plugins/alsa/alsa-pcm-source.c | 964 ++++ spa/plugins/alsa/alsa-pcm.c | 2696 ++++++++++ spa/plugins/alsa/alsa-pcm.h | 374 ++ spa/plugins/alsa/alsa-seq-bridge.c | 1006 ++++ spa/plugins/alsa/alsa-seq.c | 983 ++++ spa/plugins/alsa/alsa-seq.h | 199 + spa/plugins/alsa/alsa-udev.c | 1014 ++++ spa/plugins/alsa/alsa.c | 80 + spa/plugins/alsa/alsa.h | 39 + spa/plugins/alsa/meson.build | 60 + spa/plugins/alsa/mixer/meson.build | 7 + spa/plugins/alsa/mixer/paths/analog-input-aux.conf | 65 + .../alsa/mixer/paths/analog-input-dock-mic.conf | 104 + spa/plugins/alsa/mixer/paths/analog-input-fm.conf | 65 + .../alsa/mixer/paths/analog-input-front-mic.conf | 104 + .../mixer/paths/analog-input-headphone-mic.conf | 102 + .../alsa/mixer/paths/analog-input-headset-mic.conf | 114 + .../paths/analog-input-internal-mic-always.conf | 133 + .../mixer/paths/analog-input-internal-mic.conf | 154 + .../alsa/mixer/paths/analog-input-linein.conf | 144 + .../alsa/mixer/paths/analog-input-mic-line.conf | 66 + spa/plugins/alsa/mixer/paths/analog-input-mic.conf | 141 + .../alsa/mixer/paths/analog-input-mic.conf.common | 60 + .../alsa/mixer/paths/analog-input-rear-mic.conf | 107 + .../alsa/mixer/paths/analog-input-tvtuner.conf | 65 + .../alsa/mixer/paths/analog-input-video.conf | 64 + spa/plugins/alsa/mixer/paths/analog-input.conf | 102 + .../alsa/mixer/paths/analog-input.conf.common | 289 ++ .../alsa/mixer/paths/analog-output-chat.conf | 5 + .../mixer/paths/analog-output-headphones-2.conf | 118 + .../alsa/mixer/paths/analog-output-headphones.conf | 180 + .../alsa/mixer/paths/analog-output-lineout.conf | 214 + .../alsa/mixer/paths/analog-output-mono.conf | 99 + .../mixer/paths/analog-output-speaker-always.conf | 187 + .../alsa/mixer/paths/analog-output-speaker.conf | 246 + spa/plugins/alsa/mixer/paths/analog-output.conf | 88 + .../alsa/mixer/paths/analog-output.conf.common | 199 + spa/plugins/alsa/mixer/paths/hdmi-output-0.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-1.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-10.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-2.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-3.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-4.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-5.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-6.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-7.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-8.conf | 12 + spa/plugins/alsa/mixer/paths/hdmi-output-9.conf | 12 + .../alsa/mixer/paths/iec958-stereo-input.conf | 20 + .../alsa/mixer/paths/iec958-stereo-output.conf | 18 + .../steelseries-arctis-output-chat-common.conf | 27 + .../steelseries-arctis-output-game-common.conf | 27 + .../alsa/mixer/paths/usb-gaming-headset-input.conf | 34 + .../paths/usb-gaming-headset-output-mono.conf | 34 + .../paths/usb-gaming-headset-output-stereo.conf | 34 + .../alsa/mixer/paths/virtual-surround-7.1.conf | 5 + .../alsa/mixer/profile-sets/analog-only.conf | 102 + .../alsa/mixer/profile-sets/asus-xonar-se.conf | 93 + spa/plugins/alsa/mixer/profile-sets/audigy.conf | 94 + .../cmedia-high-speed-true-hdaudio.conf | 66 + spa/plugins/alsa/mixer/profile-sets/default.conf | 580 +++ .../profile-sets/dell-dock-tb16-usb-audio.conf | 55 + .../profile-sets/force-speaker-and-int-mic.conf | 153 + .../alsa/mixer/profile-sets/force-speaker.conf | 152 + .../mixer/profile-sets/hp-tbt-dock-120w-g2.conf | 35 + .../profile-sets/hp-tbt-dock-audio-module.conf | 36 + .../alsa/mixer/profile-sets/kinect-audio.conf | 38 + .../mixer/profile-sets/maudio-fasttrack-pro.conf | 86 + .../profile-sets/native-instruments-audio4dj.conf | 90 + .../profile-sets/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 + .../mixer/profile-sets/sb-omni-surround-5.1.conf | 112 + .../alsa/mixer/profile-sets/sennheiser-gsx.conf | 58 + .../mixer/profile-sets/simple-headphones-mic.conf | 42 + .../steelseries-arctis-common-usb-audio.conf | 23 + .../profile-sets/texas-instruments-pcm2902.conf | 75 + .../mixer/profile-sets/usb-gaming-headset.conf | 64 + .../mixer/samples/ATI IXP--Realtek ALC655 rev 0 | 150 + .../alsa/mixer/samples/Brooktree Bt878--Bt87x | 24 + .../Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3 | 135 + .../alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI | 4 + .../mixer/samples/HDA Intel--Analog Devices AD1981 | 62 + .../alsa/mixer/samples/HDA Intel--Realtek ALC889A | 113 + .../Intel 82801CA-ICH3--Analog Devices AD1881A | 128 + .../mixer/samples/Logitech USB Speaker--USB Mixer | 27 + .../alsa/mixer/samples/USB Audio--USB Mixer | 37 + .../samples/USB Device 0x46d:0x9a4--USB Mixer | 5 + .../mixer/samples/VIA 8237--Analog Devices AD1888 | 211 + .../VIA 8237--C-Media Electronics CMI9761A+ | 160 + spa/plugins/alsa/test-hw-params.c | 173 + spa/plugins/alsa/test-timer.c | 310 ++ spa/plugins/audioconvert/audioadapter.c | 1733 +++++++ spa/plugins/audioconvert/audioconvert.c | 2945 +++++++++++ spa/plugins/audioconvert/benchmark-fmt-ops.c | 323 ++ spa/plugins/audioconvert/benchmark-resample.c | 204 + spa/plugins/audioconvert/biquad.c | 111 + spa/plugins/audioconvert/biquad.h | 45 + spa/plugins/audioconvert/channelmix-ops-c.c | 533 ++ spa/plugins/audioconvert/channelmix-ops-sse.c | 522 ++ spa/plugins/audioconvert/channelmix-ops.c | 668 +++ spa/plugins/audioconvert/channelmix-ops.h | 160 + spa/plugins/audioconvert/crossover.c | 70 + spa/plugins/audioconvert/crossover.h | 31 + spa/plugins/audioconvert/delay.h | 72 + spa/plugins/audioconvert/fmt-ops-avx2.c | 1043 ++++ spa/plugins/audioconvert/fmt-ops-c.c | 433 ++ spa/plugins/audioconvert/fmt-ops-neon.c | 487 ++ spa/plugins/audioconvert/fmt-ops-sse2.c | 1438 ++++++ spa/plugins/audioconvert/fmt-ops-sse41.c | 86 + spa/plugins/audioconvert/fmt-ops-ssse3.c | 111 + spa/plugins/audioconvert/fmt-ops.c | 564 ++ spa/plugins/audioconvert/fmt-ops.h | 488 ++ spa/plugins/audioconvert/hilbert.h | 69 + spa/plugins/audioconvert/law.h | 2163 ++++++++ spa/plugins/audioconvert/meson.build | 206 + spa/plugins/audioconvert/peaks-ops-c.c | 50 + spa/plugins/audioconvert/peaks-ops-sse.c | 122 + spa/plugins/audioconvert/peaks-ops.c | 89 + spa/plugins/audioconvert/peaks-ops.h | 72 + spa/plugins/audioconvert/plugin.c | 50 + spa/plugins/audioconvert/resample-native-avx.c | 94 + spa/plugins/audioconvert/resample-native-c.c | 65 + spa/plugins/audioconvert/resample-native-impl.h | 191 + spa/plugins/audioconvert/resample-native-neon.c | 218 + spa/plugins/audioconvert/resample-native-sse.c | 94 + spa/plugins/audioconvert/resample-native-ssse3.c | 115 + spa/plugins/audioconvert/resample-native.c | 400 ++ spa/plugins/audioconvert/resample-peaks.c | 148 + spa/plugins/audioconvert/resample.h | 69 + spa/plugins/audioconvert/spa-resample.c | 341 ++ spa/plugins/audioconvert/test-audioadapter.c | 302 ++ spa/plugins/audioconvert/test-audioconvert.c | 1158 +++++ spa/plugins/audioconvert/test-channelmix.c | 374 ++ spa/plugins/audioconvert/test-fmt-ops.c | 798 +++ spa/plugins/audioconvert/test-helper.h | 97 + spa/plugins/audioconvert/test-peaks.c | 128 + spa/plugins/audioconvert/test-resample.c | 177 + spa/plugins/audioconvert/test-source.c | 931 ++++ spa/plugins/audioconvert/volume-ops-c.c | 45 + spa/plugins/audioconvert/volume-ops-sse.c | 66 + spa/plugins/audioconvert/volume-ops.c | 84 + spa/plugins/audioconvert/volume-ops.h | 68 + spa/plugins/audiomixer/audiomixer.c | 990 ++++ spa/plugins/audiomixer/benchmark-mix-ops.c | 222 + spa/plugins/audiomixer/meson.build | 126 + spa/plugins/audiomixer/mix-ops-avx.c | 88 + spa/plugins/audiomixer/mix-ops-c.c | 68 + spa/plugins/audiomixer/mix-ops-sse.c | 87 + spa/plugins/audiomixer/mix-ops-sse2.c | 87 + spa/plugins/audiomixer/mix-ops.c | 136 + spa/plugins/audiomixer/mix-ops.h | 169 + spa/plugins/audiomixer/mixer-dsp.c | 927 ++++ spa/plugins/audiomixer/plugin.c | 50 + spa/plugins/audiomixer/test-helper.h | 97 + spa/plugins/audiomixer/test-mix-ops.c | 293 ++ spa/plugins/audiotestsrc/audiotestsrc.c | 1159 +++++ spa/plugins/audiotestsrc/meson.build | 7 + spa/plugins/audiotestsrc/plugin.c | 46 + spa/plugins/audiotestsrc/render.c | 64 + spa/plugins/avb/avb-pcm-sink.c | 910 ++++ spa/plugins/avb/avb-pcm-source.c | 910 ++++ spa/plugins/avb/avb-pcm.c | 1227 +++++ spa/plugins/avb/avb-pcm.h | 343 ++ spa/plugins/avb/avb.c | 54 + spa/plugins/avb/avb.h | 39 + spa/plugins/avb/avbtp/packets.h | 220 + 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/a2dp-codec-aac.c | 661 +++ spa/plugins/bluez5/a2dp-codec-aptx.c | 748 +++ spa/plugins/bluez5/a2dp-codec-caps.h | 460 ++ spa/plugins/bluez5/a2dp-codec-faststream.c | 640 +++ spa/plugins/bluez5/a2dp-codec-lc3plus.c | 790 +++ spa/plugins/bluez5/a2dp-codec-ldac.c | 604 +++ spa/plugins/bluez5/a2dp-codec-opus.c | 1444 ++++++ spa/plugins/bluez5/a2dp-codec-sbc.c | 689 +++ spa/plugins/bluez5/backend-hsphfpd.c | 1588 ++++++ spa/plugins/bluez5/backend-native.c | 2838 ++++++++++ spa/plugins/bluez5/backend-ofono.c | 947 ++++ spa/plugins/bluez5/bap-codec-caps.h | 142 + spa/plugins/bluez5/bap-codec-lc3.c | 859 ++++ spa/plugins/bluez5/bluez-hardware.conf | 103 + spa/plugins/bluez5/bluez5-dbus.c | 5182 +++++++++++++++++++ spa/plugins/bluez5/bluez5-device.c | 2398 +++++++++ spa/plugins/bluez5/codec-loader.c | 234 + spa/plugins/bluez5/codec-loader.h | 39 + spa/plugins/bluez5/dbus-monitor.c | 265 + spa/plugins/bluez5/dbus-monitor.h | 83 + spa/plugins/bluez5/decode-buffer.h | 486 ++ spa/plugins/bluez5/defs.h | 822 +++ spa/plugins/bluez5/hci.c | 93 + spa/plugins/bluez5/media-codecs.c | 212 + spa/plugins/bluez5/media-codecs.h | 178 + spa/plugins/bluez5/media-sink.c | 1868 +++++++ spa/plugins/bluez5/media-source.c | 1707 +++++++ spa/plugins/bluez5/meson.build | 206 + spa/plugins/bluez5/midi-enum.c | 887 ++++ spa/plugins/bluez5/midi-node.c | 2151 ++++++++ spa/plugins/bluez5/midi-parser.c | 295 ++ spa/plugins/bluez5/midi-server.c | 581 +++ spa/plugins/bluez5/midi.h | 142 + spa/plugins/bluez5/modemmanager.c | 1249 +++++ spa/plugins/bluez5/modemmanager.h | 161 + spa/plugins/bluez5/org.bluez.xml | 71 + spa/plugins/bluez5/player.c | 428 ++ spa/plugins/bluez5/player.h | 51 + spa/plugins/bluez5/plugin.c | 83 + spa/plugins/bluez5/quirks.c | 406 ++ spa/plugins/bluez5/rtp.h | 74 + spa/plugins/bluez5/sco-io.c | 289 ++ spa/plugins/bluez5/sco-sink.c | 1517 ++++++ spa/plugins/bluez5/sco-source.c | 1592 ++++++ spa/plugins/bluez5/test-midi.c | 299 ++ spa/plugins/bluez5/upower.c | 311 ++ spa/plugins/bluez5/upower.h | 36 + spa/plugins/control/meson.build | 10 + spa/plugins/control/mixer.c | 869 ++++ spa/plugins/control/plugin.c | 46 + spa/plugins/ffmpeg/ffmpeg-dec.c | 529 ++ spa/plugins/ffmpeg/ffmpeg-enc.c | 507 ++ spa/plugins/ffmpeg/ffmpeg.c | 160 + spa/plugins/ffmpeg/ffmpeg.h | 20 + spa/plugins/ffmpeg/meson.build | 9 + spa/plugins/jack/jack-client.c | 113 + spa/plugins/jack/jack-client.h | 84 + spa/plugins/jack/jack-device.c | 457 ++ spa/plugins/jack/jack-sink.c | 924 ++++ spa/plugins/jack/jack-source.c | 946 ++++ spa/plugins/jack/meson.build | 12 + spa/plugins/jack/plugin.c | 54 + spa/plugins/libcamera/libcamera-client.c | 247 + spa/plugins/libcamera/libcamera-device.cpp | 332 ++ spa/plugins/libcamera/libcamera-manager.cpp | 488 ++ spa/plugins/libcamera/libcamera-manager.hpp | 29 + spa/plugins/libcamera/libcamera-source.cpp | 1073 ++++ spa/plugins/libcamera/libcamera-utils.cpp | 990 ++++ spa/plugins/libcamera/libcamera.c | 57 + spa/plugins/libcamera/libcamera.h | 51 + spa/plugins/libcamera/meson.build | 17 + spa/plugins/meson.build | 58 + spa/plugins/support/cpu-arm.c | 137 + spa/plugins/support/cpu-x86.c | 204 + spa/plugins/support/cpu.c | 313 ++ spa/plugins/support/dbus.c | 592 +++ spa/plugins/support/evl-plugin.c | 47 + spa/plugins/support/evl-system.c | 460 ++ spa/plugins/support/journal.c | 341 ++ spa/plugins/support/log-patterns.c | 108 + spa/plugins/support/log-patterns.h | 13 + spa/plugins/support/logger.c | 415 ++ spa/plugins/support/loop.c | 1024 ++++ spa/plugins/support/meson.build | 70 + spa/plugins/support/node-driver.c | 492 ++ spa/plugins/support/null-audio-sink.c | 1001 ++++ spa/plugins/support/plugin.c | 67 + spa/plugins/support/system.c | 384 ++ spa/plugins/test/fakesink.c | 846 +++ spa/plugins/test/fakesrc.c | 876 ++++ spa/plugins/test/meson.build | 7 + spa/plugins/test/plugin.c | 50 + spa/plugins/v4l2/meson.build | 10 + spa/plugins/v4l2/v4l2-device.c | 291 ++ spa/plugins/v4l2/v4l2-source.c | 1055 ++++ spa/plugins/v4l2/v4l2-udev.c | 754 +++ spa/plugins/v4l2/v4l2-utils.c | 1743 +++++++ spa/plugins/v4l2/v4l2.c | 59 + spa/plugins/v4l2/v4l2.h | 49 + spa/plugins/videoconvert/meson.build | 15 + spa/plugins/videoconvert/plugin.c | 46 + spa/plugins/videoconvert/videoadapter.c | 1671 ++++++ spa/plugins/videotestsrc/draw.c | 288 ++ spa/plugins/videotestsrc/meson.build | 7 + spa/plugins/videotestsrc/plugin.c | 46 + spa/plugins/videotestsrc/videotestsrc.c | 999 ++++ spa/plugins/volume/meson.build | 7 + spa/plugins/volume/plugin.c | 46 + spa/plugins/volume/volume.c | 894 ++++ spa/plugins/vulkan/meson.build | 12 + spa/plugins/vulkan/plugin.c | 50 + spa/plugins/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/vulkan-compute-filter.c | 808 +++ spa/plugins/vulkan/vulkan-compute-source.c | 1016 ++++ spa/plugins/vulkan/vulkan-utils.c | 758 +++ spa/plugins/vulkan/vulkan-utils.h | 86 + spa/tests/benchmark-dict.c | 145 + spa/tests/benchmark-pod.c | 294 ++ spa/tests/meson.build | 58 + spa/tests/spa-include-test-template.c | 5 + spa/tests/stress-ringbuffer.c | 147 + spa/tools/meson.build | 11 + spa/tools/spa-inspect.c | 319 ++ spa/tools/spa-json-dump.c | 165 + spa/tools/spa-monitor.c | 229 + 484 files changed, 153197 insertions(+) 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/meson.build 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/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/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/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/ra-utils.h create mode 100644 spa/include/spa/param/audio/ra.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/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/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/enum-types.h create mode 100644 spa/include/spa/utils/hook.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/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/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-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/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/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/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/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/delay.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-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.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-helper.h create mode 100644 spa/plugins/audioconvert/test-peaks.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/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-helper.h 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/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.c create mode 100644 spa/plugins/bluez5/a2dp-codec-sbc.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/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/hci.c 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/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/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/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-client.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-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/log-patterns.c create mode 100644 spa/plugins/support/log-patterns.h 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/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/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/meson.build 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/vulkan-compute-filter.c create mode 100644 spa/plugins/vulkan/vulkan-compute-source.c 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 (limited to 'spa') diff --git a/spa/examples/adapter-control.c b/spa/examples/adapter-control.c new file mode 100644 index 0000000..7caf32f --- /dev/null +++ b/spa/examples/adapter-control.c @@ -0,0 +1,771 @@ +/* Spa + * + * Copyright © 2020 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + [title] + Running audioadapter nodes. + [title] + [doc] + Runs an output audioadapter using audiotestsrc as follower + with an input audioadapter using alsa-pcm-sink as follower + for easy testing. + [doc] + */ + +#include "config.h" + +#include +#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 + + +struct buffer { + struct spa_buffer buffer; + struct spa_meta metas[1]; + struct spa_meta_header header; + struct spa_data datas[1]; + struct spa_chunk chunks[1]; +}; + +struct data { + const char *plugin_dir; + struct spa_log *log; + struct spa_system *system; + struct spa_loop *loop; + struct spa_loop_control *control; + struct spa_support support[5]; + uint32_t n_support; + + struct spa_graph graph; + struct spa_graph_state graph_state; + struct spa_graph_node graph_source_node; + struct spa_graph_node graph_sink_node; + struct spa_graph_state graph_source_state; + struct spa_graph_state graph_sink_state; + struct spa_graph_port graph_source_port_0; + struct spa_graph_port graph_sink_port_0; + + struct spa_node *source_follower_node; // audiotestsrc + struct spa_node *source_node; // adapter for audiotestsrc + struct spa_node *sink_follower_node; // alsa-pcm-sink + struct spa_node *sink_node; // adapter for alsa-pcm-sink + + struct spa_io_position position; + struct spa_io_buffers source_sink_io[1]; + struct spa_buffer *source_buffers[1]; + struct buffer source_buffer[1]; + + struct spa_io_buffers control_io; + struct spa_buffer *control_buffers[1]; + struct buffer control_buffer[1]; + + int buffer_count; + bool start_fade_in; + double volume_accum; + uint32_t volume_offs; + + bool running; + pthread_t thread; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + char *path; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) + return -ENOMEM; + + hnd = dlopen(path, RTLD_NOW); + free(path); + + if (hnd == NULL) { + printf("can't load %s: %s\n", lib, dlerror()); + return -ENOENT; + } + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + res = -ENOENT; + goto exit_cleanup; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (factory->version < 1) + continue; + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + NULL, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + goto exit_cleanup; + } + return 0; + } + return -EBADF; + +exit_cleanup: + dlclose(hnd); + return res; +} + +int init_data(struct data *data) +{ + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data->plugin_dir = str; + + /* start not doing fade-in */ + data->start_fade_in = true; + data->volume_accum = 0.0; + data->volume_offs = 0; + + /* init the graph */ + spa_graph_init(&data->graph, &data->graph_state); + + /* set the default log */ + data->log = &default_log.log; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log); + + /* load and set support system */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data->system = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data->system); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data->system); + + /* load and set support loop and loop control */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data->loop); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data->loop); + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->control = iface; + + if ((str = getenv("SPA_DEBUG"))) + data->log->level = atoi(str); + + return 0; +} + +static int make_node(struct data *data, struct spa_node **node, const char *lib, + const char *name, const struct spa_dict *props) +{ + struct spa_handle *handle; + int res = 0; + void *hnd = NULL; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + char *path; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) + return -ENOMEM; + + hnd = dlopen(path, RTLD_NOW); + free(path); + + if (hnd == NULL) { + printf("can't load %s: %s\n", lib, dlerror()); + return -ENOENT; + } + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + res = -ENOENT; + goto exit_cleanup; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + void *iface; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (factory->version < 1) + continue; + if (!spa_streq(factory->name, name)) + continue; + + handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = + spa_handle_factory_init(factory, handle, props, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + goto exit_cleanup; + } + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) { + printf("can't get interface %d\n", res); + goto exit_cleanup; + } + *node = iface; + return 0; + } + return -EBADF; + +exit_cleanup: + dlclose(hnd); + return res; +} + +static int fade_in(struct data *data) +{ + struct spa_pod_builder b; + struct spa_pod_frame f[1]; + void *buffer = data->control_buffer->datas[0].data; + uint32_t buffer_size = data->control_buffer->datas[0].maxsize; + data->control_buffer->datas[0].chunk[0].size = buffer_size; + + printf ("fading in\n"); + + spa_pod_builder_init(&b, buffer, buffer_size); + spa_pod_builder_push_sequence(&b, &f[0], 0); + data->volume_offs = 0; + do { + spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, SPA_POD_Float(data->volume_accum)); + data->volume_accum += 0.003; + data->volume_offs += 200; + } while (data->volume_accum < 1.0); + spa_pod_builder_pop(&b, &f[0]); + + return 0; +} + +static int fade_out(struct data *data) +{ + struct spa_pod_builder b; + struct spa_pod_frame f[1]; + void *buffer = data->control_buffer->datas[0].data; + uint32_t buffer_size = data->control_buffer->datas[0].maxsize; + data->control_buffer->datas[0].chunk[0].size = buffer_size; + + printf ("fading out\n"); + + spa_pod_builder_init(&b, buffer, buffer_size); + spa_pod_builder_push_sequence(&b, &f[0], 0); + data->volume_offs = 200; + do { + spa_pod_builder_control(&b, data->volume_offs, SPA_CONTROL_Properties); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_volume, SPA_POD_Float(data->volume_accum)); + data->volume_accum -= 0.003; + data->volume_offs += 200; + } while (data->volume_accum > 0.0); + spa_pod_builder_pop(&b, &f[0]); + + return 0; +} + +static void do_fade(struct data *data) +{ + switch (data->control_io.status) { + case SPA_STATUS_OK: + case SPA_STATUS_NEED_DATA: + break; + case SPA_STATUS_HAVE_DATA: + case SPA_STATUS_STOPPED: + default: + return; + } + + /* fade */ + if (data->start_fade_in) + fade_in(data); + else + fade_out(data); + + data->control_io.status = SPA_STATUS_HAVE_DATA; + data->control_io.buffer_id = 0; + + /* alternate */ + data->start_fade_in = !data->start_fade_in; +} + +static int on_sink_node_ready(void *_data, int status) +{ + struct data *data = _data; + + /* only do fade in/out when buffer count is 0 */ + if (data->buffer_count == 0) + do_fade(data); + + /* update buffer count */ + data->buffer_count++; + if (data->buffer_count > 64) + data->buffer_count = 0; + + spa_graph_node_process(&data->graph_source_node); + spa_graph_node_process(&data->graph_sink_node); + return 0; +} + +static const struct spa_node_callbacks sink_node_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = on_sink_node_ready, +}; + +static int make_nodes(struct data *data, const char *device) +{ + int res = 0; + struct spa_pod *props; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + char value[32]; + struct spa_dict_item items[2]; + struct spa_audio_info_raw info; + struct spa_pod *param; + + items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192"); + + /* make the source node (audiotestsrc) */ + if ((res = make_node(data, &data->source_follower_node, + "audiotestsrc/libspa-audiotestsrc.so", + "audiotestsrc", + &SPA_DICT_INIT(items, 1))) < 0) { + printf("can't create source follower node (audiotestsrc): %d\n", res); + return res; + } + + /* set the format on the source */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, 0, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .rate = 48000, + .channels = 2 )); + if ((res = spa_node_port_set_param(data->source_follower_node, + SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, param)) < 0) { + printf("can't set format on follower node (audiotestsrc): %d\n", res); + return res; + } + + /* make the sink adapter node */ + snprintf(value, sizeof(value), "pointer:%p", data->source_follower_node); + items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value); + if ((res = make_node(data, &data->source_node, + "audioconvert/libspa-audioconvert.so", + SPA_NAME_AUDIO_ADAPT, + &SPA_DICT_INIT(items, 2))) < 0) { + printf("can't create source adapter node: %d\n", res); + return res; + } + + /* setup the source node props */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_frequency, SPA_POD_Float(600.0), + SPA_PROP_volume, SPA_POD_Float(0.5), + SPA_PROP_live, SPA_POD_Bool(false)); + if ((res = spa_node_set_param(data->source_node, SPA_PARAM_Props, 0, props)) < 0) { + printf("can't setup source follower node %d\n", res); + return res; + } + + /* setup the source node port config */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.channels = 1; + info.rate = 48000; + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param) < 0)) { + printf("can't setup source node %d\n", res); + return res; + } + + /* make the sink follower node (alsa-pcm-sink) */ + if ((res = make_node(data, &data->sink_follower_node, + "alsa/libspa-alsa.so", + SPA_NAME_API_ALSA_PCM_SINK, + &SPA_DICT_INIT(items, 1))) < 0) { + printf("can't create sink follower node (alsa-pcm-sink): %d\n", res); + return res; + } + + /* make the sink adapter node */ + snprintf(value, sizeof(value), "pointer:%p", data->sink_follower_node); + items[1] = SPA_DICT_ITEM_INIT("audio.adapt.follower", value); + if ((res = make_node(data, &data->sink_node, + "audioconvert/libspa-audioconvert.so", + SPA_NAME_AUDIO_ADAPT, + &SPA_DICT_INIT(items, 2))) < 0) { + printf("can't create sink adapter node: %d\n", res); + return res; + } + + /* add sink follower node callbacks */ + spa_node_set_callbacks(data->sink_node, &sink_node_callbacks, data); + + /* setup the sink node props */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_device, SPA_POD_String(device ? device : "hw:0"), + SPA_PROP_minLatency, SPA_POD_Int(MIN_LATENCY)); + if ((res = spa_node_set_param(data->sink_follower_node, SPA_PARAM_Props, 0, props)) < 0) { + printf("can't setup sink follower node %d\n", res); + return res; + } + + /* setup the sink node port config */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.channels = 1; + info.rate = 48000; + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(true), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param) < 0)) { + printf("can't setup sink node %d\n", res); + return res; + } + + /* set io buffers on source and sink nodes */ + data->source_sink_io[0] = SPA_IO_BUFFERS_INIT; + if ((res = spa_node_port_set_io(data->source_node, + SPA_DIRECTION_OUTPUT, 0, + SPA_IO_Buffers, + &data->source_sink_io[0], sizeof(data->source_sink_io[0]))) < 0) { + printf("can't set io buffers on port 0 of source node: %d\n", res); + return res; + } + if ((res = spa_node_port_set_io(data->sink_node, + SPA_DIRECTION_INPUT, 0, + SPA_IO_Buffers, + &data->source_sink_io[0], sizeof(data->source_sink_io[0]))) < 0) { + printf("can't set io buffers on port 0 of sink node: %d\n", res); + return res; + } + /* set io position and clock on source and sink nodes */ + data->position.clock.rate = SPA_FRACTION(1, 48000); + data->position.clock.duration = 1024; + if ((res = spa_node_set_io(data->source_node, + SPA_IO_Position, + &data->position, sizeof(data->position))) < 0) { + printf("can't set io position on source node: %d\n", res); + return res; + } + if ((res = spa_node_set_io(data->sink_node, + SPA_IO_Position, + &data->position, sizeof(data->position))) < 0) { + printf("can't set io position on sink node: %d\n", res); + return res; + } + if ((res = spa_node_set_io(data->source_node, + SPA_IO_Clock, + &data->position.clock, sizeof(data->position.clock))) < 0) { + printf("can't set io clock on source node: %d\n", res); + return res; + } + if ((res = spa_node_set_io(data->sink_node, + SPA_IO_Clock, + &data->position.clock, sizeof(data->position.clock))) < 0) { + printf("can't set io clock on sink node: %d\n", res); + return res; + } + + /* set io buffers on control port of sink node */ + if ((res = spa_node_port_set_io(data->sink_node, + SPA_DIRECTION_INPUT, 1, + SPA_IO_Buffers, + &data->control_io, sizeof(data->control_io))) < 0) { + printf("can't set io buffers on control port 1 of sink node\n"); + return res; + } + + /* add source node to the graph */ + spa_graph_node_init(&data->graph_source_node, &data->graph_source_state); + spa_graph_node_set_callbacks(&data->graph_source_node, &spa_graph_node_impl_default, data->source_node); + spa_graph_node_add(&data->graph, &data->graph_source_node); + spa_graph_port_init(&data->graph_source_port_0, SPA_DIRECTION_OUTPUT, 0, 0); + spa_graph_port_add(&data->graph_source_node, &data->graph_source_port_0); + + /* add sink node to the graph */ + spa_graph_node_init(&data->graph_sink_node, &data->graph_sink_state); + spa_graph_node_set_callbacks(&data->graph_sink_node, &spa_graph_node_impl_default, data->sink_node); + spa_graph_node_add(&data->graph, &data->graph_sink_node); + spa_graph_port_init(&data->graph_sink_port_0, SPA_DIRECTION_INPUT, 0, 0); + spa_graph_port_add(&data->graph_sink_node, &data->graph_sink_port_0); + + /* link source and sink nodes */ + spa_graph_port_link(&data->graph_source_port_0, &data->graph_sink_port_0); + + return res; +} + +static void +init_buffer(struct data *data, struct spa_buffer **bufs, struct buffer *ba, int n_buffers, + size_t size) +{ + int i; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &ba[i]; + bufs[i] = &b->buffer; + + b->buffer.metas = b->metas; + b->buffer.n_metas = 1; + b->buffer.datas = b->datas; + b->buffer.n_datas = 1; + + b->header.flags = 0; + b->header.seq = 0; + b->header.pts = 0; + b->header.dts_offset = 0; + b->metas[0].type = SPA_META_Header; + b->metas[0].data = &b->header; + b->metas[0].size = sizeof(b->header); + + b->datas[0].type = SPA_DATA_MemPtr; + b->datas[0].flags = 0; + b->datas[0].fd = -1; + b->datas[0].mapoffset = 0; + b->datas[0].maxsize = size; + b->datas[0].data = malloc(size); + b->datas[0].chunk = &b->chunks[0]; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = 0; + b->datas[0].chunk->stride = 0; + } +} + +static int negotiate_formats(struct data *data) +{ + int res; + struct spa_pod *filter = NULL, *param = NULL; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + uint32_t state = 0; + size_t buffer_size = 1024; + + /* set the sink and source formats */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_dsp_build(&b, 0, + &SPA_AUDIO_INFO_DSP_INIT( + .format = SPA_AUDIO_FORMAT_F32P)); + if ((res = spa_node_port_set_param(data->source_node, + SPA_DIRECTION_OUTPUT, 0, SPA_PARAM_Format, 0, param)) < 0) + return res; + if ((res = spa_node_port_set_param(data->sink_node, + SPA_DIRECTION_INPUT, 0, SPA_PARAM_Format, 0, param)) < 0) + return res; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_Format, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + if ((res = spa_node_port_set_param(data->sink_node, + SPA_DIRECTION_INPUT, 1, SPA_PARAM_Format, 0, param)) < 0) + return res; + + /* get the source node buffer size */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_port_enum_params_sync(data->source_node, + SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Buffers, &state, filter, ¶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; + if ((res = spa_node_port_use_buffers(data->sink_node, + SPA_DIRECTION_INPUT, 0, 0, data->source_buffers, 1)) < 0) + return res; + + /* Set the control buffers */ + init_buffer(data, data->control_buffers, data->control_buffer, 1, CONTROL_BUFFER_SIZE); + if ((res = spa_node_port_use_buffers(data->sink_node, + SPA_DIRECTION_INPUT, 1, 0, data->control_buffers, 1)) < 0) + return res; + + return 0; +} + +static void *loop(void *user_data) +{ + struct data *data = user_data; + + printf("enter thread\n"); + spa_loop_control_enter(data->control); + + while (data->running) { + spa_loop_control_iterate(data->control, -1); + } + + printf("leave thread\n"); + spa_loop_control_leave(data->control); + return NULL; + + return NULL; +} + +static void run_async_sink(struct data *data) +{ + int res, err; + struct spa_command cmd; + + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source_node, &cmd)) < 0) + printf("got error %d\n", res); + if ((res = spa_node_send_command(data->sink_node, &cmd)) < 0) + printf("got error %d\n", res); + + spa_loop_control_leave(data->control); + + data->running = true; + if ((err = pthread_create(&data->thread, NULL, loop, data)) != 0) { + printf("can't create thread: %d %s", err, strerror(err)); + data->running = false; + } + + printf("sleeping for 1000 seconds\n"); + sleep(1000); + + if (data->running) { + data->running = false; + pthread_join(data->thread, NULL); + } + + spa_loop_control_enter(data->control); + + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source_node, &cmd)) < 0) + printf("got error %d\n", res); + if ((res = spa_node_send_command(data->sink_node, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res = 0; + + /* init data */ + if ((res = init_data(&data)) < 0) { + printf("can't init data: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + + /* make the nodes (audiotestsrc and adapter with alsa-pcm-sink as follower) */ + if ((res = make_nodes(&data, argc > 1 ? argv[1] : NULL)) < 0) { + printf("can't make nodes: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + + /* Negotiate format */ + if ((res = negotiate_formats(&data)) < 0) { + printf("can't negotiate nodes: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + + spa_loop_control_enter(data.control); + run_async_sink(&data); + spa_loop_control_leave(data.control); +} diff --git a/spa/examples/example-control.c b/spa/examples/example-control.c new file mode 100644 index 0000000..f26960c --- /dev/null +++ b/spa/examples/example-control.c @@ -0,0 +1,560 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + [title] + [title] + */ + +#include "config.h" + +#include +#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; +} + +int init_data(struct data *data) +{ + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data->plugin_dir = str; + + /* init the graph */ + spa_graph_init(&data->graph, &data->graph_state); + + /* set the default log */ + data->log = &default_log.log; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log); + + /* load and set support system */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data->system = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data->system); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data->system); + + /* load and set support loop and loop control */ + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data->loop); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data->loop); + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->control = iface; + + if ((str = getenv("SPA_DEBUG"))) + data->log->level = atoi(str); + + return 0; +} + +int main(int argc, char *argv[]) +{ + struct data data = { NULL }; + int res; + + if ((res = init_data(&data)) < 0) { + printf("can't init data: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + + if ((res = make_nodes(&data, argc > 1 ? argv[1] : NULL)) < 0) { + printf("can't make nodes: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + if ((res = negotiate_formats(&data)) < 0) { + printf("can't negotiate nodes: %d (%s)\n", res, spa_strerror(res)); + return -1; + } + + spa_loop_control_enter(data.control); + run_async_sink(&data); + spa_loop_control_leave(data.control); + + return 0; +} diff --git a/spa/examples/local-libcamera.c b/spa/examples/local-libcamera.c new file mode 100644 index 0000000..88ad02b --- /dev/null +++ b/spa/examples/local-libcamera.c @@ -0,0 +1,562 @@ +/* Spa + * + * Copyright (C) 2020, Collabora Ltd. + * Author: Raghavendra Rao Sidlagatta + * + * local-libcamera.c + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + [title] + Example using libspa-libcamera, with only \ref api_spa + [title] + */ + +#include "config.h" + +#include +#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 USE_BUFFER false + +struct buffer { + struct spa_buffer buffer; + struct spa_meta metas[1]; + struct spa_meta_header header; + struct spa_data datas[1]; + struct spa_chunk chunks[1]; + SDL_Texture *texture; +}; + +struct data { + const char *plugin_dir; + struct spa_log *log; + struct spa_system *system; + struct spa_loop *loop; + struct spa_loop_control *control; + + struct spa_support support[5]; + uint32_t n_support; + + struct spa_node *source; + struct spa_hook listener; + struct spa_io_buffers source_output[1]; + + SDL_Renderer *renderer; + SDL_Window *window; + SDL_Texture *texture; + + bool use_buffer; + + bool running; + pthread_t thread; + + struct spa_buffer *bp[MAX_BUFFERS]; + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + + char *path = NULL; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) { + return -ENOMEM; + } + if ((hnd = dlopen(path, RTLD_NOW)) == NULL) { + printf("can't load %s: %s\n", path, dlerror()); + free(path); + return -errno; + } + free(path); + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + return -errno; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + NULL, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + return res; + } + return 0; + } + return -EBADF; +} + +static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name) +{ + struct spa_handle *handle = NULL; + void *iface; + int res; + + if ((res = load_handle(data, &handle, lib, name)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + *node = iface; + return 0; +} + +static int on_source_ready(void *_data, int status) +{ + struct data *data = _data; + int res; + struct buffer *b; + void *sdata, *ddata; + int sstride, dstride; + int i; + uint8_t *src, *dst; + struct spa_data *datas; + struct spa_io_buffers *io = &data->source_output[0]; + + if (io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= MAX_BUFFERS) + return -EINVAL; + + b = &data->buffers[io->buffer_id]; + io->status = SPA_STATUS_NEED_DATA; + + datas = b->buffer.datas; + + if (b->texture) { + SDL_Texture *texture = b->texture; + + SDL_UnlockTexture(texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + } else { + uint8_t *map; + + if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + sdata = datas[0].data; + if (datas[0].type == SPA_DATA_MemFd || + datas[0].type == SPA_DATA_DmaBuf) { + map = mmap(NULL, datas[0].maxsize + datas[0].mapoffset, PROT_READ, + MAP_PRIVATE, datas[0].fd, 0); + if (map == MAP_FAILED) + return -errno; + sdata = SPA_PTROFF(map, datas[0].mapoffset, uint8_t); + } else if (datas[0].type == SPA_DATA_MemPtr) { + map = NULL; + sdata = datas[0].data; + } else + return -EIO; + + sstride = datas[0].chunk->stride; + + for (i = 0; i < HEIGHT; i++) { + src = ((uint8_t *) sdata + i * sstride); + dst = ((uint8_t *) ddata + i * dstride); + memcpy(dst, src, SPA_MIN(sstride, dstride)); + } + SDL_UnlockTexture(data->texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (map) + munmap(map, datas[0].maxsize + datas[0].mapoffset); + } + + if ((res = spa_node_process(data->source)) < 0) + printf("got process error %d\n", res); + + return 0; +} + +static const struct spa_node_callbacks source_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = on_source_ready, +}; + +static int make_nodes(struct data *data, const char *device) +{ + int res; + struct spa_pod *props; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[256]; + uint32_t index; + + if ((res = + make_node(data, &data->source, + "libcamera/libspa-libcamera.so", + SPA_NAME_API_LIBCAMERA_SOURCE)) < 0) { + printf("can't create libcamera-source: %d\n", res); + return res; + } + + spa_node_set_callbacks(data->source, &source_callbacks, data); + + index = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_enum_params_sync(data->source, SPA_PARAM_Props, + &index, NULL, &props, &b)) == 1) { + spa_debug_pod(0, NULL, props); + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_device, SPA_POD_String(device ? device : "/dev/media0")); + + if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0) + printf("got set_props error %d\n", res); + + return res; +} + +static int setup_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + + data->bp[i] = &b->buffer; + + b->texture = NULL; + + b->buffer.metas = b->metas; + b->buffer.n_metas = 1; + b->buffer.datas = b->datas; + b->buffer.n_datas = 1; + + b->header.flags = 0; + b->header.seq = 0; + b->header.pts = 0; + b->header.dts_offset = 0; + b->metas[0].type = SPA_META_Header; + b->metas[0].data = &b->header; + b->metas[0].size = sizeof(b->header); + + b->datas[0].type = SPA_DATA_DmaBuf; + b->datas[0].flags = 0; + b->datas[0].fd = -1; + b->datas[0].mapoffset = 0; + b->datas[0].maxsize = 0; + b->datas[0].data = NULL; + b->datas[0].chunk = &b->chunks[0]; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = 0; + b->datas[0].chunk->stride = 0; + } + data->n_buffers = MAX_BUFFERS; + return 0; +} + +static int sdl_alloc_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + SDL_Texture *texture; + void *ptr; + int stride; + + texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_YUY2, + SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT); + if (!texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -ENOMEM; + } + if (SDL_LockTexture(texture, NULL, &ptr, &stride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + b->texture = texture; + + b->datas[0].type = SPA_DATA_DmaBuf; + b->datas[0].maxsize = stride * HEIGHT; + b->datas[0].data = ptr; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = stride * HEIGHT; + b->datas[0].chunk->stride = stride; + } + return 0; +} + +static int negotiate_formats(struct data *data) +{ + int res; + struct spa_pod *format; + uint8_t buffer[256]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + data->source_output[0] = SPA_IO_BUFFERS_INIT; + + if ((res = + spa_node_port_set_io(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_IO_Buffers, + &data->source_output[0], sizeof(data->source_output[0]))) < 0) + return res; + + format = spa_format_video_raw_build(&b, 0, + &SPA_VIDEO_INFO_RAW_INIT( + .format = SPA_VIDEO_FORMAT_YUY2, + .size = SPA_RECTANGLE(WIDTH, HEIGHT), + .framerate = SPA_FRACTION(25,1))); + + if ((res = spa_node_port_set_param(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, + format)) < 0) + return res; + + + setup_buffers(data); + + if (data->use_buffer) { + if ((res = sdl_alloc_buffers(data)) < 0) + return res; + + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, 0, + data->bp, data->n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + } else { + unsigned int n_buffers; + + data->texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_YUY2, + SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT); + if (!data->texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -1; + } + n_buffers = MAX_BUFFERS; + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_NODE_BUFFERS_FLAG_ALLOC, + data->bp, n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + data->n_buffers = n_buffers; + } + return 0; +} + +static void *loop(void *user_data) +{ + struct data *data = user_data; + + printf("enter thread\n"); + spa_loop_control_enter(data->control); + + while (data->running) { + spa_loop_control_iterate(data->control, -1); + } + + printf("leave thread\n"); + spa_loop_control_leave(data->control); + return NULL; +} + +static void run_async_source(struct data *data) +{ + int res, err; + struct spa_command cmd; + SDL_Event event; + bool running = true; + + printf("starting...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); + + spa_loop_control_leave(data->control); + + data->running = true; + if ((err = pthread_create(&data->thread, NULL, loop, data)) != 0) { + printf("can't create thread: %d %s", err, strerror(err)); + data->running = false; + } + + while (running && SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + running = false; + break; + } + } + + if (data->running) { + data->running = false; + pthread_join(data->thread, NULL); + } + + spa_loop_control_enter(data->control); + + printf("pausing...\n\n"); + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data.plugin_dir = str; + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data.system = iface; + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data.system); + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.loop = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.control = iface; + + data.use_buffer = USE_BUFFER; + + data.log = &default_log.log; + + if ((str = getenv("SPA_DEBUG"))) + data.log->level = atoi(str); + + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data.loop); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data.loop); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("can't initialize SDL: %s\n", SDL_GetError()); + return -1; + } + + if (SDL_CreateWindowAndRenderer + (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) { + printf("can't create window: %s\n", SDL_GetError()); + return -1; + } + + if ((res = make_nodes(&data, argv[1])) < 0) { + printf("can't make nodes: %d\n", res); + return -1; + } + + if ((res = negotiate_formats(&data)) < 0) { + printf("can't negotiate nodes: %d\n", res); + return -1; + } + + spa_loop_control_enter(data.control); + run_async_source(&data); + spa_loop_control_leave(data.control); + + SDL_DestroyRenderer(data.renderer); + + return 0; +} diff --git a/spa/examples/local-v4l2.c b/spa/examples/local-v4l2.c new file mode 100644 index 0000000..bd6de7b --- /dev/null +++ b/spa/examples/local-v4l2.c @@ -0,0 +1,553 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + [title] + Example using libspa-v4l2, with only \ref api_spa + [title] + */ + +#include "config.h" + +#include +#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 + +struct buffer { + struct spa_buffer buffer; + struct spa_meta metas[1]; + struct spa_meta_header header; + struct spa_data datas[1]; + struct spa_chunk chunks[1]; + SDL_Texture *texture; +}; + +struct data { + const char *plugin_dir; + struct spa_log *log; + struct spa_system *system; + struct spa_loop *loop; + struct spa_loop_control *control; + + struct spa_support support[5]; + uint32_t n_support; + + struct spa_node *source; + struct spa_hook listener; + struct spa_io_buffers source_output[1]; + + SDL_Renderer *renderer; + SDL_Window *window; + SDL_Texture *texture; + + bool use_buffer; + + bool running; + pthread_t thread; + + struct spa_buffer *bp[MAX_BUFFERS]; + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + char *path = NULL; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) { + return -ENOMEM; + } + if ((hnd = dlopen(path, RTLD_NOW)) == NULL) { + printf("can't load %s: %s\n", path, dlerror()); + free(path); + return -ENOENT; + } + free(path); + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + return -ENOENT; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (factory->version < 1) + continue; + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + NULL, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + return res; + } + return 0; + } + return -EBADF; +} + +static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name) +{ + struct spa_handle *handle = NULL; + void *iface; + int res; + + if ((res = load_handle(data, &handle, lib, name)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + *node = iface; + return 0; +} + +static int on_source_ready(void *_data, int status) +{ + struct data *data = _data; + int res; + struct buffer *b; + void *sdata, *ddata; + int sstride, dstride; + int i; + uint8_t *src, *dst; + struct spa_data *datas; + struct spa_io_buffers *io = &data->source_output[0]; + + if (io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= MAX_BUFFERS) + return -EINVAL; + + b = &data->buffers[io->buffer_id]; + io->status = SPA_STATUS_NEED_DATA; + + datas = b->buffer.datas; + + if (b->texture) { + SDL_Texture *texture = b->texture; + + SDL_UnlockTexture(texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (SDL_LockTexture(texture, NULL, &sdata, &sstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + } else { + uint8_t *map; + + if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + sdata = datas[0].data; + if (datas[0].type == SPA_DATA_MemFd || + datas[0].type == SPA_DATA_DmaBuf) { + map = mmap(NULL, datas[0].maxsize + datas[0].mapoffset, PROT_READ, + MAP_PRIVATE, datas[0].fd, 0); + if (map == MAP_FAILED) + return -errno; + sdata = SPA_PTROFF(map, datas[0].mapoffset, uint8_t); + } else if (datas[0].type == SPA_DATA_MemPtr) { + map = NULL; + sdata = datas[0].data; + } else + return -EIO; + + sstride = datas[0].chunk->stride; + + for (i = 0; i < 240; i++) { + src = ((uint8_t *) sdata + i * sstride); + dst = ((uint8_t *) ddata + i * dstride); + memcpy(dst, src, SPA_MIN(sstride, dstride)); + } + SDL_UnlockTexture(data->texture); + + SDL_RenderClear(data->renderer); + SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + + if (map) + munmap(map, datas[0].maxsize + datas[0].mapoffset); + } + + if ((res = spa_node_process(data->source)) < 0) + printf("got process error %d\n", res); + + return 0; +} + +static const struct spa_node_callbacks source_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = on_source_ready, +}; + +static int make_nodes(struct data *data, const char *device) +{ + int res; + struct spa_pod *props; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[256]; + uint32_t index; + + if ((res = + make_node(data, &data->source, + "v4l2/libspa-v4l2.so", + SPA_NAME_API_V4L2_SOURCE)) < 0) { + printf("can't create v4l2-source: %d\n", res); + return res; + } + + spa_node_set_callbacks(data->source, &source_callbacks, data); + + index = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_enum_params_sync(data->source, SPA_PARAM_Props, + &index, NULL, &props, &b)) == 1) { + spa_debug_pod(0, NULL, props); + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + props = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, 0, + SPA_PROP_device, SPA_POD_String(device ? device : "/dev/video0")); + + if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0) + printf("got set_props error %d\n", res); + + return res; +} + +static int setup_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + + data->bp[i] = &b->buffer; + + b->texture = NULL; + + b->buffer.metas = b->metas; + b->buffer.n_metas = 1; + b->buffer.datas = b->datas; + b->buffer.n_datas = 1; + + b->header.flags = 0; + b->header.seq = 0; + b->header.pts = 0; + b->header.dts_offset = 0; + b->metas[0].type = SPA_META_Header; + b->metas[0].data = &b->header; + b->metas[0].size = sizeof(b->header); + + b->datas[0].type = 0; + b->datas[0].flags = 0; + b->datas[0].fd = -1; + b->datas[0].mapoffset = 0; + b->datas[0].maxsize = 0; + b->datas[0].data = NULL; + b->datas[0].chunk = &b->chunks[0]; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = 0; + b->datas[0].chunk->stride = 0; + } + data->n_buffers = MAX_BUFFERS; + return 0; +} + +static int sdl_alloc_buffers(struct data *data) +{ + int i; + + for (i = 0; i < MAX_BUFFERS; i++) { + struct buffer *b = &data->buffers[i]; + SDL_Texture *texture; + void *ptr; + int stride; + + texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_YUY2, + SDL_TEXTUREACCESS_STREAMING, 320, 240); + if (!texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -ENOMEM; + } + if (SDL_LockTexture(texture, NULL, &ptr, &stride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + return -EIO; + } + b->texture = texture; + + b->datas[0].type = SPA_DATA_MemPtr; + b->datas[0].maxsize = stride * 240; + b->datas[0].data = ptr; + b->datas[0].chunk->offset = 0; + b->datas[0].chunk->size = stride * 240; + b->datas[0].chunk->stride = stride; + } + return 0; +} + +static int negotiate_formats(struct data *data) +{ + int res; + struct spa_pod *format; + uint8_t buffer[256]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + data->source_output[0] = SPA_IO_BUFFERS_INIT; + + if ((res = + spa_node_port_set_io(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_IO_Buffers, + &data->source_output[0], sizeof(data->source_output[0]))) < 0) + return res; + + format = spa_format_video_raw_build(&b, 0, + &SPA_VIDEO_INFO_RAW_INIT( + .format = SPA_VIDEO_FORMAT_YUY2, + .size = SPA_RECTANGLE(320, 240), + .framerate = SPA_FRACTION(25,1))); + + if ((res = spa_node_port_set_param(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, + format)) < 0) + return res; + + + setup_buffers(data); + + if (data->use_buffer) { + if ((res = sdl_alloc_buffers(data)) < 0) + return res; + + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, 0, + data->bp, data->n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + } else { + unsigned int n_buffers; + + data->texture = SDL_CreateTexture(data->renderer, + SDL_PIXELFORMAT_YUY2, + SDL_TEXTUREACCESS_STREAMING, 320, 240); + if (!data->texture) { + printf("can't create texture: %s\n", SDL_GetError()); + return -1; + } + n_buffers = MAX_BUFFERS; + if ((res = spa_node_port_use_buffers(data->source, + SPA_DIRECTION_OUTPUT, 0, + SPA_NODE_BUFFERS_FLAG_ALLOC, + data->bp, n_buffers)) < 0) { + printf("can't allocate buffers: %s\n", spa_strerror(res)); + return -1; + } + data->n_buffers = n_buffers; + } + return 0; +} + +static void *loop(void *user_data) +{ + struct data *data = user_data; + + printf("enter thread\n"); + spa_loop_control_enter(data->control); + + while (data->running) { + spa_loop_control_iterate(data->control, -1); + } + + printf("leave thread\n"); + spa_loop_control_leave(data->control); + return NULL; +} + +static void run_async_source(struct data *data) +{ + int res, err; + struct spa_command cmd; + SDL_Event event; + bool running = true; + + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); + + spa_loop_control_leave(data->control); + + data->running = true; + if ((err = pthread_create(&data->thread, NULL, loop, data)) != 0) { + printf("can't create thread: %d %s", err, strerror(err)); + data->running = false; + } + + while (running && SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + running = false; + break; + } + } + + if (data->running) { + data->running = false; + pthread_join(data->thread, NULL); + } + + spa_loop_control_enter(data->control); + + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Pause); + if ((res = spa_node_send_command(data->source, &cmd)) < 0) + printf("got error %d\n", res); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data.plugin_dir = str; + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data.system = iface; + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data.system); + + if ((res = load_handle(&data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.loop = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data.control = iface; + + data.use_buffer = true; + + data.log = &default_log.log; + + if ((str = getenv("SPA_DEBUG"))) + data.log->level = atoi(str); + + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data.log); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data.loop); + data.support[data.n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data.loop); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + printf("can't initialize SDL: %s\n", SDL_GetError()); + return -1; + } + + if (SDL_CreateWindowAndRenderer + (320, 240, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) { + printf("can't create window: %s\n", SDL_GetError()); + return -1; + } + + + if ((res = make_nodes(&data, argv[1])) < 0) { + printf("can't make nodes: %d\n", res); + return -1; + } + if ((res = negotiate_formats(&data)) < 0) { + printf("can't negotiate nodes: %d\n", res); + return -1; + } + + spa_loop_control_enter(data.control); + run_async_source(&data); + spa_loop_control_leave(data.control); + + SDL_DestroyRenderer(data.renderer); + + return 0; +} diff --git a/spa/examples/meson.build b/spa/examples/meson.build new file mode 100644 index 0000000..7064a06 --- /dev/null +++ b/spa/examples/meson.build @@ -0,0 +1,32 @@ +# Examples, in order from simple to complicated +spa_examples = [ + 'adapter-control', + 'example-control', + 'local-libcamera', + 'local-v4l2', +] + +spa_examples_extra_deps = { + 'local-v4l2': [sdl_dep], + 'local-libcamera': [sdl_dep, libcamera_dep], +} + +foreach c : spa_examples + deps = spa_examples_extra_deps.get(c, []) + + found = true + foreach dep : deps + found = found and dep.found() + endforeach + + if found + executable( + c, + c + '.c', + include_directories : [configinc], + dependencies : [spa_dep, dl_lib, pthread_lib, mathlib] + deps, + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'examples' / 'spa' + ) + endif +endforeach diff --git a/spa/include/meson.build b/spa/include/meson.build new file mode 100644 index 0000000..443db7d --- /dev/null +++ b/spa/include/meson.build @@ -0,0 +1,18 @@ +spa_sections = [ + 'buffer', + 'control', + 'debug', + 'graph', + 'interfaces', + 'monitor', + 'node', + 'param', + 'pod', + 'support', + 'utils', +] + +spa_headers = 'spa' # used by doxygen +install_subdir('spa', + install_dir : get_option('includedir') / spa_name +) diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h new file mode 100644 index 0000000..ec85541 --- /dev/null +++ b/spa/include/spa/buffer/alloc.h @@ -0,0 +1,347 @@ +/* Simple Plugin API + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef SPA_BUFFER_ALLOC_H +#define SPA_BUFFER_ALLOC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_buffer + * \{ + */ + +/** information about the buffer layout */ +struct spa_buffer_alloc_info { +#define SPA_BUFFER_ALLOC_FLAG_INLINE_META (1<<0) /**< add metadata data in the skeleton */ +#define SPA_BUFFER_ALLOC_FLAG_INLINE_CHUNK (1<<1) /**< add chunk data in the skeleton */ +#define SPA_BUFFER_ALLOC_FLAG_INLINE_DATA (1<<2) /**< add buffer data to the skeleton */ +#define SPA_BUFFER_ALLOC_FLAG_INLINE_ALL 0b111 +#define SPA_BUFFER_ALLOC_FLAG_NO_DATA (1<<3) /**< don't set data pointers */ + uint32_t flags; + uint32_t max_align; /**< max of all alignments */ + uint32_t n_metas; + uint32_t n_datas; + struct spa_meta *metas; + struct spa_data *datas; + uint32_t *data_aligns; + size_t skel_size; /**< size of the struct spa_buffer and inlined meta/chunk/data */ + size_t meta_size; /**< size of the meta if not inlined */ + size_t chunk_size; /**< size of the chunk if not inlined */ + size_t data_size; /**< size of the data if not inlined */ + size_t mem_size; /**< size of the total memory if not inlined */ +}; + +/** + * Fill buffer allocation information + * + * Fill \a info with allocation information needed to allocate buffers + * with the given number of metadata and data members. + * + * The required size of the skeleton (the struct spa_buffer) information + * and the memory (for the metadata, chunk and buffer memory) will be + * calculated. + * + * The flags member in \a info should be configured before calling this + * functions. + * + * \param info the information to fill + * \param n_metas the number of metadatas for the buffer + * \param metas an array of metadata items + * \param n_datas the number of datas for the buffer + * \param datas an array of \a n_datas items + * \param data_aligns \a n_datas alignments + * \return 0 on success. + * */ +static inline int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info *info, + uint32_t n_metas, struct spa_meta metas[], + uint32_t n_datas, struct spa_data datas[], + uint32_t data_aligns[]) +{ + size_t size, *target; + uint32_t i; + + info->n_metas = n_metas; + info->metas = metas; + info->n_datas = n_datas; + info->datas = datas; + info->data_aligns = data_aligns; + info->max_align = 16; + info->mem_size = 0; + /* + * The buffer skeleton is placed in memory like below and can + * be accessed as a regular structure. + * + * +==============================+ + * | struct spa_buffer | + * | uint32_t n_metas | number of metas + * | uint32_t n_datas | number of datas + * +-| struct spa_meta *metas | pointer to array of metas + * +|-| struct spa_data *datas | pointer to array of datas + * || +------------------------------+ + * |+>| struct spa_meta | + * | | uint32_t type | metadata + * | | uint32_t size | size of metadata + * +|--| void *data | pointer to metadata + * || | ... | 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 + */ +static inline struct spa_buffer * +spa_buffer_alloc_layout(struct spa_buffer_alloc_info *info, + void *skel_mem, void *data_mem) +{ + struct spa_buffer *b = (struct spa_buffer*)skel_mem; + size_t size; + uint32_t i; + void **dp, *skel, *data; + struct spa_chunk *cp; + + b->n_metas = info->n_metas; + b->metas = SPA_PTROFF(b, sizeof(struct spa_buffer), struct spa_meta); + b->n_datas = info->n_datas; + b->datas = SPA_PTROFF(b->metas, info->n_metas * sizeof(struct spa_meta), struct spa_data); + + skel = SPA_PTROFF(b->datas, info->n_datas * sizeof(struct spa_data), void); + data = data_mem; + + if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_META)) + dp = &skel; + else + dp = &data; + + for (i = 0; i < info->n_metas; i++) { + struct spa_meta *m = &b->metas[i]; + *m = info->metas[i]; + m->data = *dp; + *dp = SPA_PTROFF(*dp, SPA_ROUND_UP_N(m->size, 8), void); + } + + size = info->n_datas * sizeof(struct spa_chunk); + if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_CHUNK)) { + cp = (struct spa_chunk*)skel; + skel = SPA_PTROFF(skel, size, void); + } + else { + cp = (struct spa_chunk*)data; + data = SPA_PTROFF(data, size, void); + } + + if (SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_INLINE_DATA)) + dp = &skel; + else + dp = &data; + + for (i = 0; i < info->n_datas; i++) { + struct spa_data *d = &b->datas[i]; + + *d = info->datas[i]; + d->chunk = &cp[i]; + if (!SPA_FLAG_IS_SET(info->flags, SPA_BUFFER_ALLOC_FLAG_NO_DATA)) { + *dp = SPA_PTR_ALIGN(*dp, info->data_aligns[i], void); + d->data = *dp; + *dp = SPA_PTROFF(*dp, d->maxsize, void); + } + } + return b; +} + +/** + * Layout an array of buffers + * + * Use the allocation info to layout the memory of an array of buffers. + * + * \a skel_mem should point to at least info->skel_size * \a n_buffers bytes + * of memory. + * \a data_mem should point to at least info->mem_size * \a n_buffers bytes + * of memory. + * + * \param info the allocation info for one buffer + * \param n_buffers the number of buffers to create + * \param buffers a array with space to hold \a n_buffers pointers to buffers + * \param skel_mem memory for the struct \ref spa_buffer + * \param data_mem memory for the meta, chunk, memory of the buffer if not + * inlined in the skeleton. + * \return 0 on success. + * + */ +static inline int +spa_buffer_alloc_layout_array(struct spa_buffer_alloc_info *info, + uint32_t n_buffers, struct spa_buffer *buffers[], + void *skel_mem, void *data_mem) +{ + uint32_t i; + for (i = 0; i < n_buffers; i++) { + buffers[i] = spa_buffer_alloc_layout(info, skel_mem, data_mem); + skel_mem = SPA_PTROFF(skel_mem, info->skel_size, void); + data_mem = SPA_PTROFF(data_mem, info->mem_size, void); + } + return 0; +} + +/** + * Allocate an array of buffers + * + * Allocate \a n_buffers with the given metadata, memory and alignment + * information. + * + * The buffer array, structures, data and metadata will all be allocated + * in one block of memory with the proper requested alignment. + * + * \param n_buffers the number of buffers to create + * \param flags extra flags + * \param n_metas number of metadatas + * \param metas \a n_metas metadata specification + * \param n_datas number of datas + * \param datas \a n_datas memory specification + * \param data_aligns \a n_datas alignment specifications + * \returns an array of \a n_buffers pointers to struct \ref spa_buffer + * with the given metadata, data and alignment or NULL when + * allocation failed. + * + */ +static inline struct spa_buffer ** +spa_buffer_alloc_array(uint32_t n_buffers, uint32_t flags, + uint32_t n_metas, struct spa_meta metas[], + uint32_t n_datas, struct spa_data datas[], + uint32_t data_aligns[]) +{ + + struct spa_buffer **buffers; + struct spa_buffer_alloc_info info = { flags | SPA_BUFFER_ALLOC_FLAG_INLINE_ALL, }; + void *skel; + + spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns); + + buffers = (struct spa_buffer **)calloc(1, info.max_align + + n_buffers * (sizeof(struct spa_buffer *) + info.skel_size)); + if (buffers == NULL) + return NULL; + + skel = SPA_PTROFF(buffers, sizeof(struct spa_buffer *) * n_buffers, void); + skel = SPA_PTR_ALIGN(skel, info.max_align, void); + + spa_buffer_alloc_layout_array(&info, n_buffers, buffers, skel, NULL); + + return buffers; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BUFFER_ALLOC_H */ diff --git a/spa/include/spa/buffer/buffer.h b/spa/include/spa/buffer/buffer.h new file mode 100644 index 0000000..47204ac --- /dev/null +++ b/spa/include/spa/buffer/buffer.h @@ -0,0 +1,131 @@ +/* Simple Plugin API + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BUFFER_H +#define SPA_BUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_buffer Buffers + * + * Buffers describe the data and metadata that is exchanged between + * ports of a node. + */ + +/** + * \addtogroup spa_buffer + * \{ + */ + +enum spa_data_type { + SPA_DATA_Invalid, + SPA_DATA_MemPtr, /**< pointer to memory, the data field in + * struct spa_data is set. */ + SPA_DATA_MemFd, /**< generic fd, mmap to get to memory */ + SPA_DATA_DmaBuf, /**< fd to dmabuf memory */ + SPA_DATA_MemId, /**< memory is identified with an id */ + + _SPA_DATA_LAST, /**< not part of ABI */ +}; + +/** Chunk of memory, can change for each buffer */ +struct spa_chunk { + uint32_t offset; /**< offset of valid data. Should be taken + * modulo the data maxsize to get the offset + * in the data memory. */ + uint32_t size; /**< size of valid data. Should be clamped to + * maxsize. */ + int32_t stride; /**< stride of valid data */ +#define SPA_CHUNK_FLAG_NONE 0 +#define SPA_CHUNK_FLAG_CORRUPTED (1u<<0) /**< chunk data is corrupted in some way */ +#define SPA_CHUNK_FLAG_EMPTY (1u<<1) /**< chunk data is empty with media specific + * neutral data such as silence or black. This + * could be used to optimize processing. */ + int32_t flags; /**< chunk flags */ +}; + +/** Data for a buffer this stays constant for a buffer */ +struct spa_data { + uint32_t type; /**< memory type, one of enum spa_data_type, when + * allocating memory, the type contains a bitmask + * of allowed types. SPA_ID_INVALID is a special + * value for the allocator to indicate that the + * other side did not explicitly specify any + * supported data types. It should probably use + * a memory type that does not require special + * handling in addition to simple mmap/munmap. */ +#define SPA_DATA_FLAG_NONE 0 +#define SPA_DATA_FLAG_READABLE (1u<<0) /**< data is readable */ +#define SPA_DATA_FLAG_WRITABLE (1u<<1) /**< data is writable */ +#define SPA_DATA_FLAG_DYNAMIC (1u<<2) /**< data pointer can be changed */ +#define SPA_DATA_FLAG_READWRITE (SPA_DATA_FLAG_READABLE|SPA_DATA_FLAG_WRITABLE) + uint32_t flags; /**< data flags */ + int64_t fd; /**< optional fd for data */ + uint32_t mapoffset; /**< offset to map fd at */ + uint32_t maxsize; /**< max size of data */ + void *data; /**< optional data pointer */ + struct spa_chunk *chunk; /**< valid chunk of memory */ +}; + +/** A Buffer */ +struct spa_buffer { + uint32_t n_metas; /**< number of metadata */ + uint32_t n_datas; /**< number of data members */ + struct spa_meta *metas; /**< array of metadata */ + struct spa_data *datas; /**< array of data members */ +}; + +/** Find metadata in a buffer */ +static inline struct spa_meta *spa_buffer_find_meta(const struct spa_buffer *b, uint32_t type) +{ + uint32_t i; + + for (i = 0; i < b->n_metas; i++) + if (b->metas[i].type == type) + return &b->metas[i]; + + return NULL; +} + +static inline void *spa_buffer_find_meta_data(const struct spa_buffer *b, uint32_t type, size_t size) +{ + struct spa_meta *m; + if ((m = spa_buffer_find_meta(b, type)) && m->size >= size) + return m->data; + return NULL; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BUFFER_H */ diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h new file mode 100644 index 0000000..dbd1446 --- /dev/null +++ b/spa/include/spa/buffer/meta.h @@ -0,0 +1,192 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_META_H +#define SPA_META_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_buffer + * \{ + */ + +enum spa_meta_type { + SPA_META_Invalid, + SPA_META_Header, /**< struct spa_meta_header */ + SPA_META_VideoCrop, /**< struct spa_meta_region with cropping data */ + SPA_META_VideoDamage, /**< array of struct spa_meta_region with damage, where an invalid entry or end-of-array marks the end. */ + SPA_META_Bitmap, /**< struct spa_meta_bitmap */ + SPA_META_Cursor, /**< struct spa_meta_cursor */ + SPA_META_Control, /**< metadata contains a spa_meta_control + * associated with the data */ + SPA_META_Busy, /**< don't write to buffer when count > 0 */ + SPA_META_VideoTransform, /**< struct spa_meta_transform */ + + _SPA_META_LAST, /**< not part of ABI/API */ +}; + +/** + * A metadata element. + * + * This structure is available on the buffer structure and contains + * the type of the metadata and a pointer/size to the actual metadata + * itself. + */ +struct spa_meta { + uint32_t type; /**< metadata type, one of enum spa_meta_type */ + uint32_t size; /**< size of metadata */ + void *data; /**< pointer to metadata */ +}; + +static inline void *spa_meta_first(const struct spa_meta *m) { + return m->data; +} +#define spa_meta_first spa_meta_first +static inline void *spa_meta_end(const struct spa_meta *m) { + return SPA_PTROFF(m->data,m->size,void); +} +#define spa_meta_end spa_meta_end +#define spa_meta_check(p,m) (SPA_PTROFF(p,sizeof(*(p)),void) <= spa_meta_end(m)) + +/** + * Describes essential buffer header metadata such as flags and + * timestamps. + */ +struct spa_meta_header { +#define SPA_META_HEADER_FLAG_DISCONT (1 << 0) /**< data is not continuous with previous buffer */ +#define SPA_META_HEADER_FLAG_CORRUPTED (1 << 1) /**< data might be corrupted */ +#define SPA_META_HEADER_FLAG_MARKER (1 << 2) /**< media specific marker */ +#define SPA_META_HEADER_FLAG_HEADER (1 << 3) /**< data contains a codec specific header */ +#define SPA_META_HEADER_FLAG_GAP (1 << 4) /**< data contains media neutral data */ +#define SPA_META_HEADER_FLAG_DELTA_UNIT (1 << 5) /**< cannot be decoded independently */ + uint32_t flags; /**< flags */ + uint32_t offset; /**< offset in current cycle */ + int64_t pts; /**< presentation timestamp in nanoseconds */ + int64_t dts_offset; /**< decoding timestamp as a difference with pts */ + uint64_t seq; /**< sequence number, increments with a + * media specific frequency */ +}; + +/** metadata structure for Region or an array of these for RegionArray */ +struct spa_meta_region { + struct spa_region region; +}; + +static inline bool spa_meta_region_is_valid(const struct spa_meta_region *m) { + return m->region.size.width != 0 && m->region.size.height != 0; +} +#define spa_meta_region_is_valid spa_meta_region_is_valid + +/** iterate all the items in a metadata */ +#define spa_meta_for_each(pos,meta) \ + for ((pos) = (__typeof(pos))spa_meta_first(meta); \ + spa_meta_check(pos, meta); \ + (pos)++) + +#define spa_meta_bitmap_is_valid(m) ((m)->format != 0) + +/** + * Bitmap information + * + * This metadata contains a bitmap image in the given format and size. + * It is typically used for cursor images or other small images that are + * better transferred inline. + */ +struct spa_meta_bitmap { + uint32_t format; /**< bitmap video format, one of enum spa_video_format. 0 is + * and invalid format and should be handled as if there is + * no new bitmap information. */ + struct spa_rectangle size; /**< width and height of bitmap */ + int32_t stride; /**< stride of bitmap data */ + uint32_t offset; /**< offset of bitmap data in this structure. An offset of + * 0 means no image data (invisible), an offset >= + * sizeof(struct spa_meta_bitmap) contains valid bitmap + * info. */ +}; + +#define spa_meta_cursor_is_valid(m) ((m)->id != 0) + +/** + * Cursor information + * + * Metadata to describe the position and appearance of a pointing device. + */ +struct spa_meta_cursor { + uint32_t id; /**< cursor id. an id of 0 is an invalid id and means that + * there is no new cursor data */ + uint32_t flags; /**< extra flags */ + struct spa_point position; /**< position on screen */ + struct spa_point hotspot; /**< offsets for hotspot in bitmap, this field has no meaning + * when there is no valid bitmap (see below) */ + uint32_t bitmap_offset; /**< offset of bitmap meta in this structure. When the offset + * is 0, there is no new bitmap information. When the offset is + * >= sizeof(struct spa_meta_cursor) there is a + * struct spa_meta_bitmap at the offset. */ +}; + +/** a timed set of events associated with the buffer */ +struct spa_meta_control { + struct spa_pod_sequence sequence; +}; + +/** a busy counter for the buffer */ +struct spa_meta_busy { + uint32_t flags; + uint32_t count; /**< number of users busy with the buffer */ +}; + +enum spa_meta_videotransform_value { + SPA_META_TRANSFORMATION_None = 0, /**< no transform */ + SPA_META_TRANSFORMATION_90, /**< 90 degree counter-clockwise */ + SPA_META_TRANSFORMATION_180, /**< 180 degree counter-clockwise */ + SPA_META_TRANSFORMATION_270, /**< 270 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped, /**< 180 degree flipped around the vertical axis. Equivalent + * to a reflexion through the vertical line splitting the + * bufffer in two equal sized parts */ + SPA_META_TRANSFORMATION_Flipped90, /**< flip then rotate around 90 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped180, /**< flip then rotate around 180 degree counter-clockwise */ + SPA_META_TRANSFORMATION_Flipped270, /**< flip then rotate around 270 degree counter-clockwise */ +}; + +/** a transformation of the buffer */ +struct spa_meta_videotransform { + uint32_t transform; /**< orientation transformation that was applied to the buffer, + * one of enum spa_meta_videotransform_value */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_META_H */ diff --git a/spa/include/spa/buffer/type-info.h b/spa/include/spa/buffer/type-info.h new file mode 100644 index 0000000..8b38c49 --- /dev/null +++ b/spa/include/spa/buffer/type-info.h @@ -0,0 +1,94 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BUFFER_TYPES_H +#define SPA_BUFFER_TYPES_H + +/** + * \addtogroup spa_buffer + * \{ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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 }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_Meta SPA_TYPE_INFO_POINTER_BASE "Meta" +#define SPA_TYPE_INFO_META_BASE SPA_TYPE_INFO_Meta ":" + +#define SPA_TYPE_INFO_META_Array SPA_TYPE_INFO_META_BASE "Array" +#define SPA_TYPE_INFO_META_ARRAY_BASE SPA_TYPE_INFO_META_Array ":" + +#define SPA_TYPE_INFO_META_Region SPA_TYPE_INFO_META_BASE "Region" +#define SPA_TYPE_INFO_META_REGION_BASE SPA_TYPE_INFO_META_Region ":" + +#define SPA_TYPE_INFO_META_ARRAY_Region SPA_TYPE_INFO_META_ARRAY_BASE "Region" +#define SPA_TYPE_INFO_META_ARRAY_REGION_BASE SPA_TYPE_INFO_META_ARRAY_Region ":" + +static const struct spa_type_info spa_type_meta_type[] = { + { SPA_META_Invalid, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Invalid", NULL }, + { SPA_META_Header, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Header", NULL }, + { SPA_META_VideoCrop, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_REGION_BASE "VideoCrop", NULL }, + { SPA_META_VideoDamage, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_ARRAY_REGION_BASE "VideoDamage", NULL }, + { SPA_META_Bitmap, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Bitmap", NULL }, + { SPA_META_Cursor, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Cursor", NULL }, + { SPA_META_Control, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Control", NULL }, + { SPA_META_Busy, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "Busy", NULL }, + { SPA_META_VideoTransform, SPA_TYPE_Pointer, SPA_TYPE_INFO_META_BASE "VideoTransform", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BUFFER_TYPES_H */ diff --git a/spa/include/spa/control/control.h b/spa/include/spa/control/control.h new file mode 100644 index 0000000..931ae01 --- /dev/null +++ b/spa/include/spa/control/control.h @@ -0,0 +1,62 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_CONTROL_H +#define SPA_CONTROL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_control Control + * Control type declarations + */ + +/** + * \addtogroup spa_control + * \{ + */ + +/** Different Control types */ +enum spa_control_type { + SPA_CONTROL_Invalid, + SPA_CONTROL_Properties, /**< data contains a SPA_TYPE_OBJECT_Props */ + SPA_CONTROL_Midi, /**< data contains a spa_pod_bytes with raw midi data */ + SPA_CONTROL_OSC, /**< data contains a spa_pod_bytes with an OSC packet */ + + _SPA_CONTROL_LAST, /**< not part of ABI */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_CONTROL_H */ diff --git a/spa/include/spa/control/type-info.h b/spa/include/spa/control/type-info.h new file mode 100644 index 0000000..d703783 --- /dev/null +++ b/spa/include/spa/control/type-info.h @@ -0,0 +1,61 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_CONTROL_TYPES_H +#define SPA_CONTROL_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_control + * \{ + */ + +#include +#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 }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_CONTROL_TYPES_H */ diff --git a/spa/include/spa/debug/buffer.h b/spa/include/spa/debug/buffer.h new file mode 100644 index 0000000..a1cd141 --- /dev/null +++ b/spa/include/spa/debug/buffer.h @@ -0,0 +1,133 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_BUFFER_H +#define SPA_DEBUG_BUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_debug Debug + * Debugging utilities + */ + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include +#include + +static inline int spa_debugc_buffer(struct spa_debug_context *ctx, int indent, const struct spa_buffer *buffer) +{ + uint32_t i; + + spa_debugc(ctx, "%*s" "struct spa_buffer %p:", indent, "", buffer); + spa_debugc(ctx, "%*s" " n_metas: %u (at %p)", indent, "", buffer->n_metas, buffer->metas); + for (i = 0; i < buffer->n_metas; i++) { + struct spa_meta *m = &buffer->metas[i]; + const char *type_name; + + type_name = spa_debug_type_find_name(spa_type_meta_type, m->type); + spa_debugc(ctx, "%*s" " meta %d: type %d (%s), data %p, size %d:", indent, "", i, m->type, + type_name, m->data, m->size); + + switch (m->type) { + case SPA_META_Header: + { + struct spa_meta_header *h = (struct spa_meta_header*)m->data; + spa_debugc(ctx, "%*s" " struct spa_meta_header:", indent, ""); + spa_debugc(ctx, "%*s" " flags: %08x", indent, "", h->flags); + spa_debugc(ctx, "%*s" " offset: %u", indent, "", h->offset); + spa_debugc(ctx, "%*s" " seq: %" PRIu64, indent, "", h->seq); + spa_debugc(ctx, "%*s" " pts: %" PRIi64, indent, "", h->pts); + spa_debugc(ctx, "%*s" " dts_offset: %" PRIi64, indent, "", h->dts_offset); + break; + } + case SPA_META_VideoCrop: + { + struct spa_meta_region *h = (struct spa_meta_region*)m->data; + spa_debugc(ctx, "%*s" " struct spa_meta_region:", indent, ""); + spa_debugc(ctx, "%*s" " x: %d", indent, "", h->region.position.x); + spa_debugc(ctx, "%*s" " y: %d", indent, "", h->region.position.y); + spa_debugc(ctx, "%*s" " width: %d", indent, "", h->region.size.width); + spa_debugc(ctx, "%*s" " height: %d", indent, "", h->region.size.height); + break; + } + case SPA_META_VideoDamage: + { + struct spa_meta_region *h; + spa_meta_for_each(h, m) { + spa_debugc(ctx, "%*s" " struct spa_meta_region:", indent, ""); + spa_debugc(ctx, "%*s" " x: %d", indent, "", h->region.position.x); + spa_debugc(ctx, "%*s" " y: %d", indent, "", h->region.position.y); + spa_debugc(ctx, "%*s" " width: %d", indent, "", h->region.size.width); + spa_debugc(ctx, "%*s" " height: %d", indent, "", h->region.size.height); + } + break; + } + case SPA_META_Bitmap: + break; + case SPA_META_Cursor: + break; + default: + spa_debugc(ctx, "%*s" " Unknown:", indent, ""); + spa_debugc_mem(ctx, 5, m->data, m->size); + } + } + spa_debugc(ctx, "%*s" " n_datas: \t%u (at %p)", indent, "", buffer->n_datas, buffer->datas); + for (i = 0; i < buffer->n_datas; i++) { + struct spa_data *d = &buffer->datas[i]; + spa_debugc(ctx, "%*s" " type: %d (%s)", indent, "", d->type, + spa_debug_type_find_name(spa_type_data_type, d->type)); + spa_debugc(ctx, "%*s" " flags: %d", indent, "", d->flags); + spa_debugc(ctx, "%*s" " data: %p", indent, "", d->data); + spa_debugc(ctx, "%*s" " fd: %" PRIi64, indent, "", d->fd); + spa_debugc(ctx, "%*s" " offset: %d", indent, "", d->mapoffset); + spa_debugc(ctx, "%*s" " maxsize: %u", indent, "", d->maxsize); + spa_debugc(ctx, "%*s" " chunk: %p", indent, "", d->chunk); + spa_debugc(ctx, "%*s" " offset: %d", indent, "", d->chunk->offset); + spa_debugc(ctx, "%*s" " size: %u", indent, "", d->chunk->size); + spa_debugc(ctx, "%*s" " stride: %d", indent, "", d->chunk->stride); + } + return 0; +} + +static inline int spa_debug_buffer(int indent, const struct spa_buffer *buffer) +{ + return spa_debugc_buffer(NULL, indent, buffer); +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_BUFFER_H */ diff --git a/spa/include/spa/debug/context.h b/spa/include/spa/debug/context.h new file mode 100644 index 0000000..945e502 --- /dev/null +++ b/spa/include/spa/debug/context.h @@ -0,0 +1,62 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_CONTEXT_H +#define SPA_DEBUG_CONTEXT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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 + +struct spa_debug_context { + void (*log) (struct spa_debug_context *ctx, const char *fmt, ...) SPA_PRINTF_FUNC(2, 3); +}; + +#define spa_debugc(_c,_fmt,...) (_c)?((_c)->log((_c),_fmt, ## __VA_ARGS__)):(void)spa_debug(_fmt, ## __VA_ARGS__) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_CONTEXT_H */ diff --git a/spa/include/spa/debug/dict.h b/spa/include/spa/debug/dict.h new file mode 100644 index 0000000..ad47ad6 --- /dev/null +++ b/spa/include/spa/debug/dict.h @@ -0,0 +1,62 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_DICT_H +#define SPA_DEBUG_DICT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include + +static inline int spa_debugc_dict(struct spa_debug_context *ctx, int indent, const struct spa_dict *dict) +{ + const struct spa_dict_item *item; + spa_debugc(ctx, "%*sflags:%08x n_items:%d", indent, "", dict->flags, dict->n_items); + spa_dict_for_each(item, dict) { + spa_debugc(ctx, "%*s %s = \"%s\"", indent, "", item->key, item->value); + } + return 0; +} + +static inline int spa_debug_dict(int indent, const struct spa_dict *dict) +{ + return spa_debugc_dict(NULL, indent, dict); +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_DICT_H */ diff --git a/spa/include/spa/debug/format.h b/spa/include/spa/debug/format.h new file mode 100644 index 0000000..e0bd955 --- /dev/null +++ b/spa/include/spa/debug/format.h @@ -0,0 +1,234 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_FORMAT_H +#define SPA_DEBUG_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include +#include +#include +#include + +static inline int +spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + + switch (type) { + case SPA_TYPE_Bool: + spa_strbuf_append(buffer, "%s", *(int32_t *) body ? "true" : "false"); + break; + case SPA_TYPE_Id: + { + const char *str = spa_debug_type_find_short_name(info, *(int32_t *) body); + char tmp[64]; + if (str == NULL) { + snprintf(tmp, sizeof(tmp), "%d", *(int32_t*)body); + str = tmp; + } + spa_strbuf_append(buffer, "%s", str); + break; + } + case SPA_TYPE_Int: + spa_strbuf_append(buffer, "%d", *(int32_t *) body); + break; + case SPA_TYPE_Long: + spa_strbuf_append(buffer, "%" PRIi64, *(int64_t *) body); + break; + case SPA_TYPE_Float: + spa_strbuf_append(buffer, "%f", *(float *) body); + break; + case SPA_TYPE_Double: + spa_strbuf_append(buffer, "%f", *(double *) body); + break; + case SPA_TYPE_String: + spa_strbuf_append(buffer, "%s", (char *) body); + break; + case SPA_TYPE_Rectangle: + { + struct spa_rectangle *r = (struct spa_rectangle *)body; + spa_strbuf_append(buffer, "%" PRIu32 "x%" PRIu32, r->width, r->height); + break; + } + case SPA_TYPE_Fraction: + { + struct spa_fraction *f = (struct spa_fraction *)body; + spa_strbuf_append(buffer, "%" PRIu32 "/%" PRIu32, f->num, f->denom); + break; + } + case SPA_TYPE_Bitmap: + spa_strbuf_append(buffer, "Bitmap"); + break; + case SPA_TYPE_Bytes: + spa_strbuf_append(buffer, "Bytes"); + break; + case SPA_TYPE_Array: + { + void *p; + struct spa_pod_array_body *b = (struct spa_pod_array_body *)body; + int i = 0; + info = info && info->values ? info->values : info; + spa_strbuf_append(buffer, "< "); + SPA_POD_ARRAY_BODY_FOREACH(b, size, p) { + if (i++ > 0) + spa_strbuf_append(buffer, ", "); + spa_debug_strbuf_format_value(buffer, info, b->child.type, p, b->child.size); + } + spa_strbuf_append(buffer, " >"); + break; + } + default: + spa_strbuf_append(buffer, "INVALID type %d", type); + break; + } + return 0; +} + +static inline int +spa_debug_format_value(const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + char buffer[1024]; + struct spa_strbuf buf; + spa_strbuf_init(&buf, buffer, sizeof(buffer)); + spa_debug_strbuf_format_value(&buf, info, type, body, size); + spa_debugn("%s", buffer); + return 0; +} + +static inline int spa_debugc_format(struct spa_debug_context *ctx, int indent, + const struct spa_type_info *info, const struct spa_pod *format) +{ + const char *media_type; + const char *media_subtype; + struct spa_pod_prop *prop; + uint32_t mtype, mstype; + + if (info == NULL) + info = spa_type_format; + + if (format == NULL || SPA_POD_TYPE(format) != SPA_TYPE_Object) + return -EINVAL; + + if (spa_format_parse(format, &mtype, &mstype) < 0) + return -EINVAL; + + media_type = spa_debug_type_find_name(spa_type_media_type, mtype); + media_subtype = spa_debug_type_find_name(spa_type_media_subtype, mstype); + + spa_debugc(ctx, "%*s %s/%s", indent, "", + media_type ? spa_debug_type_short_name(media_type) : "unknown", + media_subtype ? spa_debug_type_short_name(media_subtype) : "unknown"); + + SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)format, prop) { + const char *key; + const struct spa_type_info *ti; + uint32_t i, type, size, n_vals, choice; + const struct spa_pod *val; + void *vals; + char buffer[1024]; + struct spa_strbuf buf; + + if (prop->key == SPA_FORMAT_mediaType || + prop->key == SPA_FORMAT_mediaSubtype) + continue; + + val = spa_pod_get_values(&prop->value, &n_vals, &choice); + + type = val->type; + size = val->size; + vals = SPA_POD_BODY(val); + + if (type < SPA_TYPE_None || type >= _SPA_TYPE_LAST) + continue; + + ti = spa_debug_type_find(info, prop->key); + key = ti ? ti->name : NULL; + + spa_strbuf_init(&buf, buffer, sizeof(buffer)); + spa_strbuf_append(&buf, "%*s %16s : (%s) ", indent, "", + key ? spa_debug_type_short_name(key) : "unknown", + spa_debug_type_short_name(spa_types[type].name)); + + if (choice == SPA_CHOICE_None) { + spa_debug_strbuf_format_value(&buf, ti ? ti->values : NULL, type, vals, size); + } else { + const char *ssep, *esep, *sep; + + switch (choice) { + case SPA_CHOICE_Range: + case SPA_CHOICE_Step: + ssep = "[ "; + sep = ", "; + esep = " ]"; + break; + default: + case SPA_CHOICE_Enum: + case SPA_CHOICE_Flags: + ssep = "{ "; + sep = ", "; + esep = " }"; + break; + } + + spa_strbuf_append(&buf, "%s", ssep); + + for (i = 1; i < n_vals; i++) { + vals = SPA_PTROFF(vals, size, void); + if (i > 1) + spa_strbuf_append(&buf, "%s", sep); + spa_debug_strbuf_format_value(&buf, ti ? ti->values : NULL, type, vals, size); + } + spa_strbuf_append(&buf, "%s", esep); + } + spa_debugc(ctx, "%s", buffer); + } + return 0; +} + +static inline int spa_debug_format(int indent, + const struct spa_type_info *info, const struct spa_pod *format) +{ + return spa_debugc_format(NULL, indent, info, format); +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_FORMAT_H */ diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h new file mode 100644 index 0000000..9e2dc73 --- /dev/null +++ b/spa/include/spa/debug/log.h @@ -0,0 +1,103 @@ +/* Simple Plugin API + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_LOG_H +#define SPA_DEBUG_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +/** + * \addtogroup spa_debug + * \{ + */ + +struct spa_debug_log_ctx { + struct spa_debug_context ctx; + struct spa_log *log; + enum spa_log_level level; + const struct spa_log_topic *topic; + const char *file; + int line; + const char *func; +}; + +SPA_PRINTF_FUNC(2,3) +static inline void spa_debug_log_log(struct spa_debug_context *ctx, const char *fmt, ...) +{ + struct spa_debug_log_ctx *c = (struct spa_debug_log_ctx*)ctx; + va_list args; + va_start(args, fmt); + spa_log_logtv(c->log, c->level, c->topic, c->file, c->line, c->func, fmt, args); + va_end(args); +} + +#define SPA_LOGF_DEBUG_INIT(_l,_lev,_t,_file,_line,_func) \ + (struct spa_debug_log_ctx){ { spa_debug_log_log }, _l, _lev, _t, \ + _file, _line, _func } + +#define SPA_LOGT_DEBUG_INIT(_l,_lev,_t) \ + SPA_LOGF_DEBUG_INIT(_l,_lev,_t,__FILE__,__LINE__,__func__) + +#define SPA_LOG_DEBUG_INIT(l,lev) \ + SPA_LOGT_DEBUG_INIT(l,lev,SPA_LOG_TOPIC_DEFAULT) + +#define spa_debug_log_pod(l,lev,indent,info,pod) \ +({ \ + struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \ + spa_debugc_pod(&c.ctx, indent, info, pod); \ +}) + +#define spa_debug_log_format(l,lev,indent,info,format) \ +({ \ + struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \ + spa_debugc_format(&c.ctx, indent, info, format); \ +}) + +#define spa_debug_log_mem(l,lev,indent,data,len) \ +({ \ + struct spa_debug_log_ctx c = SPA_LOG_DEBUG_INIT(l,lev); \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(c.log, c.topic, c.level))) \ + spa_debugc_mem(&c.ctx, indent, data, len); \ +}) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_LOG_H */ diff --git a/spa/include/spa/debug/mem.h b/spa/include/spa/debug/mem.h new file mode 100644 index 0000000..7924aba --- /dev/null +++ b/spa/include/spa/debug/mem.h @@ -0,0 +1,71 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_MEM_H +#define SPA_DEBUG_MEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_debug + * \{ + */ + +#include + +static inline int spa_debugc_mem(struct spa_debug_context *ctx, int indent, const void *data, size_t size) +{ + const uint8_t *t = (const uint8_t*)data; + char buffer[512]; + size_t i; + int pos = 0; + + for (i = 0; i < size; i++) { + if (i % 16 == 0) + pos = sprintf(buffer, "%p: ", &t[i]); + pos += sprintf(buffer + pos, "%02x ", t[i]); + if (i % 16 == 15 || i == size - 1) { + spa_debugc(ctx, "%*s" "%s", indent, "", buffer); + } + } + return 0; +} + +static inline int spa_debug_mem(int indent, const void *data, size_t size) +{ + return spa_debugc_mem(NULL, indent, data, size); +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_MEM_H */ diff --git a/spa/include/spa/debug/node.h b/spa/include/spa/debug/node.h new file mode 100644 index 0000000..63b16d3 --- /dev/null +++ b/spa/include/spa/debug/node.h @@ -0,0 +1,67 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_NODE_H +#define SPA_DEBUG_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include + +static inline int spa_debugc_port_info(struct spa_debug_context *ctx, int indent, const struct spa_port_info *info) +{ + spa_debugc(ctx, "%*s" "struct spa_port_info %p:", indent, "", info); + spa_debugc(ctx, "%*s" " flags: \t%08" PRIx64, indent, "", info->flags); + spa_debugc(ctx, "%*s" " rate: \t%d/%d", indent, "", info->rate.num, info->rate.denom); + spa_debugc(ctx, "%*s" " props:", indent, ""); + if (info->props) + spa_debugc_dict(ctx, indent + 2, info->props); + else + spa_debugc(ctx, "%*s" " none", indent, ""); + return 0; +} + +static inline int spa_debug_port_info(int indent, const struct spa_port_info *info) +{ + return spa_debugc_port_info(NULL, indent, info); +} +/** + * \} + */ + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_NODE_H */ diff --git a/spa/include/spa/debug/pod.h b/spa/include/spa/debug/pod.h new file mode 100644 index 0000000..31b707e --- /dev/null +++ b/spa/include/spa/debug/pod.h @@ -0,0 +1,227 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_POD_H +#define SPA_DEBUG_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include +#include +#include +#include +#include + +static inline int +spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + switch (type) { + case SPA_TYPE_Bool: + spa_debugc(ctx, "%*s" "Bool %s", indent, "", (*(int32_t *) body) ? "true" : "false"); + break; + case SPA_TYPE_Id: + spa_debugc(ctx, "%*s" "Id %-8d (%s)", indent, "", *(int32_t *) body, + spa_debug_type_find_name(info, *(int32_t *) body)); + break; + case SPA_TYPE_Int: + spa_debugc(ctx, "%*s" "Int %d", indent, "", *(int32_t *) body); + break; + case SPA_TYPE_Long: + spa_debugc(ctx, "%*s" "Long %" PRIi64 "", indent, "", *(int64_t *) body); + break; + case SPA_TYPE_Float: + spa_debugc(ctx, "%*s" "Float %f", indent, "", *(float *) body); + break; + case SPA_TYPE_Double: + spa_debugc(ctx, "%*s" "Double %f", indent, "", *(double *) body); + break; + case SPA_TYPE_String: + spa_debugc(ctx, "%*s" "String \"%s\"", indent, "", (char *) body); + break; + case SPA_TYPE_Fd: + spa_debugc(ctx, "%*s" "Fd %d", indent, "", *(int *) body); + break; + case SPA_TYPE_Pointer: + { + struct spa_pod_pointer_body *b = (struct spa_pod_pointer_body *)body; + spa_debugc(ctx, "%*s" "Pointer %s %p", indent, "", + spa_debug_type_find_name(SPA_TYPE_ROOT, b->type), b->value); + break; + } + case SPA_TYPE_Rectangle: + { + struct spa_rectangle *r = (struct spa_rectangle *)body; + spa_debugc(ctx, "%*s" "Rectangle %dx%d", indent, "", r->width, r->height); + break; + } + case SPA_TYPE_Fraction: + { + struct spa_fraction *f = (struct spa_fraction *)body; + spa_debugc(ctx, "%*s" "Fraction %d/%d", indent, "", f->num, f->denom); + break; + } + case SPA_TYPE_Bitmap: + spa_debugc(ctx, "%*s" "Bitmap", indent, ""); + break; + case SPA_TYPE_Array: + { + struct spa_pod_array_body *b = (struct spa_pod_array_body *)body; + void *p; + const struct spa_type_info *ti = spa_debug_type_find(SPA_TYPE_ROOT, b->child.type); + + spa_debugc(ctx, "%*s" "Array: child.size %d, child.type %s", indent, "", + b->child.size, ti ? ti->name : "unknown"); + + info = info && info->values ? info->values : info; + SPA_POD_ARRAY_BODY_FOREACH(b, size, p) + spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + break; + } + case SPA_TYPE_Choice: + { + struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body; + void *p; + const struct spa_type_info *ti = spa_debug_type_find(spa_type_choice, b->type); + + spa_debugc(ctx, "%*s" "Choice: type %s, flags %08x %d %d", indent, "", + ti ? ti->name : "unknown", b->flags, size, b->child.size); + + SPA_POD_CHOICE_BODY_FOREACH(b, size, p) + spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + break; + } + case SPA_TYPE_Struct: + { + struct spa_pod *b = (struct spa_pod *)body, *p; + spa_debugc(ctx, "%*s" "Struct: size %d", indent, "", size); + SPA_POD_FOREACH(b, size, p) + spa_debugc_pod_value(ctx, indent + 2, info, p->type, SPA_POD_BODY(p), p->size); + break; + } + case SPA_TYPE_Object: + { + struct spa_pod_object_body *b = (struct spa_pod_object_body *)body; + struct spa_pod_prop *p; + const struct spa_type_info *ti, *ii; + + ti = spa_debug_type_find(info, b->type); + ii = ti ? spa_debug_type_find(ti->values, 0) : NULL; + ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL; + + spa_debugc(ctx, "%*s" "Object: size %d, type %s (%d), id %s (%d)", indent, "", size, + ti ? ti->name : "unknown", b->type, ii ? ii->name : "unknown", b->id); + + info = ti ? ti->values : info; + + SPA_POD_OBJECT_BODY_FOREACH(b, size, p) { + ii = spa_debug_type_find(info, p->key); + + spa_debugc(ctx, "%*s" "Prop: key %s (%d), flags %08x", indent+2, "", + ii ? ii->name : "unknown", p->key, p->flags); + + spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, + p->value.type, + SPA_POD_CONTENTS(struct spa_pod_prop, p), + p->value.size); + } + break; + } + case SPA_TYPE_Sequence: + { + struct spa_pod_sequence_body *b = (struct spa_pod_sequence_body *)body; + const struct spa_type_info *ti, *ii; + struct spa_pod_control *c; + + ti = spa_debug_type_find(info, b->unit); + + spa_debugc(ctx, "%*s" "Sequence: size %d, unit %s", indent, "", size, + ti ? ti->name : "unknown"); + + SPA_POD_SEQUENCE_BODY_FOREACH(b, size, c) { + ii = spa_debug_type_find(spa_type_control, c->type); + + spa_debugc(ctx, "%*s" "Control: offset %d, type %s", indent+2, "", + c->offset, ii ? ii->name : "unknown"); + + spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, + c->value.type, + SPA_POD_CONTENTS(struct spa_pod_control, c), + c->value.size); + } + break; + } + case SPA_TYPE_Bytes: + spa_debugc(ctx, "%*s" "Bytes", indent, ""); + spa_debugc_mem(ctx, indent + 2, body, size); + break; + case SPA_TYPE_None: + spa_debugc(ctx, "%*s" "None", indent, ""); + spa_debugc_mem(ctx, indent + 2, body, size); + break; + default: + spa_debugc(ctx, "%*s" "unhandled POD type %d", indent, "", type); + break; + } + return 0; +} + +static inline int spa_debugc_pod(struct spa_debug_context *ctx, int indent, + const struct spa_type_info *info, const struct spa_pod *pod) +{ + return spa_debugc_pod_value(ctx, indent, info ? info : SPA_TYPE_ROOT, + SPA_POD_TYPE(pod), + SPA_POD_BODY(pod), + SPA_POD_BODY_SIZE(pod)); +} + +static inline int +spa_debug_pod_value(int indent, const struct spa_type_info *info, + uint32_t type, void *body, uint32_t size) +{ + return spa_debugc_pod_value(NULL, indent, info, type, body, size); +} + +static inline int spa_debug_pod(int indent, + const struct spa_type_info *info, const struct spa_pod *pod) +{ + return spa_debugc_pod(NULL, indent, info, pod); +} +/** + * \} + */ + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_POD_H */ diff --git a/spa/include/spa/debug/types.h b/spa/include/spa/debug/types.h new file mode 100644 index 0000000..55fb379 --- /dev/null +++ b/spa/include/spa/debug/types.h @@ -0,0 +1,127 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEBUG_TYPES_H +#define SPA_DEBUG_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_debug + * \{ + */ + +#include + +#include + +static inline const struct spa_type_info *spa_debug_type_find(const struct spa_type_info *info, uint32_t type) +{ + const struct spa_type_info *res; + + if (info == NULL) + info = SPA_TYPE_ROOT; + + while (info && info->name) { + if (info->type == SPA_ID_INVALID) { + if (info->values && (res = spa_debug_type_find(info->values, type))) + return res; + } + else if (info->type == type) + return info; + info++; + } + return NULL; +} + +static inline const char *spa_debug_type_short_name(const char *name) +{ + const char *h; + if ((h = strrchr(name, ':')) != NULL) + name = h + 1; + return name; +} + +static inline const char *spa_debug_type_find_name(const struct spa_type_info *info, uint32_t type) +{ + if ((info = spa_debug_type_find(info, type)) == NULL) + return NULL; + return info->name; +} + +static inline const char *spa_debug_type_find_short_name(const struct spa_type_info *info, uint32_t type) +{ + const char *str; + if ((str = spa_debug_type_find_name(info, type)) == NULL) + return NULL; + return spa_debug_type_short_name(str); +} + +static inline uint32_t spa_debug_type_find_type(const struct spa_type_info *info, const char *name) +{ + if (info == NULL) + info = SPA_TYPE_ROOT; + + while (info && info->name) { + uint32_t res; + if (strcmp(info->name, name) == 0) + return info->type; + if (info->values && (res = spa_debug_type_find_type(info->values, name)) != SPA_ID_INVALID) + return res; + info++; + } + return SPA_ID_INVALID; +} + +static inline const struct spa_type_info *spa_debug_type_find_short(const struct spa_type_info *info, const char *name) +{ + while (info && info->name) { + if (strcmp(spa_debug_type_short_name(info->name), name) == 0) + return info; + if (strcmp(info->name, name) == 0) + return info; + if (info->type != 0 && info->type == (uint32_t)atoi(name)) + return info; + info++; + } + return NULL; +} + +static inline uint32_t spa_debug_type_find_type_short(const struct spa_type_info *info, const char *name) +{ + if ((info = spa_debug_type_find_short(info, name)) == NULL) + return SPA_ID_INVALID; + return info->type; +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEBUG_NODE_H */ diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h new file mode 100644 index 0000000..0e887cd --- /dev/null +++ b/spa/include/spa/graph/graph.h @@ -0,0 +1,365 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_GRAPH_H +#define SPA_GRAPH_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_graph Graph + * Node graph + */ + +/** + * \addtogroup spa_graph + * \{ + */ + +#include +#include +#include +#include +#include + +#ifndef spa_debug +#define spa_debug(...) +#endif + +struct spa_graph; +struct spa_graph_node; +struct spa_graph_link; +struct spa_graph_port; + +struct spa_graph_state { + int status; /**< current status */ + int32_t required; /**< required number of signals */ + int32_t pending; /**< number of pending signals */ +}; + +static inline void spa_graph_state_reset(struct spa_graph_state *state) +{ + state->pending = state->required; +} + +struct spa_graph_link { + struct spa_list link; + struct spa_graph_state *state; + int (*signal) (void *data); + void *signal_data; +}; + +#define spa_graph_link_signal(l) ((l)->signal((l)->signal_data)) + +#define spa_graph_state_dec(s,c) (__atomic_sub_fetch(&(s)->pending, c, __ATOMIC_SEQ_CST) == 0) + +static inline int spa_graph_link_trigger(struct spa_graph_link *link) +{ + struct spa_graph_state *state = link->state; + + spa_debug("link %p: state %p: pending %d/%d", link, state, + state->pending, state->required); + + if (spa_graph_state_dec(state, 1)) + spa_graph_link_signal(link); + + return state->status; +} +struct spa_graph { + uint32_t flags; /* flags */ + struct spa_graph_node *parent; /* parent node or NULL when driver */ + struct spa_graph_state *state; /* state of graph */ + struct spa_list nodes; /* list of nodes of this graph */ +}; + +struct spa_graph_node_callbacks { +#define SPA_VERSION_GRAPH_NODE_CALLBACKS 0 + uint32_t version; + + int (*process) (void *data, struct spa_graph_node *node); + int (*reuse_buffer) (void *data, struct spa_graph_node *node, + uint32_t port_id, uint32_t buffer_id); +}; + +struct spa_graph_node { + struct spa_list link; /**< link in graph nodes list */ + struct spa_graph *graph; /**< owner graph */ + struct spa_list ports[2]; /**< list of input and output ports */ + struct spa_list links; /**< list of links to next nodes */ + uint32_t flags; /**< node flags */ + struct spa_graph_state *state; /**< state of the node */ + struct spa_graph_link graph_link; /**< link in graph */ + struct spa_graph *subgraph; /**< subgraph or NULL */ + struct spa_callbacks callbacks; + struct spa_list sched_link; /**< link for scheduler */ +}; + +#define spa_graph_node_call(n,method,version,...) \ +({ \ + int __res = 0; \ + spa_callbacks_call_res(&(n)->callbacks, \ + struct spa_graph_node_callbacks, __res, \ + method, (version), ##__VA_ARGS__); \ + __res; \ +}) + +#define spa_graph_node_process(n) spa_graph_node_call((n), process, 0, (n)) +#define spa_graph_node_reuse_buffer(n,p,i) spa_graph_node_call((n), reuse_buffer, 0, (n), (p), (i)) + +struct spa_graph_port { + struct spa_list link; /**< link in node port list */ + struct spa_graph_node *node; /**< owner node */ + enum spa_direction direction; /**< port direction */ + uint32_t port_id; /**< port id */ + uint32_t flags; /**< port flags */ + struct spa_graph_port *peer; /**< peer */ +}; + +static inline int spa_graph_node_trigger(struct spa_graph_node *node) +{ + struct spa_graph_link *l; + spa_debug("node %p trigger", node); + spa_list_for_each(l, &node->links, link) + spa_graph_link_trigger(l); + return 0; +} + +static inline int spa_graph_run(struct spa_graph *graph) +{ + struct spa_graph_node *n, *t; + struct spa_list pending; + + spa_graph_state_reset(graph->state); + spa_debug("graph %p run with state %p pending %d/%d", graph, graph->state, + graph->state->pending, graph->state->required); + + spa_list_init(&pending); + + spa_list_for_each(n, &graph->nodes, link) { + struct spa_graph_state *s = n->state; + spa_graph_state_reset(s); + spa_debug("graph %p node %p: state %p pending %d/%d status %d", graph, n, + s, s->pending, s->required, s->status); + if (--s->pending == 0) + spa_list_append(&pending, &n->sched_link); + } + spa_list_for_each_safe(n, t, &pending, sched_link) + spa_graph_node_process(n); + + return 0; +} + +static inline int spa_graph_finish(struct spa_graph *graph) +{ + spa_debug("graph %p finish", graph); + if (graph->parent) + return spa_graph_node_trigger(graph->parent); + return 0; +} +static inline int spa_graph_link_signal_node(void *data) +{ + struct spa_graph_node *node = (struct spa_graph_node *)data; + spa_debug("node %p call process", node); + return spa_graph_node_process(node); +} + +static inline int spa_graph_link_signal_graph(void *data) +{ + struct spa_graph_node *node = (struct spa_graph_node *)data; + return spa_graph_finish(node->graph); +} + +static inline void spa_graph_init(struct spa_graph *graph, struct spa_graph_state *state) +{ + spa_list_init(&graph->nodes); + graph->flags = 0; + graph->state = state; + spa_debug("graph %p init state %p", graph, state); +} + +static inline void +spa_graph_link_add(struct spa_graph_node *out, + struct spa_graph_state *state, + struct spa_graph_link *link) +{ + link->state = state; + state->required++; + spa_debug("node %p add link %p to state %p %d", out, link, state, state->required); + spa_list_append(&out->links, &link->link); +} + +static inline void spa_graph_link_remove(struct spa_graph_link *link) +{ + link->state->required--; + spa_debug("link %p state %p remove %d", link, link->state, link->state->required); + spa_list_remove(&link->link); +} + +static inline void +spa_graph_node_init(struct spa_graph_node *node, struct spa_graph_state *state) +{ + spa_list_init(&node->ports[SPA_DIRECTION_INPUT]); + spa_list_init(&node->ports[SPA_DIRECTION_OUTPUT]); + spa_list_init(&node->links); + node->flags = 0; + node->subgraph = NULL; + node->state = state; + node->state->required = node->state->pending = 0; + node->state->status = SPA_STATUS_OK; + node->graph_link.signal = spa_graph_link_signal_graph; + node->graph_link.signal_data = node; + spa_debug("node %p init state %p", node, state); +} + + +static inline int spa_graph_node_impl_sub_process(void *data, struct spa_graph_node *node) +{ + struct spa_graph *graph = node->subgraph; + spa_debug("node %p: sub process %p", node, graph); + return spa_graph_run(graph); +} + +static const struct spa_graph_node_callbacks spa_graph_node_sub_impl_default = { + SPA_VERSION_GRAPH_NODE_CALLBACKS, + .process = spa_graph_node_impl_sub_process, +}; + +static inline void spa_graph_node_set_subgraph(struct spa_graph_node *node, + struct spa_graph *subgraph) +{ + node->subgraph = subgraph; + subgraph->parent = node; + spa_debug("node %p set subgraph %p", node, subgraph); +} + +static inline void +spa_graph_node_set_callbacks(struct spa_graph_node *node, + const struct spa_graph_node_callbacks *callbacks, + void *data) +{ + node->callbacks = SPA_CALLBACKS_INIT(callbacks, data); +} + +static inline void +spa_graph_node_add(struct spa_graph *graph, + struct spa_graph_node *node) +{ + node->graph = graph; + spa_list_append(&graph->nodes, &node->link); + node->state->required++; + spa_debug("node %p add to graph %p, state %p required %d", + node, graph, node->state, node->state->required); + spa_graph_link_add(node, graph->state, &node->graph_link); +} + +static inline void spa_graph_node_remove(struct spa_graph_node *node) +{ + spa_debug("node %p remove from graph %p, state %p required %d", + node, node->graph, node->state, node->state->required); + spa_graph_link_remove(&node->graph_link); + node->state->required--; + spa_list_remove(&node->link); +} + + +static inline void +spa_graph_port_init(struct spa_graph_port *port, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags) +{ + spa_debug("port %p init type %d id %d", port, direction, port_id); + port->direction = direction; + port->port_id = port_id; + port->flags = flags; +} + +static inline void +spa_graph_port_add(struct spa_graph_node *node, + struct spa_graph_port *port) +{ + spa_debug("port %p add to node %p", port, node); + port->node = node; + spa_list_append(&node->ports[port->direction], &port->link); +} + +static inline void spa_graph_port_remove(struct spa_graph_port *port) +{ + spa_debug("port %p remove", port); + spa_list_remove(&port->link); +} + +static inline void +spa_graph_port_link(struct spa_graph_port *out, struct spa_graph_port *in) +{ + spa_debug("port %p link to %p %p %p", out, in, in->node, in->node->state); + out->peer = in; + in->peer = out; +} + +static inline void +spa_graph_port_unlink(struct spa_graph_port *port) +{ + spa_debug("port %p unlink from %p", port, port->peer); + if (port->peer) { + port->peer->peer = NULL; + port->peer = NULL; + } +} + +static inline int spa_graph_node_impl_process(void *data, struct spa_graph_node *node) +{ + struct spa_node *n = (struct spa_node *)data; + struct spa_graph_state *state = node->state; + + spa_debug("node %p: process state %p: %d, node %p", node, state, state->status, n); + if ((state->status = spa_node_process(n)) != SPA_STATUS_OK) + spa_graph_node_trigger(node); + + return state->status; +} + +static inline int spa_graph_node_impl_reuse_buffer(void *data, struct spa_graph_node *node, + uint32_t port_id, uint32_t buffer_id) +{ + struct spa_node *n = (struct spa_node *)data; + return spa_node_port_reuse_buffer(n, port_id, buffer_id); +} + +static const struct spa_graph_node_callbacks spa_graph_node_impl_default = { + SPA_VERSION_GRAPH_NODE_CALLBACKS, + .process = spa_graph_node_impl_process, + .reuse_buffer = spa_graph_node_impl_reuse_buffer, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_GRAPH_H */ diff --git a/spa/include/spa/interfaces/audio/aec.h b/spa/include/spa/interfaces/audio/aec.h new file mode 100644 index 0000000..c5dcb68 --- /dev/null +++ b/spa/include/spa/interfaces/audio/aec.h @@ -0,0 +1,110 @@ +/* PipeWire + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF 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 + +#ifndef SPA_AUDIO_AEC_H +#define SPA_AUDIO_AEC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define SPA_TYPE_INTERFACE_AUDIO_AEC SPA_TYPE_INFO_INTERFACE_BASE "Audio:AEC" + +#define SPA_VERSION_AUDIO_AEC 1 +struct spa_audio_aec { + struct spa_interface iface; + const char *name; + const struct spa_dict *info; + const char *latency; +}; + +struct spa_audio_aec_info { +#define SPA_AUDIO_AEC_CHANGE_MASK_PROPS (1u<<0) + uint64_t change_mask; + + const struct spa_dict *props; +}; + +struct spa_audio_aec_events { +#define SPA_VERSION_AUDIO_AEC_EVENTS 0 + uint32_t version; /**< version of this structure */ + + /** Emitted when info changes */ + void (*info) (void *data, const struct spa_audio_aec_info *info); +}; + +struct spa_audio_aec_methods { +#define SPA_VERSION_AUDIO_AEC_METHODS 2 + uint32_t version; + + int (*add_listener) (void *object, + struct spa_hook *listener, + const struct spa_audio_aec_events *events, + void *data); + + int (*init) (void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info); + int (*run) (void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples); + int (*set_props) (void *object, const struct spa_dict *args); + /* since 0.3.58, version 1:1 */ + int (*activate) (void *object); + /* since 0.3.58, version 1:1 */ + int (*deactivate) (void *object); + + /* version 1:2 */ + int (*enum_props) (void* object, int index, struct spa_pod_builder* builder); + int (*get_params) (void* object, struct spa_pod_builder* builder); + int (*set_params) (void *object, const struct spa_pod *args); +}; + +#define spa_audio_aec_method(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_audio_aec *_o = (o); \ + spa_interface_call_res(&_o->iface, \ + struct spa_audio_aec_methods, _res, \ + method, (version), ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_audio_aec_add_listener(o,...) spa_audio_aec_method(o, add_listener, 0, __VA_ARGS__) +#define spa_audio_aec_init(o,...) spa_audio_aec_method(o, init, 0, __VA_ARGS__) +#define spa_audio_aec_run(o,...) spa_audio_aec_method(o, run, 0, __VA_ARGS__) +#define spa_audio_aec_set_props(o,...) spa_audio_aec_method(o, set_props, 0, __VA_ARGS__) +#define spa_audio_aec_activate(o) spa_audio_aec_method(o, activate, 1) +#define spa_audio_aec_deactivate(o) spa_audio_aec_method(o, deactivate, 1) +#define spa_audio_aec_enum_props(o,...) spa_audio_aec_method(o, enum_props, 2, __VA_ARGS__) +#define spa_audio_aec_get_params(o,...) spa_audio_aec_method(o, get_params, 2, __VA_ARGS__) +#define spa_audio_aec_set_params(o,...) spa_audio_aec_method(o, set_params, 2, __VA_ARGS__) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AEC_H */ diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h new file mode 100644 index 0000000..59fea51 --- /dev/null +++ b/spa/include/spa/monitor/device.h @@ -0,0 +1,307 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEVICE_H +#define SPA_DEVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +/** + * \defgroup spa_device Device + * + * The device interface can be used to monitor all kinds of devices + * and create objects as a result. Objects a typically other + * Devices or Nodes. + * + */ + +/** + * \addtogroup spa_device + * \{ + */ +#define SPA_TYPE_INTERFACE_Device SPA_TYPE_INFO_INTERFACE_BASE "Device" + +#define SPA_VERSION_DEVICE 0 +struct spa_device { struct spa_interface iface; }; + +/** + * Information about the device and parameters it supports + * + * This information is part of the info event on a device. + */ +struct spa_device_info { +#define SPA_VERSION_DEVICE_INFO 0 + uint32_t version; + +#define SPA_DEVICE_CHANGE_MASK_FLAGS (1u<<0) +#define SPA_DEVICE_CHANGE_MASK_PROPS (1u<<1) +#define SPA_DEVICE_CHANGE_MASK_PARAMS (1u<<2) + uint64_t change_mask; + uint64_t flags; + const struct spa_dict *props; /**< device properties */ + struct spa_param_info *params; /**< supported parameters */ + uint32_t n_params; /**< number of elements in params */ +}; + +#define SPA_DEVICE_INFO_INIT() ((struct spa_device_info){ SPA_VERSION_DEVICE_INFO, }) + +/** + * Information about a device object + * + * This information is part of the object_info event on the device. + */ +struct spa_device_object_info { +#define SPA_VERSION_DEVICE_OBJECT_INFO 0 + uint32_t version; + + const char *type; /**< the object type managed by this device */ + const char *factory_name; /**< a factory name that implements the object */ + +#define SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS (1u<<0) +#define SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS (1u<<1) + uint64_t change_mask; + uint64_t flags; + const struct spa_dict *props; /**< extra object properties */ +}; + +#define SPA_DEVICE_OBJECT_INFO_INIT() ((struct spa_device_object_info){ SPA_VERSION_DEVICE_OBJECT_INFO, }) + +/** the result of spa_device_enum_params() */ +#define SPA_RESULT_TYPE_DEVICE_PARAMS 1 +struct spa_result_device_params { + uint32_t id; + uint32_t index; + uint32_t next; + struct spa_pod *param; +}; + +#define SPA_DEVICE_EVENT_INFO 0 +#define SPA_DEVICE_EVENT_RESULT 1 +#define SPA_DEVICE_EVENT_EVENT 2 +#define SPA_DEVICE_EVENT_OBJECT_INFO 3 +#define SPA_DEVICE_EVENT_NUM 4 + +/** + * spa_device_events: + * + * Events are always emitted from the main thread + */ +struct spa_device_events { + /** version of the structure */ +#define SPA_VERSION_DEVICE_EVENTS 0 + uint32_t version; + + /** notify extra information about the device */ + void (*info) (void *data, const struct spa_device_info *info); + + /** notify a result */ + void (*result) (void *data, int seq, int res, uint32_t type, const void *result); + + /** a device event */ + void (*event) (void *data, const struct spa_event *event); + + /** info changed for an object managed by the device, info is NULL when + * the object is removed */ + void (*object_info) (void *data, uint32_t id, + const struct spa_device_object_info *info); +}; + +#define SPA_DEVICE_METHOD_ADD_LISTENER 0 +#define SPA_DEVICE_METHOD_SYNC 1 +#define SPA_DEVICE_METHOD_ENUM_PARAMS 2 +#define SPA_DEVICE_METHOD_SET_PARAM 3 +#define SPA_DEVICE_METHOD_NUM 4 + +/** + * spa_device_methods: + */ +struct spa_device_methods { + /* the version of the methods. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_DEVICE_METHODS 0 + uint32_t version; + + /** + * Set events to receive asynchronous notifications from + * the device. + * + * Setting the events will trigger the info event and an + * object_info event for each managed object on the new + * listener. + * + * \param object a \ref spa_device + * \param listener a listener + * \param events a struct \ref spa_device_events + * \param data data passed as first argument in functions of \a events + * \return 0 on success + * < 0 errno on error + */ + int (*add_listener) (void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data); + /** + * Perform a sync operation. + * + * This method will emit the result event with the given sequence + * number synchronously or with the returned async return value + * asynchronously. + * + * Because all methods are serialized in the device, this can be used + * to wait for completion of all previous method calls. + * + * \param seq a sequence number + * \return 0 on success + * -EINVAL when node is NULL + * an async result + */ + int (*sync) (void *object, int seq); + + /** + * Enumerate the parameters of a device. + * + * Parameters are identified with an \a id. Some parameters can have + * multiple values, see the documentation of the parameter id. + * + * Parameters can be filtered by passing a non-NULL \a filter. + * + * The result callback will be called at most \a max times with a + * struct spa_result_device_params as the result. + * + * This function must be called from the main thread. + * + * \param device a \ref spa_device + * \param seq a sequence number to pass to the result function + * \param id the param id to enumerate + * \param index the index of enumeration, pass 0 for the first item. + * \param max the maximum number of items to iterate + * \param filter and optional filter to use + * \return 0 when there are no more parameters to enumerate + * -EINVAL when invalid arguments are given + * -ENOENT the parameter \a id is unknown + * -ENOTSUP when there are no parameters + * implemented on \a device + */ + int (*enum_params) (void *object, int seq, + uint32_t id, uint32_t index, uint32_t max, + const struct spa_pod *filter); + + /** + * Set the configurable parameter in \a device. + * + * Usually, \a param will be obtained from enum_params and then + * modified but it is also possible to set another spa_pod + * as long as its keys and types match a supported object. + * + * Objects with property keys that are not known are ignored. + * + * This function must be called from the main thread. + * + * \param object \ref spa_device + * \param id the parameter id to configure + * \param flags additional flags + * \param param the parameter to configure + * + * \return 0 on success + * -EINVAL when invalid arguments are given + * -ENOTSUP when there are no parameters implemented on \a device + * -ENOENT the parameter is unknown + */ + int (*set_param) (void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param); +}; + +#define spa_device_method(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_device *_o = (o); \ + spa_interface_call_res(&_o->iface, \ + struct spa_device_methods, _res, \ + method, (version), ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_device_add_listener(d,...) spa_device_method(d, add_listener, 0, __VA_ARGS__) +#define spa_device_sync(d,...) spa_device_method(d, sync, 0, __VA_ARGS__) +#define spa_device_enum_params(d,...) spa_device_method(d, enum_params, 0, __VA_ARGS__) +#define spa_device_set_param(d,...) spa_device_method(d, set_param, 0, __VA_ARGS__) + +#define SPA_KEY_DEVICE_ENUM_API "device.enum.api" /**< the api used to discover this + * device */ +#define SPA_KEY_DEVICE_API "device.api" /**< the api used by the device + * Ex. "udev", "alsa", "v4l2". */ +#define SPA_KEY_DEVICE_NAME "device.name" /**< the name of the device */ +#define SPA_KEY_DEVICE_ALIAS "device.alias" /**< alternative name of the device */ +#define SPA_KEY_DEVICE_NICK "device.nick" /**< the device short name */ +#define SPA_KEY_DEVICE_DESCRIPTION "device.description" /**< a device description */ +#define SPA_KEY_DEVICE_ICON "device.icon" /**< icon for the device. A base64 blob + * containing PNG image data */ +#define SPA_KEY_DEVICE_ICON_NAME "device.icon-name" /**< an XDG icon name for the device. + * Ex. "sound-card-speakers-usb" */ +#define SPA_KEY_DEVICE_PLUGGED_USEC "device.plugged.usec" /**< when the device was plugged */ + +#define SPA_KEY_DEVICE_BUS_ID "device.bus-id" /**< the device bus-id */ +#define SPA_KEY_DEVICE_BUS_PATH "device.bus-path" /**< bus path to the device in the OS' + * format. + * Ex. "pci-0000:00:14.0-usb-0:3.2:1.0" */ +#define SPA_KEY_DEVICE_BUS "device.bus" /**< bus of the device if applicable. One of + * "isa", "pci", "usb", "firewire", + * "bluetooth" */ +#define SPA_KEY_DEVICE_SUBSYSTEM "device.subsystem" /**< device subsystem */ +#define SPA_KEY_DEVICE_SYSFS_PATH "device.sysfs.path" /**< device sysfs path */ + +#define SPA_KEY_DEVICE_VENDOR_ID "device.vendor.id" /**< vendor ID if applicable */ +#define SPA_KEY_DEVICE_VENDOR_NAME "device.vendor.name" /**< vendor name if applicable */ +#define SPA_KEY_DEVICE_PRODUCT_ID "device.product.id" /**< product ID if applicable */ +#define SPA_KEY_DEVICE_PRODUCT_NAME "device.product.name" /**< product name if applicable */ +#define SPA_KEY_DEVICE_SERIAL "device.serial" /**< Serial number if applicable */ +#define SPA_KEY_DEVICE_CLASS "device.class" /**< device class */ +#define SPA_KEY_DEVICE_CAPABILITIES "device.capabilities" /**< api specific device capabilities */ +#define SPA_KEY_DEVICE_FORM_FACTOR "device.form-factor" /**< form factor if applicable. One of + * "internal", "speaker", "handset", "tv", + * "webcam", "microphone", "headset", + * "headphone", "hands-free", "car", "hifi", + * "computer", "portable" */ +#define SPA_KEY_DEVICE_PROFILE "device.profile " /**< profile for the device */ +#define SPA_KEY_DEVICE_PROFILE_SET "device.profile-set" /**< profile set for the device */ +#define SPA_KEY_DEVICE_STRING "device.string" /**< device string in the underlying + * layer's format. E.g. "surround51:0" */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEVICE_H */ diff --git a/spa/include/spa/monitor/event.h b/spa/include/spa/monitor/event.h new file mode 100644 index 0000000..7b8a256 --- /dev/null +++ b/spa/include/spa/monitor/event.h @@ -0,0 +1,63 @@ +/* Simple Plugin API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_EVENT_DEVICE_H +#define SPA_EVENT_DEVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_device + * \{ + */ + +/* object id of SPA_TYPE_EVENT_Device */ +enum spa_device_event { + SPA_DEVICE_EVENT_ObjectConfig, +}; + +#define SPA_DEVICE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Device) +#define SPA_DEVICE_EVENT_INIT(id) SPA_EVENT_INIT(SPA_TYPE_EVENT_Device, id) + +/* properties for SPA_TYPE_EVENT_Device */ +enum spa_event_device { + SPA_EVENT_DEVICE_START, + + SPA_EVENT_DEVICE_Object, /* an object id (Int) */ + SPA_EVENT_DEVICE_Props, /* properties for an object (SPA_TYPE_OBJECT_Props) */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_EVENT_DEVICE */ diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h new file mode 100644 index 0000000..6bf781a --- /dev/null +++ b/spa/include/spa/monitor/type-info.h @@ -0,0 +1,67 @@ +/* Simple Plugin API + * + * Copyright © 2021 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEVICE_TYPE_INFO_H +#define SPA_DEVICE_TYPE_INFO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#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..169fe47 --- /dev/null +++ b/spa/include/spa/monitor/utils.h @@ -0,0 +1,106 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DEVICE_UTILS_H +#define SPA_DEVICE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \addtogroup spa_device + * \{ + */ + +struct spa_result_device_params_data { + struct spa_pod_builder *builder; + struct spa_result_device_params data; +}; + +static inline void spa_result_func_device_params(void *data, int seq, int res, + uint32_t type, const void *result) +{ + struct spa_result_device_params_data *d = + (struct spa_result_device_params_data *)data; + const struct spa_result_device_params *r = + (const struct spa_result_device_params *)result; + uint32_t offset = d->builder->state.offset; + if (spa_pod_builder_raw_padded(d->builder, r->param, SPA_POD_SIZE(r->param)) < 0) + return; + d->data.next = r->next; + d->data.param = spa_pod_builder_deref(d->builder, offset); +} + +static inline int spa_device_enum_params_sync(struct spa_device *device, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_device_params_data data = { builder, }; + struct spa_hook listener = {{0}}; + static const struct spa_device_events device_events = { + .version = SPA_VERSION_DEVICE_EVENTS, + .info = NULL, + .result = spa_result_func_device_params, + }; + int res; + + spa_device_add_listener(device, &listener, &device_events, &data); + res = spa_device_enum_params(device, 0, id, *index, 1, filter); + spa_hook_remove(&listener); + + if (data.data.param == NULL) { + if (res > 0) + res = 0; + } else { + *index = data.data.next; + *param = data.data.param; + res = 1; + } + return res; +} + +#define spa_device_emit(hooks,method,version,...) \ + spa_hook_list_call_simple(hooks, struct spa_device_events, \ + method, version, ##__VA_ARGS__) + +#define spa_device_emit_info(hooks,i) spa_device_emit(hooks,info, 0, i) +#define spa_device_emit_result(hooks,s,r,t,res) spa_device_emit(hooks,result, 0, s, r, t, res) +#define spa_device_emit_event(hooks,e) spa_device_emit(hooks,event, 0, e) +#define spa_device_emit_object_info(hooks,id,i) spa_device_emit(hooks,object_info, 0, id, i) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DEVICE_UTILS_H */ diff --git a/spa/include/spa/node/command.h b/spa/include/spa/node/command.h new file mode 100644 index 0000000..9bf50fb --- /dev/null +++ b/spa/include/spa/node/command.h @@ -0,0 +1,73 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_COMMAND_NODE_H +#define SPA_COMMAND_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +/* object id of SPA_TYPE_COMMAND_Node */ +enum spa_node_command { + SPA_NODE_COMMAND_Suspend, /**< suspend a node, this removes all configured + * formats and closes any devices */ + SPA_NODE_COMMAND_Pause, /**< pause a node. this makes it stop emitting + * scheduling events */ + SPA_NODE_COMMAND_Start, /**< start a node, this makes it start emitting + * scheduling events */ + SPA_NODE_COMMAND_Enable, + SPA_NODE_COMMAND_Disable, + SPA_NODE_COMMAND_Flush, + SPA_NODE_COMMAND_Drain, + SPA_NODE_COMMAND_Marker, + SPA_NODE_COMMAND_ParamBegin, /**< begin a set of parameter enumerations or + * configuration that require the device to + * remain opened, like query formats and then + * set a format */ + SPA_NODE_COMMAND_ParamEnd, /**< end a transaction */ + SPA_NODE_COMMAND_RequestProcess,/**< Sent to a driver when some other node emitted + * the RequestProcess event. */ +}; + +#define SPA_NODE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Node) +#define SPA_NODE_COMMAND_INIT(id) SPA_COMMAND_INIT(SPA_TYPE_COMMAND_Node, id) + + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_COMMAND_NODE_H */ diff --git a/spa/include/spa/node/event.h b/spa/include/spa/node/event.h new file mode 100644 index 0000000..ceb6d60 --- /dev/null +++ b/spa/include/spa/node/event.h @@ -0,0 +1,64 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_EVENT_NODE_H +#define SPA_EVENT_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +/* object id of SPA_TYPE_EVENT_Node */ +enum spa_node_event { + SPA_NODE_EVENT_Error, + SPA_NODE_EVENT_Buffering, + SPA_NODE_EVENT_RequestRefresh, + SPA_NODE_EVENT_RequestProcess, /*< Ask the driver to start processing + * the graph */ +}; + +#define SPA_NODE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Node) +#define SPA_NODE_EVENT_INIT(id) SPA_EVENT_INIT(SPA_TYPE_EVENT_Node, id) + +/* properties for SPA_TYPE_EVENT_Node */ +enum spa_event_node { + SPA_EVENT_NODE_START, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_EVENT_NODE_H */ diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h new file mode 100644 index 0000000..9211f65 --- /dev/null +++ b/spa/include/spa/node/io.h @@ -0,0 +1,304 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_IO_H +#define SPA_IO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include +#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 */ + SPA_IO_Clock, /**< area to update clock information, struct spa_io_clock */ + SPA_IO_Latency, /**< latency reporting, struct spa_io_latency */ + SPA_IO_Control, /**< area for control messages, struct spa_io_sequence */ + SPA_IO_Notify, /**< area for notify messages, struct spa_io_sequence */ + SPA_IO_Position, /**< position information in the graph, struct spa_io_position */ + SPA_IO_RateMatch, /**< rate matching between nodes, struct spa_io_rate_match */ + SPA_IO_Memory, /**< memory pointer, struct spa_io_memory */ +}; + +/** + * IO area to exchange buffers. + * + * A set of buffers should first be configured on the node/port. + * Further references to those buffers will be made by using the + * id of the buffer. + * + * If status is SPA_STATUS_OK, the host should ignore + * the io area. + * + * If status is SPA_STATUS_NEED_DATA, the host should: + * 1) recycle the buffer in buffer_id, if possible + * 2) prepare a new buffer and place the id in buffer_id. + * + * If status is SPA_STATUS_HAVE_DATA, the host should consume + * the buffer in buffer_id and set the state to + * SPA_STATUS_NEED_DATA when new data is requested. + * + * If status is SPA_STATUS_STOPPED, some error occurred on the + * port. + * + * If status is SPA_STATUS_DRAINED, data from the io area was + * used to drain. + * + * Status can also be a negative errno value to indicate errors. + * such as: + * -EINVAL: buffer_id is invalid + * -EPIPE: no more buffers available + */ +struct spa_io_buffers { +#define SPA_STATUS_OK 0 +#define SPA_STATUS_NEED_DATA (1<<0) +#define SPA_STATUS_HAVE_DATA (1<<1) +#define SPA_STATUS_STOPPED (1<<2) +#define SPA_STATUS_DRAINED (1<<3) + int32_t status; /**< the status code */ + uint32_t buffer_id; /**< a buffer id */ +}; + +#define SPA_IO_BUFFERS_INIT ((struct spa_io_buffers) { SPA_STATUS_OK, SPA_ID_INVALID, }) + +/** + * IO area to exchange a memory region + */ +struct spa_io_memory { + int32_t status; /**< the status code */ + uint32_t size; /**< the size of \a data */ + void *data; /**< a memory pointer */ +}; +#define SPA_IO_MEMORY_INIT ((struct spa_io_memory) { SPA_STATUS_OK, 0, NULL, }) + +/** A range, suitable for input ports that can suggest a range to output ports */ +struct spa_io_range { + uint64_t offset; /**< offset in range */ + uint32_t min_size; /**< minimum size of data */ + uint32_t max_size; /**< maximum size of data */ +}; + +/** + * Absolute time reporting. + * + * Nodes that can report clocking information will receive this io block. + * The application sets the id. This is usually set as part of the + * position information but can also be set separately. + * + * The clock counts the elapsed time according to the clock provider + * since the provider was last started. + */ +struct spa_io_clock { +#define SPA_IO_CLOCK_FLAG_FREEWHEEL (1u<<0) + uint32_t flags; /**< clock flags */ + uint32_t id; /**< unique clock id, set by application */ + char name[64]; /**< clock name prefixed with API, set by node. The clock name + * is unique per clock and can be used to check if nodes + * share the same clock. */ + uint64_t nsec; /**< time in nanoseconds against monotonic clock */ + struct spa_fraction rate; /**< rate for position/duration/delay */ + uint64_t position; /**< current position */ + uint64_t duration; /**< duration of current cycle */ + int64_t delay; /**< delay between position and hardware, + * positive for capture, negative for playback */ + double rate_diff; /**< rate difference between clock and monotonic time */ + uint64_t next_nsec; /**< estimated next wakeup time in nanoseconds */ + uint32_t padding[8]; +}; + +/* the size of the video in this cycle */ +struct spa_io_video_size { +#define SPA_IO_VIDEO_SIZE_VALID (1<<0) + uint32_t flags; /**< optional flags */ + uint32_t stride; /**< video stride in bytes */ + struct spa_rectangle size; /**< the video size */ + struct spa_fraction framerate; /**< the minimum framerate, the cycle duration is + * always smaller to ensure there is only one + * video frame per cycle. */ + uint32_t padding[4]; +}; + +/** latency reporting */ +struct spa_io_latency { + struct spa_fraction rate; /**< rate for min/max */ + uint64_t min; /**< min latency */ + uint64_t max; /**< max latency */ +}; + +/** control stream, io area for SPA_IO_Control and SPA_IO_Notify */ +struct spa_io_sequence { + struct spa_pod_sequence sequence; /**< sequence of timed events */ +}; + +/** bar and beat segment */ +struct spa_io_segment_bar { +#define SPA_IO_SEGMENT_BAR_FLAG_VALID (1<<0) + uint32_t flags; /**< extra flags */ + uint32_t offset; /**< offset in segment of this beat */ + float signature_num; /**< time signature numerator */ + float signature_denom; /**< time signature denominator */ + double bpm; /**< beats per minute */ + double beat; /**< current beat in segment */ + uint32_t padding[8]; +}; + +/** video frame segment */ +struct spa_io_segment_video { +#define SPA_IO_SEGMENT_VIDEO_FLAG_VALID (1<<0) +#define SPA_IO_SEGMENT_VIDEO_FLAG_DROP_FRAME (1<<1) +#define SPA_IO_SEGMENT_VIDEO_FLAG_PULL_DOWN (1<<2) +#define SPA_IO_SEGMENT_VIDEO_FLAG_INTERLACED (1<<3) + uint32_t flags; /**< flags */ + uint32_t offset; /**< offset in segment */ + struct spa_fraction framerate; + uint32_t hours; + uint32_t minutes; + uint32_t seconds; + uint32_t frames; + uint32_t field_count; /**< 0 for progressive, 1 and 2 for interlaced */ + uint32_t padding[11]; +}; + +/** + * A segment converts a running time to a segment (stream) position. + * + * The segment position is valid when the current running time is between + * start and start + duration. The position is then + * calculated as: + * + * (running time - start) * rate + position; + * + * Support for looping is done by specifying the LOOPING flags with a + * non-zero duration. When the running time reaches start + duration, + * duration is added to start and the loop repeats. + * + * Care has to be taken when the running time + clock.duration extends + * past the start + duration from the segment; the user should correctly + * wrap around and partially repeat the loop in the current cycle. + * + * Extra information can be placed in the segment by setting the valid flags + * and filling up the corresponding structures. + */ +struct spa_io_segment { + uint32_t version; +#define SPA_IO_SEGMENT_FLAG_LOOPING (1<<0) /**< after the duration, the segment repeats */ +#define SPA_IO_SEGMENT_FLAG_NO_POSITION (1<<1) /**< position is invalid. The position can be invalid + * after a seek, for example, when the exact mapping + * of the extra segment info (bar, video, ...) to + * position has not been determined yet */ + uint32_t flags; /**< extra flags */ + uint64_t start; /**< value of running time when this + * info is active. Can be in the future for + * pending changes. It does not have to be in + * exact multiples of the clock duration. */ + uint64_t duration; /**< duration when this info becomes invalid expressed + * in running time. If the duration is 0, this + * segment extends to the next segment. If the + * segment becomes invalid and the looping flag is + * set, the segment repeats. */ + double rate; /**< overall rate of the segment, can be negative for + * backwards time reporting. */ + uint64_t position; /**< The position when the running time == start. + * can be invalid when the owner of the extra segment + * information has not yet made the mapping. */ + + struct spa_io_segment_bar bar; + struct spa_io_segment_video video; +}; + +enum spa_io_position_state { + SPA_IO_POSITION_STATE_STOPPED, + SPA_IO_POSITION_STATE_STARTING, + SPA_IO_POSITION_STATE_RUNNING, +}; + +/** the maximum number of segments visible in the future */ +#define SPA_IO_POSITION_MAX_SEGMENTS 8 + +/** + * The position information adds extra meaning to the raw clock times. + * + * It is set on all nodes and the clock id will contain the clock of the + * driving node in the graph. + * + * The position information contains 1 or more segments that convert the + * raw clock times to a stream time. They are sorted based on their + * start times, and thus the order in which they will activate in + * the future. This makes it possible to look ahead in the scheduled + * segments and anticipate the changes in the timeline. + */ +struct spa_io_position { + struct spa_io_clock clock; /**< clock position of driver, always valid and + * read only */ + struct spa_io_video_size video; /**< size of the video in the current cycle */ + int64_t offset; /**< an offset to subtract from the clock position + * to get a running time. This is the time that + * the state has been in the RUNNING state and the + * time that should be used to compare the segment + * start values against. */ + uint32_t state; /**< one of enum spa_io_position_state */ + + uint32_t n_segments; /**< number of segments */ + struct spa_io_segment segments[SPA_IO_POSITION_MAX_SEGMENTS]; /**< segments */ +}; + +/** rate matching */ +struct spa_io_rate_match { + uint32_t delay; /**< extra delay in samples for resampler */ + uint32_t size; /**< requested input size for resampler */ + double rate; /**< rate for resampler */ +#define SPA_IO_RATE_MATCH_FLAG_ACTIVE (1 << 0) + uint32_t flags; /**< extra flags */ + uint32_t padding[7]; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_IO_H */ diff --git a/spa/include/spa/node/keys.h b/spa/include/spa/node/keys.h new file mode 100644 index 0000000..94a0285 --- /dev/null +++ b/spa/include/spa/node/keys.h @@ -0,0 +1,64 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_NODE_KEYS_H +#define SPA_NODE_KEYS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +/** node keys */ +#define SPA_KEY_NODE_NAME "node.name" /**< a node name */ +#define SPA_KEY_NODE_LATENCY "node.latency" /**< the requested node latency */ +#define SPA_KEY_NODE_MAX_LATENCY "node.max-latency" /**< maximum supported latency */ + +#define SPA_KEY_NODE_DRIVER "node.driver" /**< the node can be a driver */ +#define SPA_KEY_NODE_ALWAYS_PROCESS "node.always-process" /**< call the process function even if + * not linked. */ +#define SPA_KEY_NODE_PAUSE_ON_IDLE "node.pause-on-idle" /**< if the node should be paused + * immediately when idle. */ +#define SPA_KEY_NODE_MONITOR "node.monitor" /**< the node has monitor ports */ + + +/** port keys */ +#define SPA_KEY_PORT_NAME "port.name" /**< a port name */ +#define SPA_KEY_PORT_ALIAS "port.alias" /**< a port alias */ +#define SPA_KEY_PORT_MONITOR "port.monitor" /**< this port is a monitor port */ + + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_NODE_KEYS_H */ diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h new file mode 100644 index 0000000..6efa6d0 --- /dev/null +++ b/spa/include/spa/node/node.h @@ -0,0 +1,680 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_NODE_H +#define SPA_NODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_node Node + * + * A spa_node is a component that can consume and produce buffers. + */ + +/** + * \addtogroup spa_node + * \{ + */ + +#include +#include +#include +#include +#include +#include +#include + + +#define SPA_TYPE_INTERFACE_Node SPA_TYPE_INFO_INTERFACE_BASE "Node" + +#define SPA_VERSION_NODE 0 +struct spa_node { struct spa_interface iface; }; + +/** + * Node information structure + * + * Contains the basic node information. + */ +struct spa_node_info { + uint32_t max_input_ports; + uint32_t max_output_ports; +#define SPA_NODE_CHANGE_MASK_FLAGS (1u<<0) +#define SPA_NODE_CHANGE_MASK_PROPS (1u<<1) +#define SPA_NODE_CHANGE_MASK_PARAMS (1u<<2) + uint64_t change_mask; + +#define SPA_NODE_FLAG_RT (1u<<0) /**< node can do real-time processing */ +#define SPA_NODE_FLAG_IN_DYNAMIC_PORTS (1u<<1) /**< input ports can be added/removed */ +#define SPA_NODE_FLAG_OUT_DYNAMIC_PORTS (1u<<2) /**< output ports can be added/removed */ +#define SPA_NODE_FLAG_IN_PORT_CONFIG (1u<<3) /**< input ports can be reconfigured with + * PortConfig parameter */ +#define SPA_NODE_FLAG_OUT_PORT_CONFIG (1u<<4) /**< output ports can be reconfigured with + * PortConfig parameter */ +#define SPA_NODE_FLAG_NEED_CONFIGURE (1u<<5) /**< node needs configuration before it can + * be started. */ +#define SPA_NODE_FLAG_ASYNC (1u<<6) /**< the process function might not + * immediately produce or consume data + * but might offload the work to a worker + * thread. */ + uint64_t flags; + struct spa_dict *props; /**< extra node properties */ + struct spa_param_info *params; /**< parameter information */ + uint32_t n_params; /**< number of items in \a params */ +}; + +#define SPA_NODE_INFO_INIT() ((struct spa_node_info) { 0, }) + +/** + * Port information structure + * + * Contains the basic port information. + */ +struct spa_port_info { +#define SPA_PORT_CHANGE_MASK_FLAGS (1u<<0) +#define SPA_PORT_CHANGE_MASK_RATE (1u<<1) +#define SPA_PORT_CHANGE_MASK_PROPS (1u<<2) +#define SPA_PORT_CHANGE_MASK_PARAMS (1u<<3) + uint64_t change_mask; + +#define SPA_PORT_FLAG_REMOVABLE (1u<<0) /**< port can be removed */ +#define SPA_PORT_FLAG_OPTIONAL (1u<<1) /**< processing on port is optional */ +#define SPA_PORT_FLAG_CAN_ALLOC_BUFFERS (1u<<2) /**< the port can allocate buffer data */ +#define SPA_PORT_FLAG_IN_PLACE (1u<<3) /**< the port can process data in-place and + * will need a writable input buffer */ +#define SPA_PORT_FLAG_NO_REF (1u<<4) /**< the port does not keep a ref on the buffer. + * This means the node will always completely + * consume the input buffer and it will be + * recycled after process. */ +#define SPA_PORT_FLAG_LIVE (1u<<5) /**< output buffers from this port are + * timestamped against a live clock. */ +#define SPA_PORT_FLAG_PHYSICAL (1u<<6) /**< connects to some device */ +#define SPA_PORT_FLAG_TERMINAL (1u<<7) /**< data was not created from this port + * or will not be made available on another + * port */ +#define SPA_PORT_FLAG_DYNAMIC_DATA (1u<<8) /**< data pointer on buffers can be changed. + * Only the buffer data marked as DYNAMIC + * can be changed. */ + uint64_t flags; /**< port flags */ + struct spa_fraction rate; /**< rate of sequence numbers on port */ + const struct spa_dict *props; /**< extra port properties */ + struct spa_param_info *params; /**< parameter information */ + uint32_t n_params; /**< number of items in \a params */ +}; + +#define SPA_PORT_INFO_INIT() ((struct spa_port_info) { 0, }) + +#define SPA_RESULT_TYPE_NODE_ERROR 1 +#define SPA_RESULT_TYPE_NODE_PARAMS 2 + +/** an error result */ +struct spa_result_node_error { + const char *message; +}; + +/** the result of enum_params or port_enum_params. */ +struct spa_result_node_params { + uint32_t id; /**< id of parameter */ + uint32_t index; /**< index of parameter */ + uint32_t next; /**< next index of iteration */ + struct spa_pod *param; /**< the result param */ +}; + +#define SPA_NODE_EVENT_INFO 0 +#define SPA_NODE_EVENT_PORT_INFO 1 +#define SPA_NODE_EVENT_RESULT 2 +#define SPA_NODE_EVENT_EVENT 3 +#define SPA_NODE_EVENT_NUM 4 + +/** events from the spa_node. + * + * All event are called from the main thread and multiple + * listeners can be registered for the events with + * spa_node_add_listener(). + */ +struct spa_node_events { +#define SPA_VERSION_NODE_EVENTS 0 + uint32_t version; /**< version of this structure */ + + /** Emitted when info changes */ + void (*info) (void *data, const struct spa_node_info *info); + + /** Emitted when port info changes, NULL when port is removed */ + void (*port_info) (void *data, + enum spa_direction direction, uint32_t port, + const struct spa_port_info *info); + + /** notify a result. + * + * Some methods will trigger a result event with an optional + * result of the given type. Look at the documentation of the + * method to know when to expect a result event. + * + * The result event can be called synchronously, as an event + * called from inside the method itself, in which case the seq + * number passed to the method will be passed unchanged. + * + * The result event will be called asynchronously when the + * method returned an async return value. In this case, the seq + * number in the result will match the async return value of + * the method call. Users should match the seq number from + * request to the reply. + */ + void (*result) (void *data, int seq, int res, + uint32_t type, const void *result); + + /** + * \param node a spa_node + * \param event the event that was emitted + * + * This will be called when an out-of-bound event is notified + * on \a node. + */ + void (*event) (void *data, const struct spa_event *event); +}; + +#define SPA_NODE_CALLBACK_READY 0 +#define SPA_NODE_CALLBACK_REUSE_BUFFER 1 +#define SPA_NODE_CALLBACK_XRUN 2 +#define SPA_NODE_CALLBACK_NUM 3 + +/** Node callbacks + * + * Callbacks are called from the real-time data thread. Only + * one callback structure can be set on an spa_node. + */ +struct spa_node_callbacks { +#define SPA_VERSION_NODE_CALLBACKS 0 + uint32_t version; + /** + * \param node a spa_node + * + * The node is ready for processing. + * + * When this function is NULL, synchronous operation is requested + * on the ports. + */ + int (*ready) (void *data, int state); + + /** + * \param node a spa_node + * \param port_id an input port_id + * \param buffer_id the buffer id to be reused + * + * The node has a buffer that can be reused. + * + * When this function is NULL, the buffers to reuse will be set in + * the io area of the input ports. + */ + int (*reuse_buffer) (void *data, + uint32_t port_id, + uint32_t buffer_id); + + /** + * \param data user data + * \param trigger the timestamp in microseconds when the xrun happened + * \param delay the amount of microseconds of xrun. + * \param info an object with extra info (NULL for now) + * + * The node has encountered an over or underrun + * + * The info contains an object with more information + */ + int (*xrun) (void *data, uint64_t trigger, uint64_t delay, + struct spa_pod *info); +}; + + +/** flags that can be passed to set_param and port_set_param functions */ +#define SPA_NODE_PARAM_FLAG_TEST_ONLY (1 << 0) /**< Just check if the param is accepted */ +#define SPA_NODE_PARAM_FLAG_FIXATE (1 << 1) /**< Fixate the non-optional unset fields */ +#define SPA_NODE_PARAM_FLAG_NEAREST (1 << 2) /**< Allow set fields to be rounded to the + * nearest allowed field value. */ + +/** flags to pass to the use_buffers functions */ +#define SPA_NODE_BUFFERS_FLAG_ALLOC (1 << 0) /**< Allocate memory for the buffers. This flag + * is ignored when the port does not have the + * SPA_PORT_FLAG_CAN_ALLOC_BUFFERS set. */ + + +#define SPA_NODE_METHOD_ADD_LISTENER 0 +#define SPA_NODE_METHOD_SET_CALLBACKS 1 +#define SPA_NODE_METHOD_SYNC 2 +#define SPA_NODE_METHOD_ENUM_PARAMS 3 +#define SPA_NODE_METHOD_SET_PARAM 4 +#define SPA_NODE_METHOD_SET_IO 5 +#define SPA_NODE_METHOD_SEND_COMMAND 6 +#define SPA_NODE_METHOD_ADD_PORT 7 +#define SPA_NODE_METHOD_REMOVE_PORT 8 +#define SPA_NODE_METHOD_PORT_ENUM_PARAMS 9 +#define SPA_NODE_METHOD_PORT_SET_PARAM 10 +#define SPA_NODE_METHOD_PORT_USE_BUFFERS 11 +#define SPA_NODE_METHOD_PORT_SET_IO 12 +#define SPA_NODE_METHOD_PORT_REUSE_BUFFER 13 +#define SPA_NODE_METHOD_PROCESS 14 +#define SPA_NODE_METHOD_NUM 15 + +/** + * Node methods + */ +struct spa_node_methods { + /* the version of the node methods. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_NODE_METHODS 0 + uint32_t version; + + /** + * Adds an event listener on \a node. + * + * Setting the events will trigger the info event and a + * port_info event for each managed port on the new + * listener. + * + * \param node a #spa_node + * \param listener a listener + * \param events a struct \ref spa_node_events + * \param data data passed as first argument in functions of \a events + * \return 0 on success + * < 0 errno on error + */ + int (*add_listener) (void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data); + /** + * Set callbacks to on \a node. + * if \a callbacks is NULL, the current callbacks are removed. + * + * This function must be called from the main thread. + * + * All callbacks are called from the data thread. + * + * \param node a spa_node + * \param callbacks callbacks to set + * \return 0 on success + * -EINVAL when node is NULL + */ + int (*set_callbacks) (void *object, + const struct spa_node_callbacks *callbacks, + void *data); + /** + * Perform a sync operation. + * + * This method will emit the result event with the given sequence + * number synchronously or with the returned async return value + * asynchronously. + * + * Because all methods are serialized in the node, this can be used + * to wait for completion of all previous method calls. + * + * \param seq a sequence number + * \return 0 on success + * -EINVAL when node is NULL + * an async result + */ + int (*sync) (void *object, int seq); + + /** + * Enumerate the parameters of a node. + * + * Parameters are identified with an \a id. Some parameters can have + * multiple values, see the documentation of the parameter id. + * + * Parameters can be filtered by passing a non-NULL \a filter. + * + * The function will emit the result event up to \a max times with + * the result value. The seq in the result will either be the \a seq + * number when executed synchronously or the async return value of + * this function when executed asynchronously. + * + * This function must be called from the main thread. + * + * \param node a \ref spa_node + * \param seq a sequence number to pass to the result event when + * this method is executed synchronously. + * \param id the param id to enumerate + * \param start the index of enumeration, pass 0 for the first item + * \param max the maximum number of parameters to enumerate + * \param filter and optional filter to use + * + * \return 0 when no more items can be iterated. + * -EINVAL when invalid arguments are given + * -ENOENT the parameter \a id is unknown + * -ENOTSUP when there are no parameters + * implemented on \a node + * an async return value when the result event will be + * emitted later. + */ + int (*enum_params) (void *object, int seq, + uint32_t id, uint32_t start, uint32_t max, + const struct spa_pod *filter); + + /** + * Set the configurable parameter in \a node. + * + * Usually, \a param will be obtained from enum_params and then + * modified but it is also possible to set another spa_pod + * as long as its keys and types match a supported object. + * + * Objects with property keys that are not known are ignored. + * + * This function must be called from the main thread. + * + * \param node a \ref spa_node + * \param id the parameter id to configure + * \param flags additional flags + * \param param the parameter to configure + * + * \return 0 on success + * -EINVAL when node is NULL + * -ENOTSUP when there are no parameters implemented on \a node + * -ENOENT the parameter is unknown + */ + int (*set_param) (void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param); + + /** + * Configure the given memory area with \a id on \a node. This + * structure is allocated by the host and is used to exchange + * data and parameters with the node. + * + * Setting an \a io of NULL will disable the node io. + * + * This function must be called from the main thread. + * + * \param id the id of the io area, the available ids can be + * enumerated with the node parameters. + * \param data a io area memory + * \param size the size of \a data + * \return 0 on success + * -EINVAL when invalid input is given + * -ENOENT when \a id is unknown + * -ENOSPC when \a size is too small + */ + int (*set_io) (void *object, + uint32_t id, void *data, size_t size); + + /** + * Send a command to a node. + * + * Upon completion, a command might change the state of a node. + * + * This function must be called from the main thread. + * + * \param node a spa_node + * \param command a spa_command + * \return 0 on success + * -EINVAL when node or command is NULL + * -ENOTSUP when this node can't process commands + * -EINVAL \a command is an invalid command + */ + int (*send_command) (void *object, const struct spa_command *command); + + /** + * Make a new port with \a port_id. The caller should use the lowest unused + * port id for the given \a direction. + * + * Port ids should be between 0 and max_ports as obtained from the info + * event. + * + * This function must be called from the main thread. + * + * \param node a spa_node + * \param direction a enum \ref spa_direction + * \param port_id an unused port id + * \param props extra properties + * \return 0 on success + * -EINVAL when node is NULL + */ + int (*add_port) (void *object, + enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props); + + /** + * Remove a port with \a port_id. + * + * \param node a spa_node + * \param direction a enum \ref spa_direction + * \param port_id a port id + * \return 0 on success + * -EINVAL when node is NULL or when port_id is unknown or + * when the port can't be removed. + */ + int (*remove_port) (void *object, + enum spa_direction direction, uint32_t port_id); + + /** + * Enumerate all possible parameters of \a id on \a port_id of \a node + * that are compatible with \a filter. + * + * The result parameters can be queried and modified and ultimately be used + * to call port_set_param. + * + * The function will emit the result event up to \a max times with + * the result value. The seq in the result event will either be the + * \a seq number when executed synchronously or the async return + * value of this function when executed asynchronously. + * + * This function must be called from the main thread. + * + * \param node a spa_node + * \param seq a sequence number to pass to the result event when + * this method is executed synchronously. + * \param direction an spa_direction + * \param port_id the port to query + * \param id the parameter id to query + * \param start the first index to query, 0 to get the first item + * \param max the maximum number of params to query + * \param filter a parameter filter or NULL for no filter + * + * \return 0 when no more items can be iterated. + * -EINVAL when invalid parameters are given + * -ENOENT when \a id is unknown + * an async return value when the result event will be + * emitted later. + */ + int (*port_enum_params) (void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t max, + const struct spa_pod *filter); + /** + * Set a parameter on \a port_id of \a node. + * + * When \a param is NULL, the parameter will be unset. + * + * This function must be called from the main thread. + * + * \param node a struct \ref spa_node + * \param direction a enum \ref spa_direction + * \param port_id the port to configure + * \param id the parameter id to set + * \param flags optional flags + * \param param a struct \ref spa_pod with the parameter to set + * \return 0 on success + * 1 on success, the value of \a param might have been + * changed depending on \a flags and the final value can be found by + * doing port_enum_params. + * -EINVAL when node is NULL or invalid arguments are given + * -ESRCH when one of the mandatory param + * properties is not specified and SPA_NODE_PARAM_FLAG_FIXATE was + * not set in \a flags. + * -ESRCH when the type or size of a property is not correct. + * -ENOENT when the param id is not found + */ + int (*port_set_param) (void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param); + + /** + * Tell the port to use the given buffers + * + * When \a flags contains SPA_NODE_BUFFERS_FLAG_ALLOC, the data + * in the buffers should point to an array of at least 1 data entry + * with the desired supported type that will be filled by this function. + * + * The port should also have a spa_io_buffers io area configured to exchange + * the buffers with the port. + * + * For an input port, all the buffers will remain dequeued. + * Once a buffer has been queued on a port in the spa_io_buffers, + * it should not be reused until the reuse_buffer callback is notified + * or when the buffer has been returned in the spa_io_buffers of + * the port. + * + * For output ports, all buffers will be queued in the port. When process + * returns SPA_STATUS_HAVE_DATA, buffers are available in one or more + * of the spa_io_buffers areas. + * + * When a buffer can be reused, port_reuse_buffer() should be called or the + * buffer_id should be placed in the spa_io_buffers area before calling + * process. + * + * Passing NULL as \a buffers will remove the reference that the port has + * on the buffers. + * + * When this function returns async, use the spa_node_sync operation to + * wait for completion. + * + * This function must be called from the main thread. + * + * \param object an object implementing the interface + * \param direction a port direction + * \param port_id a port id + * \param flags extra flags + * \param buffers an array of buffer pointers + * \param n_buffers number of elements in \a buffers + * \return 0 on success + */ + int (*port_use_buffers) (void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers); + + /** + * Configure the given memory area with \a id on \a port_id. This + * structure is allocated by the host and is used to exchange + * data and parameters with the port. + * + * Setting an \a io of NULL will disable the port io. + * + * This function must be called from the main thread. + * + * \param direction a spa_direction + * \param port_id a port id + * \param id the id of the io area, the available ids can be + * enumerated with the port parameters. + * \param data a io area memory + * \param size the size of \a data + * \return 0 on success + * -EINVAL when invalid input is given + * -ENOENT when \a id is unknown + * -ENOSPC when \a size is too small + */ + int (*port_set_io) (void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size); + + /** + * Tell an output port to reuse a buffer. + * + * This function must be called from the data thread. + * + * \param node a spa_node + * \param port_id a port id + * \param buffer_id a buffer id to reuse + * \return 0 on success + * -EINVAL when node is NULL + */ + int (*port_reuse_buffer) (void *object, uint32_t port_id, uint32_t buffer_id); + + /** + * Process the node + * + * This function must be called from the data thread. + * + * Output io areas with SPA_STATUS_NEED_DATA will recycle the + * buffers if any. + * + * Input areas with SPA_STATUS_HAVE_DATA are consumed if possible + * and the status is set to SPA_STATUS_NEED_DATA or SPA_STATUS_OK. + * + * When the node has new output buffers, the SPA_STATUS_HAVE_DATA + * bit will be set. + * + * When the node can accept new input in the next cycle, the + * SPA_STATUS_NEED_DATA bit will be set. + * + * Note that the node might return SPA_STATUS_NEED_DATA even when + * no input ports have this status. This means that the amount of + * data still available on the input ports is likely not going to + * be enough for the next cycle and the host might need to prefetch + * data for the next cycle. + */ + int (*process) (void *object); +}; + +#define spa_node_method(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_node *_n = o; \ + spa_interface_call_res(&_n->iface, \ + struct spa_node_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_node_add_listener(n,...) spa_node_method(n, add_listener, 0, __VA_ARGS__) +#define spa_node_set_callbacks(n,...) spa_node_method(n, set_callbacks, 0, __VA_ARGS__) +#define spa_node_sync(n,...) spa_node_method(n, sync, 0, __VA_ARGS__) +#define spa_node_enum_params(n,...) spa_node_method(n, enum_params, 0, __VA_ARGS__) +#define spa_node_set_param(n,...) spa_node_method(n, set_param, 0, __VA_ARGS__) +#define spa_node_set_io(n,...) spa_node_method(n, set_io, 0, __VA_ARGS__) +#define spa_node_send_command(n,...) spa_node_method(n, send_command, 0, __VA_ARGS__) +#define spa_node_add_port(n,...) spa_node_method(n, add_port, 0, __VA_ARGS__) +#define spa_node_remove_port(n,...) spa_node_method(n, remove_port, 0, __VA_ARGS__) +#define spa_node_port_enum_params(n,...) spa_node_method(n, port_enum_params, 0, __VA_ARGS__) +#define spa_node_port_set_param(n,...) spa_node_method(n, port_set_param, 0, __VA_ARGS__) +#define spa_node_port_use_buffers(n,...) spa_node_method(n, port_use_buffers, 0, __VA_ARGS__) +#define spa_node_port_set_io(n,...) spa_node_method(n, port_set_io, 0, __VA_ARGS__) + +#define spa_node_port_reuse_buffer(n,...) spa_node_method(n, port_reuse_buffer, 0, __VA_ARGS__) +#define spa_node_process(n) spa_node_method(n, process, 0) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_NODE_H */ diff --git a/spa/include/spa/node/type-info.h b/spa/include/spa/node/type-info.h new file mode 100644 index 0000000..1ebbfe5 --- /dev/null +++ b/spa/include/spa/node/type-info.h @@ -0,0 +1,107 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_NODE_TYPES_H +#define SPA_NODE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +#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 }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_NodeEvent SPA_TYPE_INFO_EVENT_BASE "Node" +#define SPA_TYPE_INFO_NODE_EVENT_BASE SPA_TYPE_INFO_NodeEvent ":" + +static const struct spa_type_info spa_type_node_event_id[] = { + { SPA_NODE_EVENT_Error, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "Error", NULL }, + { SPA_NODE_EVENT_Buffering, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", NULL }, + { SPA_NODE_EVENT_RequestRefresh, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", NULL }, + { SPA_NODE_EVENT_RequestProcess, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestProcess", NULL }, + { 0, 0, NULL, NULL }, +}; + +static const struct spa_type_info spa_type_node_event[] = { + { SPA_EVENT_NODE_START, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_EVENT_BASE, spa_type_node_event_id }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_NodeCommand SPA_TYPE_INFO_COMMAND_BASE "Node" +#define SPA_TYPE_INFO_NODE_COMMAND_BASE SPA_TYPE_INFO_NodeCommand ":" + +static const struct spa_type_info spa_type_node_command_id[] = { + { SPA_NODE_COMMAND_Suspend, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", NULL }, + { SPA_NODE_COMMAND_Pause, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Pause", NULL }, + { SPA_NODE_COMMAND_Start, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Start", NULL }, + { SPA_NODE_COMMAND_Enable, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Enable", NULL }, + { SPA_NODE_COMMAND_Disable, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Disable", NULL }, + { SPA_NODE_COMMAND_Flush, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Flush", NULL }, + { SPA_NODE_COMMAND_Drain, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Drain", NULL }, + { SPA_NODE_COMMAND_Marker, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Marker", NULL }, + { SPA_NODE_COMMAND_ParamBegin, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamBegin", NULL }, + { SPA_NODE_COMMAND_ParamEnd, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamEnd", NULL }, + { SPA_NODE_COMMAND_RequestProcess, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "RequestProcess", NULL }, + { 0, 0, NULL, NULL }, +}; + +static const struct spa_type_info spa_type_node_command[] = { + { 0, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_COMMAND_BASE, spa_type_node_command_id }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_NODE_TYPES_H */ diff --git a/spa/include/spa/node/utils.h b/spa/include/spa/node/utils.h new file mode 100644 index 0000000..9503d8b --- /dev/null +++ b/spa/include/spa/node/utils.h @@ -0,0 +1,158 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_NODE_UTILS_H +#define SPA_NODE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_node + * \{ + */ + +#include + +#include + +struct spa_result_node_params_data { + struct spa_pod_builder *builder; + struct spa_result_node_params data; +}; + +static inline void spa_result_func_node_params(void *data, + int seq, int res, uint32_t type, const void *result) +{ + struct spa_result_node_params_data *d = + (struct spa_result_node_params_data *) data; + const struct spa_result_node_params *r = + (const struct spa_result_node_params *) result; + uint32_t offset = d->builder->state.offset; + if (spa_pod_builder_raw_padded(d->builder, r->param, SPA_POD_SIZE(r->param)) < 0) + return; + d->data.next = r->next; + d->data.param = spa_pod_builder_deref(d->builder, offset); +} + +static inline int spa_node_enum_params_sync(struct spa_node *node, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_node_params_data data = { builder, }; + struct spa_hook listener = {{0}}; + static const struct spa_node_events node_events = { + .version = SPA_VERSION_NODE_EVENTS, + .info = NULL, + .port_info = NULL, + .result = spa_result_func_node_params, + }; + int res; + + res = spa_node_add_listener(node, &listener, &node_events, &data); + if (res >= 0) { + res = spa_node_enum_params(node, 0, id, *index, 1, filter); + spa_hook_remove(&listener); + } + + if (data.data.param == NULL) { + if (res > 0) + res = 0; + } else { + *index = data.data.next; + *param = data.data.param; + res = 1; + } + return res; +} + +static inline int spa_node_port_enum_params_sync(struct spa_node *node, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t *index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_result_node_params_data data = { builder, }; + struct spa_hook listener = {{0}}; + static const struct spa_node_events node_events = { + .version = SPA_VERSION_NODE_EVENTS, + .info = NULL, + .port_info = NULL, + .result = spa_result_func_node_params, + }; + int res; + + res = spa_node_add_listener(node, &listener, &node_events, &data); + if (res >= 0) { + res = spa_node_port_enum_params(node, 0, direction, port_id, + id, *index, 1, filter); + spa_hook_remove(&listener); + } + + if (data.data.param == NULL) { + if (res > 0) + res = 0; + } else { + *index = data.data.next; + *param = data.data.param; + res = 1; + } + return res; +} + +#define spa_node_emit(hooks,method,version,...) \ + spa_hook_list_call_simple(hooks, struct spa_node_events, \ + method, version, ##__VA_ARGS__) + +#define spa_node_emit_info(hooks,...) spa_node_emit(hooks,info, 0, __VA_ARGS__) +#define spa_node_emit_port_info(hooks,...) spa_node_emit(hooks,port_info, 0, __VA_ARGS__) +#define spa_node_emit_result(hooks,...) spa_node_emit(hooks,result, 0, __VA_ARGS__) +#define spa_node_emit_event(hooks,...) spa_node_emit(hooks,event, 0, __VA_ARGS__) + + +#define spa_node_call(callbacks,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + spa_callbacks_call_res(callbacks, struct spa_node_callbacks, \ + _res, method, version, ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_node_call_ready(hook,...) spa_node_call(hook, ready, 0, __VA_ARGS__) +#define spa_node_call_reuse_buffer(hook,...) spa_node_call(hook, reuse_buffer, 0, __VA_ARGS__) +#define spa_node_call_xrun(hook,...) spa_node_call(hook, xrun, 0, __VA_ARGS__) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_NODE_UTILS_H */ diff --git a/spa/include/spa/param/audio/aac-types.h b/spa/include/spa/param/audio/aac-types.h new file mode 100644 index 0000000..096729d --- /dev/null +++ b/spa/include/spa/param/audio/aac-types.h @@ -0,0 +1,58 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AAC_TYPES_H +#define SPA_AUDIO_AAC_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_TYPE_INFO_AudioAACStreamFormat SPA_TYPE_INFO_ENUM_BASE "AudioAACStreamFormat" +#define SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE SPA_TYPE_INFO_AudioAACStreamFormat ":" + +static const struct spa_type_info spa_type_audio_aac_stream_format[] = { + { SPA_AUDIO_AAC_STREAM_FORMAT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_RAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "RAW", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_MP2ADTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP2ADTS", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4ADTS", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_MP4LOAS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4LOAS", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4LATM", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_ADIF, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "ADIF", NULL }, + { SPA_AUDIO_AAC_STREAM_FORMAT_MP4FF, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AAC_STREAM_FORMAT_BASE "MP4FF", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AAC_TYPES_H */ diff --git a/spa/include/spa/param/audio/aac-utils.h b/spa/include/spa/param/audio/aac-utils.h new file mode 100644 index 0000000..4d53987 --- /dev/null +++ b/spa/include/spa/param/audio/aac-utils.h @@ -0,0 +1,89 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AAC_UTILS_H +#define SPA_AUDIO_AAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_aac_parse(const struct spa_pod *format, struct spa_audio_info_aac *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_bitrate, SPA_POD_OPT_Int(&info->bitrate), + SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_POD_OPT_Id(&info->stream_format)); + + return res; +} + +static inline struct spa_pod * +spa_format_audio_aac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_aac *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_aac), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (info->bitrate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_bitrate, SPA_POD_Int(info->bitrate), 0); + if (info->stream_format != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_POD_Id(info->stream_format), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AAC_UTILS_H */ diff --git a/spa/include/spa/param/audio/aac.h b/spa/include/spa/param/audio/aac.h new file mode 100644 index 0000000..7faf449 --- /dev/null +++ b/spa/include/spa/param/audio/aac.h @@ -0,0 +1,71 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AAC_H +#define SPA_AUDIO_AAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +enum spa_audio_aac_stream_format { + SPA_AUDIO_AAC_STREAM_FORMAT_UNKNOWN, + /* Raw AAC frames */ + SPA_AUDIO_AAC_STREAM_FORMAT_RAW, + /* ISO/IEC 13818-7 MPEG-2 Audio Data Transport Stream (ADTS) */ + SPA_AUDIO_AAC_STREAM_FORMAT_MP2ADTS, + /* ISO/IEC 14496-3 MPEG-4 Audio Data Transport Stream (ADTS) */ + SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS, + /* ISO/IEC 14496-3 Low Overhead Audio Stream (LOAS) */ + SPA_AUDIO_AAC_STREAM_FORMAT_MP4LOAS, + /* ISO/IEC 14496-3 Low Overhead Audio Transport Multiplex (LATM) */ + SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM, + /* ISO/IEC 14496-3 Audio Data Interchange Format (ADIF) */ + SPA_AUDIO_AAC_STREAM_FORMAT_ADIF, + /* ISO/IEC 14496-12 MPEG-4 file format */ + SPA_AUDIO_AAC_STREAM_FORMAT_MP4FF, + + SPA_AUDIO_AAC_STREAM_FORMAT_CUSTOM = 0x10000, +}; + +struct spa_audio_info_aac { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ + uint32_t bitrate; /*< stream bitrate */ + enum spa_audio_aac_stream_format stream_format; /*< AAC audio stream format */ +}; + +#define SPA_AUDIO_INFO_AAC_INIT(...) ((struct spa_audio_info_aac) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AAC_H */ diff --git a/spa/include/spa/param/audio/alac-utils.h b/spa/include/spa/param/audio/alac-utils.h new file mode 100644 index 0000000..39dbd78 --- /dev/null +++ b/spa/include/spa/param/audio/alac-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_ALAC_UTILS_H +#define SPA_AUDIO_ALAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_alac_parse(const struct spa_pod *format, struct spa_audio_info_alac *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_alac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_alac *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_alac), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_ALAC_UTILS_H */ diff --git a/spa/include/spa/param/audio/alac.h b/spa/include/spa/param/audio/alac.h new file mode 100644 index 0000000..37ae170 --- /dev/null +++ b/spa/include/spa/param/audio/alac.h @@ -0,0 +1,49 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_ALAC_H +#define SPA_AUDIO_ALAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_alac { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_ALAC_INIT(...) ((struct spa_audio_info_alac) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_ALAC_H */ diff --git a/spa/include/spa/param/audio/amr-types.h b/spa/include/spa/param/audio/amr-types.h new file mode 100644 index 0000000..7d87c01 --- /dev/null +++ b/spa/include/spa/param/audio/amr-types.h @@ -0,0 +1,52 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AMR_TYPES_H +#define SPA_AUDIO_AMR_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_TYPE_INFO_AudioAMRBandMode SPA_TYPE_INFO_ENUM_BASE "AudioAMRBandMode" +#define SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE SPA_TYPE_INFO_AudioAMRBandMode ":" + +static const struct spa_type_info spa_type_audio_amr_band_mode[] = { + { SPA_AUDIO_AMR_BAND_MODE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_AMR_BAND_MODE_NB, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "NB", NULL }, + { SPA_AUDIO_AMR_BAND_MODE_WB, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_AMR_BAND_MODE_BASE "WB", NULL }, + { 0, 0, NULL, NULL }, +}; +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AMR_TYPES_H */ diff --git a/spa/include/spa/param/audio/amr-utils.h b/spa/include/spa/param/audio/amr-utils.h new file mode 100644 index 0000000..dafe1f8 --- /dev/null +++ b/spa/include/spa/param/audio/amr-utils.h @@ -0,0 +1,84 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AMR_UTILS_H +#define SPA_AUDIO_AMR_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_amr_parse(const struct spa_pod *format, struct spa_audio_info_amr *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_AMR_bandMode, SPA_POD_OPT_Id(&info->band_mode)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_amr_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_amr *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_amr), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (info->band_mode != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_AMR_bandMode, SPA_POD_Id(info->band_mode), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AMR_UTILS_H */ diff --git a/spa/include/spa/param/audio/amr.h b/spa/include/spa/param/audio/amr.h new file mode 100644 index 0000000..360b8eb --- /dev/null +++ b/spa/include/spa/param/audio/amr.h @@ -0,0 +1,56 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_AMR_H +#define SPA_AUDIO_AMR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +enum spa_audio_amr_band_mode { + SPA_AUDIO_AMR_BAND_MODE_UNKNOWN, + SPA_AUDIO_AMR_BAND_MODE_NB, + SPA_AUDIO_AMR_BAND_MODE_WB, +}; + +struct spa_audio_info_amr { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ + enum spa_audio_amr_band_mode band_mode; +}; + +#define SPA_AUDIO_INFO_AMR_INIT(...) ((struct spa_audio_info_amr) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AMR_H */ diff --git a/spa/include/spa/param/audio/ape-utils.h b/spa/include/spa/param/audio/ape-utils.h new file mode 100644 index 0000000..a625900 --- /dev/null +++ b/spa/include/spa/param/audio/ape-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_APE_UTILS_H +#define SPA_AUDIO_APE_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_ape_parse(const struct spa_pod *format, struct spa_audio_info_ape *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_ape_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_ape *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_ape), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_APE_UTILS_H */ diff --git a/spa/include/spa/param/audio/ape.h b/spa/include/spa/param/audio/ape.h new file mode 100644 index 0000000..3e48b15 --- /dev/null +++ b/spa/include/spa/param/audio/ape.h @@ -0,0 +1,49 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_APE_H +#define SPA_AUDIO_APE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_ape { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_APE_INIT(...) ((struct spa_audio_info_ape) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_APE_H */ diff --git a/spa/include/spa/param/audio/compressed.h b/spa/include/spa/param/audio/compressed.h new file mode 100644 index 0000000..ec8a38a --- /dev/null +++ b/spa/include/spa/param/audio/compressed.h @@ -0,0 +1,39 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * © 2022 Asymptotic Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_COMPRESSED_H +#define SPA_AUDIO_COMPRESSED_H + +#include +#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..795a0b1 --- /dev/null +++ b/spa/include/spa/param/audio/dsd-utils.h @@ -0,0 +1,100 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_DSD_UTILS_H +#define SPA_AUDIO_DSD_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_dsd *info) +{ + struct spa_pod *position = NULL; + int res; + info->flags = 0; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_bitorder, SPA_POD_OPT_Id(&info->bitorder), + SPA_FORMAT_AUDIO_interleave, SPA_POD_OPT_Int(&info->interleave), + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)); + if (position == NULL || + !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS)) + SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + + return res; +} + +static inline struct spa_pod * +spa_format_audio_dsd_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_dsd *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsd), + 0); + if (info->bitorder != SPA_PARAM_BITORDER_unknown) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_bitorder, SPA_POD_Id(info->bitorder), 0); + if (info->interleave != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_interleave, SPA_POD_Int(info->interleave), 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) { + spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_position, + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, + info->channels, info->position), 0); + } + } + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DSD_UTILS_H */ diff --git a/spa/include/spa/param/audio/dsd.h b/spa/include/spa/param/audio/dsd.h new file mode 100644 index 0000000..3b317f2 --- /dev/null +++ b/spa/include/spa/param/audio/dsd.h @@ -0,0 +1,81 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_DSD_H +#define SPA_AUDIO_DSD_H + +#include + +#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..4bbfe8e --- /dev/null +++ b/spa/include/spa/param/audio/dsp-utils.h @@ -0,0 +1,75 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_DSP_UTILS_H +#define SPA_AUDIO_DSP_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_dsp_parse(const struct spa_pod *format, struct spa_audio_info_dsp *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_dsp_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_dsp *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + 0); + if (info->format != SPA_AUDIO_FORMAT_UNKNOWN) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_format, SPA_POD_Id(info->format), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DSP_UTILS_H */ diff --git a/spa/include/spa/param/audio/dsp.h b/spa/include/spa/param/audio/dsp.h new file mode 100644 index 0000000..fbc0a3f --- /dev/null +++ b/spa/include/spa/param/audio/dsp.h @@ -0,0 +1,48 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_DSP_H +#define SPA_AUDIO_DSP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_dsp { + enum spa_audio_format format; /*< format, one of the DSP formats in enum spa_audio_format */ +}; + +#define SPA_AUDIO_INFO_DSP_INIT(...) ((struct spa_audio_info_dsp) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DSP_H */ diff --git a/spa/include/spa/param/audio/flac-utils.h b/spa/include/spa/param/audio/flac-utils.h new file mode 100644 index 0000000..7c612d6 --- /dev/null +++ b/spa/include/spa/param/audio/flac-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_FLAC_UTILS_H +#define SPA_AUDIO_FLAC_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_flac_parse(const struct spa_pod *format, struct spa_audio_info_flac *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_flac_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_flac *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_flac), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_FLAC_UTILS_H */ diff --git a/spa/include/spa/param/audio/flac.h b/spa/include/spa/param/audio/flac.h new file mode 100644 index 0000000..df4841e --- /dev/null +++ b/spa/include/spa/param/audio/flac.h @@ -0,0 +1,49 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_FLAC_H +#define SPA_AUDIO_FLAC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_flac { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_FLAC_INIT(...) ((struct spa_audio_info_flac) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_FLAC_H */ diff --git a/spa/include/spa/param/audio/format-utils.h b/spa/include/spa/param/audio/format-utils.h new file mode 100644 index 0000000..02aafe4 --- /dev/null +++ b/spa/include/spa/param/audio/format-utils.h @@ -0,0 +1,141 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_AUDIO_FORMAT_UTILS_H +#define SPA_PARAM_AUDIO_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * \addtogroup spa_param + * \{ + */ + +static inline int +spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info) +{ + int res; + + if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0) + return res; + + if (info->media_type != SPA_MEDIA_TYPE_audio) + return -EINVAL; + + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + return spa_format_audio_raw_parse(format, &info->info.raw); + case SPA_MEDIA_SUBTYPE_dsp: + return spa_format_audio_dsp_parse(format, &info->info.dsp); + case SPA_MEDIA_SUBTYPE_iec958: + return spa_format_audio_iec958_parse(format, &info->info.iec958); + case SPA_MEDIA_SUBTYPE_dsd: + return spa_format_audio_dsd_parse(format, &info->info.dsd); + case SPA_MEDIA_SUBTYPE_mp3: + return spa_format_audio_mp3_parse(format, &info->info.mp3); + case SPA_MEDIA_SUBTYPE_aac: + return spa_format_audio_aac_parse(format, &info->info.aac); + case SPA_MEDIA_SUBTYPE_vorbis: + return spa_format_audio_vorbis_parse(format, &info->info.vorbis); + case SPA_MEDIA_SUBTYPE_wma: + return spa_format_audio_wma_parse(format, &info->info.wma); + case SPA_MEDIA_SUBTYPE_ra: + return spa_format_audio_ra_parse(format, &info->info.ra); + case SPA_MEDIA_SUBTYPE_amr: + return spa_format_audio_amr_parse(format, &info->info.amr); + case SPA_MEDIA_SUBTYPE_alac: + return spa_format_audio_alac_parse(format, &info->info.alac); + case SPA_MEDIA_SUBTYPE_flac: + return spa_format_audio_flac_parse(format, &info->info.flac); + case SPA_MEDIA_SUBTYPE_ape: + return spa_format_audio_ape_parse(format, &info->info.ape); + } + return -ENOTSUP; +} + +static inline struct spa_pod * +spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info *info) +{ + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + return spa_format_audio_raw_build(builder, id, &info->info.raw); + case SPA_MEDIA_SUBTYPE_dsp: + return spa_format_audio_dsp_build(builder, id, &info->info.dsp); + case SPA_MEDIA_SUBTYPE_iec958: + return spa_format_audio_iec958_build(builder, id, &info->info.iec958); + case SPA_MEDIA_SUBTYPE_dsd: + return spa_format_audio_dsd_build(builder, id, &info->info.dsd); + case SPA_MEDIA_SUBTYPE_mp3: + return spa_format_audio_mp3_build(builder, id, &info->info.mp3); + case SPA_MEDIA_SUBTYPE_aac: + return spa_format_audio_aac_build(builder, id, &info->info.aac); + case SPA_MEDIA_SUBTYPE_vorbis: + return spa_format_audio_vorbis_build(builder, id, &info->info.vorbis); + case SPA_MEDIA_SUBTYPE_wma: + return spa_format_audio_wma_build(builder, id, &info->info.wma); + case SPA_MEDIA_SUBTYPE_ra: + return spa_format_audio_ra_build(builder, id, &info->info.ra); + case SPA_MEDIA_SUBTYPE_amr: + return spa_format_audio_amr_build(builder, id, &info->info.amr); + case SPA_MEDIA_SUBTYPE_alac: + return spa_format_audio_alac_build(builder, id, &info->info.alac); + case SPA_MEDIA_SUBTYPE_flac: + return spa_format_audio_flac_build(builder, id, &info->info.flac); + case SPA_MEDIA_SUBTYPE_ape: + return spa_format_audio_ape_build(builder, id, &info->info.ape); + } + errno = ENOTSUP; + return NULL; +} +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_AUDIO_FORMAT_UTILS_H */ diff --git a/spa/include/spa/param/audio/format.h b/spa/include/spa/param/audio/format.h new file mode 100644 index 0000000..0e9e6ca --- /dev/null +++ b/spa/include/spa/param/audio/format.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_AUDIO_FORMAT_H +#define SPA_PARAM_AUDIO_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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; + } info; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_AUDIO_FORMAT_H */ diff --git a/spa/include/spa/param/audio/iec958-types.h b/spa/include/spa/param/audio/iec958-types.h new file mode 100644 index 0000000..e0b8940 --- /dev/null +++ b/spa/include/spa/param/audio/iec958-types.h @@ -0,0 +1,59 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_IEC958_TYPES_H +#define SPA_AUDIO_IEC958_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_TYPE_INFO_AudioIEC958Codec SPA_TYPE_INFO_ENUM_BASE "AudioIEC958Codec" +#define SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE SPA_TYPE_INFO_AudioIEC958Codec ":" + +static const struct spa_type_info spa_type_audio_iec958_codec[] = { + { SPA_AUDIO_IEC958_CODEC_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_IEC958_CODEC_PCM, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "PCM", NULL }, + { SPA_AUDIO_IEC958_CODEC_DTS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "DTS", NULL }, + { SPA_AUDIO_IEC958_CODEC_AC3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "AC3", NULL }, + { SPA_AUDIO_IEC958_CODEC_MPEG, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "MPEG", NULL }, + { SPA_AUDIO_IEC958_CODEC_MPEG2_AAC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "MPEG2-AAC", NULL }, + { SPA_AUDIO_IEC958_CODEC_EAC3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "EAC3", NULL }, + { SPA_AUDIO_IEC958_CODEC_TRUEHD, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "TrueHD", NULL }, + { SPA_AUDIO_IEC958_CODEC_DTSHD, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_IEC958_CODEC_BASE "DTS-HD", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RAW_IEC958_TYPES_H */ diff --git a/spa/include/spa/param/audio/iec958-utils.h b/spa/include/spa/param/audio/iec958-utils.h new file mode 100644 index 0000000..da953d7 --- /dev/null +++ b/spa/include/spa/param/audio/iec958-utils.h @@ -0,0 +1,79 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_IEC958_UTILS_H +#define SPA_AUDIO_IEC958_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_iec958_parse(const struct spa_pod *format, struct spa_audio_info_iec958 *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_iec958Codec, SPA_POD_OPT_Id(&info->codec), + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_iec958_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_iec958 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_iec958), + 0); + if (info->codec != SPA_AUDIO_IEC958_CODEC_UNKNOWN) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_iec958Codec, SPA_POD_Id(info->codec), 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_IEC958_UTILS_H */ diff --git a/spa/include/spa/param/audio/iec958.h b/spa/include/spa/param/audio/iec958.h new file mode 100644 index 0000000..9445138 --- /dev/null +++ b/spa/include/spa/param/audio/iec958.h @@ -0,0 +1,69 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_IEC958_H +#define SPA_AUDIO_IEC958_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ +enum spa_audio_iec958_codec { + SPA_AUDIO_IEC958_CODEC_UNKNOWN, + + SPA_AUDIO_IEC958_CODEC_PCM, + SPA_AUDIO_IEC958_CODEC_DTS, + SPA_AUDIO_IEC958_CODEC_AC3, + SPA_AUDIO_IEC958_CODEC_MPEG, /**< MPEG-1 or MPEG-2 (Part 3, not AAC) */ + SPA_AUDIO_IEC958_CODEC_MPEG2_AAC, /**< MPEG-2 AAC */ + + SPA_AUDIO_IEC958_CODEC_EAC3, + + SPA_AUDIO_IEC958_CODEC_TRUEHD, /**< Dolby TrueHD */ + SPA_AUDIO_IEC958_CODEC_DTSHD, /**< DTS-HD Master Audio */ +}; + +struct spa_audio_info_iec958 { + enum spa_audio_iec958_codec codec; /*< format, one of the DSP formats in enum spa_audio_format_dsp */ + uint32_t flags; /*< extra flags */ + uint32_t rate; /*< sample rate */ +}; + +#define SPA_AUDIO_INFO_IEC958_INIT(...) ((struct spa_audio_info_iec958) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_IEC958_H */ diff --git a/spa/include/spa/param/audio/layout.h b/spa/include/spa/param/audio/layout.h new file mode 100644 index 0000000..66154bf --- /dev/null +++ b/spa/include/spa/param/audio/layout.h @@ -0,0 +1,192 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_LAYOUT_H +#define SPA_AUDIO_LAYOUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif + +/** + * \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..b697275 --- /dev/null +++ b/spa/include/spa/param/audio/mp3-types.h @@ -0,0 +1,54 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_MP3_TYPES_H +#define SPA_AUDIO_MP3_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_TYPE_INFO_AudioMP3ChannelMode SPA_TYPE_INFO_ENUM_BASE "AudioMP3ChannelMode" +#define SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE SPA_TYPE_INFO_AudioMP3ChannelMode ":" + +static const struct spa_type_info spa_type_audio_mp3_channel_mode[] = { + { SPA_AUDIO_MP3_CHANNEL_MODE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_MP3_CHANNEL_MODE_MONO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Mono", NULL }, + { SPA_AUDIO_MP3_CHANNEL_MODE_STEREO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Stereo", NULL }, + { SPA_AUDIO_MP3_CHANNEL_MODE_JOINTSTEREO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Joint-stereo", NULL }, + { SPA_AUDIO_MP3_CHANNEL_MODE_DUAL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_MP3_CHANNEL_MODE_BASE "Dual", NULL }, + { 0, 0, NULL, NULL }, +}; +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_MP3_TYPES_H */ diff --git a/spa/include/spa/param/audio/mp3-utils.h b/spa/include/spa/param/audio/mp3-utils.h new file mode 100644 index 0000000..b764578 --- /dev/null +++ b/spa/include/spa/param/audio/mp3-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_MP3_UTILS_H +#define SPA_AUDIO_MP3_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_mp3 *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_mp3_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_mp3 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mp3), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_MP3_UTILS_H */ diff --git a/spa/include/spa/param/audio/mp3.h b/spa/include/spa/param/audio/mp3.h new file mode 100644 index 0000000..ff724c7 --- /dev/null +++ b/spa/include/spa/param/audio/mp3.h @@ -0,0 +1,57 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_MP3_H +#define SPA_AUDIO_MP3_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +enum spa_audio_mp3_channel_mode { + SPA_AUDIO_MP3_CHANNEL_MODE_UNKNOWN, + SPA_AUDIO_MP3_CHANNEL_MODE_MONO, + SPA_AUDIO_MP3_CHANNEL_MODE_STEREO, + SPA_AUDIO_MP3_CHANNEL_MODE_JOINTSTEREO, + SPA_AUDIO_MP3_CHANNEL_MODE_DUAL, +}; + +struct spa_audio_info_mp3 { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_MP3_INIT(...) ((struct spa_audio_info_mp3) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_MP3_H */ diff --git a/spa/include/spa/param/audio/ra-utils.h b/spa/include/spa/param/audio/ra-utils.h new file mode 100644 index 0000000..88356ba --- /dev/null +++ b/spa/include/spa/param/audio/ra-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_RA_UTILS_H +#define SPA_AUDIO_RA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_ra_parse(const struct spa_pod *format, struct spa_audio_info_ra *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_ra_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_ra *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_ra), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RA_UTILS_H */ diff --git a/spa/include/spa/param/audio/ra.h b/spa/include/spa/param/audio/ra.h new file mode 100644 index 0000000..041c3a3 --- /dev/null +++ b/spa/include/spa/param/audio/ra.h @@ -0,0 +1,49 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_RA_H +#define SPA_AUDIO_RA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_ra { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_RA_INIT(...) ((struct spa_audio_info_ra) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RA_H */ diff --git a/spa/include/spa/param/audio/raw-types.h b/spa/include/spa/param/audio/raw-types.h new file mode 100644 index 0000000..94e6364 --- /dev/null +++ b/spa/include/spa/param/audio/raw-types.h @@ -0,0 +1,278 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_RAW_TYPES_H +#define SPA_AUDIO_RAW_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +#define SPA_TYPE_INFO_AudioFormat SPA_TYPE_INFO_ENUM_BASE "AudioFormat" +#define SPA_TYPE_INFO_AUDIO_FORMAT_BASE SPA_TYPE_INFO_AudioFormat ":" + +static const struct spa_type_info spa_type_audio_format[] = { + { SPA_AUDIO_FORMAT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_FORMAT_ENCODED, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ENCODED", NULL }, + { SPA_AUDIO_FORMAT_S8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8", NULL }, + { SPA_AUDIO_FORMAT_U8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8", NULL }, + { SPA_AUDIO_FORMAT_S16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16LE", NULL }, + { SPA_AUDIO_FORMAT_S16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16BE", NULL }, + { SPA_AUDIO_FORMAT_U16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16LE", NULL }, + { SPA_AUDIO_FORMAT_U16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16BE", NULL }, + { SPA_AUDIO_FORMAT_S24_32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32LE", NULL }, + { SPA_AUDIO_FORMAT_S24_32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32BE", NULL }, + { SPA_AUDIO_FORMAT_U24_32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32LE", NULL }, + { SPA_AUDIO_FORMAT_U24_32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32BE", NULL }, + { SPA_AUDIO_FORMAT_S32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32LE", NULL }, + { SPA_AUDIO_FORMAT_S32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32BE", NULL }, + { SPA_AUDIO_FORMAT_U32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32LE", NULL }, + { SPA_AUDIO_FORMAT_U32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32BE", NULL }, + { SPA_AUDIO_FORMAT_S24_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24LE", NULL }, + { SPA_AUDIO_FORMAT_S24_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24BE", NULL }, + { SPA_AUDIO_FORMAT_U24_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24LE", NULL }, + { SPA_AUDIO_FORMAT_U24_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24BE", NULL }, + { SPA_AUDIO_FORMAT_S20_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20LE", NULL }, + { SPA_AUDIO_FORMAT_S20_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20BE", NULL }, + { SPA_AUDIO_FORMAT_U20_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20LE", NULL }, + { SPA_AUDIO_FORMAT_U20_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20BE", NULL }, + { SPA_AUDIO_FORMAT_S18_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18LE", NULL }, + { SPA_AUDIO_FORMAT_S18_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18BE", NULL }, + { SPA_AUDIO_FORMAT_U18_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18LE", NULL }, + { SPA_AUDIO_FORMAT_U18_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18BE", NULL }, + { SPA_AUDIO_FORMAT_F32_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32LE", NULL }, + { SPA_AUDIO_FORMAT_F32_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32BE", NULL }, + { SPA_AUDIO_FORMAT_F64_LE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64LE", NULL }, + { SPA_AUDIO_FORMAT_F64_BE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64BE", NULL }, + + { SPA_AUDIO_FORMAT_ULAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ULAW", NULL }, + { SPA_AUDIO_FORMAT_ALAW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ALAW", NULL }, + + { SPA_AUDIO_FORMAT_U8P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8P", NULL }, + { SPA_AUDIO_FORMAT_S16P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16P", NULL }, + { SPA_AUDIO_FORMAT_S24_32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32P", NULL }, + { SPA_AUDIO_FORMAT_S32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32P", NULL }, + { SPA_AUDIO_FORMAT_S24P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24P", NULL }, + { SPA_AUDIO_FORMAT_F32P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32P", NULL }, + { SPA_AUDIO_FORMAT_F64P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64P", NULL }, + { SPA_AUDIO_FORMAT_S8P, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8P", NULL }, + +#if __BYTE_ORDER == __BIG_ENDIAN + { SPA_AUDIO_FORMAT_S16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16OE", NULL }, + { SPA_AUDIO_FORMAT_S16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16", NULL }, + { SPA_AUDIO_FORMAT_U16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16OE", NULL }, + { SPA_AUDIO_FORMAT_U16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16", NULL }, + { SPA_AUDIO_FORMAT_S24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32OE", NULL }, + { SPA_AUDIO_FORMAT_S24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32", NULL }, + { SPA_AUDIO_FORMAT_U24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32OE", NULL }, + { SPA_AUDIO_FORMAT_U24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32", NULL }, + { SPA_AUDIO_FORMAT_S32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32OE", NULL }, + { SPA_AUDIO_FORMAT_S32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32", NULL }, + { SPA_AUDIO_FORMAT_U32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32OE", NULL }, + { SPA_AUDIO_FORMAT_U32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32", NULL }, + { SPA_AUDIO_FORMAT_S24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24OE", NULL }, + { SPA_AUDIO_FORMAT_S24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24", NULL }, + { SPA_AUDIO_FORMAT_U24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24OE", NULL }, + { SPA_AUDIO_FORMAT_U24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24", NULL }, + { SPA_AUDIO_FORMAT_S20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20OE", NULL }, + { SPA_AUDIO_FORMAT_S20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20", NULL }, + { SPA_AUDIO_FORMAT_U20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20OE", NULL }, + { SPA_AUDIO_FORMAT_U20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20", NULL }, + { SPA_AUDIO_FORMAT_S18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18OE", NULL }, + { SPA_AUDIO_FORMAT_S18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18", NULL }, + { SPA_AUDIO_FORMAT_U18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18OE", NULL }, + { SPA_AUDIO_FORMAT_U18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18", NULL }, + { SPA_AUDIO_FORMAT_F32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32OE", NULL }, + { SPA_AUDIO_FORMAT_F32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32", NULL }, + { SPA_AUDIO_FORMAT_F64_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64OE", NULL }, + { SPA_AUDIO_FORMAT_F64, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64", NULL }, +#elif __BYTE_ORDER == __LITTLE_ENDIAN + { SPA_AUDIO_FORMAT_S16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16", NULL }, + { SPA_AUDIO_FORMAT_S16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16OE", NULL }, + { SPA_AUDIO_FORMAT_U16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16", NULL }, + { SPA_AUDIO_FORMAT_U16_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16OE", NULL }, + { SPA_AUDIO_FORMAT_S24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32", NULL }, + { SPA_AUDIO_FORMAT_S24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32OE", NULL }, + { SPA_AUDIO_FORMAT_U24_32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32", NULL }, + { SPA_AUDIO_FORMAT_U24_32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32OE", NULL }, + { SPA_AUDIO_FORMAT_S32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32", NULL }, + { SPA_AUDIO_FORMAT_S32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32OE", NULL }, + { SPA_AUDIO_FORMAT_U32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32", NULL }, + { SPA_AUDIO_FORMAT_U32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32OE", NULL }, + { SPA_AUDIO_FORMAT_S24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24", NULL }, + { SPA_AUDIO_FORMAT_S24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24OE", NULL }, + { SPA_AUDIO_FORMAT_U24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24", NULL }, + { SPA_AUDIO_FORMAT_U24_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24OE", NULL }, + { SPA_AUDIO_FORMAT_S20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20", NULL }, + { SPA_AUDIO_FORMAT_S20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20OE", NULL }, + { SPA_AUDIO_FORMAT_U20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20", NULL }, + { SPA_AUDIO_FORMAT_U20_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20OE", NULL }, + { SPA_AUDIO_FORMAT_S18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18", NULL }, + { SPA_AUDIO_FORMAT_S18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18OE", NULL }, + { SPA_AUDIO_FORMAT_U18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18", NULL }, + { SPA_AUDIO_FORMAT_U18_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18OE", NULL }, + { SPA_AUDIO_FORMAT_F32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32", NULL }, + { SPA_AUDIO_FORMAT_F32_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32OE", NULL }, + { SPA_AUDIO_FORMAT_F64, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64", NULL }, + { SPA_AUDIO_FORMAT_F64_OE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64OE", NULL }, +#endif + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_AudioFlags SPA_TYPE_INFO_FLAGS_BASE "AudioFlags" +#define SPA_TYPE_INFO_AUDIO_FLAGS_BASE SPA_TYPE_INFO_AudioFlags ":" + +static const struct spa_type_info spa_type_audio_flags[] = { + { SPA_AUDIO_FLAG_NONE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FLAGS_BASE "none", NULL }, + { SPA_AUDIO_FLAG_UNPOSITIONED, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_FLAGS_BASE "unpositioned", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_AudioChannel SPA_TYPE_INFO_ENUM_BASE "AudioChannel" +#define SPA_TYPE_INFO_AUDIO_CHANNEL_BASE SPA_TYPE_INFO_AudioChannel ":" + +static const struct spa_type_info spa_type_audio_channel[] = { + { SPA_AUDIO_CHANNEL_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "UNK", NULL }, + { SPA_AUDIO_CHANNEL_NA, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "NA", NULL }, + { SPA_AUDIO_CHANNEL_MONO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "MONO", NULL }, + { SPA_AUDIO_CHANNEL_FL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FL", NULL }, + { SPA_AUDIO_CHANNEL_FR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FR", NULL }, + { SPA_AUDIO_CHANNEL_FC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FC", NULL }, + { SPA_AUDIO_CHANNEL_LFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LFE", NULL }, + { SPA_AUDIO_CHANNEL_SL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "SL", NULL }, + { SPA_AUDIO_CHANNEL_SR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "SR", NULL }, + { SPA_AUDIO_CHANNEL_FLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLC", NULL }, + { SPA_AUDIO_CHANNEL_FRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRC", NULL }, + { SPA_AUDIO_CHANNEL_RC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RC", NULL }, + { SPA_AUDIO_CHANNEL_RL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RL", NULL }, + { SPA_AUDIO_CHANNEL_RR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RR", NULL }, + { SPA_AUDIO_CHANNEL_TC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TC", NULL }, + { SPA_AUDIO_CHANNEL_TFL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFL", NULL }, + { SPA_AUDIO_CHANNEL_TFC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFC", NULL }, + { SPA_AUDIO_CHANNEL_TFR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFR", NULL }, + { SPA_AUDIO_CHANNEL_TRL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRL", NULL }, + { SPA_AUDIO_CHANNEL_TRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRC", NULL }, + { SPA_AUDIO_CHANNEL_TRR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TRR", NULL }, + { SPA_AUDIO_CHANNEL_RLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RLC", NULL }, + { SPA_AUDIO_CHANNEL_RRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RRC", NULL }, + { SPA_AUDIO_CHANNEL_FLW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLW", NULL }, + { SPA_AUDIO_CHANNEL_FRW, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRW", NULL }, + { SPA_AUDIO_CHANNEL_LFE2, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LFE2", NULL }, + { SPA_AUDIO_CHANNEL_FLH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FLH", NULL }, + { SPA_AUDIO_CHANNEL_FCH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FCH", NULL }, + { SPA_AUDIO_CHANNEL_FRH, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "FRH", NULL }, + { SPA_AUDIO_CHANNEL_TFLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFLC", NULL }, + { SPA_AUDIO_CHANNEL_TFRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFRC", NULL }, + { SPA_AUDIO_CHANNEL_TSL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSL", NULL }, + { SPA_AUDIO_CHANNEL_TSR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSR", NULL }, + { SPA_AUDIO_CHANNEL_LLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LLFR", NULL }, + { SPA_AUDIO_CHANNEL_RLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RLFE", NULL }, + { SPA_AUDIO_CHANNEL_BC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BC", NULL }, + { SPA_AUDIO_CHANNEL_BLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BLC", NULL }, + { SPA_AUDIO_CHANNEL_BRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BRC", NULL }, + + { SPA_AUDIO_CHANNEL_AUX0, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX0", NULL }, + { SPA_AUDIO_CHANNEL_AUX1, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX1", NULL }, + { SPA_AUDIO_CHANNEL_AUX2, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX2", NULL }, + { SPA_AUDIO_CHANNEL_AUX3, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX3", NULL }, + { SPA_AUDIO_CHANNEL_AUX4, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX4", NULL }, + { SPA_AUDIO_CHANNEL_AUX5, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX5", NULL }, + { SPA_AUDIO_CHANNEL_AUX6, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX6", NULL }, + { SPA_AUDIO_CHANNEL_AUX7, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX7", NULL }, + { SPA_AUDIO_CHANNEL_AUX8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX8", NULL }, + { SPA_AUDIO_CHANNEL_AUX9, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX9", NULL }, + { SPA_AUDIO_CHANNEL_AUX10, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX10", NULL }, + { SPA_AUDIO_CHANNEL_AUX11, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX11", NULL }, + { SPA_AUDIO_CHANNEL_AUX12, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX12", NULL }, + { SPA_AUDIO_CHANNEL_AUX13, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX13", NULL }, + { SPA_AUDIO_CHANNEL_AUX14, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX14", NULL }, + { SPA_AUDIO_CHANNEL_AUX15, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX15", NULL }, + { SPA_AUDIO_CHANNEL_AUX16, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX16", NULL }, + { SPA_AUDIO_CHANNEL_AUX17, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX17", NULL }, + { SPA_AUDIO_CHANNEL_AUX18, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX18", NULL }, + { SPA_AUDIO_CHANNEL_AUX19, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX19", NULL }, + { SPA_AUDIO_CHANNEL_AUX20, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX20", NULL }, + { SPA_AUDIO_CHANNEL_AUX21, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX21", NULL }, + { SPA_AUDIO_CHANNEL_AUX22, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX22", NULL }, + { SPA_AUDIO_CHANNEL_AUX23, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX23", NULL }, + { SPA_AUDIO_CHANNEL_AUX24, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX24", NULL }, + { SPA_AUDIO_CHANNEL_AUX25, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX25", NULL }, + { SPA_AUDIO_CHANNEL_AUX26, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX26", NULL }, + { SPA_AUDIO_CHANNEL_AUX27, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX27", NULL }, + { SPA_AUDIO_CHANNEL_AUX28, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX28", NULL }, + { SPA_AUDIO_CHANNEL_AUX29, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX29", NULL }, + { SPA_AUDIO_CHANNEL_AUX30, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX30", NULL }, + { SPA_AUDIO_CHANNEL_AUX31, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX31", NULL }, + { SPA_AUDIO_CHANNEL_AUX32, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX32", NULL }, + { SPA_AUDIO_CHANNEL_AUX33, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX33", NULL }, + { SPA_AUDIO_CHANNEL_AUX34, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX34", NULL }, + { SPA_AUDIO_CHANNEL_AUX35, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX35", NULL }, + { SPA_AUDIO_CHANNEL_AUX36, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX36", NULL }, + { SPA_AUDIO_CHANNEL_AUX37, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX37", NULL }, + { SPA_AUDIO_CHANNEL_AUX38, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX38", NULL }, + { SPA_AUDIO_CHANNEL_AUX39, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX39", NULL }, + { SPA_AUDIO_CHANNEL_AUX40, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX40", NULL }, + { SPA_AUDIO_CHANNEL_AUX41, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX41", NULL }, + { SPA_AUDIO_CHANNEL_AUX42, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX42", NULL }, + { SPA_AUDIO_CHANNEL_AUX43, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX43", NULL }, + { SPA_AUDIO_CHANNEL_AUX44, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX44", NULL }, + { SPA_AUDIO_CHANNEL_AUX45, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX45", NULL }, + { SPA_AUDIO_CHANNEL_AUX46, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX46", NULL }, + { SPA_AUDIO_CHANNEL_AUX47, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX47", NULL }, + { SPA_AUDIO_CHANNEL_AUX48, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX48", NULL }, + { SPA_AUDIO_CHANNEL_AUX49, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX49", NULL }, + { SPA_AUDIO_CHANNEL_AUX50, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX50", NULL }, + { SPA_AUDIO_CHANNEL_AUX51, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX51", NULL }, + { SPA_AUDIO_CHANNEL_AUX52, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX52", NULL }, + { SPA_AUDIO_CHANNEL_AUX53, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX53", NULL }, + { SPA_AUDIO_CHANNEL_AUX54, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX54", NULL }, + { SPA_AUDIO_CHANNEL_AUX55, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX55", NULL }, + { SPA_AUDIO_CHANNEL_AUX56, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX56", NULL }, + { SPA_AUDIO_CHANNEL_AUX57, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX57", NULL }, + { SPA_AUDIO_CHANNEL_AUX58, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX58", NULL }, + { SPA_AUDIO_CHANNEL_AUX59, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX59", NULL }, + { SPA_AUDIO_CHANNEL_AUX60, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX60", NULL }, + { SPA_AUDIO_CHANNEL_AUX61, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX61", NULL }, + { SPA_AUDIO_CHANNEL_AUX62, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX62", NULL }, + { SPA_AUDIO_CHANNEL_AUX63, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "AUX63", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RAW_RAW_TYPES_H */ diff --git a/spa/include/spa/param/audio/raw-utils.h b/spa/include/spa/param/audio/raw-utils.h new file mode 100644 index 0000000..53378e2 --- /dev/null +++ b/spa/include/spa/param/audio/raw-utils.h @@ -0,0 +1,96 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_RAW_UTILS_H +#define SPA_AUDIO_RAW_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info) +{ + struct spa_pod *position = NULL; + int res; + info->flags = 0; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_format, SPA_POD_OPT_Id(&info->format), + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)); + if (position == NULL || + !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS)) + SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + + return res; +} + +static inline struct spa_pod * +spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + 0); + if (info->format != SPA_AUDIO_FORMAT_UNKNOWN) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_format, SPA_POD_Id(info->format), 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) { + spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_position, + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, + info->channels, info->position), 0); + } + } + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RAW_UTILS_H */ diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h new file mode 100644 index 0000000..ce9d511 --- /dev/null +++ b/spa/include/spa/param/audio/raw.h @@ -0,0 +1,317 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_RAW_H +#define SPA_AUDIO_RAW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#define SPA_AUDIO_MAX_CHANNELS 64u + +enum spa_audio_format { + SPA_AUDIO_FORMAT_UNKNOWN, + SPA_AUDIO_FORMAT_ENCODED, + + /* interleaved formats */ + SPA_AUDIO_FORMAT_START_Interleaved = 0x100, + SPA_AUDIO_FORMAT_S8, + SPA_AUDIO_FORMAT_U8, + SPA_AUDIO_FORMAT_S16_LE, + SPA_AUDIO_FORMAT_S16_BE, + SPA_AUDIO_FORMAT_U16_LE, + SPA_AUDIO_FORMAT_U16_BE, + SPA_AUDIO_FORMAT_S24_32_LE, + SPA_AUDIO_FORMAT_S24_32_BE, + SPA_AUDIO_FORMAT_U24_32_LE, + SPA_AUDIO_FORMAT_U24_32_BE, + SPA_AUDIO_FORMAT_S32_LE, + SPA_AUDIO_FORMAT_S32_BE, + SPA_AUDIO_FORMAT_U32_LE, + SPA_AUDIO_FORMAT_U32_BE, + SPA_AUDIO_FORMAT_S24_LE, + SPA_AUDIO_FORMAT_S24_BE, + SPA_AUDIO_FORMAT_U24_LE, + SPA_AUDIO_FORMAT_U24_BE, + SPA_AUDIO_FORMAT_S20_LE, + SPA_AUDIO_FORMAT_S20_BE, + SPA_AUDIO_FORMAT_U20_LE, + SPA_AUDIO_FORMAT_U20_BE, + SPA_AUDIO_FORMAT_S18_LE, + SPA_AUDIO_FORMAT_S18_BE, + SPA_AUDIO_FORMAT_U18_LE, + SPA_AUDIO_FORMAT_U18_BE, + SPA_AUDIO_FORMAT_F32_LE, + SPA_AUDIO_FORMAT_F32_BE, + SPA_AUDIO_FORMAT_F64_LE, + SPA_AUDIO_FORMAT_F64_BE, + + SPA_AUDIO_FORMAT_ULAW, + SPA_AUDIO_FORMAT_ALAW, + + /* planar formats */ + SPA_AUDIO_FORMAT_START_Planar = 0x200, + SPA_AUDIO_FORMAT_U8P, + SPA_AUDIO_FORMAT_S16P, + SPA_AUDIO_FORMAT_S24_32P, + SPA_AUDIO_FORMAT_S32P, + SPA_AUDIO_FORMAT_S24P, + SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_F64P, + SPA_AUDIO_FORMAT_S8P, + + /* other formats start here */ + SPA_AUDIO_FORMAT_START_Other = 0x400, + + /* Aliases */ + + /* DSP formats */ + SPA_AUDIO_FORMAT_DSP_S32 = SPA_AUDIO_FORMAT_S24_32P, + SPA_AUDIO_FORMAT_DSP_F32 = SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_DSP_F64 = SPA_AUDIO_FORMAT_F64P, + + /* native endian */ +#if __BYTE_ORDER == __BIG_ENDIAN + SPA_AUDIO_FORMAT_S16 = SPA_AUDIO_FORMAT_S16_BE, + SPA_AUDIO_FORMAT_U16 = SPA_AUDIO_FORMAT_U16_BE, + SPA_AUDIO_FORMAT_S24_32 = SPA_AUDIO_FORMAT_S24_32_BE, + SPA_AUDIO_FORMAT_U24_32 = SPA_AUDIO_FORMAT_U24_32_BE, + SPA_AUDIO_FORMAT_S32 = SPA_AUDIO_FORMAT_S32_BE, + SPA_AUDIO_FORMAT_U32 = SPA_AUDIO_FORMAT_U32_BE, + SPA_AUDIO_FORMAT_S24 = SPA_AUDIO_FORMAT_S24_BE, + SPA_AUDIO_FORMAT_U24 = SPA_AUDIO_FORMAT_U24_BE, + SPA_AUDIO_FORMAT_S20 = SPA_AUDIO_FORMAT_S20_BE, + SPA_AUDIO_FORMAT_U20 = SPA_AUDIO_FORMAT_U20_BE, + SPA_AUDIO_FORMAT_S18 = SPA_AUDIO_FORMAT_S18_BE, + SPA_AUDIO_FORMAT_U18 = SPA_AUDIO_FORMAT_U18_BE, + SPA_AUDIO_FORMAT_F32 = SPA_AUDIO_FORMAT_F32_BE, + SPA_AUDIO_FORMAT_F64 = SPA_AUDIO_FORMAT_F64_BE, + SPA_AUDIO_FORMAT_S16_OE = SPA_AUDIO_FORMAT_S16_LE, + SPA_AUDIO_FORMAT_U16_OE = SPA_AUDIO_FORMAT_U16_LE, + SPA_AUDIO_FORMAT_S24_32_OE = SPA_AUDIO_FORMAT_S24_32_LE, + SPA_AUDIO_FORMAT_U24_32_OE = SPA_AUDIO_FORMAT_U24_32_LE, + SPA_AUDIO_FORMAT_S32_OE = SPA_AUDIO_FORMAT_S32_LE, + SPA_AUDIO_FORMAT_U32_OE = SPA_AUDIO_FORMAT_U32_LE, + SPA_AUDIO_FORMAT_S24_OE = SPA_AUDIO_FORMAT_S24_LE, + SPA_AUDIO_FORMAT_U24_OE = SPA_AUDIO_FORMAT_U24_LE, + SPA_AUDIO_FORMAT_S20_OE = SPA_AUDIO_FORMAT_S20_LE, + SPA_AUDIO_FORMAT_U20_OE = SPA_AUDIO_FORMAT_U20_LE, + SPA_AUDIO_FORMAT_S18_OE = SPA_AUDIO_FORMAT_S18_LE, + SPA_AUDIO_FORMAT_U18_OE = SPA_AUDIO_FORMAT_U18_LE, + SPA_AUDIO_FORMAT_F32_OE = SPA_AUDIO_FORMAT_F32_LE, + SPA_AUDIO_FORMAT_F64_OE = SPA_AUDIO_FORMAT_F64_LE, +#elif __BYTE_ORDER == __LITTLE_ENDIAN + SPA_AUDIO_FORMAT_S16 = SPA_AUDIO_FORMAT_S16_LE, + SPA_AUDIO_FORMAT_U16 = SPA_AUDIO_FORMAT_U16_LE, + SPA_AUDIO_FORMAT_S24_32 = SPA_AUDIO_FORMAT_S24_32_LE, + SPA_AUDIO_FORMAT_U24_32 = SPA_AUDIO_FORMAT_U24_32_LE, + SPA_AUDIO_FORMAT_S32 = SPA_AUDIO_FORMAT_S32_LE, + SPA_AUDIO_FORMAT_U32 = SPA_AUDIO_FORMAT_U32_LE, + SPA_AUDIO_FORMAT_S24 = SPA_AUDIO_FORMAT_S24_LE, + SPA_AUDIO_FORMAT_U24 = SPA_AUDIO_FORMAT_U24_LE, + SPA_AUDIO_FORMAT_S20 = SPA_AUDIO_FORMAT_S20_LE, + SPA_AUDIO_FORMAT_U20 = SPA_AUDIO_FORMAT_U20_LE, + SPA_AUDIO_FORMAT_S18 = SPA_AUDIO_FORMAT_S18_LE, + SPA_AUDIO_FORMAT_U18 = SPA_AUDIO_FORMAT_U18_LE, + SPA_AUDIO_FORMAT_F32 = SPA_AUDIO_FORMAT_F32_LE, + SPA_AUDIO_FORMAT_F64 = SPA_AUDIO_FORMAT_F64_LE, + SPA_AUDIO_FORMAT_S16_OE = SPA_AUDIO_FORMAT_S16_BE, + SPA_AUDIO_FORMAT_U16_OE = SPA_AUDIO_FORMAT_U16_BE, + SPA_AUDIO_FORMAT_S24_32_OE = SPA_AUDIO_FORMAT_S24_32_BE, + SPA_AUDIO_FORMAT_U24_32_OE = SPA_AUDIO_FORMAT_U24_32_BE, + SPA_AUDIO_FORMAT_S32_OE = SPA_AUDIO_FORMAT_S32_BE, + SPA_AUDIO_FORMAT_U32_OE = SPA_AUDIO_FORMAT_U32_BE, + SPA_AUDIO_FORMAT_S24_OE = SPA_AUDIO_FORMAT_S24_BE, + SPA_AUDIO_FORMAT_U24_OE = SPA_AUDIO_FORMAT_U24_BE, + SPA_AUDIO_FORMAT_S20_OE = SPA_AUDIO_FORMAT_S20_BE, + SPA_AUDIO_FORMAT_U20_OE = SPA_AUDIO_FORMAT_U20_BE, + SPA_AUDIO_FORMAT_S18_OE = SPA_AUDIO_FORMAT_S18_BE, + SPA_AUDIO_FORMAT_U18_OE = SPA_AUDIO_FORMAT_U18_BE, + SPA_AUDIO_FORMAT_F32_OE = SPA_AUDIO_FORMAT_F32_BE, + SPA_AUDIO_FORMAT_F64_OE = SPA_AUDIO_FORMAT_F64_BE, +#endif +}; + +#define SPA_AUDIO_FORMAT_IS_INTERLEAVED(fmt) ((fmt) > SPA_AUDIO_FORMAT_START_Interleaved && (fmt) < SPA_AUDIO_FORMAT_START_Planar) +#define SPA_AUDIO_FORMAT_IS_PLANAR(fmt) ((fmt) > SPA_AUDIO_FORMAT_START_Planar && (fmt) < SPA_AUDIO_FORMAT_START_Other) + +enum spa_audio_channel { + SPA_AUDIO_CHANNEL_UNKNOWN, /**< unspecified */ + SPA_AUDIO_CHANNEL_NA, /**< N/A, silent */ + + SPA_AUDIO_CHANNEL_MONO, /**< mono stream */ + + SPA_AUDIO_CHANNEL_FL, /**< front left */ + SPA_AUDIO_CHANNEL_FR, /**< front right */ + SPA_AUDIO_CHANNEL_FC, /**< front center */ + SPA_AUDIO_CHANNEL_LFE, /**< LFE */ + SPA_AUDIO_CHANNEL_SL, /**< side left */ + SPA_AUDIO_CHANNEL_SR, /**< side right */ + SPA_AUDIO_CHANNEL_FLC, /**< front left center */ + SPA_AUDIO_CHANNEL_FRC, /**< front right center */ + SPA_AUDIO_CHANNEL_RC, /**< rear center */ + SPA_AUDIO_CHANNEL_RL, /**< rear left */ + SPA_AUDIO_CHANNEL_RR, /**< rear right */ + SPA_AUDIO_CHANNEL_TC, /**< top center */ + SPA_AUDIO_CHANNEL_TFL, /**< top front left */ + SPA_AUDIO_CHANNEL_TFC, /**< top front center */ + SPA_AUDIO_CHANNEL_TFR, /**< top front right */ + SPA_AUDIO_CHANNEL_TRL, /**< top rear left */ + SPA_AUDIO_CHANNEL_TRC, /**< top rear center */ + SPA_AUDIO_CHANNEL_TRR, /**< top rear right */ + SPA_AUDIO_CHANNEL_RLC, /**< rear left center */ + SPA_AUDIO_CHANNEL_RRC, /**< rear right center */ + SPA_AUDIO_CHANNEL_FLW, /**< front left wide */ + SPA_AUDIO_CHANNEL_FRW, /**< front right wide */ + SPA_AUDIO_CHANNEL_LFE2, /**< LFE 2 */ + SPA_AUDIO_CHANNEL_FLH, /**< front left high */ + SPA_AUDIO_CHANNEL_FCH, /**< front center high */ + SPA_AUDIO_CHANNEL_FRH, /**< front right high */ + SPA_AUDIO_CHANNEL_TFLC, /**< top front left center */ + SPA_AUDIO_CHANNEL_TFRC, /**< top front right center */ + SPA_AUDIO_CHANNEL_TSL, /**< top side left */ + SPA_AUDIO_CHANNEL_TSR, /**< top side right */ + SPA_AUDIO_CHANNEL_LLFE, /**< left LFE */ + SPA_AUDIO_CHANNEL_RLFE, /**< right LFE */ + SPA_AUDIO_CHANNEL_BC, /**< bottom center */ + SPA_AUDIO_CHANNEL_BLC, /**< bottom left center */ + SPA_AUDIO_CHANNEL_BRC, /**< bottom right center */ + + SPA_AUDIO_CHANNEL_START_Aux = 0x1000, /**< aux channels */ + SPA_AUDIO_CHANNEL_AUX0 = SPA_AUDIO_CHANNEL_START_Aux, + SPA_AUDIO_CHANNEL_AUX1, + SPA_AUDIO_CHANNEL_AUX2, + SPA_AUDIO_CHANNEL_AUX3, + SPA_AUDIO_CHANNEL_AUX4, + SPA_AUDIO_CHANNEL_AUX5, + SPA_AUDIO_CHANNEL_AUX6, + SPA_AUDIO_CHANNEL_AUX7, + SPA_AUDIO_CHANNEL_AUX8, + SPA_AUDIO_CHANNEL_AUX9, + SPA_AUDIO_CHANNEL_AUX10, + SPA_AUDIO_CHANNEL_AUX11, + SPA_AUDIO_CHANNEL_AUX12, + SPA_AUDIO_CHANNEL_AUX13, + SPA_AUDIO_CHANNEL_AUX14, + SPA_AUDIO_CHANNEL_AUX15, + SPA_AUDIO_CHANNEL_AUX16, + SPA_AUDIO_CHANNEL_AUX17, + SPA_AUDIO_CHANNEL_AUX18, + SPA_AUDIO_CHANNEL_AUX19, + SPA_AUDIO_CHANNEL_AUX20, + SPA_AUDIO_CHANNEL_AUX21, + SPA_AUDIO_CHANNEL_AUX22, + SPA_AUDIO_CHANNEL_AUX23, + SPA_AUDIO_CHANNEL_AUX24, + SPA_AUDIO_CHANNEL_AUX25, + SPA_AUDIO_CHANNEL_AUX26, + SPA_AUDIO_CHANNEL_AUX27, + SPA_AUDIO_CHANNEL_AUX28, + SPA_AUDIO_CHANNEL_AUX29, + SPA_AUDIO_CHANNEL_AUX30, + SPA_AUDIO_CHANNEL_AUX31, + SPA_AUDIO_CHANNEL_AUX32, + SPA_AUDIO_CHANNEL_AUX33, + SPA_AUDIO_CHANNEL_AUX34, + SPA_AUDIO_CHANNEL_AUX35, + SPA_AUDIO_CHANNEL_AUX36, + SPA_AUDIO_CHANNEL_AUX37, + SPA_AUDIO_CHANNEL_AUX38, + SPA_AUDIO_CHANNEL_AUX39, + SPA_AUDIO_CHANNEL_AUX40, + SPA_AUDIO_CHANNEL_AUX41, + SPA_AUDIO_CHANNEL_AUX42, + SPA_AUDIO_CHANNEL_AUX43, + SPA_AUDIO_CHANNEL_AUX44, + SPA_AUDIO_CHANNEL_AUX45, + SPA_AUDIO_CHANNEL_AUX46, + SPA_AUDIO_CHANNEL_AUX47, + SPA_AUDIO_CHANNEL_AUX48, + SPA_AUDIO_CHANNEL_AUX49, + SPA_AUDIO_CHANNEL_AUX50, + SPA_AUDIO_CHANNEL_AUX51, + SPA_AUDIO_CHANNEL_AUX52, + SPA_AUDIO_CHANNEL_AUX53, + SPA_AUDIO_CHANNEL_AUX54, + SPA_AUDIO_CHANNEL_AUX55, + SPA_AUDIO_CHANNEL_AUX56, + SPA_AUDIO_CHANNEL_AUX57, + SPA_AUDIO_CHANNEL_AUX58, + SPA_AUDIO_CHANNEL_AUX59, + SPA_AUDIO_CHANNEL_AUX60, + SPA_AUDIO_CHANNEL_AUX61, + SPA_AUDIO_CHANNEL_AUX62, + SPA_AUDIO_CHANNEL_AUX63, + + SPA_AUDIO_CHANNEL_LAST_Aux = 0x1fff, /**< aux channels */ + + SPA_AUDIO_CHANNEL_START_Custom = 0x10000, +}; + +/** Extra audio flags */ +#define SPA_AUDIO_FLAG_NONE (0) /*< no valid flag */ +#define SPA_AUDIO_FLAG_UNPOSITIONED (1 << 0) /*< the position array explicitly + * contains unpositioned channels. */ +/** Audio information description */ +struct spa_audio_info_raw { + enum spa_audio_format format; /*< format, one of enum spa_audio_format */ + uint32_t flags; /*< extra flags */ + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; /*< channel position from enum spa_audio_channel */ +}; + +#define SPA_AUDIO_INFO_RAW_INIT(...) ((struct spa_audio_info_raw) { __VA_ARGS__ }) + +#define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string, + * Ex. "S16LE" */ +#define SPA_KEY_AUDIO_CHANNEL "audio.channel" /**< an audio channel as string, + * Ex. "FL" */ +#define SPA_KEY_AUDIO_CHANNELS "audio.channels" /**< an audio channel count as int */ +#define SPA_KEY_AUDIO_RATE "audio.rate" /**< an audio sample rate as int */ +#define SPA_KEY_AUDIO_POSITION "audio.position" /**< channel positions as comma separated list + * of channels ex. "FL,FR" */ +#define SPA_KEY_AUDIO_ALLOWED_RATES "audio.allowed-rates" /**< a list of allowed samplerates + * ex. "[ 44100 48000 ]" */ +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_RAW_H */ diff --git a/spa/include/spa/param/audio/type-info.h b/spa/include/spa/param/audio/type-info.h new file mode 100644 index 0000000..d0532d7 --- /dev/null +++ b/spa/include/spa/param/audio/type-info.h @@ -0,0 +1,35 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_TYPES_H +#define SPA_AUDIO_TYPES_H + +#include +#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..475906e --- /dev/null +++ b/spa/include/spa/param/audio/vorbis-utils.h @@ -0,0 +1,80 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_VORBIS_UTILS_H +#define SPA_AUDIO_VORBIS_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_vorbis_parse(const struct spa_pod *format, struct spa_audio_info_vorbis *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_vorbis_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_vorbis *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_vorbis), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_VORBIS_UTILS_H */ diff --git a/spa/include/spa/param/audio/vorbis.h b/spa/include/spa/param/audio/vorbis.h new file mode 100644 index 0000000..5fcfb66 --- /dev/null +++ b/spa/include/spa/param/audio/vorbis.h @@ -0,0 +1,49 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_VORBIS_H +#define SPA_AUDIO_VORBIS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_audio_info_vorbis { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_VORBIS_INIT(...) ((struct spa_audio_info_vorbis) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_VORBIS_H */ diff --git a/spa/include/spa/param/audio/wma-types.h b/spa/include/spa/param/audio/wma-types.h new file mode 100644 index 0000000..ae213c2 --- /dev/null +++ b/spa/include/spa/param/audio/wma-types.h @@ -0,0 +1,57 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_WMA_TYPES_H +#define SPA_AUDIO_WMA_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_TYPE_INFO_AudioWMAProfile SPA_TYPE_INFO_ENUM_BASE "AudioWMAProfile" +#define SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE SPA_TYPE_INFO_AudioWMAProfile ":" + +static const struct spa_type_info spa_type_audio_wma_profile[] = { + { SPA_AUDIO_WMA_PROFILE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA7, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA7", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA8, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA8", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA9, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA10, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA10", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA9_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9-Pro", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA9-Lossless", NULL }, + { SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_WMA_PROFILE_BASE "WMA10-Lossless", NULL }, + { 0, 0, NULL, NULL }, +}; +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_WMA_TYPES_H */ diff --git a/spa/include/spa/param/audio/wma-utils.h b/spa/include/spa/param/audio/wma-utils.h new file mode 100644 index 0000000..f3fe094 --- /dev/null +++ b/spa/include/spa/param/audio/wma-utils.h @@ -0,0 +1,93 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_WMA_UTILS_H +#define SPA_AUDIO_WMA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include +#include + +static inline int +spa_format_audio_wma_parse(const struct spa_pod *format, struct spa_audio_info_wma *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_bitrate, SPA_POD_OPT_Int(&info->bitrate), + SPA_FORMAT_AUDIO_blockAlign, SPA_POD_OPT_Int(&info->block_align), + SPA_FORMAT_AUDIO_WMA_profile, SPA_POD_OPT_Id(&info->profile)); + return res; +} + +static inline struct spa_pod * +spa_format_audio_wma_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_wma *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_wma), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (info->bitrate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_bitrate, SPA_POD_Int(info->bitrate), 0); + if (info->block_align != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_blockAlign, SPA_POD_Int(info->block_align), 0); + if (info->profile != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_WMA_profile, SPA_POD_Id(info->profile), 0); + + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_WMA_UTILS_H */ diff --git a/spa/include/spa/param/audio/wma.h b/spa/include/spa/param/audio/wma.h new file mode 100644 index 0000000..6e098ee --- /dev/null +++ b/spa/include/spa/param/audio/wma.h @@ -0,0 +1,67 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AUDIO_WMA_H +#define SPA_AUDIO_WMA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +enum spa_audio_wma_profile { + SPA_AUDIO_WMA_PROFILE_UNKNOWN, + + SPA_AUDIO_WMA_PROFILE_WMA7, + SPA_AUDIO_WMA_PROFILE_WMA8, + SPA_AUDIO_WMA_PROFILE_WMA9, + SPA_AUDIO_WMA_PROFILE_WMA10, + SPA_AUDIO_WMA_PROFILE_WMA9_PRO, + SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS, + SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS, + + SPA_AUDIO_WMA_PROFILE_CUSTOM = 0x10000, +}; + +struct spa_audio_info_wma { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ + uint32_t bitrate; /*< stream bitrate */ + uint32_t block_align; /*< block alignment */ + enum spa_audio_wma_profile profile; /*< WMA profile */ + +}; + +#define SPA_AUDIO_INFO_WMA_INIT(...) ((struct spa_audio_info_wma) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_WMA_H */ diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h new file mode 100644 index 0000000..27f3197 --- /dev/null +++ b/spa/include/spa/param/bluetooth/audio.h @@ -0,0 +1,74 @@ +/* Simple Plugin API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUETOOTH_AUDIO_H +#define SPA_BLUETOOTH_AUDIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ +enum spa_bluetooth_audio_codec { + SPA_BLUETOOTH_AUDIO_CODEC_START, + + /* A2DP */ + SPA_BLUETOOTH_AUDIO_CODEC_SBC, + SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, + SPA_BLUETOOTH_AUDIO_CODEC_MPEG, + SPA_BLUETOOTH_AUDIO_CODEC_AAC, + SPA_BLUETOOTH_AUDIO_CODEC_APTX, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, + SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, + + /* HFP */ + SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100, + SPA_BLUETOOTH_AUDIO_CODEC_MSBC, + + /* BAP */ + SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BLUETOOTH_AUDIO_H */ diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h new file mode 100644 index 0000000..729bd58 --- /dev/null +++ b/spa/include/spa/param/bluetooth/type-info.h @@ -0,0 +1,78 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUETOOTH_TYPES_H +#define SPA_BLUETOOTH_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#define SPA_TYPE_INFO_BluetoothAudioCodec SPA_TYPE_INFO_ENUM_BASE "BluetoothAudioCodec" +#define SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE SPA_TYPE_INFO_BluetoothAudioCodec ":" + +static const struct spa_type_info spa_type_bluetooth_audio_codec[] = { + /* A2DP */ + { SPA_BLUETOOTH_AUDIO_CODEC_SBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "sbc", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "sbc_xq", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_MPEG, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "mpeg", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_AAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aac", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_APTX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_hd", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_LDAC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "ldac", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "aptx_ll_duplex", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "faststream_duplex", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3plus_hr", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_51", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_71", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_duplex", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "opus_05_pro", NULL }, + + { SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL }, + + { SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL }, + + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BLUETOOTH_TYPES_H */ diff --git a/spa/include/spa/param/buffers-types.h b/spa/include/spa/param/buffers-types.h new file mode 100644 index 0000000..50a0932 --- /dev/null +++ b/spa/include/spa/param/buffers-types.h @@ -0,0 +1,90 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_BUFFERS_TYPES_H +#define SPA_PARAM_BUFFERS_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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 }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_BUFFERS_TYPES_H */ diff --git a/spa/include/spa/param/buffers.h b/spa/include/spa/param/buffers.h new file mode 100644 index 0000000..d3733a6 --- /dev/null +++ b/spa/include/spa/param/buffers.h @@ -0,0 +1,72 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_BUFFERS_H +#define SPA_PARAM_BUFFERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_ParamBuffers */ +enum spa_param_buffers { + SPA_PARAM_BUFFERS_START, + SPA_PARAM_BUFFERS_buffers, /**< number of buffers (Int) */ + SPA_PARAM_BUFFERS_blocks, /**< number of data blocks per buffer (Int) */ + SPA_PARAM_BUFFERS_size, /**< size of a data block memory (Int)*/ + SPA_PARAM_BUFFERS_stride, /**< stride of data block memory (Int) */ + SPA_PARAM_BUFFERS_align, /**< alignment of data block memory (Int) */ + SPA_PARAM_BUFFERS_dataType, /**< possible memory types (Int, mask of enum spa_data_type) */ +}; + +/** properties for SPA_TYPE_OBJECT_ParamMeta */ +enum spa_param_meta { + SPA_PARAM_META_START, + SPA_PARAM_META_type, /**< the metadata, one of enum spa_meta_type (Id enum spa_meta_type) */ + SPA_PARAM_META_size, /**< the expected maximum size the meta (Int) */ +}; + +/** properties for SPA_TYPE_OBJECT_ParamIO */ +enum spa_param_io { + SPA_PARAM_IO_START, + SPA_PARAM_IO_id, /**< type ID, uniquely identifies the io area (Id enum spa_io_type) */ + SPA_PARAM_IO_size, /**< size of the io area (Int) */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_BUFFERS_H */ diff --git a/spa/include/spa/param/format-types.h b/spa/include/spa/param/format-types.h new file mode 100644 index 0000000..7708276 --- /dev/null +++ b/spa/include/spa/param/format-types.h @@ -0,0 +1,191 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_FORMAT_TYPES_H +#define SPA_PARAM_FORMAT_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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 }, + /* video subtypes */ + { SPA_MEDIA_SUBTYPE_h264, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", NULL }, + { SPA_MEDIA_SUBTYPE_mjpg, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", NULL }, + { SPA_MEDIA_SUBTYPE_dv, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dv", NULL }, + { SPA_MEDIA_SUBTYPE_mpegts, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegts", NULL }, + { SPA_MEDIA_SUBTYPE_h263, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h263", NULL }, + { SPA_MEDIA_SUBTYPE_mpeg1, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg1", NULL }, + { SPA_MEDIA_SUBTYPE_mpeg2, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg2", NULL }, + { SPA_MEDIA_SUBTYPE_mpeg4, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg4", NULL }, + { SPA_MEDIA_SUBTYPE_xvid, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "xvid", NULL }, + { SPA_MEDIA_SUBTYPE_vc1, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vc1", NULL }, + { SPA_MEDIA_SUBTYPE_vp8, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp8", NULL }, + { SPA_MEDIA_SUBTYPE_vp9, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp9", NULL }, + { SPA_MEDIA_SUBTYPE_bayer, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "bayer", NULL }, + /* image subtypes */ + { SPA_MEDIA_SUBTYPE_jpeg, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "jpeg", NULL }, + /* stream subtypes */ + { SPA_MEDIA_SUBTYPE_midi, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "midi", NULL }, + /* application subtypes */ + { SPA_MEDIA_SUBTYPE_control, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "control", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_FormatAudio SPA_TYPE_INFO_FORMAT_BASE "Audio" +#define SPA_TYPE_INFO_FORMAT_AUDIO_BASE SPA_TYPE_INFO_FormatAudio ":" + +#define SPA_TYPE_INFO_FORMAT_AUDIO_AAC SPA_TYPE_INFO_FORMAT_AUDIO_BASE "AAC" +#define SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE SPA_TYPE_INFO_FORMAT_AUDIO_AAC ":" +#define SPA_TYPE_INFO_FORMAT_AUDIO_WMA SPA_TYPE_INFO_FORMAT_AUDIO_BASE "WMA" +#define SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE SPA_TYPE_INFO_FORMAT_AUDIO_WMA ":" +#define SPA_TYPE_INFO_FORMAT_AUDIO_AMR SPA_TYPE_INFO_FORMAT_AUDIO_BASE "AMR" +#define SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE SPA_TYPE_INFO_FORMAT_AUDIO_AMR ":" + +#define SPA_TYPE_INFO_FormatVideo SPA_TYPE_INFO_FORMAT_BASE "Video" +#define SPA_TYPE_INFO_FORMAT_VIDEO_BASE SPA_TYPE_INFO_FormatVideo ":" + +#define SPA_TYPE_INFO_FORMAT_VIDEO_H264 SPA_TYPE_INFO_FORMAT_VIDEO_BASE "H264" +#define SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE SPA_TYPE_INFO_FORMAT_VIDEO_H264 ":" + +static const struct spa_type_info spa_type_format[] = { + { SPA_FORMAT_START, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE, spa_type_param, }, + + { SPA_FORMAT_mediaType, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE "mediaType", + spa_type_media_type, }, + { SPA_FORMAT_mediaSubtype, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_BASE "mediaSubtype", + spa_type_media_subtype, }, + + { SPA_FORMAT_AUDIO_format, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "format", + spa_type_audio_format }, + { SPA_FORMAT_AUDIO_flags, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "flags", + spa_type_audio_flags }, + { SPA_FORMAT_AUDIO_rate, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "rate", NULL }, + { SPA_FORMAT_AUDIO_channels, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "channels", NULL }, + { SPA_FORMAT_AUDIO_position, SPA_TYPE_Array, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "position", + spa_type_prop_channel_map }, + + { SPA_FORMAT_AUDIO_iec958Codec, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "iec958Codec", + spa_type_audio_iec958_codec }, + + { SPA_FORMAT_AUDIO_bitorder, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "bitorder", + spa_type_param_bitorder }, + { SPA_FORMAT_AUDIO_interleave, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "interleave", NULL }, + { SPA_FORMAT_AUDIO_bitrate, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "bitrate", NULL }, + { SPA_FORMAT_AUDIO_blockAlign, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_AUDIO_BASE "blockAlign", NULL }, + + { SPA_FORMAT_AUDIO_AAC_streamFormat, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AAC_BASE "streamFormat", + spa_type_audio_aac_stream_format }, + { SPA_FORMAT_AUDIO_WMA_profile, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE "profile", + spa_type_audio_wma_profile }, + { SPA_FORMAT_AUDIO_AMR_bandMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE "bandMode", + spa_type_audio_amr_band_mode }, + + { SPA_FORMAT_VIDEO_format, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format", + spa_type_video_format, }, + { SPA_FORMAT_VIDEO_modifier, SPA_TYPE_Long, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "modifier", NULL }, + { SPA_FORMAT_VIDEO_size, SPA_TYPE_Rectangle, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "size", NULL }, + { SPA_FORMAT_VIDEO_framerate, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "framerate", NULL }, + { SPA_FORMAT_VIDEO_maxFramerate, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "maxFramerate", NULL }, + { SPA_FORMAT_VIDEO_views, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "views", NULL }, + { SPA_FORMAT_VIDEO_interlaceMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "interlaceMode", + spa_type_video_interlace_mode, }, + { SPA_FORMAT_VIDEO_pixelAspectRatio, SPA_TYPE_Fraction, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "pixelAspectRatio", NULL }, + { SPA_FORMAT_VIDEO_multiviewMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewMode", NULL }, + { SPA_FORMAT_VIDEO_multiviewFlags, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewFlags", NULL }, + { SPA_FORMAT_VIDEO_chromaSite, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "chromaSite", NULL }, + { SPA_FORMAT_VIDEO_colorRange, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorRange", NULL }, + { SPA_FORMAT_VIDEO_colorMatrix, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorMatrix", NULL }, + { SPA_FORMAT_VIDEO_transferFunction, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "transferFunction", NULL }, + { SPA_FORMAT_VIDEO_colorPrimaries, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorPrimaries", NULL }, + { SPA_FORMAT_VIDEO_profile, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "profile", NULL }, + { SPA_FORMAT_VIDEO_level, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "level", NULL }, + + { SPA_FORMAT_VIDEO_H264_streamFormat, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE "streamFormat", NULL }, + { SPA_FORMAT_VIDEO_H264_alignment, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_H264_BASE "alignment", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_FORMAT_TYPES_H */ diff --git a/spa/include/spa/param/format-utils.h b/spa/include/spa/param/format-utils.h new file mode 100644 index 0000000..e2c83e2 --- /dev/null +++ b/spa/include/spa/param/format-utils.h @@ -0,0 +1,58 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_FORMAT_UTILS_H +#define SPA_PARAM_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include + +static inline int +spa_format_parse(const struct spa_pod *format, uint32_t *media_type, uint32_t *media_subtype) +{ + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_mediaType, SPA_POD_Id(media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(media_subtype)); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_FORMAT_UTILS_H */ diff --git a/spa/include/spa/param/format.h b/spa/include/spa/param/format.h new file mode 100644 index 0000000..d184790 --- /dev/null +++ b/spa/include/spa/param/format.h @@ -0,0 +1,176 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_FORMAT_H +#define SPA_PARAM_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** media type for SPA_TYPE_OBJECT_Format */ +enum spa_media_type { + SPA_MEDIA_TYPE_unknown, + SPA_MEDIA_TYPE_audio, + SPA_MEDIA_TYPE_video, + SPA_MEDIA_TYPE_image, + SPA_MEDIA_TYPE_binary, + SPA_MEDIA_TYPE_stream, + SPA_MEDIA_TYPE_application, +}; + +/** media subtype for SPA_TYPE_OBJECT_Format */ +enum spa_media_subtype { + SPA_MEDIA_SUBTYPE_unknown, + SPA_MEDIA_SUBTYPE_raw, + SPA_MEDIA_SUBTYPE_dsp, + SPA_MEDIA_SUBTYPE_iec958, /** S/PDIF */ + SPA_MEDIA_SUBTYPE_dsd, + + SPA_MEDIA_SUBTYPE_START_Audio = 0x10000, + SPA_MEDIA_SUBTYPE_mp3, + SPA_MEDIA_SUBTYPE_aac, + SPA_MEDIA_SUBTYPE_vorbis, + SPA_MEDIA_SUBTYPE_wma, + SPA_MEDIA_SUBTYPE_ra, + SPA_MEDIA_SUBTYPE_sbc, + SPA_MEDIA_SUBTYPE_adpcm, + SPA_MEDIA_SUBTYPE_g723, + SPA_MEDIA_SUBTYPE_g726, + SPA_MEDIA_SUBTYPE_g729, + SPA_MEDIA_SUBTYPE_amr, + SPA_MEDIA_SUBTYPE_gsm, + SPA_MEDIA_SUBTYPE_alac, /** since 0.3.65 */ + SPA_MEDIA_SUBTYPE_flac, /** since 0.3.65 */ + SPA_MEDIA_SUBTYPE_ape, /** since 0.3.65 */ + + SPA_MEDIA_SUBTYPE_START_Video = 0x20000, + SPA_MEDIA_SUBTYPE_h264, + SPA_MEDIA_SUBTYPE_mjpg, + SPA_MEDIA_SUBTYPE_dv, + SPA_MEDIA_SUBTYPE_mpegts, + SPA_MEDIA_SUBTYPE_h263, + SPA_MEDIA_SUBTYPE_mpeg1, + SPA_MEDIA_SUBTYPE_mpeg2, + SPA_MEDIA_SUBTYPE_mpeg4, + SPA_MEDIA_SUBTYPE_xvid, + SPA_MEDIA_SUBTYPE_vc1, + SPA_MEDIA_SUBTYPE_vp8, + SPA_MEDIA_SUBTYPE_vp9, + SPA_MEDIA_SUBTYPE_bayer, + + SPA_MEDIA_SUBTYPE_START_Image = 0x30000, + SPA_MEDIA_SUBTYPE_jpeg, + + SPA_MEDIA_SUBTYPE_START_Binary = 0x40000, + + SPA_MEDIA_SUBTYPE_START_Stream = 0x50000, + SPA_MEDIA_SUBTYPE_midi, + + SPA_MEDIA_SUBTYPE_START_Application = 0x60000, + SPA_MEDIA_SUBTYPE_control, /**< control stream, data contains + * spa_pod_sequence with control info. */ +}; + +/** properties for audio SPA_TYPE_OBJECT_Format */ +enum spa_format { + SPA_FORMAT_START, + + SPA_FORMAT_mediaType, /**< media type (Id enum spa_media_type) */ + SPA_FORMAT_mediaSubtype, /**< media subtype (Id enum spa_media_subtype) */ + + /* Audio format keys */ + SPA_FORMAT_START_Audio = 0x10000, + SPA_FORMAT_AUDIO_format, /**< audio format, (Id enum spa_audio_format) */ + SPA_FORMAT_AUDIO_flags, /**< optional flags (Int) */ + SPA_FORMAT_AUDIO_rate, /**< sample rate (Int) */ + SPA_FORMAT_AUDIO_channels, /**< number of audio channels (Int) */ + SPA_FORMAT_AUDIO_position, /**< channel positions (Id enum spa_audio_position) */ + + SPA_FORMAT_AUDIO_iec958Codec, /**< codec used (IEC958) (Id enum spa_audio_iec958_codec) */ + + SPA_FORMAT_AUDIO_bitorder, /**< bit order (Id enum spa_param_bitorder) */ + SPA_FORMAT_AUDIO_interleave, /**< Interleave bytes (Int) */ + SPA_FORMAT_AUDIO_bitrate, /**< bit rate (Int) */ + SPA_FORMAT_AUDIO_blockAlign, /**< audio data block alignment (Int) */ + + SPA_FORMAT_AUDIO_AAC_streamFormat, /**< AAC stream format, (Id enum spa_audio_aac_stream_format) */ + + SPA_FORMAT_AUDIO_WMA_profile, /**< WMA profile (Id enum spa_audio_wma_profile) */ + + SPA_FORMAT_AUDIO_AMR_bandMode, /**< AMR band mode (Id enum spa_audio_amr_band_mode) */ + + + /* Video Format keys */ + SPA_FORMAT_START_Video = 0x20000, + SPA_FORMAT_VIDEO_format, /**< video format (Id enum spa_video_format) */ + SPA_FORMAT_VIDEO_modifier, /**< format modifier (Long) + * use only with DMA-BUF and omit for other buffer types */ + SPA_FORMAT_VIDEO_size, /**< size (Rectangle) */ + SPA_FORMAT_VIDEO_framerate, /**< frame rate (Fraction) */ + SPA_FORMAT_VIDEO_maxFramerate, /**< maximum frame rate (Fraction) */ + SPA_FORMAT_VIDEO_views, /**< number of views (Int) */ + SPA_FORMAT_VIDEO_interlaceMode, /**< (Id enum spa_video_interlace_mode) */ + SPA_FORMAT_VIDEO_pixelAspectRatio, /**< (Rectangle) */ + SPA_FORMAT_VIDEO_multiviewMode, /**< (Id enum spa_video_multiview_mode) */ + SPA_FORMAT_VIDEO_multiviewFlags, /**< (Id enum spa_video_multiview_flags) */ + SPA_FORMAT_VIDEO_chromaSite, /**< /Id enum spa_video_chroma_site) */ + SPA_FORMAT_VIDEO_colorRange, /**< /Id enum spa_video_color_range) */ + SPA_FORMAT_VIDEO_colorMatrix, /**< /Id enum spa_video_color_matrix) */ + SPA_FORMAT_VIDEO_transferFunction, /**< /Id enum spa_video_transfer_function) */ + SPA_FORMAT_VIDEO_colorPrimaries, /**< /Id enum spa_video_color_primaries) */ + SPA_FORMAT_VIDEO_profile, /**< (Int) */ + SPA_FORMAT_VIDEO_level, /**< (Int) */ + SPA_FORMAT_VIDEO_H264_streamFormat, /**< (Id enum spa_h264_stream_format) */ + SPA_FORMAT_VIDEO_H264_alignment, /**< (Id enum spa_h264_alignment) */ + + /* Image Format keys */ + SPA_FORMAT_START_Image = 0x30000, + /* Binary Format keys */ + SPA_FORMAT_START_Binary = 0x40000, + /* Stream Format keys */ + SPA_FORMAT_START_Stream = 0x50000, + /* Application Format keys */ + SPA_FORMAT_START_Application = 0x60000, +}; + +#define SPA_KEY_FORMAT_DSP "format.dsp" /**< a predefined DSP format, + * Ex. "32 bit float mono audio" */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_FORMAT_H */ diff --git a/spa/include/spa/param/latency-types.h b/spa/include/spa/param/latency-types.h new file mode 100644 index 0000000..aa58b9f --- /dev/null +++ b/spa/include/spa/param/latency-types.h @@ -0,0 +1,75 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_LATENCY_TYPES_H +#define SPA_PARAM_LATENCY_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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..b467058 --- /dev/null +++ b/spa/include/spa/param/latency-utils.h @@ -0,0 +1,177 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_LATENCY_UTILS_H +#define SPA_PARAM_LATENCY_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#include +#include +#include + +static inline int +spa_latency_info_compare(const struct spa_latency_info *a, struct spa_latency_info *b) +{ + if (a->min_quantum == b->min_quantum && + a->max_quantum == b->max_quantum && + a->min_rate == b->min_rate && + a->max_rate == b->max_rate && + a->min_ns == b->min_ns && + a->max_ns == b->max_ns) + return 0; + return 1; +} + +static inline void +spa_latency_info_combine_start(struct spa_latency_info *info, enum spa_direction direction) +{ + *info = SPA_LATENCY_INFO(direction, + .min_quantum = FLT_MAX, + .max_quantum = 0.0f, + .min_rate = UINT32_MAX, + .max_rate = 0, + .min_ns = UINT64_MAX, + .max_ns = 0); +} +static inline void +spa_latency_info_combine_finish(struct spa_latency_info *info) +{ + if (info->min_quantum == FLT_MAX) + info->min_quantum = 0; + if (info->min_rate == UINT32_MAX) + info->min_rate = 0; + if (info->min_ns == UINT64_MAX) + info->min_ns = 0; +} + +static inline int +spa_latency_info_combine(struct spa_latency_info *info, const struct spa_latency_info *other) +{ + if (info->direction != other->direction) + return -EINVAL; + if (other->min_quantum < info->min_quantum) + info->min_quantum = other->min_quantum; + if (other->max_quantum > info->max_quantum) + info->max_quantum = other->max_quantum; + if (other->min_rate < info->min_rate) + info->min_rate = other->min_rate; + if (other->max_rate > info->max_rate) + info->max_rate = other->max_rate; + if (other->min_ns < info->min_ns) + info->min_ns = other->min_ns; + if (other->max_ns > info->max_ns) + info->max_ns = other->max_ns; + return 0; +} + +static inline int +spa_latency_parse(const struct spa_pod *latency, struct spa_latency_info *info) +{ + int res; + spa_zero(*info); + if ((res = spa_pod_parse_object(latency, + SPA_TYPE_OBJECT_ParamLatency, NULL, + SPA_PARAM_LATENCY_direction, SPA_POD_Id(&info->direction), + SPA_PARAM_LATENCY_minQuantum, SPA_POD_OPT_Float(&info->min_quantum), + SPA_PARAM_LATENCY_maxQuantum, SPA_POD_OPT_Float(&info->max_quantum), + SPA_PARAM_LATENCY_minRate, SPA_POD_OPT_Int(&info->min_rate), + SPA_PARAM_LATENCY_maxRate, SPA_POD_OPT_Int(&info->max_rate), + SPA_PARAM_LATENCY_minNs, SPA_POD_OPT_Long(&info->min_ns), + SPA_PARAM_LATENCY_maxNs, SPA_POD_OPT_Long(&info->max_ns))) < 0) + return res; + info->direction = (enum spa_direction)(info->direction & 1); + return 0; +} + +static inline struct spa_pod * +spa_latency_build(struct spa_pod_builder *builder, uint32_t id, const struct spa_latency_info *info) +{ + return (struct spa_pod *)spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_ParamLatency, id, + SPA_PARAM_LATENCY_direction, SPA_POD_Id(info->direction), + SPA_PARAM_LATENCY_minQuantum, SPA_POD_Float(info->min_quantum), + SPA_PARAM_LATENCY_maxQuantum, SPA_POD_Float(info->max_quantum), + SPA_PARAM_LATENCY_minRate, SPA_POD_Int(info->min_rate), + SPA_PARAM_LATENCY_maxRate, SPA_POD_Int(info->max_rate), + SPA_PARAM_LATENCY_minNs, SPA_POD_Long(info->min_ns), + SPA_PARAM_LATENCY_maxNs, SPA_POD_Long(info->max_ns)); +} + +static inline int +spa_process_latency_parse(const struct spa_pod *latency, struct spa_process_latency_info *info) +{ + int res; + spa_zero(*info); + if ((res = spa_pod_parse_object(latency, + SPA_TYPE_OBJECT_ParamProcessLatency, NULL, + SPA_PARAM_PROCESS_LATENCY_quantum, SPA_POD_OPT_Float(&info->quantum), + SPA_PARAM_PROCESS_LATENCY_rate, SPA_POD_OPT_Int(&info->rate), + SPA_PARAM_PROCESS_LATENCY_ns, SPA_POD_OPT_Long(&info->ns))) < 0) + return res; + return 0; +} + +static inline struct spa_pod * +spa_process_latency_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_process_latency_info *info) +{ + return (struct spa_pod *)spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_ParamProcessLatency, id, + SPA_PARAM_PROCESS_LATENCY_quantum, SPA_POD_Float(info->quantum), + SPA_PARAM_PROCESS_LATENCY_rate, SPA_POD_Int(info->rate), + SPA_PARAM_PROCESS_LATENCY_ns, SPA_POD_Long(info->ns)); +} + +static inline int +spa_process_latency_info_add(const struct spa_process_latency_info *process, + struct spa_latency_info *info) +{ + info->min_quantum += process->quantum; + info->max_quantum += process->quantum; + info->min_rate += process->rate; + info->max_rate += process->rate; + info->min_ns += process->ns; + info->max_ns += process->ns; + return 0; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_LATENCY_UTILS_H */ diff --git a/spa/include/spa/param/latency.h b/spa/include/spa/param/latency.h new file mode 100644 index 0000000..da3f66d --- /dev/null +++ b/spa/include/spa/param/latency.h @@ -0,0 +1,89 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_LATENY_H +#define SPA_PARAM_LATENY_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_ParamLatency */ +enum spa_param_latency { + SPA_PARAM_LATENCY_START, + SPA_PARAM_LATENCY_direction, /**< direction, input/output (Id enum spa_direction) */ + SPA_PARAM_LATENCY_minQuantum, /**< min latency relative to quantum (Float) */ + SPA_PARAM_LATENCY_maxQuantum, /**< max latency relative to quantum (Float) */ + SPA_PARAM_LATENCY_minRate, /**< min latency (Int) relative to rate */ + SPA_PARAM_LATENCY_maxRate, /**< max latency (Int) relative to rate */ + SPA_PARAM_LATENCY_minNs, /**< min latency (Long) in nanoseconds */ + SPA_PARAM_LATENCY_maxNs, /**< max latency (Long) in nanoseconds */ +}; + +/** helper structure for managing latency objects */ +struct spa_latency_info { + enum spa_direction direction; + float min_quantum; + float max_quantum; + uint32_t min_rate; + uint32_t max_rate; + uint64_t min_ns; + uint64_t max_ns; +}; + +#define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ }) + +/** properties for SPA_TYPE_OBJECT_ParamProcessLatency */ +enum spa_param_process_latency { + SPA_PARAM_PROCESS_LATENCY_START, + SPA_PARAM_PROCESS_LATENCY_quantum, /**< latency relative to quantum (Float) */ + SPA_PARAM_PROCESS_LATENCY_rate, /**< latency (Int) relative to rate */ + SPA_PARAM_PROCESS_LATENCY_ns, /**< latency (Long) in nanoseconds */ +}; + +/** Helper structure for managing process latency objects */ +struct spa_process_latency_info { + float quantum; + uint32_t rate; + uint64_t ns; +}; + +#define SPA_PROCESS_LATENCY_INFO_INIT(...) ((struct spa_process_latency_info) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_LATENY_H */ diff --git a/spa/include/spa/param/param-types.h b/spa/include/spa/param/param-types.h new file mode 100644 index 0000000..56d1b18 --- /dev/null +++ b/spa/include/spa/param/param-types.h @@ -0,0 +1,115 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_TYPES_H +#define SPA_PARAM_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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 }, + { 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_channel_map[] = { + { SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_BASE "channelMap", spa_type_audio_channel, }, + { 0, 0, NULL, NULL }, +}; + +static const struct spa_type_info spa_type_prop_iec958_codec[] = { + { SPA_PROP_START, SPA_TYPE_Id, SPA_TYPE_INFO_BASE "iec958Codec", spa_type_audio_iec958_codec, }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_ParamBitorder SPA_TYPE_INFO_ENUM_BASE "ParamBitorder" +#define SPA_TYPE_INFO_PARAM_BITORDER_BASE SPA_TYPE_INFO_ParamBitorder ":" + +static const struct spa_type_info spa_type_param_bitorder[] = { + { SPA_PARAM_BITORDER_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "unknown", NULL }, + { SPA_PARAM_BITORDER_msb, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "msb", NULL }, + { SPA_PARAM_BITORDER_lsb, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_BITORDER_BASE "lsb", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_ParamAvailability SPA_TYPE_INFO_ENUM_BASE "ParamAvailability" +#define SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE SPA_TYPE_INFO_ParamAvailability ":" + +static const struct spa_type_info spa_type_param_availability[] = { + { SPA_PARAM_AVAILABILITY_unknown, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "unknown", NULL }, + { SPA_PARAM_AVAILABILITY_no, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "no", NULL }, + { SPA_PARAM_AVAILABILITY_yes, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_AVAILABILITY_BASE "yes", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_TYPES_H */ diff --git a/spa/include/spa/param/param.h b/spa/include/spa/param/param.h new file mode 100644 index 0000000..1c26ef8 --- /dev/null +++ b/spa/include/spa/param/param.h @@ -0,0 +1,107 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_H +#define SPA_PARAM_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_param Parameters + * Parameter value enumerations and type information + */ + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** different parameter types that can be queried */ +enum spa_param_type { + SPA_PARAM_Invalid, /**< invalid */ + SPA_PARAM_PropInfo, /**< property information as SPA_TYPE_OBJECT_PropInfo */ + SPA_PARAM_Props, /**< properties as SPA_TYPE_OBJECT_Props */ + SPA_PARAM_EnumFormat, /**< available formats as SPA_TYPE_OBJECT_Format */ + SPA_PARAM_Format, /**< configured format as SPA_TYPE_OBJECT_Format */ + SPA_PARAM_Buffers, /**< buffer configurations as SPA_TYPE_OBJECT_ParamBuffers*/ + SPA_PARAM_Meta, /**< allowed metadata for buffers as SPA_TYPE_OBJECT_ParamMeta*/ + SPA_PARAM_IO, /**< configurable IO areas as SPA_TYPE_OBJECT_ParamIO */ + SPA_PARAM_EnumProfile, /**< profile enumeration as SPA_TYPE_OBJECT_ParamProfile */ + SPA_PARAM_Profile, /**< profile configuration as SPA_TYPE_OBJECT_ParamProfile */ + SPA_PARAM_EnumPortConfig, /**< port configuration enumeration as SPA_TYPE_OBJECT_ParamPortConfig */ + SPA_PARAM_PortConfig, /**< port configuration as SPA_TYPE_OBJECT_ParamPortConfig */ + SPA_PARAM_EnumRoute, /**< routing enumeration as SPA_TYPE_OBJECT_ParamRoute */ + SPA_PARAM_Route, /**< routing configuration as SPA_TYPE_OBJECT_ParamRoute */ + SPA_PARAM_Control, /**< Control parameter, a SPA_TYPE_Sequence */ + SPA_PARAM_Latency, /**< latency reporting, a SPA_TYPE_OBJECT_ParamLatency */ + SPA_PARAM_ProcessLatency, /**< processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency */ +}; + +/** information about a parameter */ +struct spa_param_info { + uint32_t id; /**< enum spa_param_type */ +#define SPA_PARAM_INFO_SERIAL (1<<0) /**< bit to signal update even when the + * read/write flags don't change */ +#define SPA_PARAM_INFO_READ (1<<1) +#define SPA_PARAM_INFO_WRITE (1<<2) +#define SPA_PARAM_INFO_READWRITE (SPA_PARAM_INFO_WRITE|SPA_PARAM_INFO_READ) + uint32_t flags; + uint32_t user; /**< private user field. You can use this to keep + * state. */ + int32_t seq; /**< private seq field. You can use this to keep + * state of a pending update. */ + uint32_t padding[4]; +}; + +#define SPA_PARAM_INFO(id,flags) ((struct spa_param_info){ (id), (flags) }) + +enum spa_param_bitorder { + SPA_PARAM_BITORDER_unknown, /**< unknown bitorder */ + SPA_PARAM_BITORDER_msb, /**< most significant bit */ + SPA_PARAM_BITORDER_lsb, /**< least significant bit */ +}; + +enum spa_param_availability { + SPA_PARAM_AVAILABILITY_unknown, /**< unknown availability */ + SPA_PARAM_AVAILABILITY_no, /**< not available */ + SPA_PARAM_AVAILABILITY_yes, /**< available */ +}; + +#include +#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..f05cdeb --- /dev/null +++ b/spa/include/spa/param/port-config-types.h @@ -0,0 +1,73 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PORT_CONFIG_TYPES_H +#define SPA_PARAM_PORT_CONFIG_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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..d88a3ff --- /dev/null +++ b/spa/include/spa/param/port-config.h @@ -0,0 +1,66 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PORT_CONFIG_H +#define SPA_PARAM_PORT_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +enum spa_param_port_config_mode { + SPA_PARAM_PORT_CONFIG_MODE_none, /**< no configuration */ + SPA_PARAM_PORT_CONFIG_MODE_passthrough, /**< passthrough configuration */ + SPA_PARAM_PORT_CONFIG_MODE_convert, /**< convert configuration */ + SPA_PARAM_PORT_CONFIG_MODE_dsp, /**< dsp configuration, depending on the external + * format. For audio, ports will be configured for + * the given number of channels with F32 format. */ +}; + +/** properties for SPA_TYPE_OBJECT_ParamPortConfig */ +enum spa_param_port_config { + SPA_PARAM_PORT_CONFIG_START, + SPA_PARAM_PORT_CONFIG_direction, /**< direction, input/output (Id enum spa_direction) */ + SPA_PARAM_PORT_CONFIG_mode, /**< (Id enum spa_param_port_config_mode) mode */ + SPA_PARAM_PORT_CONFIG_monitor, /**< (Bool) enable monitor output ports on input ports */ + SPA_PARAM_PORT_CONFIG_control, /**< (Bool) enable control ports */ + SPA_PARAM_PORT_CONFIG_format, /**< (Object) format filter */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PORT_CONFIG_H */ diff --git a/spa/include/spa/param/profile-types.h b/spa/include/spa/param/profile-types.h new file mode 100644 index 0000000..e8a0831 --- /dev/null +++ b/spa/include/spa/param/profile-types.h @@ -0,0 +1,65 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROFILE_TYPES_H +#define SPA_PARAM_PROFILE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#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..3242433 --- /dev/null +++ b/spa/include/spa/param/profile.h @@ -0,0 +1,72 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROFILE_H +#define SPA_PARAM_PROFILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_ParamProfile */ +enum spa_param_profile { + SPA_PARAM_PROFILE_START, + SPA_PARAM_PROFILE_index, /**< profile index (Int) */ + SPA_PARAM_PROFILE_name, /**< profile name (String) */ + SPA_PARAM_PROFILE_description, /**< profile description (String) */ + SPA_PARAM_PROFILE_priority, /**< profile priority (Int) */ + SPA_PARAM_PROFILE_available, /**< availability of the profile + * (Id enum spa_param_availability) */ + SPA_PARAM_PROFILE_info, /**< info (Struct( + * Int : n_items, + * (String : key, + * String : value)*)) */ + SPA_PARAM_PROFILE_classes, /**< node classes provided by this profile + * (Struct( + * Int : number of items following + * Struct( + * String : class name (eg. "Audio/Source"), + * Int : number of nodes + * String : property (eg. "card.profile.devices"), + * Array of Int: device indexes + * )*)) */ + SPA_PARAM_PROFILE_save, /**< If profile should be saved (Bool) */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PROFILE_H */ diff --git a/spa/include/spa/param/profiler-types.h b/spa/include/spa/param/profiler-types.h new file mode 100644 index 0000000..3f7297f --- /dev/null +++ b/spa/include/spa/param/profiler-types.h @@ -0,0 +1,60 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROFILER_TYPES_H +#define SPA_PARAM_PROFILER_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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, }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PROFILER_TYPES_H */ diff --git a/spa/include/spa/param/profiler.h b/spa/include/spa/param/profiler.h new file mode 100644 index 0000000..44b5688 --- /dev/null +++ b/spa/include/spa/param/profiler.h @@ -0,0 +1,97 @@ +/* Simple Plugin API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROFILER_H +#define SPA_PARAM_PROFILER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_Profiler */ +enum spa_profiler { + SPA_PROFILER_START, + + SPA_PROFILER_START_Driver = 0x10000, /**< driver related profiler properties */ + SPA_PROFILER_info, /**< Generic info, counter and CPU load, + * (Struct( + * Long : counter, + * Float : cpu_load fast, + * Float : cpu_load medium, + * Float : cpu_load slow), + * Int : xrun-count)) */ + SPA_PROFILER_clock, /**< clock information + * (Struct( + * Int : clock flags, + * Int : clock id, + * String: clock name, + * Long : clock nsec, + * Fraction : clock rate, + * Long : clock position, + * Long : clock duration, + * Long : clock delay, + * Double : clock rate_diff, + * Long : clock next_nsec)) */ + SPA_PROFILER_driverBlock, /**< generic driver info block + * (Struct( + * Int : driver_id, + * String : name, + * Long : driver prev_signal, + * Long : driver signal, + * Long : driver awake, + * Long : driver finish, + * Int : driver status), + * Fraction : latency)) */ + + SPA_PROFILER_START_Follower = 0x20000, /**< follower related profiler properties */ + SPA_PROFILER_followerBlock, /**< generic follower info block + * (Struct( + * Int : id, + * String : name, + * Long : prev_signal, + * Long : signal, + * Long : awake, + * Long : finish, + * Int : status, + * Fraction : latency)) */ + + SPA_PROFILER_START_CUSTOM = 0x1000000, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PROFILER_H */ diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h new file mode 100644 index 0000000..aa3ea24 --- /dev/null +++ b/spa/include/spa/param/props-types.h @@ -0,0 +1,119 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROPS_TYPES_H +#define SPA_PARAM_PROPS_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +#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_brightness, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL }, + { SPA_PROP_contrast, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL }, + { SPA_PROP_saturation, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "saturation", NULL }, + { SPA_PROP_hue, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "hue", NULL }, + { SPA_PROP_gamma, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "gamma", NULL }, + { SPA_PROP_exposure, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "exposure", NULL }, + { SPA_PROP_gain, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "gain", NULL }, + { SPA_PROP_sharpness, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "sharpness", NULL }, + + { SPA_PROP_params, SPA_TYPE_Struct, SPA_TYPE_INFO_PROPS_BASE "params", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** Enum Property info */ +#define SPA_TYPE_INFO_PropInfo SPA_TYPE_INFO_PARAM_BASE "PropInfo" +#define SPA_TYPE_INFO_PROP_INFO_BASE SPA_TYPE_INFO_PropInfo ":" + +static const struct spa_type_info spa_type_prop_info[] = { + { SPA_PROP_INFO_START, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE, spa_type_param, }, + { SPA_PROP_INFO_id, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE "id", spa_type_props }, + { SPA_PROP_INFO_name, SPA_TYPE_String, SPA_TYPE_INFO_PROP_INFO_BASE "name", NULL }, + { SPA_PROP_INFO_type, SPA_TYPE_Pod, SPA_TYPE_INFO_PROP_INFO_BASE "type", NULL }, + { SPA_PROP_INFO_labels, SPA_TYPE_Struct, SPA_TYPE_INFO_PROP_INFO_BASE "labels", NULL }, + { SPA_PROP_INFO_container, SPA_TYPE_Id, SPA_TYPE_INFO_PROP_INFO_BASE "container", NULL }, + { SPA_PROP_INFO_params, SPA_TYPE_Bool, SPA_TYPE_INFO_PROP_INFO_BASE "params", NULL }, + { SPA_PROP_INFO_description, SPA_TYPE_String, SPA_TYPE_INFO_PROP_INFO_BASE "description", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PROPS_TYPES_H */ diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h new file mode 100644 index 0000000..900dffa --- /dev/null +++ b/spa/include/spa/param/props.h @@ -0,0 +1,132 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_PROPS_H +#define SPA_PARAM_PROPS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties of SPA_TYPE_OBJECT_PropInfo */ +enum spa_prop_info { + SPA_PROP_INFO_START, + SPA_PROP_INFO_id, /**< associated id of the property */ + SPA_PROP_INFO_name, /**< name of the property */ + SPA_PROP_INFO_type, /**< type and range/enums of property */ + SPA_PROP_INFO_labels, /**< labels of property if any, this is a + * struct with pairs of values, the first one + * is of the type of the property, the second + * one is a string with a user readable label + * for the value. */ + SPA_PROP_INFO_container, /**< type of container if any (Id) */ + SPA_PROP_INFO_params, /**< is part of params property (Bool) */ + SPA_PROP_INFO_description, /**< User readable description */ +}; + +/** predefined properties for SPA_TYPE_OBJECT_Props */ +enum spa_prop { + SPA_PROP_START, + + SPA_PROP_unknown, /**< an unknown property */ + + SPA_PROP_START_Device = 0x100, /**< device related properties */ + SPA_PROP_device, + SPA_PROP_deviceName, + SPA_PROP_deviceFd, + SPA_PROP_card, + SPA_PROP_cardName, + + SPA_PROP_minLatency, + SPA_PROP_maxLatency, + SPA_PROP_periods, + SPA_PROP_periodSize, + SPA_PROP_periodEvent, + SPA_PROP_live, + SPA_PROP_rate, + SPA_PROP_quality, + SPA_PROP_bluetoothAudioCodec, + SPA_PROP_bluetoothOffloadActive, + + SPA_PROP_START_Audio = 0x10000, /**< audio related properties */ + SPA_PROP_waveType, + SPA_PROP_frequency, + SPA_PROP_volume, /**< a volume (Float), 0.0 silence, 1.0 normal */ + SPA_PROP_mute, /**< mute (Bool) */ + SPA_PROP_patternType, + SPA_PROP_ditherType, + SPA_PROP_truncate, + SPA_PROP_channelVolumes, /**< a volume array, one volume per + * channel (Array of Float) */ + SPA_PROP_volumeBase, /**< a volume base (Float) */ + SPA_PROP_volumeStep, /**< a volume step (Float) */ + SPA_PROP_channelMap, /**< a channelmap array + * (Array (Id enum spa_audio_channel)) */ + SPA_PROP_monitorMute, /**< mute (Bool) */ + SPA_PROP_monitorVolumes, /**< a volume array, one volume per + * channel (Array of Float) */ + SPA_PROP_latencyOffsetNsec, /**< delay adjustment */ + SPA_PROP_softMute, /**< mute (Bool) */ + SPA_PROP_softVolumes, /**< a volume array, one volume per + * channel (Array of Float) */ + + SPA_PROP_iec958Codecs, /**< enabled IEC958 (S/PDIF) codecs, + * (Array (Id enum spa_audio_iec958_codec) */ + + SPA_PROP_START_Video = 0x20000, /**< video related properties */ + SPA_PROP_brightness, + SPA_PROP_contrast, + SPA_PROP_saturation, + SPA_PROP_hue, + SPA_PROP_gamma, + SPA_PROP_exposure, + SPA_PROP_gain, + SPA_PROP_sharpness, + + SPA_PROP_START_Other = 0x80000, /**< other properties */ + SPA_PROP_params, /**< simple control params + * (Struct( + * (String : key, + * Pod : value)*)) */ + + + SPA_PROP_START_CUSTOM = 0x1000000, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_PROPS_H */ diff --git a/spa/include/spa/param/route-types.h b/spa/include/spa/param/route-types.h new file mode 100644 index 0000000..b370c6c --- /dev/null +++ b/spa/include/spa/param/route-types.h @@ -0,0 +1,71 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_ROUTE_TYPES_H +#define SPA_PARAM_ROUTE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profiles", NULL, }, + { SPA_PARAM_ROUTE_props, SPA_TYPE_OBJECT_Props, SPA_TYPE_INFO_PARAM_ROUTE_BASE "props", NULL, }, + { SPA_PARAM_ROUTE_devices, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "devices", NULL, }, + { SPA_PARAM_ROUTE_profile, SPA_TYPE_Int, SPA_TYPE_INFO_PARAM_ROUTE_BASE "profile", NULL, }, + { SPA_PARAM_ROUTE_save, SPA_TYPE_Bool, SPA_TYPE_INFO_PARAM_ROUTE_BASE "save", NULL, }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_ROUTE_TYPES_H */ diff --git a/spa/include/spa/param/route.h b/spa/include/spa/param/route.h new file mode 100644 index 0000000..2e79b94 --- /dev/null +++ b/spa/include/spa/param/route.h @@ -0,0 +1,69 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_ROUTE_H +#define SPA_PARAM_ROUTE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +/** properties for SPA_TYPE_OBJECT_ParamRoute */ +enum spa_param_route { + SPA_PARAM_ROUTE_START, + SPA_PARAM_ROUTE_index, /**< index of the routing destination (Int) */ + SPA_PARAM_ROUTE_direction, /**< direction, input/output (Id enum spa_direction) */ + SPA_PARAM_ROUTE_device, /**< device id (Int) */ + SPA_PARAM_ROUTE_name, /**< name of the routing destination (String) */ + SPA_PARAM_ROUTE_description, /**< description of the destination (String) */ + SPA_PARAM_ROUTE_priority, /**< priority of the destination (Int) */ + SPA_PARAM_ROUTE_available, /**< availability of the destination + * (Id enum spa_param_availability) */ + SPA_PARAM_ROUTE_info, /**< info (Struct( + * Int : n_items, + * (String : key, + * String : value)*)) */ + SPA_PARAM_ROUTE_profiles, /**< associated profile indexes (Array of Int) */ + SPA_PARAM_ROUTE_props, /**< properties SPA_TYPE_OBJECT_Props */ + SPA_PARAM_ROUTE_devices, /**< associated device indexes (Array of Int) */ + SPA_PARAM_ROUTE_profile, /**< profile id (Int) */ + SPA_PARAM_ROUTE_save, /**< If route should be saved (Bool) */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_ROUTE_H */ diff --git a/spa/include/spa/param/type-info.h b/spa/include/spa/param/type-info.h new file mode 100644 index 0000000..c6df3e0 --- /dev/null +++ b/spa/include/spa/param/type-info.h @@ -0,0 +1,38 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_TYPE_INFO_H +#define SPA_PARAM_TYPE_INFO_H + +#include +#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..0ad2072 --- /dev/null +++ b/spa/include/spa/param/video/chroma.h @@ -0,0 +1,64 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_CHROMA_H +#define SPA_VIDEO_CHROMA_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** Various Chroma settings. + */ +enum spa_video_chroma_site { + SPA_VIDEO_CHROMA_SITE_UNKNOWN = 0, /**< unknown cositing */ + SPA_VIDEO_CHROMA_SITE_NONE = (1 << 0), /**< no cositing */ + SPA_VIDEO_CHROMA_SITE_H_COSITED = (1 << 1), /**< chroma is horizontally cosited */ + SPA_VIDEO_CHROMA_SITE_V_COSITED = (1 << 2), /**< chroma is vertically cosited */ + SPA_VIDEO_CHROMA_SITE_ALT_LINE = (1 << 3), /**< chroma samples are sited on alternate lines */ + /* some common chroma cositing */ + /** chroma samples cosited with luma samples */ + SPA_VIDEO_CHROMA_SITE_COSITED = (SPA_VIDEO_CHROMA_SITE_H_COSITED | SPA_VIDEO_CHROMA_SITE_V_COSITED), + /** jpeg style cositing, also for mpeg1 and mjpeg */ + SPA_VIDEO_CHROMA_SITE_JPEG = (SPA_VIDEO_CHROMA_SITE_NONE), + /** mpeg2 style cositing */ + SPA_VIDEO_CHROMA_SITE_MPEG2 = (SPA_VIDEO_CHROMA_SITE_H_COSITED), + /**< DV style cositing */ + SPA_VIDEO_CHROMA_SITE_DV = (SPA_VIDEO_CHROMA_SITE_COSITED | SPA_VIDEO_CHROMA_SITE_ALT_LINE), +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_CHROMA_H */ diff --git a/spa/include/spa/param/video/color.h b/spa/include/spa/param/video/color.h new file mode 100644 index 0000000..028239c --- /dev/null +++ b/spa/include/spa/param/video/color.h @@ -0,0 +1,125 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_COLOR_H +#define SPA_VIDEO_COLOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** + * Possible color range values. These constants are defined for 8 bit color + * values and can be scaled for other bit depths. + */ +enum spa_video_color_range { + SPA_VIDEO_COLOR_RANGE_UNKNOWN = 0, /**< unknown range */ + SPA_VIDEO_COLOR_RANGE_0_255, /**< [0..255] for 8 bit components */ + SPA_VIDEO_COLOR_RANGE_16_235 /**< [16..235] for 8 bit components. Chroma has + [16..240] range. */ +}; + +/** + * The color matrix is used to convert between Y'PbPr and + * non-linear RGB (R'G'B') + */ +enum spa_video_color_matrix { + SPA_VIDEO_COLOR_MATRIX_UNKNOWN = 0, /**< unknown matrix */ + SPA_VIDEO_COLOR_MATRIX_RGB, /**< identity matrix */ + SPA_VIDEO_COLOR_MATRIX_FCC, /**< FCC color matrix */ + SPA_VIDEO_COLOR_MATRIX_BT709, /**< ITU BT.709 color matrix */ + SPA_VIDEO_COLOR_MATRIX_BT601, /**< ITU BT.601 color matrix */ + SPA_VIDEO_COLOR_MATRIX_SMPTE240M, /**< SMTPE 240M color matrix */ + SPA_VIDEO_COLOR_MATRIX_BT2020, /**< ITU-R BT.2020 color matrix. since 1.6. */ +}; + +/** + * The video transfer function defines the formula for converting between + * non-linear RGB (R'G'B') and linear RGB + */ +enum spa_video_transfer_function { + SPA_VIDEO_TRANSFER_UNKNOWN = 0, /**< unknown transfer function */ + SPA_VIDEO_TRANSFER_GAMMA10, /**< linear RGB, gamma 1.0 curve */ + SPA_VIDEO_TRANSFER_GAMMA18, /**< Gamma 1.8 curve */ + SPA_VIDEO_TRANSFER_GAMMA20, /**< Gamma 2.0 curve */ + SPA_VIDEO_TRANSFER_GAMMA22, /**< Gamma 2.2 curve */ + SPA_VIDEO_TRANSFER_BT709, /**< Gamma 2.2 curve with a linear segment in the lower range */ + SPA_VIDEO_TRANSFER_SMPTE240M, /**< Gamma 2.2 curve with a linear segment in the lower range */ + SPA_VIDEO_TRANSFER_SRGB, /**< Gamma 2.4 curve with a linear segment in the lower range */ + SPA_VIDEO_TRANSFER_GAMMA28, /**< Gamma 2.8 curve */ + SPA_VIDEO_TRANSFER_LOG100, /**< Logarithmic transfer characteristic 100:1 range */ + SPA_VIDEO_TRANSFER_LOG316, /**< Logarithmic transfer characteristic 316.22777:1 range */ + SPA_VIDEO_TRANSFER_BT2020_12, /**< Gamma 2.2 curve with a linear segment in the lower + * range. Used for BT.2020 with 12 bits per + * component. \since 1.6. */ + SPA_VIDEO_TRANSFER_ADOBERGB, /**< Gamma 2.19921875. \since 1.8 */ +}; + +/** + * The color primaries define the how to transform linear RGB values to and from + * the CIE XYZ colorspace. + */ +enum spa_video_color_primaries { + SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN = 0, /**< unknown color primaries */ + SPA_VIDEO_COLOR_PRIMARIES_BT709, /**< BT709 primaries */ + SPA_VIDEO_COLOR_PRIMARIES_BT470M, /**< BT470M primaries */ + SPA_VIDEO_COLOR_PRIMARIES_BT470BG, /**< BT470BG primaries */ + SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, /**< SMPTE170M primaries */ + SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, /**< SMPTE240M primaries */ + SPA_VIDEO_COLOR_PRIMARIES_FILM, /**< Generic film */ + SPA_VIDEO_COLOR_PRIMARIES_BT2020, /**< BT2020 primaries. \since 1.6. */ + SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, /**< Adobe RGB primaries. \since 1.8 */ +}; + +/** + * spa_video_colorimetry: + * + * Structure describing the color info. + */ +struct spa_video_colorimetry { + enum spa_video_color_range range; /**< The color range. This is the valid range for the + * samples. It is used to convert the samples to Y'PbPr + * values. */ + enum spa_video_color_matrix matrix; /**< the color matrix. Used to convert between Y'PbPr and + * non-linear RGB (R'G'B') */ + enum spa_video_transfer_function transfer; /**< The transfer function. Used to convert between + * R'G'B' and RGB */ + enum spa_video_color_primaries primaries; /**< Color primaries. Used to convert between R'G'B' + * and CIE XYZ */ +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_COLOR_H */ diff --git a/spa/include/spa/param/video/dsp-utils.h b/spa/include/spa/param/video/dsp-utils.h new file mode 100644 index 0000000..524655f --- /dev/null +++ b/spa/include/spa/param/video/dsp-utils.h @@ -0,0 +1,83 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_DSP_UTILS_H +#define SPA_VIDEO_DSP_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +static inline int +spa_format_video_dsp_parse(const struct spa_pod *format, + struct spa_video_info_dsp *info) +{ + info->flags = SPA_VIDEO_FLAG_NONE; + if (spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) { + info->flags |= SPA_VIDEO_FLAG_MODIFIER; + } + + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_format, SPA_POD_OPT_Id(&info->format), + SPA_FORMAT_VIDEO_modifier, SPA_POD_OPT_Long(&info->modifier)); +} + +static inline struct spa_pod * +spa_format_video_dsp_build(struct spa_pod_builder *builder, uint32_t id, + struct spa_video_info_dsp *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + 0); + if (info->format != SPA_VIDEO_FORMAT_UNKNOWN) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_format, SPA_POD_Id(info->format), 0); + if (info->modifier != 0 || info->flags & SPA_VIDEO_FLAG_MODIFIER) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_modifier, SPA_POD_Long(info->modifier), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_DSP_UTILS_H */ diff --git a/spa/include/spa/param/video/dsp.h b/spa/include/spa/param/video/dsp.h new file mode 100644 index 0000000..f97046f --- /dev/null +++ b/spa/include/spa/param/video/dsp.h @@ -0,0 +1,55 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_DSP_H +#define SPA_VIDEO_DSP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +struct spa_video_info_dsp { + enum spa_video_format format; + uint32_t flags; + uint64_t modifier; +}; + +#define SPA_VIDEO_INFO_DSP_INIT(...) ((struct spa_video_info_dsp) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_DSP_H */ diff --git a/spa/include/spa/param/video/encoded.h b/spa/include/spa/param/video/encoded.h new file mode 100644 index 0000000..b34e0d8 --- /dev/null +++ b/spa/include/spa/param/video/encoded.h @@ -0,0 +1,31 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_ENCODED_H +#define SPA_VIDEO_ENCODED_H + +#include +#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..560f7f7 --- /dev/null +++ b/spa/include/spa/param/video/format-utils.h @@ -0,0 +1,84 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_VIDEO_FORMAT_UTILS_H +#define SPA_PARAM_VIDEO_FORMAT_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +static inline int +spa_format_video_parse(const struct spa_pod *format, struct spa_video_info *info) +{ + int res; + + if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0) + return res; + + if (info->media_type != SPA_MEDIA_TYPE_video) + return -EINVAL; + + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + return spa_format_video_raw_parse(format, &info->info.raw); + case SPA_MEDIA_SUBTYPE_dsp: + return spa_format_video_dsp_parse(format, &info->info.dsp); + case SPA_MEDIA_SUBTYPE_h264: + return spa_format_video_h264_parse(format, &info->info.h264); + case SPA_MEDIA_SUBTYPE_mjpg: + return spa_format_video_mjpg_parse(format, &info->info.mjpg); + } + return -ENOTSUP; +} + +static inline struct spa_pod * +spa_format_video_build(struct spa_pod_builder *builder, uint32_t id, struct spa_video_info *info) +{ + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + return spa_format_video_raw_build(builder, id, &info->info.raw); + case SPA_MEDIA_SUBTYPE_dsp: + return spa_format_video_dsp_build(builder, id, &info->info.dsp); + case SPA_MEDIA_SUBTYPE_h264: + return spa_format_video_h264_build(builder, id, &info->info.h264); + case SPA_MEDIA_SUBTYPE_mjpg: + return spa_format_video_mjpg_build(builder, id, &info->info.mjpg); + } + errno = ENOTSUP; + return NULL; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PARAM_VIDEO_FORMAT_UTILS_H */ diff --git a/spa/include/spa/param/video/format.h b/spa/include/spa/param/video/format.h new file mode 100644 index 0000000..0098bec --- /dev/null +++ b/spa/include/spa/param/video/format.h @@ -0,0 +1,61 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PARAM_VIDEO_FORMAT_H +#define SPA_PARAM_VIDEO_FORMAT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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..c105965 --- /dev/null +++ b/spa/include/spa/param/video/h264-utils.h @@ -0,0 +1,90 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_H264_UTILS_H +#define SPA_VIDEO_H264_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +static inline int +spa_format_video_h264_parse(const struct spa_pod *format, + struct spa_video_info_h264 *info) +{ + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate), + SPA_FORMAT_VIDEO_H264_streamFormat, SPA_POD_OPT_Id(&info->stream_format), + SPA_FORMAT_VIDEO_H264_alignment, SPA_POD_OPT_Id(&info->alignment)); +} + +static inline struct spa_pod * +spa_format_video_h264_build(struct spa_pod_builder *builder, uint32_t id, + struct spa_video_info_h264 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_h264), + 0); + if (info->size.width != 0 && info->size.height != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0); + if (info->framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0); + if (info->max_framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0); + if (info->stream_format != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_H264_streamFormat, SPA_POD_Id(info->stream_format), 0); + if (info->alignment != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_H264_alignment, SPA_POD_Id(info->alignment), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_H264_UTILS_H */ diff --git a/spa/include/spa/param/video/h264.h b/spa/include/spa/param/video/h264.h new file mode 100644 index 0000000..a33dda7 --- /dev/null +++ b/spa/include/spa/param/video/h264.h @@ -0,0 +1,68 @@ +/* Simple Plugin API + * + * Copyright © 2023 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_H264_H +#define SPA_VIDEO_H264_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +enum spa_h264_stream_format { + SPA_H264_STREAM_FORMAT_UNKNOWN = 0, + SPA_H264_STREAM_FORMAT_AVC, + SPA_H264_STREAM_FORMAT_AVC3, + SPA_H264_STREAM_FORMAT_BYTESTREAM +}; + +enum spa_h264_alignment { + SPA_H264_ALIGNMENT_UNKNOWN = 0, + SPA_H264_ALIGNMENT_AU, + SPA_H264_ALIGNMENT_NAL +}; + +struct spa_video_info_h264 { + struct spa_rectangle size; + struct spa_fraction framerate; + struct spa_fraction max_framerate; + enum spa_h264_stream_format stream_format; + enum spa_h264_alignment alignment; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_H264_H */ diff --git a/spa/include/spa/param/video/mjpg-utils.h b/spa/include/spa/param/video/mjpg-utils.h new file mode 100644 index 0000000..59b3965 --- /dev/null +++ b/spa/include/spa/param/video/mjpg-utils.h @@ -0,0 +1,82 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_MJPG_UTILS_H +#define SPA_VIDEO_MJPG_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +static inline int +spa_format_video_mjpg_parse(const struct spa_pod *format, + struct spa_video_info_mjpg *info) +{ + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate)); +} + +static inline struct spa_pod * +spa_format_video_mjpg_build(struct spa_pod_builder *builder, uint32_t id, + struct spa_video_info_mjpg *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg), + 0); + if (info->size.width != 0 && info->size.height != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0); + if (info->framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0); + if (info->max_framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_MJPG_UTILS_H */ diff --git a/spa/include/spa/param/video/mjpg.h b/spa/include/spa/param/video/mjpg.h new file mode 100644 index 0000000..d5d91d4 --- /dev/null +++ b/spa/include/spa/param/video/mjpg.h @@ -0,0 +1,53 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_MJPG_H +#define SPA_VIDEO_MJPG_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +struct spa_video_info_mjpg { + struct spa_rectangle size; + struct spa_fraction framerate; + struct spa_fraction max_framerate; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_MJPG_H */ diff --git a/spa/include/spa/param/video/multiview.h b/spa/include/spa/param/video/multiview.h new file mode 100644 index 0000000..ea16da8 --- /dev/null +++ b/spa/include/spa/param/video/multiview.h @@ -0,0 +1,134 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_MULTIVIEW_H +#define SPA_VIDEO_MULTIVIEW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** + * All possible stereoscopic 3D and multiview representations. + * In conjunction with \ref spa_video_multiview_flags, describes how + * multiview content is being transported in the stream. + */ +enum spa_video_multiview_mode { + /** A special value indicating no multiview information. Used in spa_video_info and other + * places to indicate that no specific multiview handling has been requested or provided. + * This value is never carried on caps. */ + SPA_VIDEO_MULTIVIEW_MODE_NONE = -1, + SPA_VIDEO_MULTIVIEW_MODE_MONO = 0, /**< All frames are monoscopic */ + /* Single view modes */ + SPA_VIDEO_MULTIVIEW_MODE_LEFT, /**< All frames represent a left-eye view */ + SPA_VIDEO_MULTIVIEW_MODE_RIGHT, /**< All frames represent a right-eye view */ + /* Stereo view modes */ + SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE, /**< Left and right eye views are provided + * in the left and right half of the frame + * respectively. */ + SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE_QUINCUNX, /**< Left and right eye views are provided + * in the left and right half of the + * frame, but have been sampled using + * quincunx method, with half-pixel offset + * between the 2 views. */ + SPA_VIDEO_MULTIVIEW_MODE_COLUMN_INTERLEAVED, /**< Alternating vertical columns of pixels + * represent the left and right eye view + * respectively. */ + SPA_VIDEO_MULTIVIEW_MODE_ROW_INTERLEAVED, /**< Alternating horizontal rows of pixels + * represent the left and right eye view + * respectively. */ + SPA_VIDEO_MULTIVIEW_MODE_TOP_BOTTOM, /**< The top half of the frame contains the + * left eye, and the bottom half the right + * eye. */ + SPA_VIDEO_MULTIVIEW_MODE_CHECKERBOARD, /**< Pixels are arranged with alternating + * pixels representing left and right eye + * views in a checkerboard fashion. */ + /* Padding for new frame packing modes */ + + SPA_VIDEO_MULTIVIEW_MODE_FRAME_BY_FRAME = 32, /**< Left and right eye views are provided + * in separate frames alternately. */ + /* Multiview mode(s) */ + SPA_VIDEO_MULTIVIEW_MODE_MULTIVIEW_FRAME_BY_FRAME, /**< Multipleindependent views are + * provided in separate frames in + * sequence. This method only applies to + * raw video buffers at the moment. + * Specific view identification is via + * \ref spa_video_multiview_meta on raw + * video buffers. */ + SPA_VIDEO_MULTIVIEW_MODE_SEPARATED, /**< Multiple views are provided as separate + * \ref spa_data framebuffers attached + * to each \ref spa_buffer, described + * by the \ref spa_video_multiview_meta */ + /* future expansion for annotated modes */ +}; + +/** + * spa_video_multiview_flags are used to indicate extra properties of a + * stereo/multiview stream beyond the frame layout and buffer mapping + * that is conveyed in the \ref spa_video_multiview_mode. + */ +enum spa_video_multiview_flags { + SPA_VIDEO_MULTIVIEW_FLAGS_NONE = 0, /**< No flags */ + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_VIEW_FIRST = (1 << 0), /**< For stereo streams, the normal arrangement + * of left and right views is reversed */ + SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLIPPED = (1 << 1), /**< The left view is vertically mirrored */ + SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLOPPED = (1 << 2), /**< The left view is horizontally mirrored */ + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLIPPED = (1 << 3), /**< The right view is vertically mirrored */ + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLOPPED = (1 << 4), /**< The right view is horizontally mirrored */ + SPA_VIDEO_MULTIVIEW_FLAGS_HALF_ASPECT = (1 << 14), /**< For frame-packed multiview + * modes, indicates that the individual + * views have been encoded with half the true + * width or height and should be scaled back + * up for display. This flag is used for + * overriding input layout interpretation + * by adjusting pixel-aspect-ratio. + * For side-by-side, column interleaved or + * checkerboard packings, the + * pixel width will be doubled. + * For row interleaved and + * top-bottom encodings, pixel height will + * be doubled */ + SPA_VIDEO_MULTIVIEW_FLAGS_MIXED_MONO = (1 << 15), /**< The video stream contains both + * mono and multiview portions, + * signalled on each buffer by the + * absence or presence of the + * \ref SPA_VIDEO_BUFFER_FLAG_MULTIPLE_VIEW + * buffer flag. */ +}; + + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_MULTIVIEW_H */ diff --git a/spa/include/spa/param/video/raw-types.h b/spa/include/spa/param/video/raw-types.h new file mode 100644 index 0000000..45fc230 --- /dev/null +++ b/spa/include/spa/param/video/raw-types.h @@ -0,0 +1,164 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_RAW_TYPES_H +#define SPA_VIDEO_RAW_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ +#include +#include + +#define SPA_TYPE_INFO_VideoFormat SPA_TYPE_INFO_ENUM_BASE "VideoFormat" +#define SPA_TYPE_INFO_VIDEO_FORMAT_BASE SPA_TYPE_INFO_VideoFormat ":" + +static const struct spa_type_info spa_type_video_format[] = { + { SPA_VIDEO_FORMAT_ENCODED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "encoded", NULL }, + { SPA_VIDEO_FORMAT_I420, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", NULL }, + { SPA_VIDEO_FORMAT_YV12, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", NULL }, + { SPA_VIDEO_FORMAT_YUY2, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", NULL }, + { SPA_VIDEO_FORMAT_UYVY, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVY", NULL }, + { SPA_VIDEO_FORMAT_AYUV, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV", NULL }, + { SPA_VIDEO_FORMAT_RGBx, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx", NULL }, + { SPA_VIDEO_FORMAT_BGRx, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx", NULL }, + { SPA_VIDEO_FORMAT_xRGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB", NULL }, + { SPA_VIDEO_FORMAT_xBGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR", NULL }, + { SPA_VIDEO_FORMAT_RGBA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA", NULL }, + { SPA_VIDEO_FORMAT_BGRA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA", NULL }, + { SPA_VIDEO_FORMAT_ARGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB", NULL }, + { SPA_VIDEO_FORMAT_ABGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR", NULL }, + { SPA_VIDEO_FORMAT_RGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB", NULL }, + { SPA_VIDEO_FORMAT_BGR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR", NULL }, + { SPA_VIDEO_FORMAT_Y41B, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y41B", NULL }, + { SPA_VIDEO_FORMAT_Y42B, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y42B", NULL }, + { SPA_VIDEO_FORMAT_YVYU, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVYU", NULL }, + { SPA_VIDEO_FORMAT_Y444, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444", NULL }, + { SPA_VIDEO_FORMAT_v210, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v210", NULL }, + { SPA_VIDEO_FORMAT_v216, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v216", NULL }, + { SPA_VIDEO_FORMAT_NV12, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12", NULL }, + { SPA_VIDEO_FORMAT_NV21, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV21", NULL }, + { SPA_VIDEO_FORMAT_GRAY8, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY8", NULL }, + { SPA_VIDEO_FORMAT_GRAY16_BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_BE", NULL }, + { SPA_VIDEO_FORMAT_GRAY16_LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_LE", NULL }, + { SPA_VIDEO_FORMAT_v308, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v308", NULL }, + { SPA_VIDEO_FORMAT_RGB16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB16", NULL }, + { SPA_VIDEO_FORMAT_BGR16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR16", NULL }, + { SPA_VIDEO_FORMAT_RGB15, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB15", NULL }, + { SPA_VIDEO_FORMAT_BGR15, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR15", NULL }, + { SPA_VIDEO_FORMAT_UYVP, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVP", NULL }, + { SPA_VIDEO_FORMAT_A420, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420", NULL }, + { SPA_VIDEO_FORMAT_RGB8P, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB8P", NULL }, + { SPA_VIDEO_FORMAT_YUV9, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUV9", NULL }, + { SPA_VIDEO_FORMAT_YVU9, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVU9", NULL }, + { SPA_VIDEO_FORMAT_IYU1, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU1", NULL }, + { SPA_VIDEO_FORMAT_ARGB64, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB64", NULL }, + { SPA_VIDEO_FORMAT_AYUV64, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV64", NULL }, + { SPA_VIDEO_FORMAT_r210, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "r210", NULL }, + { SPA_VIDEO_FORMAT_I420_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10BE", NULL }, + { SPA_VIDEO_FORMAT_I420_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10LE", NULL }, + { SPA_VIDEO_FORMAT_I422_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10BE", NULL }, + { SPA_VIDEO_FORMAT_I422_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10LE", NULL }, + { SPA_VIDEO_FORMAT_Y444_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10BE", NULL }, + { SPA_VIDEO_FORMAT_Y444_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10LE", NULL }, + { SPA_VIDEO_FORMAT_GBR, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR", NULL }, + { SPA_VIDEO_FORMAT_GBR_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10BE", NULL }, + { SPA_VIDEO_FORMAT_GBR_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10LE", NULL }, + { SPA_VIDEO_FORMAT_NV16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV16", NULL }, + { SPA_VIDEO_FORMAT_NV24, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV24", NULL }, + { SPA_VIDEO_FORMAT_NV12_64Z32, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12_64Z32", NULL }, + { SPA_VIDEO_FORMAT_A420_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10BE", NULL }, + { SPA_VIDEO_FORMAT_A420_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10LE", NULL }, + { SPA_VIDEO_FORMAT_A422_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10BE", NULL }, + { SPA_VIDEO_FORMAT_A422_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10LE", NULL }, + { SPA_VIDEO_FORMAT_A444_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10BE", NULL }, + { SPA_VIDEO_FORMAT_A444_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10LE", NULL }, + { SPA_VIDEO_FORMAT_NV61, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV61", NULL }, + { SPA_VIDEO_FORMAT_P010_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10BE", NULL }, + { SPA_VIDEO_FORMAT_P010_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10LE", NULL }, + { SPA_VIDEO_FORMAT_IYU2, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU2", NULL }, + { SPA_VIDEO_FORMAT_VYUY, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "VYUY", NULL }, + { SPA_VIDEO_FORMAT_GBRA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA", NULL }, + { SPA_VIDEO_FORMAT_GBRA_10BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10BE", NULL }, + { SPA_VIDEO_FORMAT_GBRA_10LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10LE", NULL }, + { SPA_VIDEO_FORMAT_GBR_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12BE", NULL }, + { SPA_VIDEO_FORMAT_GBR_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12LE", NULL }, + { SPA_VIDEO_FORMAT_GBRA_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12BE", NULL }, + { SPA_VIDEO_FORMAT_GBRA_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12LE", NULL }, + { SPA_VIDEO_FORMAT_I420_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12BE", NULL }, + { SPA_VIDEO_FORMAT_I420_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12LE", NULL }, + { SPA_VIDEO_FORMAT_I422_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12BE", NULL }, + { SPA_VIDEO_FORMAT_I422_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12LE", NULL }, + { SPA_VIDEO_FORMAT_Y444_12BE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12BE", NULL }, + { SPA_VIDEO_FORMAT_Y444_12LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12LE", NULL }, + { SPA_VIDEO_FORMAT_RGBA_F16, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_F16", NULL }, + { SPA_VIDEO_FORMAT_RGBA_F32, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_F32", NULL }, + { SPA_VIDEO_FORMAT_xRGB_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB_210LE", NULL }, + { SPA_VIDEO_FORMAT_xBGR_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR_210LE", NULL }, + { SPA_VIDEO_FORMAT_RGBx_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx_102LE", NULL }, + { SPA_VIDEO_FORMAT_BGRx_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx_102LE", NULL }, + { SPA_VIDEO_FORMAT_ARGB_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB_210LE", NULL }, + { SPA_VIDEO_FORMAT_ABGR_210LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR_210LE", NULL }, + { SPA_VIDEO_FORMAT_RGBA_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_102LE", NULL }, + { SPA_VIDEO_FORMAT_BGRA_102LE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA_102LE", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_VideoFlags SPA_TYPE_INFO_FLAGS_BASE "VideoFlags" +#define SPA_TYPE_INFO_VIDEO_FLAGS_BASE SPA_TYPE_INFO_VideoFlags ":" + +static const struct spa_type_info spa_type_video_flags[] = { + + { SPA_VIDEO_FLAG_NONE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "none", NULL }, + { SPA_VIDEO_FLAG_VARIABLE_FPS, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "variable-fps", NULL }, + { SPA_VIDEO_FLAG_PREMULTIPLIED_ALPHA, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "premultiplied-alpha", NULL }, + { SPA_VIDEO_FLAG_MODIFIER, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_FLAGS_BASE "modifier", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_VideoInterlaceMode SPA_TYPE_INFO_ENUM_BASE "VideoInterlaceMode" +#define SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE SPA_TYPE_INFO_VideoInterlaceMode ":" + +static const struct spa_type_info spa_type_video_interlace_mode[] = { + { SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "progressive", NULL }, + { SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "interleaved", NULL }, + { SPA_VIDEO_INTERLACE_MODE_MIXED, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "mixed", NULL }, + { SPA_VIDEO_INTERLACE_MODE_FIELDS, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_INTERLACE_MODE_BASE "fields", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_RAW_TYPES_H */ diff --git a/spa/include/spa/param/video/raw-utils.h b/spa/include/spa/param/video/raw-utils.h new file mode 100644 index 0000000..f48fcb9 --- /dev/null +++ b/spa/include/spa/param/video/raw-utils.h @@ -0,0 +1,135 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_RAW_UTILS_H +#define SPA_VIDEO_RAW_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +static inline int +spa_format_video_raw_parse(const struct spa_pod *format, + struct spa_video_info_raw *info) +{ + info->flags = SPA_VIDEO_FLAG_NONE; + if (spa_pod_find_prop (format, NULL, SPA_FORMAT_VIDEO_modifier)) { + info->flags |= SPA_VIDEO_FLAG_MODIFIER; + } + + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_format, SPA_POD_OPT_Id(&info->format), + SPA_FORMAT_VIDEO_modifier, SPA_POD_OPT_Long(&info->modifier), + SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate), + SPA_FORMAT_VIDEO_views, SPA_POD_OPT_Int(&info->views), + SPA_FORMAT_VIDEO_interlaceMode, SPA_POD_OPT_Id(&info->interlace_mode), + SPA_FORMAT_VIDEO_pixelAspectRatio, SPA_POD_OPT_Fraction(&info->pixel_aspect_ratio), + SPA_FORMAT_VIDEO_multiviewMode, SPA_POD_OPT_Id(&info->multiview_mode), + SPA_FORMAT_VIDEO_multiviewFlags, SPA_POD_OPT_Id(&info->multiview_flags), + SPA_FORMAT_VIDEO_chromaSite, SPA_POD_OPT_Id(&info->chroma_site), + SPA_FORMAT_VIDEO_colorRange, SPA_POD_OPT_Id(&info->color_range), + SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_OPT_Id(&info->color_matrix), + SPA_FORMAT_VIDEO_transferFunction, SPA_POD_OPT_Id(&info->transfer_function), + SPA_FORMAT_VIDEO_colorPrimaries, SPA_POD_OPT_Id(&info->color_primaries)); +} + +static inline struct spa_pod * +spa_format_video_raw_build(struct spa_pod_builder *builder, uint32_t id, + struct spa_video_info_raw *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + 0); + if (info->format != SPA_VIDEO_FORMAT_UNKNOWN) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_format, SPA_POD_Id(info->format), 0); + if (info->size.width != 0 && info->size.height != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0); + if (info->framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0); + if (info->modifier != 0 || info->flags & SPA_VIDEO_FLAG_MODIFIER) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_modifier, SPA_POD_Long(info->modifier), 0); + if (info->max_framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(info->max_framerate), 0); + if (info->views != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_views, SPA_POD_Int(info->views), 0); + if (info->interlace_mode != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_interlaceMode, SPA_POD_Id(info->interlace_mode), 0); + if (info->pixel_aspect_ratio.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_pixelAspectRatio,SPA_POD_Fraction(info->pixel_aspect_ratio), 0); + if (info->multiview_mode != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_multiviewMode, SPA_POD_Id(info->multiview_mode), 0); + if (info->multiview_flags != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_multiviewFlags,SPA_POD_Id(info->multiview_flags), 0); + if (info->chroma_site != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_chromaSite, SPA_POD_Id(info->chroma_site), 0); + if (info->color_range != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_colorRange, SPA_POD_Id(info->color_range), 0); + if (info->color_matrix != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_Id(info->color_matrix), 0); + if (info->transfer_function != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_transferFunction,SPA_POD_Id(info->transfer_function), 0); + if (info->color_primaries != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_colorPrimaries,SPA_POD_Id(info->color_primaries), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_RAW_UTILS_H */ diff --git a/spa/include/spa/param/video/raw.h b/spa/include/spa/param/video/raw.h new file mode 100644 index 0000000..51c8827 --- /dev/null +++ b/spa/include/spa/param/video/raw.h @@ -0,0 +1,221 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_RAW_H +#define SPA_VIDEO_RAW_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#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 */ +}; + +/** + * The possible values of the #spa_video_interlace_mode describing the interlace + * mode of the stream. + */ +enum spa_video_interlace_mode { + SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE = 0, /**< all frames are progressive */ + SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, /**< 2 fields are interleaved in one video frame. + * Extra buffer flags describe the field order. */ + SPA_VIDEO_INTERLACE_MODE_MIXED, /**< frames contains both interlaced and progressive + * video, the buffer flags describe the frame and + * fields. */ + SPA_VIDEO_INTERLACE_MODE_FIELDS, /**< 2 fields are stored in one buffer, use the + * frame ID to get access to the required + * field. For multiview (the 'views' + * property > 1) the fields of view N can + * be found at frame ID (N * 2) and (N * + * 2) + 1. Each field has only half the + * amount of lines as noted in the height + * property. This mode requires multiple + * spa_data to describe the fields. */ +}; + +/** + */ +struct spa_video_info_raw { + enum spa_video_format format; /**< the format */ + uint32_t flags; /**< extra video flags */ + uint64_t modifier; /**< format modifier + * only used with DMA-BUF */ + struct spa_rectangle size; /**< the frame size of the video */ + struct spa_fraction framerate; /**< the framerate of the video, 0/1 means variable rate */ + struct spa_fraction max_framerate; /**< the maximum framerate of the video. This is only valid when + \ref framerate is 0/1 */ + uint32_t views; /**< the number of views in this video */ + enum spa_video_interlace_mode interlace_mode; /**< the interlace mode */ + struct spa_fraction pixel_aspect_ratio; /**< the pixel aspect ratio */ + enum spa_video_multiview_mode multiview_mode; /**< multiview mode */ + enum spa_video_multiview_flags multiview_flags; /**< multiview flags */ + enum spa_video_chroma_site chroma_site; /**< the chroma siting */ + enum spa_video_color_range color_range; /**< the color range. This is the valid range for the samples. + * It is used to convert the samples to Y'PbPr values. */ + enum spa_video_color_matrix color_matrix; /**< the color matrix. Used to convert between Y'PbPr and + * non-linear RGB (R'G'B') */ + enum spa_video_transfer_function transfer_function; /**< the transfer function. used to convert between R'G'B' and RGB */ + enum spa_video_color_primaries color_primaries; /**< color primaries. used to convert between R'G'B' and CIE XYZ */ +}; + +#define SPA_VIDEO_INFO_RAW_INIT(...) ((struct spa_video_info_raw) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_RAW_H */ diff --git a/spa/include/spa/param/video/type-info.h b/spa/include/spa/param/video/type-info.h new file mode 100644 index 0000000..0bc0f24 --- /dev/null +++ b/spa/include/spa/param/video/type-info.h @@ -0,0 +1,30 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_VIDEO_TYPES_H +#define SPA_VIDEO_TYPES_H + +#include + +#endif /* SPA_VIDEO_TYPES_H */ diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h new file mode 100644 index 0000000..860673a --- /dev/null +++ b/spa/include/spa/pod/builder.h @@ -0,0 +1,701 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_BUILDER_H +#define SPA_POD_BUILDER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_pod POD + * Binary data serialization format + */ + +/** + * \addtogroup spa_pod + * \{ + */ + +#include + +#include +#include +#include + +struct spa_pod_builder_state { + uint32_t offset; +#define SPA_POD_BUILDER_FLAG_BODY (1<<0) +#define SPA_POD_BUILDER_FLAG_FIRST (1<<1) + uint32_t flags; + struct spa_pod_frame *frame; +}; + +struct spa_pod_builder; + +struct spa_pod_builder_callbacks { +#define SPA_VERSION_POD_BUILDER_CALLBACKS 0 + uint32_t version; + + int (*overflow) (void *data, uint32_t size); +}; + +struct spa_pod_builder { + void *data; + uint32_t size; + uint32_t _padding; + struct spa_pod_builder_state state; + struct spa_callbacks callbacks; +}; + +#define SPA_POD_BUILDER_INIT(buffer,size) ((struct spa_pod_builder){ (buffer), (size), 0, {}, {} }) + +static inline void +spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builder_state *state) +{ + *state = builder->state; +} + +static inline void +spa_pod_builder_set_callbacks(struct spa_pod_builder *builder, + const struct spa_pod_builder_callbacks *callbacks, void *data) +{ + builder->callbacks = SPA_CALLBACKS_INIT(callbacks, data); +} + +static inline void +spa_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_state *state) +{ + struct spa_pod_frame *f; + uint32_t size = builder->state.offset - state->offset; + builder->state = *state; + for (f = builder->state.frame; f ; f = f->parent) + f->pod.size -= size; +} + +static inline void spa_pod_builder_init(struct spa_pod_builder *builder, void *data, uint32_t size) +{ + *builder = SPA_POD_BUILDER_INIT(data, size); +} + +static inline struct spa_pod * +spa_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset) +{ + uint32_t size = builder->size; + if (offset + 8 <= size) { + struct spa_pod *pod = SPA_PTROFF(builder->data, offset, struct spa_pod); + if (offset + SPA_POD_SIZE(pod) <= size) + return pod; + } + return NULL; +} + +static inline struct spa_pod * +spa_pod_builder_frame(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + if (frame->offset + SPA_POD_SIZE(&frame->pod) <= builder->size) + return SPA_PTROFF(builder->data, frame->offset, struct spa_pod); + return NULL; +} + +static inline void +spa_pod_builder_push(struct spa_pod_builder *builder, + struct spa_pod_frame *frame, + const struct spa_pod *pod, + uint32_t offset) +{ + frame->pod = *pod; + frame->offset = offset; + frame->parent = builder->state.frame; + frame->flags = builder->state.flags; + builder->state.frame = frame; + + if (frame->pod.type == SPA_TYPE_Array || frame->pod.type == SPA_TYPE_Choice) + builder->state.flags = SPA_POD_BUILDER_FLAG_FIRST | SPA_POD_BUILDER_FLAG_BODY; +} + +static inline int spa_pod_builder_raw(struct spa_pod_builder *builder, const void *data, uint32_t size) +{ + int res = 0; + struct spa_pod_frame *f; + uint32_t offset = builder->state.offset; + + if (offset + size > builder->size) { + res = -ENOSPC; + if (offset <= builder->size) + spa_callbacks_call_res(&builder->callbacks, + struct spa_pod_builder_callbacks, res, + overflow, 0, offset + size); + } + if (res == 0 && data) + memcpy(SPA_PTROFF(builder->data, offset, void), data, size); + + builder->state.offset += size; + + for (f = builder->state.frame; f ; f = f->parent) + f->pod.size += size; + + return res; +} + +static inline int spa_pod_builder_pad(struct spa_pod_builder *builder, uint32_t size) +{ + uint64_t zeroes = 0; + size = SPA_ROUND_UP_N(size, 8) - size; + return size ? spa_pod_builder_raw(builder, &zeroes, size) : 0; +} + +static inline int +spa_pod_builder_raw_padded(struct spa_pod_builder *builder, const void *data, uint32_t size) +{ + int r, res = spa_pod_builder_raw(builder, data, size); + if ((r = spa_pod_builder_pad(builder, size)) < 0) + res = r; + return res; +} + +static inline void *spa_pod_builder_pop(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + struct spa_pod *pod; + + if (SPA_FLAG_IS_SET(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST)) { + const struct spa_pod p = { 0, SPA_TYPE_None }; + spa_pod_builder_raw(builder, &p, sizeof(p)); + } + if ((pod = (struct spa_pod*)spa_pod_builder_frame(builder, frame)) != NULL) + *pod = frame->pod; + + builder->state.frame = frame->parent; + builder->state.flags = frame->flags; + spa_pod_builder_pad(builder, builder->state.offset); + return pod; +} + +static inline int +spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod *p) +{ + const void *data; + uint32_t size; + int r, res; + + if (builder->state.flags == SPA_POD_BUILDER_FLAG_BODY) { + data = SPA_POD_BODY_CONST(p); + size = SPA_POD_BODY_SIZE(p); + } else { + data = p; + size = SPA_POD_SIZE(p); + SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST); + } + res = spa_pod_builder_raw(builder, data, size); + if (builder->state.flags != SPA_POD_BUILDER_FLAG_BODY) + if ((r = spa_pod_builder_pad(builder, size)) < 0) + res = r; + return res; +} + +#define SPA_POD_INIT(size,type) ((struct spa_pod) { (size), (type) }) + +#define SPA_POD_INIT_None() SPA_POD_INIT(0, SPA_TYPE_None) + +static inline int spa_pod_builder_none(struct spa_pod_builder *builder) +{ + const struct spa_pod p = SPA_POD_INIT_None(); + return spa_pod_builder_primitive(builder, &p); +} + +static inline int spa_pod_builder_child(struct spa_pod_builder *builder, uint32_t size, uint32_t type) +{ + const struct spa_pod p = SPA_POD_INIT(size,type); + SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST); + return spa_pod_builder_raw(builder, &p, sizeof(p)); +} + +#define SPA_POD_INIT_Bool(val) ((struct spa_pod_bool){ { sizeof(uint32_t), SPA_TYPE_Bool }, (val) ? 1 : 0, 0 }) + +static inline int spa_pod_builder_bool(struct spa_pod_builder *builder, bool val) +{ + const struct spa_pod_bool p = SPA_POD_INIT_Bool(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Id(val) ((struct spa_pod_id){ { sizeof(uint32_t), SPA_TYPE_Id }, (val), 0 }) + +static inline int spa_pod_builder_id(struct spa_pod_builder *builder, uint32_t val) +{ + const struct spa_pod_id p = SPA_POD_INIT_Id(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Int(val) ((struct spa_pod_int){ { sizeof(int32_t), SPA_TYPE_Int }, (val), 0 }) + +static inline int spa_pod_builder_int(struct spa_pod_builder *builder, int32_t val) +{ + const struct spa_pod_int p = SPA_POD_INIT_Int(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Long(val) ((struct spa_pod_long){ { sizeof(int64_t), SPA_TYPE_Long }, (val) }) + +static inline int spa_pod_builder_long(struct spa_pod_builder *builder, int64_t val) +{ + const struct spa_pod_long p = SPA_POD_INIT_Long(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Float(val) ((struct spa_pod_float){ { sizeof(float), SPA_TYPE_Float }, (val), 0 }) + +static inline int spa_pod_builder_float(struct spa_pod_builder *builder, float val) +{ + const struct spa_pod_float p = SPA_POD_INIT_Float(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Double(val) ((struct spa_pod_double){ { sizeof(double), SPA_TYPE_Double }, (val) }) + +static inline int spa_pod_builder_double(struct spa_pod_builder *builder, double val) +{ + const struct spa_pod_double p = SPA_POD_INIT_Double(val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_String(len) ((struct spa_pod_string){ { (len), SPA_TYPE_String } }) + +static inline int +spa_pod_builder_write_string(struct spa_pod_builder *builder, const char *str, uint32_t len) +{ + int r, res; + res = spa_pod_builder_raw(builder, str, len); + if ((r = spa_pod_builder_raw(builder, "", 1)) < 0) + res = r; + if ((r = spa_pod_builder_pad(builder, builder->state.offset)) < 0) + res = r; + return res; +} + +static inline int +spa_pod_builder_string_len(struct spa_pod_builder *builder, const char *str, uint32_t len) +{ + const struct spa_pod_string p = SPA_POD_INIT_String(len+1); + int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); + if ((r = spa_pod_builder_write_string(builder, str, len)) < 0) + res = r; + return res; +} + +static inline int spa_pod_builder_string(struct spa_pod_builder *builder, const char *str) +{ + uint32_t len = str ? strlen(str) : 0; + return spa_pod_builder_string_len(builder, str ? str : "", len); +} + +#define SPA_POD_INIT_Bytes(len) ((struct spa_pod_bytes){ { (len), SPA_TYPE_Bytes } }) + +static inline int +spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len) +{ + const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(len); + int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); + if ((r = spa_pod_builder_raw_padded(builder, bytes, len)) < 0) + res = r; + return res; +} +static inline void * +spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len) +{ + uint32_t offset = builder->state.offset; + if (spa_pod_builder_bytes(builder, NULL, len) < 0) + return NULL; + return SPA_POD_BODY(spa_pod_builder_deref(builder, offset)); +} + +#define SPA_POD_INIT_Pointer(type,value) ((struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { (type), 0, (value) } }) + +static inline int +spa_pod_builder_pointer(struct spa_pod_builder *builder, uint32_t type, const void *val) +{ + const struct spa_pod_pointer p = SPA_POD_INIT_Pointer(type, val); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Fd(fd) ((struct spa_pod_fd){ { sizeof(int64_t), SPA_TYPE_Fd }, (fd) }) + +static inline int spa_pod_builder_fd(struct spa_pod_builder *builder, int64_t fd) +{ + const struct spa_pod_fd p = SPA_POD_INIT_Fd(fd); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Rectangle(val) ((struct spa_pod_rectangle){ { sizeof(struct spa_rectangle), SPA_TYPE_Rectangle }, (val) }) + +static inline int +spa_pod_builder_rectangle(struct spa_pod_builder *builder, uint32_t width, uint32_t height) +{ + const struct spa_pod_rectangle p = SPA_POD_INIT_Rectangle(SPA_RECTANGLE(width, height)); + return spa_pod_builder_primitive(builder, &p.pod); +} + +#define SPA_POD_INIT_Fraction(val) ((struct spa_pod_fraction){ { sizeof(struct spa_fraction), SPA_TYPE_Fraction }, (val) }) + +static inline int +spa_pod_builder_fraction(struct spa_pod_builder *builder, uint32_t num, uint32_t denom) +{ + const struct spa_pod_fraction p = SPA_POD_INIT_Fraction(SPA_FRACTION(num, denom)); + return spa_pod_builder_primitive(builder, &p.pod); +} + +static inline int +spa_pod_builder_push_array(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + const struct spa_pod_array p = + { {sizeof(struct spa_pod_array_body) - sizeof(struct spa_pod), SPA_TYPE_Array}, + {{0, 0}} }; + uint32_t offset = builder->state.offset; + int res = spa_pod_builder_raw(builder, &p, sizeof(p) - sizeof(struct spa_pod)); + spa_pod_builder_push(builder, frame, &p.pod, offset); + return res; +} + +static inline int +spa_pod_builder_array(struct spa_pod_builder *builder, + uint32_t child_size, uint32_t child_type, uint32_t n_elems, const void *elems) +{ + const struct spa_pod_array p = { + {(uint32_t)(sizeof(struct spa_pod_array_body) + n_elems * child_size), SPA_TYPE_Array}, + {{child_size, child_type}} + }; + int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); + if ((r = spa_pod_builder_raw_padded(builder, elems, child_size * n_elems)) < 0) + res = r; + return res; +} + +#define SPA_POD_INIT_CHOICE_BODY(type, flags, child_size, child_type) \ + ((struct spa_pod_choice_body) { (type), (flags), { (child_size), (child_type) }}) + +#define SPA_POD_INIT_Choice(type, ctype, child_type, n_vals, ...) \ + ((struct { struct spa_pod_choice choice; ctype vals[(n_vals)];}) \ + { { { (n_vals) * sizeof(ctype) + sizeof(struct spa_pod_choice_body), SPA_TYPE_Choice }, \ + { (type), 0, { sizeof(ctype), (child_type) } } }, { __VA_ARGS__ } }) + +static inline int +spa_pod_builder_push_choice(struct spa_pod_builder *builder, struct spa_pod_frame *frame, + uint32_t type, uint32_t flags) +{ + const struct spa_pod_choice p = + { {sizeof(struct spa_pod_choice_body) - sizeof(struct spa_pod), SPA_TYPE_Choice}, + { type, flags, {0, 0}} }; + uint32_t offset = builder->state.offset; + int res = spa_pod_builder_raw(builder, &p, sizeof(p) - sizeof(struct spa_pod)); + spa_pod_builder_push(builder, frame, &p.pod, offset); + return res; +} + +#define SPA_POD_INIT_Struct(size) ((struct spa_pod_struct){ { (size), SPA_TYPE_Struct } }) + +static inline int +spa_pod_builder_push_struct(struct spa_pod_builder *builder, struct spa_pod_frame *frame) +{ + const struct spa_pod_struct p = SPA_POD_INIT_Struct(0); + uint32_t offset = builder->state.offset; + int res = spa_pod_builder_raw(builder, &p, sizeof(p)); + spa_pod_builder_push(builder, frame, &p.pod, offset); + return res; +} + +#define SPA_POD_INIT_Object(size,type,id,...) ((struct spa_pod_object){ { (size), SPA_TYPE_Object }, { (type), (id) }, ##__VA_ARGS__ }) + +static inline int +spa_pod_builder_push_object(struct spa_pod_builder *builder, struct spa_pod_frame *frame, + uint32_t type, uint32_t id) +{ + const struct spa_pod_object p = + SPA_POD_INIT_Object(sizeof(struct spa_pod_object_body), type, id); + uint32_t offset = builder->state.offset; + int res = spa_pod_builder_raw(builder, &p, sizeof(p)); + spa_pod_builder_push(builder, frame, &p.pod, offset); + return res; +} + +#define SPA_POD_INIT_Prop(key,flags,size,type) \ + ((struct spa_pod_prop){ (key), (flags), { (size), (type) } }) + +static inline int +spa_pod_builder_prop(struct spa_pod_builder *builder, uint32_t key, uint32_t flags) +{ + const struct { uint32_t key; uint32_t flags; } p = { key, flags }; + return spa_pod_builder_raw(builder, &p, sizeof(p)); +} + +#define SPA_POD_INIT_Sequence(size,unit) \ + ((struct spa_pod_sequence){ { (size), SPA_TYPE_Sequence}, {(unit), 0 } }) + +static inline int +spa_pod_builder_push_sequence(struct spa_pod_builder *builder, struct spa_pod_frame *frame, uint32_t unit) +{ + const struct spa_pod_sequence p = + SPA_POD_INIT_Sequence(sizeof(struct spa_pod_sequence_body), unit); + uint32_t offset = builder->state.offset; + int res = spa_pod_builder_raw(builder, &p, sizeof(p)); + spa_pod_builder_push(builder, frame, &p.pod, offset); + return res; +} + +static inline uint32_t +spa_pod_builder_control(struct spa_pod_builder *builder, uint32_t offset, uint32_t type) +{ + const struct { uint32_t offset; uint32_t type; } p = { offset, type }; + return spa_pod_builder_raw(builder, &p, sizeof(p)); +} + +static inline uint32_t spa_choice_from_id(char id) +{ + switch (id) { + case 'r': + return SPA_CHOICE_Range; + case 's': + return SPA_CHOICE_Step; + case 'e': + return SPA_CHOICE_Enum; + case 'f': + return SPA_CHOICE_Flags; + case 'n': + default: + return SPA_CHOICE_None; + } +} + +#define SPA_POD_BUILDER_COLLECT(builder,type,args) \ +do { \ + switch (type) { \ + case 'b': \ + spa_pod_builder_bool(builder, !!va_arg(args, int)); \ + break; \ + case 'I': \ + spa_pod_builder_id(builder, va_arg(args, uint32_t)); \ + break; \ + case 'i': \ + spa_pod_builder_int(builder, va_arg(args, int)); \ + break; \ + case 'l': \ + spa_pod_builder_long(builder, va_arg(args, int64_t)); \ + break; \ + case 'f': \ + spa_pod_builder_float(builder, va_arg(args, double)); \ + break; \ + case 'd': \ + spa_pod_builder_double(builder, va_arg(args, double)); \ + break; \ + case 's': \ + { \ + char *strval = va_arg(args, char *); \ + if (strval != NULL) { \ + size_t len = strlen(strval); \ + spa_pod_builder_string_len(builder, strval, len); \ + } \ + else \ + spa_pod_builder_none(builder); \ + break; \ + } \ + case 'S': \ + { \ + char *strval = va_arg(args, char *); \ + size_t len = va_arg(args, int); \ + spa_pod_builder_string_len(builder, strval, len); \ + break; \ + } \ + case 'y': \ + { \ + void *ptr = va_arg(args, void *); \ + int len = va_arg(args, int); \ + spa_pod_builder_bytes(builder, ptr, len); \ + break; \ + } \ + case 'R': \ + { \ + struct spa_rectangle *rectval = \ + va_arg(args, struct spa_rectangle *); \ + spa_pod_builder_rectangle(builder, \ + rectval->width, rectval->height); \ + break; \ + } \ + case 'F': \ + { \ + struct spa_fraction *fracval = \ + va_arg(args, struct spa_fraction *); \ + spa_pod_builder_fraction(builder, fracval->num, fracval->denom);\ + break; \ + } \ + case 'a': \ + { \ + int child_size = va_arg(args, int); \ + int child_type = va_arg(args, int); \ + int n_elems = va_arg(args, int); \ + void *elems = va_arg(args, void *); \ + spa_pod_builder_array(builder, child_size, \ + child_type, n_elems, elems); \ + break; \ + } \ + case 'p': \ + { \ + int t = va_arg(args, uint32_t); \ + spa_pod_builder_pointer(builder, t, va_arg(args, void *)); \ + break; \ + } \ + case 'h': \ + spa_pod_builder_fd(builder, va_arg(args, int)); \ + break; \ + case 'P': \ + case 'O': \ + case 'T': \ + case 'V': \ + { \ + struct spa_pod *pod = va_arg(args, struct spa_pod *); \ + if (pod == NULL) \ + spa_pod_builder_none(builder); \ + else \ + spa_pod_builder_primitive(builder, pod); \ + break; \ + } \ + } \ +} while(false) + +static inline int +spa_pod_builder_addv(struct spa_pod_builder *builder, va_list args) +{ + int res = 0; + struct spa_pod_frame *frame = builder->state.frame; + uint32_t ftype = frame ? frame->pod.type : (uint32_t)SPA_TYPE_None; + + do { + const char *format; + int n_values = 1; + struct spa_pod_frame f; + bool choice; + + switch (ftype) { + case SPA_TYPE_Object: + { + uint32_t key = va_arg(args, uint32_t); + if (key == 0) + goto exit; + spa_pod_builder_prop(builder, key, 0); + break; + } + case SPA_TYPE_Sequence: + { + uint32_t offset = va_arg(args, uint32_t); + uint32_t type = va_arg(args, uint32_t); + if (type == 0) + goto exit; + spa_pod_builder_control(builder, offset, type); + SPA_FALLTHROUGH + } + default: + break; + } + if ((format = va_arg(args, const char *)) == NULL) + break; + + choice = *format == '?'; + if (choice) { + uint32_t type = spa_choice_from_id(*++format); + if (*format != '\0') + format++; + + spa_pod_builder_push_choice(builder, &f, type, 0); + + n_values = va_arg(args, int); + } + while (n_values-- > 0) + SPA_POD_BUILDER_COLLECT(builder, *format, args); + + if (choice) + spa_pod_builder_pop(builder, &f); + } while (true); + + exit: + return res; +} + +static inline int spa_pod_builder_add(struct spa_pod_builder *builder, ...) +{ + int res; + va_list args; + + va_start(args, builder); + res = spa_pod_builder_addv(builder, args); + va_end(args); + + return res; +} + +#define spa_pod_builder_add_object(b,type,id,...) \ +({ \ + struct spa_pod_builder *_b = (b); \ + struct spa_pod_frame _f; \ + spa_pod_builder_push_object(_b, &_f, type, id); \ + spa_pod_builder_add(_b, ##__VA_ARGS__, 0); \ + spa_pod_builder_pop(_b, &_f); \ +}) + +#define spa_pod_builder_add_struct(b,...) \ +({ \ + struct spa_pod_builder *_b = (b); \ + struct spa_pod_frame _f; \ + spa_pod_builder_push_struct(_b, &_f); \ + spa_pod_builder_add(_b, ##__VA_ARGS__, NULL); \ + spa_pod_builder_pop(_b, &_f); \ +}) + +#define spa_pod_builder_add_sequence(b,unit,...) \ +({ \ + struct spa_pod_builder *_b = (b); \ + struct spa_pod_frame _f; \ + spa_pod_builder_push_sequence(_b, &_f, unit); \ + spa_pod_builder_add(_b, ##__VA_ARGS__, 0, 0); \ + spa_pod_builder_pop(_b, &_f); \ +}) + +/** Copy a pod structure */ +static inline struct spa_pod * +spa_pod_copy(const struct spa_pod *pod) +{ + size_t size; + struct spa_pod *c; + + size = SPA_POD_SIZE(pod); + if ((c = (struct spa_pod *) malloc(size)) == NULL) + return NULL; + return (struct spa_pod *) memcpy(c, pod, size); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_BUILDER_H */ diff --git a/spa/include/spa/pod/command.h b/spa/include/spa/pod/command.h new file mode 100644 index 0000000..3cd30a0 --- /dev/null +++ b/spa/include/spa/pod/command.h @@ -0,0 +1,69 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_COMMAND_H +#define SPA_COMMAND_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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..3fd9d00 --- /dev/null +++ b/spa/include/spa/pod/compare.h @@ -0,0 +1,190 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_COMPARE_H +#define SPA_POD_COMPARE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * \addtogroup spa_pod + * \{ + */ + +static inline int spa_pod_compare_value(uint32_t type, const void *r1, const void *r2, uint32_t size) +{ + switch (type) { + case SPA_TYPE_None: + return 0; + case SPA_TYPE_Bool: + case SPA_TYPE_Id: + return *(uint32_t *) r1 == *(uint32_t *) r2 ? 0 : 1; + case SPA_TYPE_Int: + return *(int32_t *) r1 - *(int32_t *) r2; + case SPA_TYPE_Long: + return *(int64_t *) r1 - *(int64_t *) r2; + case SPA_TYPE_Float: + return *(float *) r1 - *(float *) r2; + case SPA_TYPE_Double: + return *(double *) r1 - *(double *) r2; + case SPA_TYPE_String: + return strcmp((char *)r1, (char *)r2); + case SPA_TYPE_Bytes: + return memcmp((char *)r1, (char *)r2, size); + case SPA_TYPE_Rectangle: + { + const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1, + *rec2 = (struct spa_rectangle *) r2; + if (rec1->width == rec2->width && rec1->height == rec2->height) + return 0; + else if (rec1->width < rec2->width || rec1->height < rec2->height) + return -1; + else + return 1; + } + case SPA_TYPE_Fraction: + { + const struct spa_fraction *f1 = (struct spa_fraction *) r1, + *f2 = (struct spa_fraction *) r2; + int64_t n1, n2; + n1 = ((int64_t) f1->num) * f2->denom; + n2 = ((int64_t) f2->num) * f1->denom; + if (n1 < n2) + return -1; + else if (n1 > n2) + return 1; + else + return 0; + } + default: + break; + } + return 0; +} + +static inline int spa_pod_compare(const struct spa_pod *pod1, + const struct spa_pod *pod2) +{ + int res = 0; + uint32_t n_vals1, n_vals2; + uint32_t choice1, choice2; + + spa_return_val_if_fail(pod1 != NULL, -EINVAL); + spa_return_val_if_fail(pod2 != NULL, -EINVAL); + + pod1 = spa_pod_get_values(pod1, &n_vals1, &choice1); + pod2 = spa_pod_get_values(pod2, &n_vals2, &choice2); + + if (n_vals1 != n_vals2) + return -EINVAL; + + if (SPA_POD_TYPE(pod1) != SPA_POD_TYPE(pod2)) + return -EINVAL; + + switch (SPA_POD_TYPE(pod1)) { + case SPA_TYPE_Struct: + { + const struct spa_pod *p1, *p2; + size_t p1s, p2s; + + p1 = (const struct spa_pod*)SPA_POD_BODY_CONST(pod1); + p1s = SPA_POD_BODY_SIZE(pod1); + p2 = (const struct spa_pod*)SPA_POD_BODY_CONST(pod2); + p2s = SPA_POD_BODY_SIZE(pod2); + + while (true) { + if (!spa_pod_is_inside(pod1, p1s, p1) || + !spa_pod_is_inside(pod2, p2s, p2)) + return -EINVAL; + + if ((res = spa_pod_compare(p1, p2)) != 0) + return res; + + p1 = (const struct spa_pod*)spa_pod_next(p1); + p2 = (const struct spa_pod*)spa_pod_next(p2); + } + break; + } + case SPA_TYPE_Object: + { + const struct spa_pod_prop *p1, *p2; + const struct spa_pod_object *o1, *o2; + + o1 = (const struct spa_pod_object*)pod1; + o2 = (const struct spa_pod_object*)pod2; + + p2 = NULL; + SPA_POD_OBJECT_FOREACH(o1, p1) { + if ((p2 = spa_pod_object_find_prop(o2, p2, p1->key)) == NULL) + return 1; + if ((res = spa_pod_compare(&p1->value, &p2->value)) != 0) + return res; + } + p1 = NULL; + SPA_POD_OBJECT_FOREACH(o2, p2) { + if ((p1 = spa_pod_object_find_prop(o1, p1, p2->key)) == NULL) + return -1; + } + break; + } + case SPA_TYPE_Array: + { + if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2)) + return -EINVAL; + res = memcmp(SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), SPA_POD_BODY_SIZE(pod2)); + break; + } + default: + if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2)) + return -EINVAL; + res = spa_pod_compare_value(SPA_POD_TYPE(pod1), + SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), + SPA_POD_BODY_SIZE(pod1)); + break; + } + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/spa/include/spa/pod/dynamic.h b/spa/include/spa/pod/dynamic.h new file mode 100644 index 0000000..1fe77be --- /dev/null +++ b/spa/include/spa/pod/dynamic.h @@ -0,0 +1,81 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_DYNAMIC_H +#define SPA_POD_DYNAMIC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct spa_pod_dynamic_builder { + struct spa_pod_builder b; + void *data; + uint32_t extend; + uint32_t _padding; +}; + +static int spa_pod_dynamic_builder_overflow(void *data, uint32_t size) +{ + struct spa_pod_dynamic_builder *d = (struct spa_pod_dynamic_builder*)data; + int32_t old_size = d->b.size; + int32_t new_size = SPA_ROUND_UP_N(size, d->extend); + void *old_data = d->b.data; + + if (old_data == d->data) + d->b.data = NULL; + if ((d->b.data = realloc(d->b.data, new_size)) == NULL) + return -errno; + if (old_data == d->data && d->b.data != old_data && old_size > 0) + memcpy(d->b.data, old_data, old_size); + d->b.size = new_size; + return 0; +} + +static inline void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_builder *builder, + void *data, uint32_t size, uint32_t extend) +{ + static const struct spa_pod_builder_callbacks spa_pod_dynamic_builder_callbacks = { + SPA_VERSION_POD_BUILDER_CALLBACKS, + .overflow = spa_pod_dynamic_builder_overflow + }; + builder->b = SPA_POD_BUILDER_INIT(data, size); + spa_pod_builder_set_callbacks(&builder->b, &spa_pod_dynamic_builder_callbacks, builder); + builder->extend = extend; + builder->data = data; +} + +static inline void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder) +{ + if (builder->data != builder->b.data) + free(builder->b.data); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_DYNAMIC_H */ diff --git a/spa/include/spa/pod/event.h b/spa/include/spa/pod/event.h new file mode 100644 index 0000000..2328af7 --- /dev/null +++ b/spa/include/spa/pod/event.h @@ -0,0 +1,68 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_EVENT_H +#define SPA_EVENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_pod + * \{ + */ + +struct spa_event_body { + struct spa_pod_object_body body; +}; + +struct spa_event { + struct spa_pod pod; + struct spa_event_body body; +}; + +#define SPA_EVENT_TYPE(ev) ((ev)->body.body.type) +#define SPA_EVENT_ID(ev,type) (SPA_EVENT_TYPE(ev) == (type) ? \ + (ev)->body.body.id : SPA_ID_INVALID) + +#define SPA_EVENT_INIT_FULL(t,size,type,id,...) ((t) \ + { { (size), SPA_TYPE_OBJECT }, \ + { { (type), (id) }, ##__VA_ARGS__ } }) \ + +#define SPA_EVENT_INIT(type,id) \ + SPA_EVENT_INIT_FULL(struct spa_event, \ + sizeof(struct spa_event_body), type, id) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_EVENT_H */ diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h new file mode 100644 index 0000000..423a10b --- /dev/null +++ b/spa/include/spa/pod/filter.h @@ -0,0 +1,481 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_FILTER_H +#define SPA_POD_FILTER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * \addtogroup spa_pod + * \{ + */ + +static inline int spa_pod_choice_fix_default(struct spa_pod_choice *choice) +{ + void *val, *alt; + int i, nvals; + uint32_t type, size; + + nvals = SPA_POD_CHOICE_N_VALUES(choice); + type = SPA_POD_CHOICE_VALUE_TYPE(choice); + size = SPA_POD_CHOICE_VALUE_SIZE(choice); + alt = val = SPA_POD_CHOICE_VALUES(choice); + + switch (choice->body.type) { + case SPA_CHOICE_None: + break; + case SPA_CHOICE_Range: + case SPA_CHOICE_Step: + if (nvals > 1) { + alt = SPA_PTROFF(alt, size, void); + if (spa_pod_compare_value(type, val, alt, size) < 0) + memcpy(val, alt, size); + } + if (nvals > 2) { + alt = SPA_PTROFF(alt, size, void); + if (spa_pod_compare_value(type, val, alt, size) > 0) + memcpy(val, alt, size); + } + break; + case SPA_CHOICE_Flags: + case SPA_CHOICE_Enum: + { + void *best = NULL; + + for (i = 1; i < nvals; i++) { + alt = SPA_PTROFF(alt, size, void); + if (spa_pod_compare_value(type, val, alt, size) == 0) { + best = alt; + break; + } + if (best == NULL) + best = alt; + } + if (best) + memcpy(val, best, size); + + if (nvals <= 1) + choice->body.type = SPA_CHOICE_None; + break; + } + } + return 0; +} + +static inline int spa_pod_filter_flags_value(struct spa_pod_builder *b, + uint32_t type, const void *r1, const void *r2, uint32_t size) +{ + switch (type) { + case SPA_TYPE_Int: + { + int32_t val = (*(int32_t *) r1) & (*(int32_t *) r2); + if (val == 0) + return 0; + spa_pod_builder_int(b, val); + break; + } + case SPA_TYPE_Long: + { + int64_t val = (*(int64_t *) r1) & (*(int64_t *) r2); + if (val == 0) + return 0; + spa_pod_builder_long(b, val); + break; + } + default: + return -ENOTSUP; + } + return 1; +} + +static inline int spa_pod_filter_is_step_of(uint32_t type, const void *r1, + const void *r2, uint32_t size) +{ + switch (type) { + case SPA_TYPE_Int: + return *(int32_t *) r1 % *(int32_t *) r2 == 0; + case SPA_TYPE_Long: + return *(int64_t *) r1 % *(int64_t *) r2 == 0; + case SPA_TYPE_Rectangle: + { + const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1, + *rec2 = (struct spa_rectangle *) r2; + + return (rec1->width % rec2->width == 0 && + rec1->height % rec2->height == 0); + } + default: + return -ENOTSUP; + } + return 0; +} + +static inline int +spa_pod_filter_prop(struct spa_pod_builder *b, + const struct spa_pod_prop *p1, + const struct spa_pod_prop *p2) +{ + const struct spa_pod *v1, *v2; + struct spa_pod_choice *nc; + uint32_t j, k, nalt1, nalt2; + void *alt1, *alt2, *a1, *a2; + uint32_t type, size, p1c, p2c; + struct spa_pod_frame f; + + v1 = spa_pod_get_values(&p1->value, &nalt1, &p1c); + alt1 = SPA_POD_BODY(v1); + v2 = spa_pod_get_values(&p2->value, &nalt2, &p2c); + alt2 = SPA_POD_BODY(v2); + + type = v1->type; + size = v1->size; + + /* incompatible property types */ + if (type != v2->type || size != v2->size || p1->key != p2->key) + return -EINVAL; + + if (p1c == SPA_CHOICE_None || p1c == SPA_CHOICE_Flags) { + nalt1 = 1; + } else { + alt1 = SPA_PTROFF(alt1, size, void); + nalt1--; + } + + if (p2c == SPA_CHOICE_None || p2c == SPA_CHOICE_Flags) { + nalt2 = 1; + } else { + alt2 = SPA_PTROFF(alt2, size, void); + nalt2--; + } + + /* start with copying the property */ + spa_pod_builder_prop(b, p1->key, p1->flags & p2->flags); + spa_pod_builder_push_choice(b, &f, 0, 0); + nc = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f); + + /* default value */ + spa_pod_builder_primitive(b, v1); + + if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Enum) || + (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Enum)) { + int n_copied = 0; + /* copy all equal values but don't copy the default value again */ + for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1, size, void)) { + for (k = 0, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) { + if (spa_pod_compare_value(type, a1, a2, size) == 0) { + if (p1c == SPA_CHOICE_Enum || j > 0) + spa_pod_builder_raw(b, a1, size); + n_copied++; + } + } + } + if (n_copied == 0) + return -EINVAL; + nc->body.type = SPA_CHOICE_Enum; + } + + if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Range) || + (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Range)) { + int n_copied = 0; + /* copy all values inside the range */ + for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { + if (spa_pod_compare_value(type, a1, a2, size) < 0) + continue; + if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0) + continue; + spa_pod_builder_raw(b, a1, size); + n_copied++; + } + if (n_copied == 0) + return -EINVAL; + nc->body.type = SPA_CHOICE_Enum; + } + + if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Step) || + (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Step)) { + int n_copied = 0; + for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { + int res; + if (spa_pod_compare_value(type, a1, a2, size) < 0) + continue; + if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0) + continue; + + res = spa_pod_filter_is_step_of(type, a1, SPA_PTROFF(a2,size*2,void), size); + if (res == 0) + continue; + if (res == -ENOTSUP) + return -EINVAL; + + spa_pod_builder_raw(b, a1, size); + n_copied++; + } + if (n_copied == 0) + return -EINVAL; + nc->body.type = SPA_CHOICE_Enum; + } + + if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Enum)) { + int n_copied = 0; + /* copy all values inside the range */ + for (k = 0, a1 = alt1, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) { + if (spa_pod_compare_value(type, a2, a1, size) < 0) + continue; + if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0) + continue; + spa_pod_builder_raw(b, a2, size); + n_copied++; + } + if (n_copied == 0) + return -EINVAL; + nc->body.type = SPA_CHOICE_Enum; + } + + if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Range) || + (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Step) || + (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Range) || + (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Step)) { + if (spa_pod_compare_value(type, alt1, alt2, size) < 0) + spa_pod_builder_raw(b, alt2, size); + else + spa_pod_builder_raw(b, alt1, size); + + alt1 = SPA_PTROFF(alt1,size,void); + alt2 = SPA_PTROFF(alt2,size,void); + + if (spa_pod_compare_value(type, alt1, alt2, size) < 0) + spa_pod_builder_raw(b, alt1, size); + else + spa_pod_builder_raw(b, alt2, size); + + nc->body.type = SPA_CHOICE_Range; + } + + if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Flags) || + (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Flags)) { + if (spa_pod_filter_flags_value(b, type, alt1, alt2, size) != 1) + return -EINVAL; + nc->body.type = SPA_CHOICE_Flags; + } + + if (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + + if (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + + if ((p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Enum)) { + int n_copied = 0; + for (j = 0, a1 = alt1, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a1,size,void)) { + int res; + if (spa_pod_compare_value(type, a2, a1, size) < 0) + continue; + if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0) + continue; + + res = spa_pod_filter_is_step_of(type, a2, SPA_PTROFF(a1,size*2,void), size); + if (res == 0) + continue; + if (res == -ENOTSUP) + return -EINVAL; + + spa_pod_builder_raw(b, a2, size); + n_copied++; + } + if (n_copied == 0) + return -EINVAL; + nc->body.type = SPA_CHOICE_Enum; + } + if (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + + if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Range) + return -ENOTSUP; + if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Step) + return -ENOTSUP; + if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Enum) + return -ENOTSUP; + + spa_pod_builder_pop(b, &f); + spa_pod_choice_fix_default(nc); + + return 0; +} + +static inline int spa_pod_filter_part(struct spa_pod_builder *b, + const struct spa_pod *pod, uint32_t pod_size, + const struct spa_pod *filter, uint32_t filter_size) +{ + const struct spa_pod *pp, *pf; + int res = 0; + + pf = filter; + + SPA_POD_FOREACH(pod, pod_size, pp) { + bool do_copy = false, do_advance = false; + uint32_t filter_offset = 0; + struct spa_pod_frame f; + + switch (SPA_POD_TYPE(pp)) { + case SPA_TYPE_Object: + if (pf != NULL) { + struct spa_pod_object *op = (struct spa_pod_object *) pp; + struct spa_pod_object *of = (struct spa_pod_object *) pf; + const struct spa_pod_prop *p1, *p2; + + if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp)) + return -EINVAL; + + spa_pod_builder_push_object(b, &f, op->body.type, op->body.id); + p2 = NULL; + SPA_POD_OBJECT_FOREACH(op, p1) { + p2 = spa_pod_object_find_prop(of, p2, p1->key); + if (p2 != NULL) + res = spa_pod_filter_prop(b, p1, p2); + else if ((p1->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0) + res = -EINVAL; + else + spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1)); + if (res < 0) + break; + } + if (res >= 0) { + p1 = NULL; + SPA_POD_OBJECT_FOREACH(of, p2) { + p1 = spa_pod_object_find_prop(op, p1, p2->key); + if (p1 != NULL) + continue; + if ((p2->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0) + res = -EINVAL; + if (res < 0) + break; + spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2)); + } + } + spa_pod_builder_pop(b, &f); + do_advance = true; + } + else + do_copy = true; + break; + + case SPA_TYPE_Struct: + if (pf != NULL) { + if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp)) + return -EINVAL; + + filter_offset = sizeof(struct spa_pod_struct); + spa_pod_builder_push_struct(b, &f); + res = spa_pod_filter_part(b, + SPA_PTROFF(pp,filter_offset,const struct spa_pod), + SPA_POD_SIZE(pp) - filter_offset, + SPA_PTROFF(pf,filter_offset,const struct spa_pod), + SPA_POD_SIZE(pf) - filter_offset); + spa_pod_builder_pop(b, &f); + do_advance = true; + } + else + do_copy = true; + break; + + default: + if (pf != NULL) { + if (SPA_POD_SIZE(pp) != SPA_POD_SIZE(pf)) + return -EINVAL; + if (memcmp(pp, pf, SPA_POD_SIZE(pp)) != 0) + return -EINVAL; + do_advance = true; + } + do_copy = true; + break; + } + if (do_copy) + spa_pod_builder_raw_padded(b, pp, SPA_POD_SIZE(pp)); + if (do_advance) { + pf = (const struct spa_pod*)spa_pod_next(pf); + if (!spa_pod_is_inside(filter, filter_size, pf)) + pf = NULL; + } + if (res < 0) + break; + } + return res; +} + +static inline int +spa_pod_filter(struct spa_pod_builder *b, + struct spa_pod **result, + const struct spa_pod *pod, + const struct spa_pod *filter) +{ + int res; + struct spa_pod_builder_state state; + + spa_return_val_if_fail(pod != NULL, -EINVAL); + spa_return_val_if_fail(b != NULL, -EINVAL); + + spa_pod_builder_get_state(b, &state); + if (filter == NULL) + res = spa_pod_builder_raw_padded(b, pod, SPA_POD_SIZE(pod)); + else + res = spa_pod_filter_part(b, pod, SPA_POD_SIZE(pod), filter, SPA_POD_SIZE(filter)); + + if (res < 0) { + spa_pod_builder_reset(b, &state); + } else if (result) { + *result = (struct spa_pod*)spa_pod_builder_deref(b, state.offset); + if (*result == NULL) + res = -ENOSPC; + } + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_FILTER_H */ diff --git a/spa/include/spa/pod/iter.h b/spa/include/spa/pod/iter.h new file mode 100644 index 0000000..d3dcf13 --- /dev/null +++ b/spa/include/spa/pod/iter.h @@ -0,0 +1,475 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_ITER_H +#define SPA_POD_ITER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +/** + * \addtogroup spa_pod + * \{ + */ + +struct spa_pod_frame { + struct spa_pod pod; + struct spa_pod_frame *parent; + uint32_t offset; + uint32_t flags; +}; + +static inline bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter) +{ + return SPA_POD_BODY(iter) <= SPA_PTROFF(pod, size, void) && + SPA_PTROFF(iter, SPA_POD_SIZE(iter), void) <= SPA_PTROFF(pod, size, void); +} + +static inline void *spa_pod_next(const void *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_SIZE(iter), 8), void); +} + +static inline struct spa_pod_prop *spa_pod_prop_first(const struct spa_pod_object_body *body) +{ + return SPA_PTROFF(body, sizeof(struct spa_pod_object_body), struct spa_pod_prop); +} + +static inline bool spa_pod_prop_is_inside(const struct spa_pod_object_body *body, + uint32_t size, const struct spa_pod_prop *iter) +{ + return SPA_POD_CONTENTS(struct spa_pod_prop, iter) <= SPA_PTROFF(body, size, void) && + SPA_PTROFF(iter, SPA_POD_PROP_SIZE(iter), void) <= SPA_PTROFF(body, size, void); +} + +static inline struct spa_pod_prop *spa_pod_prop_next(const struct spa_pod_prop *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(iter), 8), struct spa_pod_prop); +} + +static inline struct spa_pod_control *spa_pod_control_first(const struct spa_pod_sequence_body *body) +{ + return SPA_PTROFF(body, sizeof(struct spa_pod_sequence_body), struct spa_pod_control); +} + +static inline bool spa_pod_control_is_inside(const struct spa_pod_sequence_body *body, + uint32_t size, const struct spa_pod_control *iter) +{ + return SPA_POD_CONTENTS(struct spa_pod_control, iter) <= SPA_PTROFF(body, size, void) && + SPA_PTROFF(iter, SPA_POD_CONTROL_SIZE(iter), void) <= SPA_PTROFF(body, size, void); +} + +static inline struct spa_pod_control *spa_pod_control_next(const struct spa_pod_control *iter) +{ + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(iter), 8), struct spa_pod_control); +} + +#define SPA_POD_ARRAY_BODY_FOREACH(body, _size, iter) \ + for ((iter) = (__typeof__(iter))SPA_PTROFF((body), sizeof(struct spa_pod_array_body), void); \ + (iter) < (__typeof__(iter))SPA_PTROFF((body), (_size), void); \ + (iter) = (__typeof__(iter))SPA_PTROFF((iter), (body)->child.size, void)) + +#define SPA_POD_ARRAY_FOREACH(obj, iter) \ + SPA_POD_ARRAY_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter) + +#define SPA_POD_CHOICE_BODY_FOREACH(body, _size, iter) \ + for ((iter) = (__typeof__(iter))SPA_PTROFF((body), sizeof(struct spa_pod_choice_body), void); \ + (iter) < (__typeof__(iter))SPA_PTROFF((body), (_size), void); \ + (iter) = (__typeof__(iter))SPA_PTROFF((iter), (body)->child.size, void)) + +#define SPA_POD_CHOICE_FOREACH(obj, iter) \ + SPA_POD_CHOICE_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter) + +#define SPA_POD_FOREACH(pod, size, iter) \ + for ((iter) = (pod); \ + spa_pod_is_inside(pod, size, iter); \ + (iter) = (__typeof__(iter))spa_pod_next(iter)) + +#define SPA_POD_STRUCT_FOREACH(obj, iter) \ + SPA_POD_FOREACH(SPA_POD_BODY(obj), SPA_POD_BODY_SIZE(obj), iter) + +#define SPA_POD_OBJECT_BODY_FOREACH(body, size, iter) \ + for ((iter) = spa_pod_prop_first(body); \ + spa_pod_prop_is_inside(body, size, iter); \ + (iter) = spa_pod_prop_next(iter)) + +#define SPA_POD_OBJECT_FOREACH(obj, iter) \ + SPA_POD_OBJECT_BODY_FOREACH(&(obj)->body, SPA_POD_BODY_SIZE(obj), iter) + +#define SPA_POD_SEQUENCE_BODY_FOREACH(body, size, iter) \ + for ((iter) = spa_pod_control_first(body); \ + spa_pod_control_is_inside(body, size, iter); \ + (iter) = spa_pod_control_next(iter)) + +#define SPA_POD_SEQUENCE_FOREACH(seq, iter) \ + SPA_POD_SEQUENCE_BODY_FOREACH(&(seq)->body, SPA_POD_BODY_SIZE(seq), iter) + + +static inline void *spa_pod_from_data(void *data, size_t maxsize, off_t offset, size_t size) +{ + void *pod; + if (size < sizeof(struct spa_pod) || offset + size > maxsize) + return NULL; + pod = SPA_PTROFF(data, offset, void); + if (SPA_POD_SIZE(pod) > size) + return NULL; + return pod; +} + +static inline int spa_pod_is_none(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_None); +} + +static inline int spa_pod_is_bool(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Bool && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); +} + +static inline int spa_pod_get_bool(const struct spa_pod *pod, bool *value) +{ + if (!spa_pod_is_bool(pod)) + return -EINVAL; + *value = !!SPA_POD_VALUE(struct spa_pod_bool, pod); + return 0; +} + +static inline int spa_pod_is_id(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Id && SPA_POD_BODY_SIZE(pod) >= sizeof(uint32_t)); +} + +static inline int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value) +{ + if (!spa_pod_is_id(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_id, pod); + return 0; +} + +static inline int spa_pod_is_int(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Int && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); +} + +static inline int spa_pod_get_int(const struct spa_pod *pod, int32_t *value) +{ + if (!spa_pod_is_int(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_int, pod); + return 0; +} + +static inline int spa_pod_is_long(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Long && SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); +} + +static inline int spa_pod_get_long(const struct spa_pod *pod, int64_t *value) +{ + if (!spa_pod_is_long(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_long, pod); + return 0; +} + +static inline int spa_pod_is_float(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Float && SPA_POD_BODY_SIZE(pod) >= sizeof(float)); +} + +static inline int spa_pod_get_float(const struct spa_pod *pod, float *value) +{ + if (!spa_pod_is_float(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_float, pod); + return 0; +} + +static inline int spa_pod_is_double(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Double && SPA_POD_BODY_SIZE(pod) >= sizeof(double)); +} + +static inline int spa_pod_get_double(const struct spa_pod *pod, double *value) +{ + if (!spa_pod_is_double(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_double, pod); + return 0; +} + +static inline int spa_pod_is_string(const struct spa_pod *pod) +{ + const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + return (SPA_POD_TYPE(pod) == SPA_TYPE_String && + SPA_POD_BODY_SIZE(pod) > 0 && + s[SPA_POD_BODY_SIZE(pod)-1] == '\0'); +} + +static inline int spa_pod_get_string(const struct spa_pod *pod, const char **value) +{ + if (!spa_pod_is_string(pod)) + return -EINVAL; + *value = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + return 0; +} + +static inline int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen, char *dest) +{ + const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); + if (!spa_pod_is_string(pod) || maxlen < 1) + return -EINVAL; + strncpy(dest, s, maxlen-1); + dest[maxlen-1]= '\0'; + return 0; +} + +static inline int spa_pod_is_bytes(const struct spa_pod *pod) +{ + return SPA_POD_TYPE(pod) == SPA_TYPE_Bytes; +} + +static inline int spa_pod_get_bytes(const struct spa_pod *pod, const void **value, uint32_t *len) +{ + if (!spa_pod_is_bytes(pod)) + return -EINVAL; + *value = (const void *)SPA_POD_CONTENTS(struct spa_pod_bytes, pod); + *len = SPA_POD_BODY_SIZE(pod); + return 0; +} + +static inline int spa_pod_is_pointer(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Pointer && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_pointer_body)); +} + +static inline int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type, const void **value) +{ + if (!spa_pod_is_pointer(pod)) + return -EINVAL; + *type = ((struct spa_pod_pointer*)pod)->body.type; + *value = ((struct spa_pod_pointer*)pod)->body.value; + return 0; +} + +static inline int spa_pod_is_fd(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Fd && + SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); +} + +static inline int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value) +{ + if (!spa_pod_is_fd(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_fd, pod); + return 0; +} + +static inline int spa_pod_is_rectangle(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Rectangle && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_rectangle)); +} + +static inline int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_rectangle *value) +{ + if (!spa_pod_is_rectangle(pod)) + return -EINVAL; + *value = SPA_POD_VALUE(struct spa_pod_rectangle, pod); + return 0; +} + +static inline int spa_pod_is_fraction(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Fraction && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_fraction)); +} + +static inline int spa_pod_get_fraction(const struct spa_pod *pod, struct spa_fraction *value) +{ + spa_return_val_if_fail(spa_pod_is_fraction(pod), -EINVAL); + *value = SPA_POD_VALUE(struct spa_pod_fraction, pod); + return 0; +} + +static inline int spa_pod_is_bitmap(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Bitmap && + SPA_POD_BODY_SIZE(pod) >= sizeof(uint8_t)); +} + +static inline int spa_pod_is_array(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Array && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_array_body)); +} + +static inline void *spa_pod_get_array(const struct spa_pod *pod, uint32_t *n_values) +{ + spa_return_val_if_fail(spa_pod_is_array(pod), NULL); + *n_values = SPA_POD_ARRAY_N_VALUES(pod); + return SPA_POD_ARRAY_VALUES(pod); +} + +static inline uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t type, + void *values, uint32_t max_values) +{ + uint32_t n_values; + void *v = spa_pod_get_array(pod, &n_values); + if (v == NULL || max_values == 0 || SPA_POD_ARRAY_VALUE_TYPE(pod) != type) + return 0; + n_values = SPA_MIN(n_values, max_values); + memcpy(values, v, SPA_POD_ARRAY_VALUE_SIZE(pod) * n_values); + return n_values; +} + +static inline int spa_pod_is_choice(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Choice && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_choice_body)); +} + +static inline struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint32_t *n_vals, uint32_t *choice) +{ + if (pod->type == SPA_TYPE_Choice) { + *n_vals = SPA_POD_CHOICE_N_VALUES(pod); + if ((*choice = SPA_POD_CHOICE_TYPE(pod)) == SPA_CHOICE_None) + *n_vals = SPA_MIN(1u, SPA_POD_CHOICE_N_VALUES(pod)); + return (struct spa_pod*)SPA_POD_CHOICE_CHILD(pod); + } else { + *n_vals = 1; + *choice = SPA_CHOICE_None; + return (struct spa_pod*)pod; + } +} + +static inline int spa_pod_is_struct(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Struct); +} + +static inline int spa_pod_is_object(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Object && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_object_body)); +} + +static inline bool spa_pod_is_object_type(const struct spa_pod *pod, uint32_t type) +{ + return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_TYPE(pod) == type); +} + +static inline bool spa_pod_is_object_id(const struct spa_pod *pod, uint32_t id) +{ + return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_ID(pod) == id); +} + +static inline int spa_pod_is_sequence(const struct spa_pod *pod) +{ + return (SPA_POD_TYPE(pod) == SPA_TYPE_Sequence && + SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_sequence_body)); +} + +static inline const struct spa_pod_prop *spa_pod_object_find_prop(const struct spa_pod_object *pod, + const struct spa_pod_prop *start, uint32_t key) +{ + const struct spa_pod_prop *first, *res; + + first = spa_pod_prop_first(&pod->body); + start = start ? spa_pod_prop_next(start) : first; + + for (res = start; spa_pod_prop_is_inside(&pod->body, pod->pod.size, res); + res = spa_pod_prop_next(res)) { + if (res->key == key) + return res; + } + for (res = first; res != start; res = spa_pod_prop_next(res)) { + if (res->key == key) + return res; + } + return NULL; +} + +static inline const struct spa_pod_prop *spa_pod_find_prop(const struct spa_pod *pod, + const struct spa_pod_prop *start, uint32_t key) +{ + if (!spa_pod_is_object(pod)) + return NULL; + return spa_pod_object_find_prop((const struct spa_pod_object *)pod, start, key); +} + +static inline int spa_pod_object_fixate(struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) { + if (res->value.type == SPA_TYPE_Choice && + !SPA_FLAG_IS_SET(res->flags, SPA_POD_PROP_FLAG_DONT_FIXATE)) + ((struct spa_pod_choice*)&res->value)->body.type = SPA_CHOICE_None; + } + return 0; +} + +static inline int spa_pod_fixate(struct spa_pod *pod) +{ + if (!spa_pod_is_object(pod)) + return -EINVAL; + return spa_pod_object_fixate((struct spa_pod_object *)pod); +} + +static inline int spa_pod_object_is_fixated(const struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) { + if (res->value.type == SPA_TYPE_Choice && + ((struct spa_pod_choice*)&res->value)->body.type != SPA_CHOICE_None) + return 0; + } + return 1; +} + +static inline int spa_pod_is_fixated(const struct spa_pod *pod) +{ + if (!spa_pod_is_object(pod)) + return -EINVAL; + return spa_pod_object_is_fixated((const struct spa_pod_object *)pod); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_H */ diff --git a/spa/include/spa/pod/parser.h b/spa/include/spa/pod/parser.h new file mode 100644 index 0000000..e054675 --- /dev/null +++ b/spa/include/spa/pod/parser.h @@ -0,0 +1,594 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_PARSER_H +#define SPA_POD_PARSER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + +/** + * \addtogroup spa_pod + * \{ + */ + +struct spa_pod_parser_state { + uint32_t offset; + uint32_t flags; + struct spa_pod_frame *frame; +}; + +struct spa_pod_parser { + const void *data; + uint32_t size; + uint32_t _padding; + struct spa_pod_parser_state state; +}; + +#define SPA_POD_PARSER_INIT(buffer,size) ((struct spa_pod_parser){ (buffer), (size), 0, {} }) + +static inline void spa_pod_parser_init(struct spa_pod_parser *parser, + const void *data, uint32_t size) +{ + *parser = SPA_POD_PARSER_INIT(data, size); +} + +static inline void spa_pod_parser_pod(struct spa_pod_parser *parser, + const struct spa_pod *pod) +{ + spa_pod_parser_init(parser, pod, SPA_POD_SIZE(pod)); +} + +static inline void +spa_pod_parser_get_state(struct spa_pod_parser *parser, struct spa_pod_parser_state *state) +{ + *state = parser->state; +} + +static inline void +spa_pod_parser_reset(struct spa_pod_parser *parser, struct spa_pod_parser_state *state) +{ + parser->state = *state; +} + +static inline struct spa_pod * +spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t size) +{ + /* Cast to uint64_t to avoid wraparound. Add 8 for the pod itself. */ + const uint64_t long_offset = (uint64_t)offset + 8; + if (long_offset <= size && (offset & 7) == 0) { + /* Use void* because creating a misaligned pointer is undefined. */ + void *pod = SPA_PTROFF(parser->data, offset, void); + /* + * Check that the pointer is aligned and that the size (rounded + * to the next multiple of 8) is in bounds. + */ + if (SPA_IS_ALIGNED(pod, __alignof__(struct spa_pod)) && + long_offset + SPA_ROUND_UP_N((uint64_t)SPA_POD_BODY_SIZE(pod), 8) <= size) + return (struct spa_pod *)pod; + } + return NULL; +} + +static inline struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame) +{ + return SPA_PTROFF(parser->data, frame->offset, struct spa_pod); +} + +static inline void spa_pod_parser_push(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, const struct spa_pod *pod, uint32_t offset) +{ + frame->pod = *pod; + frame->offset = offset; + frame->parent = parser->state.frame; + frame->flags = parser->state.flags; + parser->state.frame = frame; +} + +static inline struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser) +{ + struct spa_pod_frame *f = parser->state.frame; + uint32_t size = f ? f->offset + SPA_POD_SIZE(&f->pod) : parser->size; + return spa_pod_parser_deref(parser, parser->state.offset, size); +} + +static inline void spa_pod_parser_advance(struct spa_pod_parser *parser, const struct spa_pod *pod) +{ + parser->state.offset += SPA_ROUND_UP_N(SPA_POD_SIZE(pod), 8); +} + +static inline struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser) +{ + struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod) + spa_pod_parser_advance(parser, pod); + return pod; +} + +static inline int spa_pod_parser_pop(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + parser->state.frame = frame->parent; + parser->state.offset = frame->offset + SPA_ROUND_UP_N(SPA_POD_SIZE(&frame->pod), 8); + return 0; +} + +static inline int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_bool(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_id(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_int(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_long(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_float(struct spa_pod_parser *parser, float *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_float(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_double(struct spa_pod_parser *parser, double *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_double(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_string(struct spa_pod_parser *parser, const char **value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_string(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const void **value, uint32_t *len) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_bytes(pod, value, len)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint32_t *type, const void **value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_pointer(pod, type, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_fd(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, struct spa_rectangle *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_rectangle(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, struct spa_fraction *value) +{ + int res = -EPIPE; + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod != NULL && (res = spa_pod_get_fraction(pod, value)) >= 0) + spa_pod_parser_advance(parser, pod); + return res; +} + +static inline int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct spa_pod **value) +{ + struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod == NULL) + return -EPIPE; + *value = pod; + spa_pod_parser_advance(parser, pod); + return 0; +} +static inline int spa_pod_parser_push_struct(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod == NULL) + return -EPIPE; + if (!spa_pod_is_struct(pod)) + return -EINVAL; + spa_pod_parser_push(parser, frame, pod, parser->state.offset); + parser->state.offset += sizeof(struct spa_pod_struct); + return 0; +} + +static inline int spa_pod_parser_push_object(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, uint32_t type, uint32_t *id) +{ + const struct spa_pod *pod = spa_pod_parser_current(parser); + if (pod == NULL) + return -EPIPE; + if (!spa_pod_is_object(pod)) + return -EINVAL; + if (type != SPA_POD_OBJECT_TYPE(pod)) + return -EPROTO; + if (id != NULL) + *id = SPA_POD_OBJECT_ID(pod); + spa_pod_parser_push(parser, frame, pod, parser->state.offset); + parser->state.offset = parser->size; + return 0; +} + +static inline bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type) +{ + if (pod == NULL) + return false; + + if (SPA_POD_TYPE(pod) == SPA_TYPE_Choice) { + if (!spa_pod_is_choice(pod)) + return false; + if (type == 'V') + return true; + if (SPA_POD_CHOICE_TYPE(pod) != SPA_CHOICE_None) + return false; + pod = SPA_POD_CHOICE_CHILD(pod); + } + + switch (type) { + case 'P': + return true; + case 'b': + return spa_pod_is_bool(pod); + case 'I': + return spa_pod_is_id(pod); + case 'i': + return spa_pod_is_int(pod); + case 'l': + return spa_pod_is_long(pod); + case 'f': + return spa_pod_is_float(pod); + case 'd': + return spa_pod_is_double(pod); + case 's': + return spa_pod_is_string(pod) || spa_pod_is_none(pod); + case 'S': + return spa_pod_is_string(pod); + case 'y': + return spa_pod_is_bytes(pod); + case 'R': + return spa_pod_is_rectangle(pod); + case 'F': + return spa_pod_is_fraction(pod); + case 'B': + return spa_pod_is_bitmap(pod); + case 'a': + return spa_pod_is_array(pod); + case 'p': + return spa_pod_is_pointer(pod); + case 'h': + return spa_pod_is_fd(pod); + case 'T': + return spa_pod_is_struct(pod) || spa_pod_is_none(pod); + case 'O': + return spa_pod_is_object(pod) || spa_pod_is_none(pod); + case 'V': + default: + return false; + } +} + +#define SPA_POD_PARSER_COLLECT(pod,_type,args) \ +do { \ + switch (_type) { \ + case 'b': \ + *va_arg(args, bool*) = SPA_POD_VALUE(struct spa_pod_bool, pod); \ + break; \ + case 'I': \ + case 'i': \ + *va_arg(args, int32_t*) = SPA_POD_VALUE(struct spa_pod_int, pod); \ + break; \ + case 'l': \ + *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_long, pod); \ + break; \ + case 'f': \ + *va_arg(args, float*) = SPA_POD_VALUE(struct spa_pod_float, pod); \ + break; \ + case 'd': \ + *va_arg(args, double*) = SPA_POD_VALUE(struct spa_pod_double, pod); \ + break; \ + case 's': \ + *va_arg(args, char**) = \ + ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \ + ? NULL \ + : (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod)); \ + break; \ + case 'S': \ + { \ + char *dest = va_arg(args, char*); \ + uint32_t maxlen = va_arg(args, uint32_t); \ + strncpy(dest, (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod), maxlen-1); \ + dest[maxlen-1] = '\0'; \ + break; \ + } \ + case 'y': \ + *(va_arg(args, void **)) = SPA_POD_CONTENTS(struct spa_pod_bytes, pod); \ + *(va_arg(args, uint32_t *)) = SPA_POD_BODY_SIZE(pod); \ + break; \ + case 'R': \ + *va_arg(args, struct spa_rectangle*) = \ + SPA_POD_VALUE(struct spa_pod_rectangle, pod); \ + break; \ + case 'F': \ + *va_arg(args, struct spa_fraction*) = \ + SPA_POD_VALUE(struct spa_pod_fraction, pod); \ + break; \ + case 'B': \ + *va_arg(args, uint32_t **) = \ + (uint32_t *) SPA_POD_CONTENTS(struct spa_pod_bitmap, pod); \ + break; \ + case 'a': \ + *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_SIZE(pod); \ + *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_TYPE(pod); \ + *va_arg(args, uint32_t*) = SPA_POD_ARRAY_N_VALUES(pod); \ + *va_arg(args, void**) = SPA_POD_ARRAY_VALUES(pod); \ + break; \ + case 'p': \ + { \ + struct spa_pod_pointer_body *b = \ + (struct spa_pod_pointer_body *) SPA_POD_BODY(pod); \ + *(va_arg(args, uint32_t *)) = b->type; \ + *(va_arg(args, const void **)) = b->value; \ + break; \ + } \ + case 'h': \ + *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_fd, pod); \ + break; \ + case 'P': \ + case 'T': \ + case 'O': \ + case 'V': \ + { \ + const struct spa_pod **d = va_arg(args, const struct spa_pod**); \ + if (d) \ + *d = ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \ + ? NULL : (pod)); \ + break; \ + } \ + default: \ + break; \ + } \ +} while(false) + +#define SPA_POD_PARSER_SKIP(_type,args) \ +do { \ + switch (_type) { \ + case 'S': \ + va_arg(args, char*); \ + va_arg(args, uint32_t); \ + break; \ + case 'a': \ + va_arg(args, void*); \ + va_arg(args, void*); \ + SPA_FALLTHROUGH \ + case 'p': \ + case 'y': \ + va_arg(args, void*); \ + SPA_FALLTHROUGH \ + case 'b': \ + case 'I': \ + case 'i': \ + case 'l': \ + case 'f': \ + case 'd': \ + case 's': \ + case 'R': \ + case 'F': \ + case 'B': \ + case 'h': \ + case 'V': \ + case 'P': \ + case 'T': \ + case 'O': \ + va_arg(args, void*); \ + break; \ + } \ +} while(false) + +static inline int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list args) +{ + struct spa_pod_frame *f = parser->state.frame; + uint32_t ftype = f ? f->pod.type : (uint32_t)SPA_TYPE_Struct; + const struct spa_pod_prop *prop = NULL; + int count = 0; + + do { + bool optional; + const struct spa_pod *pod = NULL; + const char *format; + + if (ftype == SPA_TYPE_Object) { + uint32_t key = va_arg(args, uint32_t); + const struct spa_pod_object *object; + + if (key == 0) + break; + + object = (const struct spa_pod_object *)spa_pod_parser_frame(parser, f); + prop = spa_pod_object_find_prop(object, prop, key); + pod = prop ? &prop->value : NULL; + } + + if ((format = va_arg(args, char *)) == NULL) + break; + + if (ftype == SPA_TYPE_Struct) + pod = spa_pod_parser_next(parser); + + if ((optional = (*format == '?'))) + format++; + + if (!spa_pod_parser_can_collect(pod, *format)) { + if (!optional) { + if (pod == NULL) + return -ESRCH; + else + return -EPROTO; + } + SPA_POD_PARSER_SKIP(*format, args); + } else { + if (pod->type == SPA_TYPE_Choice && *format != 'V') + pod = SPA_POD_CHOICE_CHILD(pod); + + SPA_POD_PARSER_COLLECT(pod, *format, args); + count++; + } + } while (true); + + return count; +} + +static inline int spa_pod_parser_get(struct spa_pod_parser *parser, ...) +{ + int res; + va_list args; + + va_start(args, parser); + res = spa_pod_parser_getv(parser, args); + va_end(args); + + return res; +} + +#define SPA_POD_OPT_Bool(val) "?" SPA_POD_Bool(val) +#define SPA_POD_OPT_Id(val) "?" SPA_POD_Id(val) +#define SPA_POD_OPT_Int(val) "?" SPA_POD_Int(val) +#define SPA_POD_OPT_Long(val) "?" SPA_POD_Long(val) +#define SPA_POD_OPT_Float(val) "?" SPA_POD_Float(val) +#define SPA_POD_OPT_Double(val) "?" SPA_POD_Double(val) +#define SPA_POD_OPT_String(val) "?" SPA_POD_String(val) +#define SPA_POD_OPT_Stringn(val,len) "?" SPA_POD_Stringn(val,len) +#define SPA_POD_OPT_Bytes(val,len) "?" SPA_POD_Bytes(val,len) +#define SPA_POD_OPT_Rectangle(val) "?" SPA_POD_Rectangle(val) +#define SPA_POD_OPT_Fraction(val) "?" SPA_POD_Fraction(val) +#define SPA_POD_OPT_Array(csize,ctype,n_vals,vals) "?" SPA_POD_Array(csize,ctype,n_vals,vals) +#define SPA_POD_OPT_Pointer(type,val) "?" SPA_POD_Pointer(type,val) +#define SPA_POD_OPT_Fd(val) "?" SPA_POD_Fd(val) +#define SPA_POD_OPT_Pod(val) "?" SPA_POD_Pod(val) +#define SPA_POD_OPT_PodObject(val) "?" SPA_POD_PodObject(val) +#define SPA_POD_OPT_PodStruct(val) "?" SPA_POD_PodStruct(val) +#define SPA_POD_OPT_PodChoice(val) "?" SPA_POD_PodChoice(val) + +#define spa_pod_parser_get_object(p,type,id,...) \ +({ \ + struct spa_pod_frame _f; \ + int _res; \ + if ((_res = spa_pod_parser_push_object(p, &_f, type, id)) == 0) { \ + _res = spa_pod_parser_get(p,##__VA_ARGS__, 0); \ + spa_pod_parser_pop(p, &_f); \ + } \ + _res; \ +}) + +#define spa_pod_parser_get_struct(p,...) \ +({ \ + struct spa_pod_frame _f; \ + int _res; \ + if ((_res = spa_pod_parser_push_struct(p, &_f)) == 0) { \ + _res = spa_pod_parser_get(p,##__VA_ARGS__, NULL); \ + spa_pod_parser_pop(p, &_f); \ + } \ + _res; \ +}) + +#define spa_pod_parse_object(pod,type,id,...) \ +({ \ + struct spa_pod_parser _p; \ + spa_pod_parser_pod(&_p, pod); \ + spa_pod_parser_get_object(&_p,type,id,##__VA_ARGS__); \ +}) + +#define spa_pod_parse_struct(pod,...) \ +({ \ + struct spa_pod_parser _p; \ + spa_pod_parser_pod(&_p, pod); \ + spa_pod_parser_get_struct(&_p,##__VA_ARGS__); \ +}) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_PARSER_H */ diff --git a/spa/include/spa/pod/pod.h b/spa/include/spa/pod/pod.h new file mode 100644 index 0000000..1864b19 --- /dev/null +++ b/spa/include/spa/pod/pod.h @@ -0,0 +1,246 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_H +#define SPA_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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..53dc654 --- /dev/null +++ b/spa/include/spa/pod/vararg.h @@ -0,0 +1,113 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_POD_VARARG_H +#define SPA_POD_VARARG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#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..39a82f4 --- /dev/null +++ b/spa/include/spa/support/cpu.h @@ -0,0 +1,168 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_CPU_H +#define SPA_CPU_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** \defgroup spa_cpu CPU + * Querying CPU properties + */ + +/** + * \addtogroup spa_cpu + * \{ + */ + +/** + * The CPU features interface + */ +#define SPA_TYPE_INTERFACE_CPU SPA_TYPE_INFO_INTERFACE_BASE "CPU" + +#define SPA_VERSION_CPU 0 +struct spa_cpu { struct spa_interface iface; }; + +/* x86 specific */ +#define SPA_CPU_FLAG_MMX (1<<0) /**< standard MMX */ +#define SPA_CPU_FLAG_MMXEXT (1<<1) /**< SSE integer or AMD MMX ext */ +#define SPA_CPU_FLAG_3DNOW (1<<2) /**< AMD 3DNOW */ +#define SPA_CPU_FLAG_SSE (1<<3) /**< SSE */ +#define SPA_CPU_FLAG_SSE2 (1<<4) /**< SSE2 */ +#define SPA_CPU_FLAG_3DNOWEXT (1<<5) /**< AMD 3DNowExt */ +#define SPA_CPU_FLAG_SSE3 (1<<6) /**< Prescott SSE3 */ +#define SPA_CPU_FLAG_SSSE3 (1<<7) /**< Conroe SSSE3 */ +#define SPA_CPU_FLAG_SSE41 (1<<8) /**< Penryn SSE4.1 */ +#define SPA_CPU_FLAG_SSE42 (1<<9) /**< Nehalem SSE4.2 */ +#define SPA_CPU_FLAG_AESNI (1<<10) /**< Advanced Encryption Standard */ +#define SPA_CPU_FLAG_AVX (1<<11) /**< AVX */ +#define SPA_CPU_FLAG_XOP (1<<12) /**< Bulldozer XOP */ +#define SPA_CPU_FLAG_FMA4 (1<<13) /**< Bulldozer FMA4 */ +#define SPA_CPU_FLAG_CMOV (1<<14) /**< supports cmov */ +#define SPA_CPU_FLAG_AVX2 (1<<15) /**< AVX2 */ +#define SPA_CPU_FLAG_FMA3 (1<<16) /**< Haswell FMA3 */ +#define SPA_CPU_FLAG_BMI1 (1<<17) /**< Bit Manipulation Instruction Set 1 */ +#define SPA_CPU_FLAG_BMI2 (1<<18) /**< Bit Manipulation Instruction Set 2 */ +#define SPA_CPU_FLAG_AVX512 (1<<19) /**< AVX-512 */ +#define SPA_CPU_FLAG_SLOW_UNALIGNED (1<<20) /**< unaligned loads/stores are slow */ + +/* PPC specific */ +#define SPA_CPU_FLAG_ALTIVEC (1<<0) /**< standard */ +#define SPA_CPU_FLAG_VSX (1<<1) /**< ISA 2.06 */ +#define SPA_CPU_FLAG_POWER8 (1<<2) /**< ISA 2.07 */ + +/* ARM specific */ +#define SPA_CPU_FLAG_ARMV5TE (1 << 0) +#define SPA_CPU_FLAG_ARMV6 (1 << 1) +#define SPA_CPU_FLAG_ARMV6T2 (1 << 2) +#define SPA_CPU_FLAG_VFP (1 << 3) +#define SPA_CPU_FLAG_VFPV3 (1 << 4) +#define SPA_CPU_FLAG_NEON (1 << 5) +#define SPA_CPU_FLAG_ARMV8 (1 << 6) + +#define SPA_CPU_FORCE_AUTODETECT ((uint32_t)-1) + +#define SPA_CPU_VM_NONE (0) +#define SPA_CPU_VM_OTHER (1 << 0) +#define SPA_CPU_VM_KVM (1 << 1) +#define SPA_CPU_VM_QEMU (1 << 2) +#define SPA_CPU_VM_BOCHS (1 << 3) +#define SPA_CPU_VM_XEN (1 << 4) +#define SPA_CPU_VM_UML (1 << 5) +#define SPA_CPU_VM_VMWARE (1 << 6) +#define SPA_CPU_VM_ORACLE (1 << 7) +#define SPA_CPU_VM_MICROSOFT (1 << 8) +#define SPA_CPU_VM_ZVM (1 << 9) +#define SPA_CPU_VM_PARALLELS (1 << 10) +#define SPA_CPU_VM_BHYVE (1 << 11) +#define SPA_CPU_VM_QNX (1 << 12) +#define SPA_CPU_VM_ACRN (1 << 13) +#define SPA_CPU_VM_POWERVM (1 << 14) + +/** + * methods + */ +struct spa_cpu_methods { + /** the version of the methods. This can be used to expand this + structure in the future */ +#define SPA_VERSION_CPU_METHODS 2 + uint32_t version; + + /** get CPU flags */ + uint32_t (*get_flags) (void *object); + + /** force CPU flags, use SPA_CPU_FORCE_AUTODETECT to autodetect CPU flags */ + int (*force_flags) (void *object, uint32_t flags); + + /** get number of CPU cores */ + uint32_t (*get_count) (void *object); + + /** get maximum required alignment of data */ + uint32_t (*get_max_align) (void *object); + + /* check if running in a VM. Since:1 */ + uint32_t (*get_vm_type) (void *object); + + /* denormals will be handled as zero, either with FTZ or DAZ. + * Since:2 */ + int (*zero_denormals) (void *object, bool enable); +}; + +#define spa_cpu_method(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_cpu *_c = o; \ + spa_interface_call_res(&_c->iface, \ + struct spa_cpu_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) +#define spa_cpu_get_flags(c) spa_cpu_method(c, get_flags, 0) +#define spa_cpu_force_flags(c,f) spa_cpu_method(c, force_flags, 0, f) +#define spa_cpu_get_count(c) spa_cpu_method(c, get_count, 0) +#define spa_cpu_get_max_align(c) spa_cpu_method(c, get_max_align, 0) +#define spa_cpu_get_vm_type(c) spa_cpu_method(c, get_vm_type, 1) +#define spa_cpu_zero_denormals(c,e) spa_cpu_method(c, zero_denormals, 2, e) + +/** keys can be given when initializing the cpu handle */ +#define SPA_KEY_CPU_FORCE "cpu.force" /**< force cpu flags */ +#define SPA_KEY_CPU_VM_TYPE "cpu.vm.type" /**< force a VM type */ +#define SPA_KEY_CPU_ZERO_DENORMALS "cpu.zero.denormals" /**< zero denormals */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_CPU_H */ diff --git a/spa/include/spa/support/dbus.h b/spa/include/spa/support/dbus.h new file mode 100644 index 0000000..9ebb7d8 --- /dev/null +++ b/spa/include/spa/support/dbus.h @@ -0,0 +1,167 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DBUS_H +#define SPA_DBUS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** \defgroup spa_dbus DBus + * DBus communication + */ + +/** + * \addtogroup spa_dbus + * \{ + */ + +#define SPA_TYPE_INTERFACE_DBus SPA_TYPE_INFO_INTERFACE_BASE "DBus" + +#define SPA_VERSION_DBUS 0 +struct spa_dbus { struct spa_interface iface; }; + +enum spa_dbus_type { + SPA_DBUS_TYPE_SESSION, /**< The login session bus */ + SPA_DBUS_TYPE_SYSTEM, /**< The systemwide bus */ + SPA_DBUS_TYPE_STARTER /**< The bus that started us, if any */ +}; + +#define SPA_DBUS_CONNECTION_EVENT_DESTROY 0 +#define SPA_DBUS_CONNECTION_EVENT_DISCONNECTED 1 +#define SPA_DBUS_CONNECTION_EVENT_NUM 2 + +struct spa_dbus_connection_events { +#define SPA_VERSION_DBUS_CONNECTION_EVENTS 0 + uint32_t version; + + /* a connection is destroyed */ + void (*destroy) (void *data); + + /* a connection disconnected */ + void (*disconnected) (void *data); +}; + +struct spa_dbus_connection { +#define SPA_VERSION_DBUS_CONNECTION 1 + uint32_t version; + /** + * Get the DBusConnection from a wrapper + * + * Note that the returned handle is closed and unref'd by spa_dbus + * immediately before emitting the asynchronous "disconnected" event. + * The caller must either deal with the invalidation, or keep an extra + * ref on the handle returned. + * + * \param conn the spa_dbus_connection wrapper + * \return a pointer of type DBusConnection + */ + void *(*get) (struct spa_dbus_connection *conn); + /** + * Destroy a dbus connection wrapper + * + * \param conn the wrapper to destroy + */ + void (*destroy) (struct spa_dbus_connection *conn); + + /** + * Add a listener for events + * + * Since version 1 + */ + void (*add_listener) (struct spa_dbus_connection *conn, + struct spa_hook *listener, + const struct spa_dbus_connection_events *events, + void *data); +}; + +#define spa_dbus_connection_call(c,method,vers,...) \ +({ \ + if (SPA_LIKELY(SPA_CALLBACK_CHECK(c,method,vers))) \ + c->method((c), ## __VA_ARGS__); \ +}) + +#define spa_dbus_connection_call_vp(c,method,vers,...) \ +({ \ + void *_res = NULL; \ + if (SPA_LIKELY(SPA_CALLBACK_CHECK(c,method,vers))) \ + _res = c->method((c), ## __VA_ARGS__); \ + _res; \ +}) + +/** \copydoc spa_dbus_connection.get + * \sa spa_dbus_connection.get */ +#define spa_dbus_connection_get(c) spa_dbus_connection_call_vp(c,get,0) +/** \copydoc spa_dbus_connection.destroy + * \sa spa_dbus_connection.destroy */ +#define spa_dbus_connection_destroy(c) spa_dbus_connection_call(c,destroy,0) +/** \copydoc spa_dbus_connection.add_listener + * \sa spa_dbus_connection.add_listener */ +#define spa_dbus_connection_add_listener(c,...) spa_dbus_connection_call(c,add_listener,1,__VA_ARGS__) + +struct spa_dbus_methods { +#define SPA_VERSION_DBUS_METHODS 0 + uint32_t version; + + /** + * Get a new connection wrapper for the given bus type. + * + * The connection wrapper is completely configured to operate + * in the main context of the handle that manages the spa_dbus + * interface. + * + * \param dbus the dbus manager + * \param type the bus type to wrap + * \param error location for the DBusError + * \return a new dbus connection wrapper or NULL on error + */ + struct spa_dbus_connection * (*get_connection) (void *object, + enum spa_dbus_type type); +}; + +/** \copydoc spa_dbus_methods.get_connection + * \sa spa_dbus_methods.get_connection + */ +static inline struct spa_dbus_connection * +spa_dbus_get_connection(struct spa_dbus *dbus, enum spa_dbus_type type) +{ + struct spa_dbus_connection *res = NULL; + spa_interface_call_res(&dbus->iface, + struct spa_dbus_methods, res, + get_connection, 0, type); + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DBUS_H */ diff --git a/spa/include/spa/support/i18n.h b/spa/include/spa/support/i18n.h new file mode 100644 index 0000000..fc45b07 --- /dev/null +++ b/spa/include/spa/support/i18n.h @@ -0,0 +1,107 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_I18N_H +#define SPA_I18N_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_i18n I18N + * Gettext interface + */ + +/** + * \addtogroup spa_i18n + * \{ + */ + +#define SPA_TYPE_INTERFACE_I18N SPA_TYPE_INFO_INTERFACE_BASE "I18N" + +#define SPA_VERSION_I18N 0 +struct spa_i18n { struct spa_interface iface; }; + +struct spa_i18n_methods { +#define SPA_VERSION_I18N_METHODS 0 + uint32_t version; + + /** + * Translate a message + * + * \param object the i18n interface + * \param msgid the message + * \return a translated message + */ + const char *(*text) (void *object, const char *msgid); + + /** + * Translate a message for a number + * + * \param object the i18n interface + * \param msgid the message to translate + * \param msgid_plural the plural form of \a msgid + * \param n a number + * \return a translated message for the number \a n + */ + const char *(*ntext) (void *object, const char *msgid, + const char *msgid_plural, unsigned long int n); +}; + +SPA_FORMAT_ARG_FUNC(2) +static inline const char * +spa_i18n_text(struct spa_i18n *i18n, const char *msgid) +{ + const char *res = msgid; + if (SPA_LIKELY(i18n != NULL)) + spa_interface_call_res(&i18n->iface, + struct spa_i18n_methods, res, + text, 0, msgid); + return res; +} + +static inline const char * +spa_i18n_ntext(struct spa_i18n *i18n, const char *msgid, + const char *msgid_plural, unsigned long int n) +{ + const char *res = n == 1 ? msgid : msgid_plural; + if (SPA_LIKELY(i18n != NULL)) + spa_interface_call_res(&i18n->iface, + struct spa_i18n_methods, res, + ntext, 0, msgid, msgid_plural, n); + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_I18N_H */ diff --git a/spa/include/spa/support/log-impl.h b/spa/include/spa/support/log-impl.h new file mode 100644 index 0000000..e1ee21d --- /dev/null +++ b/spa/include/spa/support/log-impl.h @@ -0,0 +1,143 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_LOG_IMPL_H +#define SPA_LOG_IMPL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include + +/** + * \addtogroup spa_log + * \{ + */ + +static inline SPA_PRINTF_FUNC(7, 0) void spa_log_impl_logtv(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ + static const char * const levels[] = { "-", "E", "W", "I", "D", "T" }; + + const char *basename = strrchr(file, '/'); + char text[512], location[1024]; + char topicstr[32] = {0}; + + if (basename) + basename += 1; /* skip '/' */ + else + basename = file; /* use whole string if no '/' is found */ + + if (topic && topic->topic) + snprintf(topicstr, sizeof(topicstr), " %s ", topic->topic); + vsnprintf(text, sizeof(text), fmt, args); + snprintf(location, sizeof(location), "[%s]%s[%s:%i %s()] %s\n", + levels[level], + topicstr, + basename, line, func, text); + fputs(location, stderr); +} + +static inline SPA_PRINTF_FUNC(7,8) void spa_log_impl_logt(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + spa_log_impl_logtv(object, level, topic, file, line, func, fmt, args); + va_end(args); +} + +static inline SPA_PRINTF_FUNC(6, 0) void spa_log_impl_logv(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ + + spa_log_impl_logtv(object, level, NULL, file, line, func, fmt, args); +} + +static inline SPA_PRINTF_FUNC(6,7) void spa_log_impl_log(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + spa_log_impl_logv(object, level, file, line, func, fmt, args); + va_end(args); +} + +static inline void spa_log_impl_topic_init(void *object, struct spa_log_topic *topic) +{ + /* noop */ +} + +#define SPA_LOG_IMPL_DEFINE(name) \ +struct { \ + struct spa_log log; \ + struct spa_log_methods methods; \ +} name + +#define SPA_LOG_IMPL_INIT(name) \ + { { { SPA_TYPE_INTERFACE_Log, SPA_VERSION_LOG, \ + SPA_CALLBACKS_INIT(&(name).methods, &(name)) }, \ + SPA_LOG_LEVEL_INFO, }, \ + { SPA_VERSION_LOG_METHODS, \ + spa_log_impl_log, \ + spa_log_impl_logv, \ + spa_log_impl_logt, \ + spa_log_impl_logtv, \ + } } + +#define SPA_LOG_IMPL(name) \ + SPA_LOG_IMPL_DEFINE(name) = SPA_LOG_IMPL_INIT(name) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif +#endif /* SPA_LOG_IMPL_H */ diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h new file mode 100644 index 0000000..74818a4 --- /dev/null +++ b/spa/include/spa/support/log.h @@ -0,0 +1,321 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_LOG_H +#define SPA_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include +#include +#include + +/** \defgroup spa_log Log + * Logging interface + */ + +/** + * \addtogroup spa_log + * \{ + */ + +/** The default log topic. Redefine this in your code to + * allow for the spa_log_* macros to work correctly, e.g: + * + * \code{.c} + * struct spa_log_topic *mylogger; + * #undef SPA_LOG_TOPIC_DEFAULT + * #define SPA_LOG_TOPIC_DEFAULT mylogger + * \endcode + */ +#define SPA_LOG_TOPIC_DEFAULT NULL + +enum spa_log_level { + SPA_LOG_LEVEL_NONE = 0, + SPA_LOG_LEVEL_ERROR, + SPA_LOG_LEVEL_WARN, + SPA_LOG_LEVEL_INFO, + SPA_LOG_LEVEL_DEBUG, + SPA_LOG_LEVEL_TRACE, +}; + +/** + * The Log interface + */ +#define SPA_TYPE_INTERFACE_Log SPA_TYPE_INFO_INTERFACE_BASE "Log" + + +struct spa_log { + /** the version of this log. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_LOG 0 + struct spa_interface iface; + /** + * Logging level, everything above this level is not logged + */ + enum spa_log_level level; +}; + +/** + * \struct spa_log_topic + * + * Identifier for a topic. Topics are string-based filters that logically + * group messages together. An implementation may decide to filter different + * topics on different levels, for example the "protocol" topic may require + * debug level TRACE while the "core" topic defaults to debug level INFO. + * + * spa_log_topics require a spa_log_methods version of 1 or higher. + */ +struct spa_log_topic { +#define SPA_VERSION_LOG_TOPIC 0 + /** the version of this topic. This can be used to expand this + * structure in the future */ + uint32_t version; + /** The string identifier for the topic */ + const char *topic; + /** Logging level set for this topic */ + enum spa_log_level level; + /** False if this topic follows the \ref spa_log level */ + bool has_custom_level; +}; + +struct spa_log_methods { +#define SPA_VERSION_LOG_METHODS 1 + uint32_t version; + /** + * Log a message with the given log level. + * + * \note If compiled with this header, this function is only called + * for implementations of version 0. For versions 1 and above, see + * logt() instead. + * + * \param log a spa_log + * \param level a spa_log_level + * \param file the file name + * \param line the line number + * \param func the function name + * \param fmt printf style format + * \param ... format arguments + */ + void (*log) (void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, ...) SPA_PRINTF_FUNC(6, 7); + + /** + * Log a message with the given log level. + * + * \note If compiled with this header, this function is only called + * for implementations of version 0. For versions 1 and above, see + * logtv() instead. + * + * \param log a spa_log + * \param level a spa_log_level + * \param file the file name + * \param line the line number + * \param func the function name + * \param fmt printf style format + * \param args format arguments + */ + void (*logv) (void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) SPA_PRINTF_FUNC(6, 0); + /** + * Log a message with the given log level for the given topic. + * + * \note Callers that do not use topic-based logging (version 0), the \a + * topic is NULL + * + * \param log a spa_log + * \param level a spa_log_level + * \param topic the topic for this message, may be NULL + * \param file the file name + * \param line the line number + * \param func the function name + * \param fmt printf style format + * \param ... format arguments + * + * \since 1 + */ + void (*logt) (void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, ...) SPA_PRINTF_FUNC(7, 8); + + /** + * Log a message with the given log level for the given topic. + * + * \note For callers that do not use topic-based logging (version 0), + * the \a topic is NULL + * + * \param log a spa_log + * \param level a spa_log_level + * \param topic the topic for this message, may be NULL + * \param file the file name + * \param line the line number + * \param func the function name + * \param fmt printf style format + * \param args format arguments + * + * \since 1 + */ + void (*logtv) (void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) SPA_PRINTF_FUNC(7, 0); + + /** + * Initializes a \ref spa_log_topic to the correct logging level. + * + * \since 1 + */ + void (*topic_init) (void *object, struct spa_log_topic *topic); +}; + + +#define SPA_LOG_TOPIC(v, t) \ + (struct spa_log_topic){ .version = (v), .topic = (t)} + +#define spa_log_topic_init(l, topic) \ +do { \ + struct spa_log *_l = l; \ + if (SPA_LIKELY(_l)) { \ + struct spa_interface *_if = &_l->iface; \ + spa_interface_call(_if, struct spa_log_methods, \ + topic_init, 1, topic); \ + } \ +} while(0) + +/* Unused, left for backwards compat */ +#define spa_log_level_enabled(l,lev) ((l) && (l)->level >= (lev)) + +#define spa_log_level_topic_enabled(l,topic,lev) \ +({ \ + struct spa_log *_log = l; \ + enum spa_log_level _lev = _log ? _log->level : SPA_LOG_LEVEL_NONE; \ + struct spa_log_topic *_t = (struct spa_log_topic *)(topic); \ + if (_t && _t->has_custom_level) \ + _lev = _t->level; \ + _lev >= (lev); \ +}) + +/* Transparently calls to version 0 log if v1 is not supported */ +#define spa_log_logt(l,lev,topic,...) \ +({ \ + struct spa_log *_l = l; \ + struct spa_interface *_if = &_l->iface; \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \ + if (!spa_interface_call(_if, \ + struct spa_log_methods, logt, 1, \ + lev, topic, \ + __VA_ARGS__)) \ + spa_interface_call(_if, \ + struct spa_log_methods, log, 0, \ + lev, __VA_ARGS__); \ + } \ +}) + +/* Transparently calls to version 0 logv if v1 is not supported */ +#define spa_log_logtv(l,lev,topic,...) \ +({ \ + struct spa_log *_l = l; \ + struct spa_interface *_if = &_l->iface; \ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(_l, topic, lev))) { \ + if (!spa_interface_call(_if, \ + struct spa_log_methods, logtv, 1, \ + lev, topic, \ + __VA_ARGS__)) \ + spa_interface_call(_if, \ + struct spa_log_methods, logv, 0, \ + lev, __VA_ARGS__); \ + } \ +}) + +#define spa_logt_lev(l,lev,t,...) \ + spa_log_logt(l,lev,t,__FILE__,__LINE__,__func__,__VA_ARGS__) + +#define spa_log_lev(l,lev,...) \ + spa_logt_lev(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__) + +#define spa_log_log(l,lev,...) \ + spa_log_logt(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__) + +#define spa_log_logv(l,lev,...) \ + spa_log_logtv(l,lev,SPA_LOG_TOPIC_DEFAULT,__VA_ARGS__) + +#define spa_log_error(l,...) spa_log_lev(l,SPA_LOG_LEVEL_ERROR,__VA_ARGS__) +#define spa_log_warn(l,...) spa_log_lev(l,SPA_LOG_LEVEL_WARN,__VA_ARGS__) +#define spa_log_info(l,...) spa_log_lev(l,SPA_LOG_LEVEL_INFO,__VA_ARGS__) +#define spa_log_debug(l,...) spa_log_lev(l,SPA_LOG_LEVEL_DEBUG,__VA_ARGS__) +#define spa_log_trace(l,...) spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__) + +#define spa_logt_error(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_ERROR,t,__VA_ARGS__) +#define spa_logt_warn(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_WARN,t,__VA_ARGS__) +#define spa_logt_info(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_INFO,t,__VA_ARGS__) +#define spa_logt_debug(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_DEBUG,t,__VA_ARGS__) +#define spa_logt_trace(l,t,...) spa_logt_lev(l,SPA_LOG_LEVEL_TRACE,t,__VA_ARGS__) + +#ifndef FASTPATH +#define spa_log_trace_fp(l,...) spa_log_lev(l,SPA_LOG_LEVEL_TRACE,__VA_ARGS__) +#else +#define spa_log_trace_fp(l,...) +#endif + + +/** \fn spa_log_error */ + +/** keys can be given when initializing the logger handle */ +#define SPA_KEY_LOG_LEVEL "log.level" /**< the default log level */ +#define SPA_KEY_LOG_COLORS "log.colors" /**< enable colors in the logger */ +#define SPA_KEY_LOG_FILE "log.file" /**< log to the specified file instead of + * stderr. */ +#define SPA_KEY_LOG_TIMESTAMP "log.timestamp" /**< log timestamps */ +#define SPA_KEY_LOG_LINE "log.line" /**< log file and line numbers */ +#define SPA_KEY_LOG_PATTERNS "log.patterns" /**< Spa:String:JSON array of [ {"pattern" : level}, ... ] */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif +#endif /* SPA_LOG_H */ diff --git a/spa/include/spa/support/loop.h b/spa/include/spa/support/loop.h new file mode 100644 index 0000000..3c93573 --- /dev/null +++ b/spa/include/spa/support/loop.h @@ -0,0 +1,327 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_LOOP_H +#define SPA_LOOP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/** \defgroup spa_loop Loop + * Event loop interface + */ + +/** + * \addtogroup spa_loop + * \{ + */ + +#define SPA_TYPE_INTERFACE_Loop SPA_TYPE_INFO_INTERFACE_BASE "Loop" +#define SPA_TYPE_INTERFACE_DataLoop SPA_TYPE_INFO_INTERFACE_BASE "DataLoop" +#define SPA_VERSION_LOOP 0 +struct spa_loop { struct spa_interface iface; }; + +#define SPA_TYPE_INTERFACE_LoopControl SPA_TYPE_INFO_INTERFACE_BASE "LoopControl" +#define SPA_VERSION_LOOP_CONTROL 0 +struct spa_loop_control { struct spa_interface iface; }; + +#define SPA_TYPE_INTERFACE_LoopUtils SPA_TYPE_INFO_INTERFACE_BASE "LoopUtils" +#define SPA_VERSION_LOOP_UTILS 0 +struct spa_loop_utils { struct spa_interface iface; }; + +struct spa_source; + +typedef void (*spa_source_func_t) (struct spa_source *source); + +struct spa_source { + struct spa_loop *loop; + spa_source_func_t func; + void *data; + int fd; + uint32_t mask; + uint32_t rmask; + /* private data for the loop implementer */ + void *priv; +}; + +typedef int (*spa_invoke_func_t) (struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data); + +/** + * Register sources and work items to an event loop + */ +struct spa_loop_methods { + /* the version of this structure. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_LOOP_METHODS 0 + uint32_t version; + + /** add a source to the loop */ + int (*add_source) (void *object, + struct spa_source *source); + + /** update the source io mask */ + int (*update_source) (void *object, + struct spa_source *source); + + /** remove a source from the loop */ + int (*remove_source) (void *object, + struct spa_source *source); + + /** invoke a function in the context of this loop */ + int (*invoke) (void *object, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + bool block, + void *user_data); +}; + +#define spa_loop_method(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_loop *_o = o; \ + spa_interface_call_res(&_o->iface, \ + struct spa_loop_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_loop_add_source(l,...) spa_loop_method(l,add_source,0,##__VA_ARGS__) +#define spa_loop_update_source(l,...) spa_loop_method(l,update_source,0,##__VA_ARGS__) +#define spa_loop_remove_source(l,...) spa_loop_method(l,remove_source,0,##__VA_ARGS__) +#define spa_loop_invoke(l,...) spa_loop_method(l,invoke,0,##__VA_ARGS__) + + +/** Control hooks. These hooks can't be removed from their + * callbacks and must be removed from a safe place (when the loop + * is not running or when it is locked). */ +struct spa_loop_control_hooks { +#define SPA_VERSION_LOOP_CONTROL_HOOKS 0 + uint32_t version; + /** Executed right before waiting for events. It is typically used to + * release locks. */ + void (*before) (void *data); + /** Executed right after waiting for events. It is typically used to + * reacquire locks. */ + void (*after) (void *data); +}; + +#define spa_loop_control_hook_before(l) \ +({ \ + struct spa_hook_list *_l = l; \ + struct spa_hook *_h; \ + spa_list_for_each_reverse(_h, &_l->list, link) \ + spa_callbacks_call(&_h->cb, struct spa_loop_control_hooks, before, 0); \ +}) + +#define spa_loop_control_hook_after(l) \ +({ \ + struct spa_hook_list *_l = l; \ + struct spa_hook *_h; \ + spa_list_for_each(_h, &_l->list, link) \ + spa_callbacks_call(&_h->cb, struct spa_loop_control_hooks, after, 0); \ +}) + +/** + * Control an event loop + */ +struct spa_loop_control_methods { + /* the version of this structure. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_LOOP_CONTROL_METHODS 0 + uint32_t version; + + int (*get_fd) (void *object); + + /** Add a hook + * \param ctrl the control to change + * \param hooks the hooks to add + * + * Adds hooks to the loop controlled by \a ctrl. + */ + void (*add_hook) (void *object, + struct spa_hook *hook, + const struct spa_loop_control_hooks *hooks, + void *data); + + /** Enter a loop + * \param ctrl the control + * + * Start an iteration of the loop. This function should be called + * before calling iterate and is typically used to capture the thread + * that this loop will run in. + */ + void (*enter) (void *object); + /** Leave a loop + * \param ctrl the control + * + * Ends the iteration of a loop. This should be called after calling + * iterate. + */ + void (*leave) (void *object); + + /** Perform one iteration of the loop. + * \param ctrl the control + * \param timeout an optional timeout in milliseconds. + * 0 for no timeout, -1 for infinite timeout. + * + * This function will block + * up to \a timeout milliseconds and then dispatch the fds with activity. + * The number of dispatched fds is returned. + */ + int (*iterate) (void *object, int timeout); +}; + +#define spa_loop_control_method_v(o,method,version,...) \ +({ \ + struct spa_loop_control *_o = o; \ + spa_interface_call(&_o->iface, \ + struct spa_loop_control_methods, \ + method, version, ##__VA_ARGS__); \ +}) + +#define spa_loop_control_method_r(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_loop_control *_o = o; \ + spa_interface_call_res(&_o->iface, \ + struct spa_loop_control_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) + +#define spa_loop_control_get_fd(l) spa_loop_control_method_r(l,get_fd,0) +#define spa_loop_control_add_hook(l,...) spa_loop_control_method_v(l,add_hook,0,__VA_ARGS__) +#define spa_loop_control_enter(l) spa_loop_control_method_v(l,enter,0) +#define spa_loop_control_leave(l) spa_loop_control_method_v(l,leave,0) +#define spa_loop_control_iterate(l,...) spa_loop_control_method_r(l,iterate,0,__VA_ARGS__) + +typedef void (*spa_source_io_func_t) (void *data, int fd, uint32_t mask); +typedef void (*spa_source_idle_func_t) (void *data); +typedef void (*spa_source_event_func_t) (void *data, uint64_t count); +typedef void (*spa_source_timer_func_t) (void *data, uint64_t expirations); +typedef void (*spa_source_signal_func_t) (void *data, int signal_number); + +/** + * Create sources for an event loop + */ +struct spa_loop_utils_methods { + /* the version of this structure. This can be used to expand this + * structure in the future */ +#define SPA_VERSION_LOOP_UTILS_METHODS 0 + uint32_t version; + + struct spa_source *(*add_io) (void *object, + int fd, + uint32_t mask, + bool close, + spa_source_io_func_t func, void *data); + + int (*update_io) (void *object, struct spa_source *source, uint32_t mask); + + struct spa_source *(*add_idle) (void *object, + bool enabled, + spa_source_idle_func_t func, void *data); + int (*enable_idle) (void *object, struct spa_source *source, bool enabled); + + struct spa_source *(*add_event) (void *object, + spa_source_event_func_t func, void *data); + int (*signal_event) (void *object, struct spa_source *source); + + struct spa_source *(*add_timer) (void *object, + spa_source_timer_func_t func, void *data); + int (*update_timer) (void *object, + struct spa_source *source, + struct timespec *value, + struct timespec *interval, + bool absolute); + struct spa_source *(*add_signal) (void *object, + int signal_number, + spa_source_signal_func_t func, void *data); + + /** destroy a source allocated with this interface. This function + * should only be called when the loop is not running or from the + * context of the running loop */ + void (*destroy_source) (void *object, struct spa_source *source); +}; + +#define spa_loop_utils_method_v(o,method,version,...) \ +({ \ + struct spa_loop_utils *_o = o; \ + spa_interface_call(&_o->iface, \ + struct spa_loop_utils_methods, \ + method, version, ##__VA_ARGS__); \ +}) + +#define spa_loop_utils_method_r(o,method,version,...) \ +({ \ + int _res = -ENOTSUP; \ + struct spa_loop_utils *_o = o; \ + spa_interface_call_res(&_o->iface, \ + struct spa_loop_utils_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) +#define spa_loop_utils_method_s(o,method,version,...) \ +({ \ + struct spa_source *_res = NULL; \ + struct spa_loop_utils *_o = o; \ + spa_interface_call_res(&_o->iface, \ + struct spa_loop_utils_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) + + +#define spa_loop_utils_add_io(l,...) spa_loop_utils_method_s(l,add_io,0,__VA_ARGS__) +#define spa_loop_utils_update_io(l,...) spa_loop_utils_method_r(l,update_io,0,__VA_ARGS__) +#define spa_loop_utils_add_idle(l,...) spa_loop_utils_method_s(l,add_idle,0,__VA_ARGS__) +#define spa_loop_utils_enable_idle(l,...) spa_loop_utils_method_r(l,enable_idle,0,__VA_ARGS__) +#define spa_loop_utils_add_event(l,...) spa_loop_utils_method_s(l,add_event,0,__VA_ARGS__) +#define spa_loop_utils_signal_event(l,...) spa_loop_utils_method_r(l,signal_event,0,__VA_ARGS__) +#define spa_loop_utils_add_timer(l,...) spa_loop_utils_method_s(l,add_timer,0,__VA_ARGS__) +#define spa_loop_utils_update_timer(l,...) spa_loop_utils_method_r(l,update_timer,0,__VA_ARGS__) +#define spa_loop_utils_add_signal(l,...) spa_loop_utils_method_s(l,add_signal,0,__VA_ARGS__) +#define spa_loop_utils_destroy_source(l,...) spa_loop_utils_method_v(l,destroy_source,0,__VA_ARGS__) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_LOOP_H */ diff --git a/spa/include/spa/support/plugin-loader.h b/spa/include/spa/support/plugin-loader.h new file mode 100644 index 0000000..ced58cb --- /dev/null +++ b/spa/include/spa/support/plugin-loader.h @@ -0,0 +1,101 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PLUGIN_LOADER_H +#define SPA_PLUGIN_LOADER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_plugin_loader Plugin Loader + * SPA plugin loader + */ + +/** + * \addtogroup spa_plugin_loader + * \{ + */ + +#define SPA_TYPE_INTERFACE_PluginLoader SPA_TYPE_INFO_INTERFACE_BASE "PluginLoader" + +#define SPA_VERSION_PLUGIN_LOADER 0 +struct spa_plugin_loader { struct spa_interface iface; }; + +struct spa_plugin_loader_methods { +#define SPA_VERSION_PLUGIN_LOADER_METHODS 0 + uint32_t version; + + /** + * Load a SPA plugin. + * + * \param factory_name Plugin factory name + * \param info Info dictionary for plugin. NULL if none. + * \return plugin handle, or NULL on error + */ + struct spa_handle *(*load) (void *object, const char *factory_name, const struct spa_dict *info); + + /** + * Unload a SPA plugin. + * + * \param handle Plugin handle. + * \return 0 on success, < 0 on error + */ + int (*unload)(void *object, struct spa_handle *handle); +}; + +static inline struct spa_handle * +spa_plugin_loader_load(struct spa_plugin_loader *loader, const char *factory_name, const struct spa_dict *info) +{ + struct spa_handle *res = NULL; + if (SPA_LIKELY(loader != NULL)) + spa_interface_call_res(&loader->iface, + struct spa_plugin_loader_methods, res, + load, 0, factory_name, info); + return res; +} + +static inline int +spa_plugin_loader_unload(struct spa_plugin_loader *loader, struct spa_handle *handle) +{ + int res = -1; + if (SPA_LIKELY(loader != NULL)) + spa_interface_call_res(&loader->iface, + struct spa_plugin_loader_methods, res, + unload, 0, handle); + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PLUGIN_LOADER_H */ diff --git a/spa/include/spa/support/plugin.h b/spa/include/spa/support/plugin.h new file mode 100644 index 0000000..80cadda --- /dev/null +++ b/spa/include/spa/support/plugin.h @@ -0,0 +1,229 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_PLUGIN_H +#define SPA_PLUGIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * \defgroup spa_handle Plugin Handle + * SPA plugin handle and factory interfaces + */ + +/** + * \addtogroup spa_handle + * \{ + */ + +struct spa_handle { + /** Version of this struct */ +#define SPA_VERSION_HANDLE 0 + uint32_t version; + + /** + * Get the interface provided by \a handle with \a type. + * + * \a interface is always a struct spa_interface but depending on + * \a type, the struct might contain other information. + * + * \param handle a spa_handle + * \param type the interface type + * \param interface result to hold the interface. + * \return 0 on success + * -ENOTSUP when there are no interfaces + * -EINVAL when handle or info is NULL + */ + int (*get_interface) (struct spa_handle *handle, const char *type, void **interface); + /** + * Clean up the memory of \a handle. After this, \a handle should not be used + * anymore. + * + * \param handle a pointer to memory + * \return 0 on success + */ + int (*clear) (struct spa_handle *handle); +}; + +#define spa_handle_get_interface(h,...) (h)->get_interface((h),__VA_ARGS__) +#define spa_handle_clear(h) (h)->clear((h)) + +/** + * This structure lists the information about available interfaces on + * handles. + */ +struct spa_interface_info { + const char *type; /*< the type of the interface, can be + * used to get the interface */ +}; + +/** + * Extra supporting infrastructure passed to the init() function of + * a factory. It can be extra information or interfaces such as logging. + */ +struct spa_support { + const char *type; /*< the type of the support item */ + void *data; /*< specific data for the item */ +}; + +/** Find a support item of the given type */ +static inline void *spa_support_find(const struct spa_support *support, + uint32_t n_support, + const char *type) +{ + uint32_t i; + for (i = 0; i < n_support; i++) { + if (strcmp(support[i].type, type) == 0) + return support[i].data; + } + return NULL; +} + +#define SPA_SUPPORT_INIT(type,data) ((struct spa_support) { (type), (data) }) + +struct spa_handle_factory { + /** The version of this structure */ +#define SPA_VERSION_HANDLE_FACTORY 1 + uint32_t version; + /** + * The name of the factory contains a logical name that describes + * the function of the handle. Other plugins might contain an alternative + * implementation with the same name. + * + * See utils/names.h for the list of standard names. + * + * Examples include: + * + * api.alsa.pcm.sink: an object to write PCM samples to an alsa PLAYBACK + * device + * api.v4l2.source: an object to read from a v4l2 source. + */ + const char *name; + /** + * Extra information about the handles of this factory. + */ + const struct spa_dict *info; + /** + * Get the size of handles from this factory. + * + * \param factory a spa_handle_factory + * \param params extra parameters that determine the size of the + * handle. + */ + size_t (*get_size) (const struct spa_handle_factory *factory, + const struct spa_dict *params); + + /** + * Initialize an instance of this factory. The caller should allocate + * memory at least size bytes and pass this as \a handle. + * + * \a support can optionally contain extra interfaces or data items that the + * plugin can use such as a logger. + * + * \param factory a spa_handle_factory + * \param handle a pointer to memory + * \param info extra handle specific information, usually obtained + * from a spa_device. This can be used to configure the handle. + * \param support support items + * \param n_support number of elements in \a support + * \return 0 on success + * < 0 errno type error + */ + int (*init) (const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support); + + /** + * spa_handle_factory::enum_interface_info: + * \param factory: a #spa_handle_factory + * \param info: result to hold spa_interface_info. + * \param index: index to keep track of the enumeration, 0 for first item + * + * Enumerate the interface information for \a factory. + * + * \return 1 when an item is available + * 0 when no more items are available + * < 0 errno type error + */ + int (*enum_interface_info) (const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index); +}; + +#define spa_handle_factory_get_size(h,...) (h)->get_size((h),__VA_ARGS__) +#define spa_handle_factory_init(h,...) (h)->init((h),__VA_ARGS__) +#define spa_handle_factory_enum_interface_info(h,...) (h)->enum_interface_info((h),__VA_ARGS__) + +/** + * The function signature of the entry point in a plugin. + * + * \param factory a location to hold the factory result + * \param index index to keep track of the enumeration + * \return 1 on success + * 0 when there are no more factories + * -EINVAL when factory is NULL + */ +typedef int (*spa_handle_factory_enum_func_t) (const struct spa_handle_factory **factory, + uint32_t *index); + +#define SPA_HANDLE_FACTORY_ENUM_FUNC_NAME "spa_handle_factory_enum" + +/** + * The entry point in a plugin. + * + * \param factory a location to hold the factory result + * \param index index to keep track of the enumeration + * \return 1 on success + * 0 when no more items are available + * < 0 errno type error + */ +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index); + + + +#define SPA_KEY_FACTORY_NAME "factory.name" /**< the name of a factory */ +#define SPA_KEY_FACTORY_AUTHOR "factory.author" /**< a comma separated list of factory authors */ +#define SPA_KEY_FACTORY_DESCRIPTION "factory.description" /**< description of a factory */ +#define SPA_KEY_FACTORY_USAGE "factory.usage" /**< usage of a factory */ + +#define SPA_KEY_LIBRARY_NAME "library.name" /**< the name of a library. This is usually + * the filename of the plugin without the + * path or the plugin extension. */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_PLUGIN_H */ diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h new file mode 100644 index 0000000..79127d9 --- /dev/null +++ b/spa/include/spa/support/system.h @@ -0,0 +1,165 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_SYSTEM_H +#define SPA_SYSTEM_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct itimerspec; + +#include +#include + +#include +#include + +/** \defgroup spa_system System + * I/O, clock, polling, timer, and signal interfaces + */ + +/** + * \addtogroup spa_system + * \{ + */ + +/** + * a collection of core system functions + */ +#define SPA_TYPE_INTERFACE_System SPA_TYPE_INFO_INTERFACE_BASE "System" +#define SPA_TYPE_INTERFACE_DataSystem SPA_TYPE_INFO_INTERFACE_BASE "DataSystem" + +#define SPA_VERSION_SYSTEM 0 +struct spa_system { struct spa_interface iface; }; + +/* IO events */ +#define SPA_IO_IN (1 << 0) +#define SPA_IO_OUT (1 << 2) +#define SPA_IO_ERR (1 << 3) +#define SPA_IO_HUP (1 << 4) + +/* flags */ +#define SPA_FD_CLOEXEC (1<<0) +#define SPA_FD_NONBLOCK (1<<1) +#define SPA_FD_EVENT_SEMAPHORE (1<<2) +#define SPA_FD_TIMER_ABSTIME (1<<3) +#define SPA_FD_TIMER_CANCEL_ON_SET (1<<4) + +struct spa_poll_event { + uint32_t events; + void *data; +}; + +struct spa_system_methods { +#define SPA_VERSION_SYSTEM_METHODS 0 + uint32_t version; + + /* read/write/ioctl */ + ssize_t (*read) (void *object, int fd, void *buf, size_t count); + ssize_t (*write) (void *object, int fd, const void *buf, size_t count); + int (*ioctl) (void *object, int fd, unsigned long request, ...); + int (*close) (void *object, int fd); + + /* clock */ + int (*clock_gettime) (void *object, + int clockid, struct timespec *value); + int (*clock_getres) (void *object, + int clockid, struct timespec *res); + + /* poll */ + int (*pollfd_create) (void *object, int flags); + int (*pollfd_add) (void *object, int pfd, int fd, uint32_t events, void *data); + int (*pollfd_mod) (void *object, int pfd, int fd, uint32_t events, void *data); + int (*pollfd_del) (void *object, int pfd, int fd); + int (*pollfd_wait) (void *object, int pfd, + struct spa_poll_event *ev, int n_ev, int timeout); + + /* timers */ + int (*timerfd_create) (void *object, int clockid, int flags); + int (*timerfd_settime) (void *object, + int fd, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value); + int (*timerfd_gettime) (void *object, + int fd, struct itimerspec *curr_value); + int (*timerfd_read) (void *object, int fd, uint64_t *expirations); + + /* events */ + int (*eventfd_create) (void *object, int flags); + int (*eventfd_write) (void *object, int fd, uint64_t count); + int (*eventfd_read) (void *object, int fd, uint64_t *count); + + /* signals */ + int (*signalfd_create) (void *object, int signal, int flags); + int (*signalfd_read) (void *object, int fd, int *signal); +}; + +#define spa_system_method_r(o,method,version,...) \ +({ \ + volatile int _res = -ENOTSUP; \ + struct spa_system *_o = o; \ + spa_interface_call_res(&_o->iface, \ + struct spa_system_methods, _res, \ + method, version, ##__VA_ARGS__); \ + _res; \ +}) + + +#define spa_system_read(s,...) spa_system_method_r(s,read,0,__VA_ARGS__) +#define spa_system_write(s,...) spa_system_method_r(s,write,0,__VA_ARGS__) +#define spa_system_ioctl(s,...) spa_system_method_r(s,ioctl,0,__VA_ARGS__) +#define spa_system_close(s,...) spa_system_method_r(s,close,0,__VA_ARGS__) + +#define spa_system_clock_gettime(s,...) spa_system_method_r(s,clock_gettime,0,__VA_ARGS__) +#define spa_system_clock_getres(s,...) spa_system_method_r(s,clock_getres,0,__VA_ARGS__) + +#define spa_system_pollfd_create(s,...) spa_system_method_r(s,pollfd_create,0,__VA_ARGS__) +#define spa_system_pollfd_add(s,...) spa_system_method_r(s,pollfd_add,0,__VA_ARGS__) +#define spa_system_pollfd_mod(s,...) spa_system_method_r(s,pollfd_mod,0,__VA_ARGS__) +#define spa_system_pollfd_del(s,...) spa_system_method_r(s,pollfd_del,0,__VA_ARGS__) +#define spa_system_pollfd_wait(s,...) spa_system_method_r(s,pollfd_wait,0,__VA_ARGS__) + +#define spa_system_timerfd_create(s,...) spa_system_method_r(s,timerfd_create,0,__VA_ARGS__) +#define spa_system_timerfd_settime(s,...) spa_system_method_r(s,timerfd_settime,0,__VA_ARGS__) +#define spa_system_timerfd_gettime(s,...) spa_system_method_r(s,timerfd_gettime,0,__VA_ARGS__) +#define spa_system_timerfd_read(s,...) spa_system_method_r(s,timerfd_read,0,__VA_ARGS__) + +#define spa_system_eventfd_create(s,...) spa_system_method_r(s,eventfd_create,0,__VA_ARGS__) +#define spa_system_eventfd_write(s,...) spa_system_method_r(s,eventfd_write,0,__VA_ARGS__) +#define spa_system_eventfd_read(s,...) spa_system_method_r(s,eventfd_read,0,__VA_ARGS__) + +#define spa_system_signalfd_create(s,...) spa_system_method_r(s,signalfd_create,0,__VA_ARGS__) +#define spa_system_signalfd_read(s,...) spa_system_method_r(s,signalfd_read,0,__VA_ARGS__) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_SYSTEM_H */ diff --git a/spa/include/spa/support/thread.h b/spa/include/spa/support/thread.h new file mode 100644 index 0000000..ef3866f --- /dev/null +++ b/spa/include/spa/support/thread.h @@ -0,0 +1,149 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_THREAD_H +#define SPA_THREAD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +/** \defgroup spa_thread Thread + * Threading utility interfaces + */ + +/** + * \addtogroup spa_thread + * \{ + */ + +/** a thread object. + * This can be cast to a platform native thread, like pthread on posix systems + */ +#define SPA_TYPE_INFO_Thread SPA_TYPE_INFO_POINTER_BASE "Thread" +struct spa_thread; + +#define SPA_TYPE_INTERFACE_ThreadUtils SPA_TYPE_INFO_INTERFACE_BASE "ThreadUtils" +#define SPA_VERSION_THREAD_UTILS 0 +struct spa_thread_utils { struct spa_interface iface; }; + +/** thread utils */ +struct spa_thread_utils_methods { +#define SPA_VERSION_THREAD_UTILS_METHODS 0 + uint32_t version; + + /** create a new thread that runs \a start with \a arg */ + struct spa_thread * (*create) (void *object, const struct spa_dict *props, + void *(*start)(void*), void *arg); + /** stop and join a thread */ + int (*join)(void *object, struct spa_thread *thread, void **retval); + + /** get realtime priority range for threads created with \a props */ + int (*get_rt_range) (void *object, const struct spa_dict *props, int *min, int *max); + /** acquire realtime priority, a priority of -1 refers to the priority + * configured in the realtime module + */ + int (*acquire_rt) (void *object, struct spa_thread *thread, int priority); + /** drop realtime priority */ + int (*drop_rt) (void *object, struct spa_thread *thread); +}; + +/** \copydoc spa_thread_utils_methods.create + * \sa spa_thread_utils_methods.create */ +static inline struct spa_thread *spa_thread_utils_create(struct spa_thread_utils *o, + const struct spa_dict *props, void *(*start_routine)(void*), void *arg) +{ + struct spa_thread *res = NULL; + spa_interface_call_res(&o->iface, + struct spa_thread_utils_methods, res, create, 0, + props, start_routine, arg); + return res; +} + +/** \copydoc spa_thread_utils_methods.join + * \sa spa_thread_utils_methods.join */ +static inline int spa_thread_utils_join(struct spa_thread_utils *o, + struct spa_thread *thread, void **retval) +{ + int res = -ENOTSUP; + spa_interface_call_res(&o->iface, + struct spa_thread_utils_methods, res, join, 0, + thread, retval); + return res; +} + +/** \copydoc spa_thread_utils_methods.get_rt_range + * \sa spa_thread_utils_methods.get_rt_range */ +static inline int spa_thread_utils_get_rt_range(struct spa_thread_utils *o, + const struct spa_dict *props, int *min, int *max) +{ + int res = -ENOTSUP; + spa_interface_call_res(&o->iface, + struct spa_thread_utils_methods, res, get_rt_range, 0, + props, min, max); + return res; +} + +/** \copydoc spa_thread_utils_methods.acquire_rt + * \sa spa_thread_utils_methods.acquire_rt */ +static inline int spa_thread_utils_acquire_rt(struct spa_thread_utils *o, + struct spa_thread *thread, int priority) +{ + int res = -ENOTSUP; + spa_interface_call_res(&o->iface, + struct spa_thread_utils_methods, res, acquire_rt, 0, + thread, priority); + return res; +} + +/** \copydoc spa_thread_utils_methods.drop_rt + * \sa spa_thread_utils_methods.drop_rt */ +static inline int spa_thread_utils_drop_rt(struct spa_thread_utils *o, + struct spa_thread *thread) +{ + int res = -ENOTSUP; + spa_interface_call_res(&o->iface, + struct spa_thread_utils_methods, res, drop_rt, 0, thread); + return res; +} + +#define SPA_KEY_THREAD_NAME "thread.name" /* the thread name */ +#define SPA_KEY_THREAD_STACK_SIZE "thread.stack-size" /* the stack size of the thread */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_THREAD_H */ diff --git a/spa/include/spa/utils/ansi.h b/spa/include/spa/utils/ansi.h new file mode 100644 index 0000000..83726f8 --- /dev/null +++ b/spa/include/spa/utils/ansi.h @@ -0,0 +1,114 @@ +/* Simple Plugin API + * + * Copyright © 2021 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_ANSI_H +#define SPA_UTILS_ANSI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \defgroup spa_ansi ANSI codes + * ANSI color code macros + */ + +/** + * \addtogroup spa_ansi + * \{ + */ + +/** + * Ansi escape sequences. Note that the color names are approximate only and + * the actual rendering of the color depends on the terminal. + */ + +#define SPA_ANSI_RESET "\x1B[0m" +#define SPA_ANSI_BOLD "\x1B[1m" +#define SPA_ANSI_ITALIC "\x1B[3m" +#define SPA_ANSI_UNDERLINE "\x1B[4m" + +#define SPA_ANSI_BLACK "\x1B[0;30m" +#define SPA_ANSI_RED "\x1B[0;31m" +#define SPA_ANSI_GREEN "\x1B[0;32m" +#define SPA_ANSI_YELLOW "\x1B[0;33m" +#define SPA_ANSI_BLUE "\x1B[0;34m" +#define SPA_ANSI_MAGENTA "\x1B[0;35m" +#define SPA_ANSI_CYAN "\x1B[0;36m" +#define SPA_ANSI_WHITE "\x1B[0;37m" +#define SPA_ANSI_BRIGHT_BLACK "\x1B[90m" +#define SPA_ANSI_BRIGHT_RED "\x1B[91m" +#define SPA_ANSI_BRIGHT_GREEN "\x1B[92m" +#define SPA_ANSI_BRIGHT_YELLOW "\x1B[93m" +#define SPA_ANSI_BRIGHT_BLUE "\x1B[94m" +#define SPA_ANSI_BRIGHT_MAGENTA "\x1B[95m" +#define SPA_ANSI_BRIGHT_CYAN "\x1B[96m" +#define SPA_ANSI_BRIGHT_WHITE "\x1B[97m" + +/* Shortcut because it's a common use-case and easier than combining both */ +#define SPA_ANSI_BOLD_BLACK "\x1B[1;30m" +#define SPA_ANSI_BOLD_RED "\x1B[1;31m" +#define SPA_ANSI_BOLD_GREEN "\x1B[1;32m" +#define SPA_ANSI_BOLD_YELLOW "\x1B[1;33m" +#define SPA_ANSI_BOLD_BLUE "\x1B[1;34m" +#define SPA_ANSI_BOLD_MAGENTA "\x1B[1;35m" +#define SPA_ANSI_BOLD_CYAN "\x1B[1;36m" +#define SPA_ANSI_BOLD_WHITE "\x1B[1;37m" + +#define SPA_ANSI_DARK_BLACK "\x1B[2;30m" +#define SPA_ANSI_DARK_RED "\x1B[2;31m" +#define SPA_ANSI_DARK_GREEN "\x1B[2;32m" +#define SPA_ANSI_DARK_YELLOW "\x1B[2;33m" +#define SPA_ANSI_DARK_BLUE "\x1B[2;34m" +#define SPA_ANSI_DARK_MAGENTA "\x1B[2;35m" +#define SPA_ANSI_DARK_CYAN "\x1B[2;36m" +#define SPA_ANSI_DARK_WHITE "\x1B[2;37m" + +/* Background colors */ +#define SPA_ANSI_BG_BLACK "\x1B[0;40m" +#define SPA_ANSI_BG_RED "\x1B[0;41m" +#define SPA_ANSI_BG_GREEN "\x1B[0;42m" +#define SPA_ANSI_BG_YELLOW "\x1B[0;43m" +#define SPA_ANSI_BG_BLUE "\x1B[0;44m" +#define SPA_ANSI_BG_MAGENTA "\x1B[0;45m" +#define SPA_ANSI_BG_CYAN "\x1B[0;46m" +#define SPA_ANSI_BG_WHITE "\x1B[0;47m" +#define SPA_ANSI_BG_BRIGHT_BLACK "\x1B[100m" +#define SPA_ANSI_BG_BRIGHT_RED "\x1B[101m" +#define SPA_ANSI_BG_BRIGHT_GREEN "\x1B[102m" +#define SPA_ANSI_BG_BRIGHT_YELLOW "\x1B[103m" +#define SPA_ANSI_BG_BRIGHT_BLUE "\x1B[104m" +#define SPA_ANSI_BG_BRIGHT_MAGENTA "\x1B[105m" +#define SPA_ANSI_BG_BRIGHT_CYAN "\x1B[106m" +#define SPA_ANSI_BG_BRIGHT_WHITE "\x1B[107m" + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_ANSI_H */ diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h new file mode 100644 index 0000000..3b48626 --- /dev/null +++ b/spa/include/spa/utils/defs.h @@ -0,0 +1,399 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_DEFS_H +#define SPA_UTILS_DEFS_H + +#ifdef __cplusplus +extern "C" { +# if __cplusplus >= 201103L +# define SPA_STATIC_ASSERT static_assert +# endif +#else +# include +# if __STDC_VERSION__ >= 201112L +# define SPA_STATIC_ASSERT _Static_assert +# endif +#endif +#ifndef SPA_STATIC_ASSERT +#define SPA_STATIC_ASSERT(a, b) \ + ((void)sizeof(struct { int spa_static_assertion_failed : 2 * !!(a) - 1; })) +#endif +#include +#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; (void*)(ptr) < SPA_PTROFF(arr, sizeof(arr), void); (ptr)++) + +#define SPA_FOR_EACH_ELEMENT_VAR(arr, var) \ + for (__typeof__((arr)[0])* (var) = arr; (void*)(var) < SPA_PTROFF(arr, sizeof(arr), void); (var)++) + +#define SPA_ABS(a) \ +({ \ + __typeof__(a) _a = (a); \ + SPA_LIKELY(_a >= 0) ? _a : -_a; \ +}) +#define SPA_MIN(a,b) \ +({ \ + __typeof__(a) _min_a = (a); \ + __typeof__(b) _min_b = (b); \ + SPA_LIKELY(_min_a <= _min_b) ? _min_a : _min_b; \ +}) +#define SPA_MAX(a,b) \ +({ \ + __typeof__(a) _max_a = (a); \ + __typeof__(b) _max_b = (b); \ + SPA_LIKELY(_max_a >= _max_b) ? _max_a : _max_b; \ +}) +#define SPA_CLAMP(v,low,high) \ +({ \ + __typeof__(v) _v = (v); \ + __typeof__(low) _low = (low); \ + __typeof__(high) _high = (high); \ + SPA_MIN(SPA_MAX(_v, _low), _high); \ +}) + +#define SPA_CLAMPF(v,low,high) \ +({ \ + fminf(fmaxf(v, low), high); \ +}) + + +#define SPA_SWAP(a,b) \ +({ \ + __typeof__(a) _t = (a); \ + (a) = b; (b) = _t; \ +}) + +#define SPA_TYPECHECK(type,x) \ +({ type _dummy; \ + typeof(x) _dummy2; \ + (void)(&_dummy == &_dummy2); \ + x; \ +}) + +/** + * Return the address (buffer + offset) as pointer of \a type + */ +#define SPA_PTROFF(ptr_,offset_,type_) ((type_*)((uintptr_t)(ptr_) + (ptrdiff_t)(offset_))) +#define SPA_PTROFF_ALIGN(ptr_,offset_,alignment_,type_) \ + SPA_PTR_ALIGN(SPA_PTROFF(ptr_,offset_,type_),alignment_,type_) + + +/** + * Deprecated, use SPA_PTROFF and SPA_PTROFF_ALIGN instead + */ +#define SPA_MEMBER(b,o,t) SPA_PTROFF(b,o,t) +#define SPA_MEMBER_ALIGN(b,o,a,t) SPA_PTROFF_ALIGN(b,o,a,t) + +#define SPA_CONTAINER_OF(p,t,m) ((t*)((uintptr_t)(p) - offsetof(t,m))) + +#define SPA_PTRDIFF(p1,p2) ((intptr_t)(p1) - (intptr_t)(p2)) + +#define SPA_PTR_TO_INT(p) ((int) ((intptr_t) (p))) +#define SPA_INT_TO_PTR(u) ((void*) ((intptr_t) (u))) + +#define SPA_PTR_TO_UINT32(p) ((uint32_t) ((uintptr_t) (p))) +#define SPA_UINT32_TO_PTR(u) ((void*) ((uintptr_t) (u))) + +#define SPA_TIME_INVALID ((int64_t)INT64_MIN) +#define SPA_IDX_INVALID ((unsigned int)-1) +#define SPA_ID_INVALID ((uint32_t)0xffffffff) + +#define SPA_NSEC_PER_SEC (1000000000LL) +#define SPA_NSEC_PER_MSEC (1000000ll) +#define SPA_NSEC_PER_USEC (1000ll) +#define SPA_USEC_PER_SEC (1000000ll) +#define SPA_USEC_PER_MSEC (1000ll) +#define SPA_MSEC_PER_SEC (1000ll) + +#define SPA_TIMESPEC_TO_NSEC(ts) ((ts)->tv_sec * SPA_NSEC_PER_SEC + (ts)->tv_nsec) +#define SPA_TIMESPEC_TO_USEC(ts) ((ts)->tv_sec * SPA_USEC_PER_SEC + (ts)->tv_nsec / SPA_NSEC_PER_USEC) +#define SPA_TIMEVAL_TO_NSEC(tv) ((tv)->tv_sec * SPA_NSEC_PER_SEC + (tv)->tv_usec * SPA_NSEC_PER_USEC) +#define SPA_TIMEVAL_TO_USEC(tv) ((tv)->tv_sec * SPA_USEC_PER_SEC + (tv)->tv_usec) + +#ifdef __GNUC__ +#define SPA_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1))) +#define SPA_FORMAT_ARG_FUNC(arg1) __attribute__((format_arg(arg1))) +#define SPA_ALIGNED(align) __attribute__((aligned(align))) +#define SPA_DEPRECATED __attribute__ ((deprecated)) +#define SPA_EXPORT __attribute__((visibility("default"))) +#define SPA_SENTINEL __attribute__((__sentinel__)) +#define SPA_UNUSED __attribute__ ((unused)) +#define SPA_NORETURN __attribute__ ((noreturn)) +#define SPA_WARN_UNUSED_RESULT __attribute__ ((warn_unused_result)) +#else +#define SPA_PRINTF_FUNC(fmt, arg1) +#define SPA_FORMAT_ARG_FUNC(arg1) +#define SPA_ALIGNED(align) +#define SPA_DEPRECATED +#define SPA_EXPORT +#define SPA_SENTINEL +#define SPA_UNUSED +#define SPA_NORETURN +#define SPA_WARN_UNUSED_RESULT +#endif + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +#define SPA_RESTRICT restrict +#elif defined(__GNUC__) && __GNUC__ >= 4 +#define SPA_RESTRICT __restrict__ +#else +#define SPA_RESTRICT +#endif + +#define SPA_ROUND_DOWN(num,value) \ +({ \ + __typeof__(num) _num = (num); \ + ((_num) - ((_num) % (value))); \ +}) +#define SPA_ROUND_UP(num,value) \ +({ \ + __typeof__(value) _v = (value); \ + ((((num) + (_v) - 1) / (_v)) * (_v)); \ +}) + +#define SPA_ROUND_MASK(num,mask) ((__typeof__(num))((mask)-1)) + +#define SPA_ROUND_DOWN_N(num,align) ((num) & ~SPA_ROUND_MASK(num, align)) +#define SPA_ROUND_UP_N(num,align) ((((num)-1) | SPA_ROUND_MASK(num, align))+1) + +#define SPA_SCALE32_UP(val,num,denom) \ +({ \ + uint64_t _val = (val); \ + uint64_t _denom = (denom); \ + (uint32_t)(((_val) * (num) + (_denom)-1) / (_denom)); \ +}) + + +#define SPA_PTR_ALIGNMENT(p,align) ((intptr_t)(p) & ((align)-1)) +#define SPA_IS_ALIGNED(p,align) (SPA_PTR_ALIGNMENT(p,align) == 0) +#define SPA_PTR_ALIGN(p,align,type) ((type*)SPA_ROUND_UP_N((intptr_t)(p), (intptr_t)(align))) + +#ifndef SPA_LIKELY +#ifdef __GNUC__ +#define SPA_LIKELY(x) (__builtin_expect(!!(x),1)) +#define SPA_UNLIKELY(x) (__builtin_expect(!!(x),0)) +#else +#define SPA_LIKELY(x) (x) +#define SPA_UNLIKELY(x) (x) +#endif +#endif + +#define SPA_STRINGIFY_1(...) #__VA_ARGS__ +#define SPA_STRINGIFY(...) SPA_STRINGIFY_1(__VA_ARGS__) + +#define spa_return_if_fail(expr) \ + do { \ + if (SPA_UNLIKELY(!(expr))) { \ + fprintf(stderr, "'%s' failed at %s:%u %s()\n", \ + #expr , __FILE__, __LINE__, __func__); \ + return; \ + } \ + } while(false) + +#define spa_return_val_if_fail(expr, val) \ + do { \ + if (SPA_UNLIKELY(!(expr))) { \ + fprintf(stderr, "'%s' failed at %s:%u %s()\n", \ + #expr , __FILE__, __LINE__, __func__); \ + return (val); \ + } \ + } while(false) + +/* spa_assert_se() is an assert which guarantees side effects of x, + * i.e. is never optimized away, regardless of NDEBUG or FASTPATH. */ +#ifndef __COVERITY__ +#define spa_assert_se(expr) \ + do { \ + if (SPA_UNLIKELY(!(expr))) { \ + fprintf(stderr, "'%s' failed at %s:%u %s()\n", \ + #expr , __FILE__, __LINE__, __func__); \ + abort(); \ + } \ + } while (false) +#else +#define spa_assert_se(expr) \ + do { \ + int _unique_var = (expr); \ + if (!_unique_var) \ + abort(); \ + } while (false) +#endif + +/* Does exactly nothing */ +#define spa_nop() do {} while (false) + +#ifdef NDEBUG +#define spa_assert(expr) spa_nop() +#elif defined (FASTPATH) +#define spa_assert(expr) spa_assert_se(expr) +#else +#define spa_assert(expr) spa_assert_se(expr) +#endif + +#ifdef NDEBUG +#define spa_assert_not_reached() abort() +#else +#define spa_assert_not_reached() \ + do { \ + fprintf(stderr, "Code should not be reached at %s:%u %s()\n", \ + __FILE__, __LINE__, __func__); \ + abort(); \ + } while (false) +#endif + +#define spa_memzero(x,l) (memset((x), 0, (l))) +#define spa_zero(x) (spa_memzero(&(x), sizeof(x))) + +#ifdef SPA_DEBUG_MEMCPY +#define spa_memcpy(d,s,n) \ +({ \ + fprintf(stderr, "%s:%u %s() memcpy(%p, %p, %zd)\n", \ + __FILE__, __LINE__, __func__, (d), (s), (size_t)(n)); \ + memcpy(d,s,n); \ +}) +#define spa_memmove(d,s,n) \ +({ \ + fprintf(stderr, "%s:%u %s() memmove(%p, %p, %zd)\n", \ + __FILE__, __LINE__, __func__, (d), (s), (size_t)(n)); \ + memmove(d,s,n); \ +}) +#else +#define spa_memcpy(d,s,n) memcpy(d,s,n) +#define spa_memmove(d,s,n) memmove(d,s,n) +#endif + +#define spa_aprintf(_fmt, ...) \ +({ \ + char *_strp; \ + if (asprintf(&(_strp), (_fmt), ## __VA_ARGS__ ) == -1) \ + _strp = NULL; \ + _strp; \ +}) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_DEFS_H */ diff --git a/spa/include/spa/utils/dict.h b/spa/include/spa/utils/dict.h new file mode 100644 index 0000000..f9a0b5b --- /dev/null +++ b/spa/include/spa/utils/dict.h @@ -0,0 +1,120 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DICT_H +#define SPA_DICT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include + +/** + * \defgroup spa_dict Dictionary + * Dictionary data structure + */ + +/** + * \addtogroup spa_dict + * \{ + */ + +struct spa_dict_item { + const char *key; + const char *value; +}; + +#define SPA_DICT_ITEM_INIT(key,value) ((struct spa_dict_item) { (key), (value) }) + +struct spa_dict { +#define SPA_DICT_FLAG_SORTED (1<<0) /**< items are sorted */ + uint32_t flags; + uint32_t n_items; + const struct spa_dict_item *items; +}; + +#define SPA_DICT_INIT(items,n_items) ((struct spa_dict) { 0, (n_items), (items) }) +#define SPA_DICT_INIT_ARRAY(items) ((struct spa_dict) { 0, SPA_N_ELEMENTS(items), (items) }) + +#define spa_dict_for_each(item, dict) \ + for ((item) = (dict)->items; \ + (item) < &(dict)->items[(dict)->n_items]; \ + (item)++) + +static inline int spa_dict_item_compare(const void *i1, const void *i2) +{ + const struct spa_dict_item *it1 = (const struct spa_dict_item *)i1, + *it2 = (const struct spa_dict_item *)i2; + return strcmp(it1->key, it2->key); +} + +static inline void spa_dict_qsort(struct spa_dict *dict) +{ + if (dict->n_items > 0) + qsort((void*)dict->items, dict->n_items, sizeof(struct spa_dict_item), + spa_dict_item_compare); + SPA_FLAG_SET(dict->flags, SPA_DICT_FLAG_SORTED); +} + +static inline const struct spa_dict_item *spa_dict_lookup_item(const struct spa_dict *dict, + const char *key) +{ + const struct spa_dict_item *item; + + if (SPA_FLAG_IS_SET(dict->flags, SPA_DICT_FLAG_SORTED) && + dict->n_items > 0) { + struct spa_dict_item k = SPA_DICT_ITEM_INIT(key, NULL); + item = (const struct spa_dict_item *)bsearch(&k, + (const void *) dict->items, dict->n_items, + sizeof(struct spa_dict_item), + spa_dict_item_compare); + if (item != NULL) + return item; + } else { + spa_dict_for_each(item, dict) { + if (!strcmp(item->key, key)) + return item; + } + } + return NULL; +} + +static inline const char *spa_dict_lookup(const struct spa_dict *dict, const char *key) +{ + const struct spa_dict_item *item = spa_dict_lookup_item(dict, key); + return item ? item->value : NULL; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DICT_H */ diff --git a/spa/include/spa/utils/dll.h b/spa/include/spa/utils/dll.h new file mode 100644 index 0000000..65d8cd6 --- /dev/null +++ b/spa/include/spa/utils/dll.h @@ -0,0 +1,71 @@ +/* Simple DLL + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_DLL_H +#define SPA_DLL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define SPA_DLL_BW_MAX 0.128 +#define SPA_DLL_BW_MIN 0.016 + +struct spa_dll { + double bw; + double z1, z2, z3; + double w0, w1, w2; +}; + +static inline void spa_dll_init(struct spa_dll *dll) +{ + dll->bw = 0.0; + dll->z1 = dll->z2 = dll->z3 = 0.0; +} + +static inline void spa_dll_set_bw(struct spa_dll *dll, double bw, unsigned period, unsigned rate) +{ + double w = 2 * M_PI * bw * period / rate; + dll->w0 = 1.0 - exp (-20.0 * w); + dll->w1 = w * 1.5 / period; + dll->w2 = w / 1.5; + dll->bw = bw; +} + +static inline double spa_dll_update(struct spa_dll *dll, double err) +{ + dll->z1 += dll->w0 * (dll->w1 * err - dll->z1); + dll->z2 += dll->w0 * (dll->z1 - dll->z2); + dll->z3 += dll->w2 * dll->z2; + return 1.0 - (dll->z2 + dll->z3); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_DLL_H */ diff --git a/spa/include/spa/utils/enum-types.h b/spa/include/spa/utils/enum-types.h new file mode 100644 index 0000000..030d8f2 --- /dev/null +++ b/spa/include/spa/utils/enum-types.h @@ -0,0 +1,65 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_ENUM_TYPES_H +#define SPA_ENUM_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define SPA_TYPE_INFO_Direction SPA_TYPE_INFO_ENUM_BASE "Direction" +#define SPA_TYPE_INFO_DIRECTION_BASE SPA_TYPE_INFO_Direction ":" + +static const struct spa_type_info spa_type_direction[] = { + { SPA_DIRECTION_INPUT, SPA_TYPE_Int, SPA_TYPE_INFO_DIRECTION_BASE "Input", NULL }, + { SPA_DIRECTION_OUTPUT, SPA_TYPE_Int, SPA_TYPE_INFO_DIRECTION_BASE "Output", NULL }, + { 0, 0, NULL, NULL } +}; + +#include + +#define SPA_TYPE_INFO_Choice SPA_TYPE_INFO_ENUM_BASE "Choice" +#define SPA_TYPE_INFO_CHOICE_BASE SPA_TYPE_INFO_Choice ":" + +static const struct spa_type_info spa_type_choice[] = { + { SPA_CHOICE_None, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "None", NULL }, + { SPA_CHOICE_Range, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Range", NULL }, + { SPA_CHOICE_Step, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Step", NULL }, + { SPA_CHOICE_Enum, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Enum", NULL }, + { SPA_CHOICE_Flags, SPA_TYPE_Int, SPA_TYPE_INFO_CHOICE_BASE "Flags", NULL }, + { 0, 0, NULL, NULL } +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_TYPE_INFO_H */ diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h new file mode 100644 index 0000000..edcb716 --- /dev/null +++ b/spa/include/spa/utils/hook.h @@ -0,0 +1,472 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_HOOK_H +#define SPA_HOOK_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** \defgroup spa_interfaces Interfaces + * + * \brief Generic implementation of implementation-independent interfaces + * + * A SPA Interface is a generic struct that, together with a few macros, + * provides a generic way of invoking methods on objects without knowing the + * details of the implementation. + * + * The primary interaction with interfaces is through macros that expand into + * the right method call. For the implementation of an interface, we need two + * structs and a macro to invoke the `bar` method: + * + * \code{.c} + * // this struct must be public and defines the interface to a + * // struct foo + * struct foo_methods { + * uint32_t version; + * void (*bar)(void *object, const char *msg); + * }; + * + * // this struct does not need to be public + * struct foo { + * struct spa_interface iface; // must be first element, see foo_bar() + * int some_other_field; + * ... + * }; + * + * // if struct foo is private, we need to cast to a + * // generic spa_interface object + * #define foo_bar(obj, ...) ({ \ + * struct foo *f = obj; + * spa_interface_call((struct spa_interface *)f, // pointer to spa_interface in foo + * struct foo_methods, // type of callbacks + * bar, // name of methods + * 0, // hardcoded version to match foo_methods->version + * __VA_ARGS__ // pass rest of args through + * );/ + * }) + * \endcode + * + * The `struct foo_methods` and the invocation macro `foo_bar()` must be + * available to the caller. The implementation of `struct foo` can be private. + * + * \code{.c} + * void main(void) { + * struct foo *myfoo = get_foo_from_somewhere(); + * foo_bar(myfoo, "Invoking bar() on myfoo"); + * } + * \endcode + * The expansion of `foo_bar()` resolves roughly into this code: + * \code{.c} + * void main(void) { + * struct foo *myfoo = get_foo_from_somewhere(); + * // foo_bar(myfoo, "Invoking bar() on myfoo"); + * const struct foo_methods *methods = ((struct spa_interface*)myfoo)->cb; + * if (0 >= methods->version && // version check + * methods->bar) // compile error if this function does not exist, + * methods->bar(myfoo, "Invoking bar() on myfoo"); + * } + * \endcode + * + * The typecast used in `foo_bar()` allows `struct foo` to be opaque to the + * caller. The implementation may assign the callback methods at object + * instantiation, and the caller will transparently invoke the method on the + * given object. For example, the following code assigns a different `bar()` method on + * Mondays - the caller does not need to know this. + * \code{.c} + * + * static void bar_stdout(struct foo *f, const char *msg) { + * printf(msg); + * } + * static void bar_stderr(struct foo *f, const char *msg) { + * fprintf(stderr, msg); + * } + * + * struct foo* get_foo_from_somewhere() { + * struct foo *f = calloc(sizeof struct foo); + * // illustrative only, use SPA_INTERFACE_INIT() + * f->iface->cb = (struct foo_methods*) { .bar = bar_stdout }; + * if (today_is_monday) + * f->iface->cb = (struct foo_methods*) { .bar = bar_stderr }; + * return f; + * } + * \endcode + */ + +/** + * \addtogroup spa_interfaces + * \{ + */ + +/** \struct spa_callbacks + * Callbacks, contains the structure with functions and the data passed + * to the functions. The structure should also contain a version field that + * is checked. */ +struct spa_callbacks { + const void *funcs; + void *data; +}; + +/** Check if a callback \a c is of at least version \a v */ +#define SPA_CALLBACK_VERSION_MIN(c,v) ((c) && ((v) == 0 || (c)->version > (v)-1)) + +/** Check if a callback \a c has method \a m of version \a v */ +#define SPA_CALLBACK_CHECK(c,m,v) (SPA_CALLBACK_VERSION_MIN(c,v) && (c)->m) + +/** + * Initialize the set of functions \a funcs as a \ref spa_callbacks, together + * with \a _data. + */ +#define SPA_CALLBACKS_INIT(_funcs,_data) ((struct spa_callbacks){ (_funcs), (_data), }) + +/** \struct spa_interface + */ +struct spa_interface { + const char *type; + uint32_t version; + struct spa_callbacks cb; +}; + +/** + * Initialize a \ref spa_interface. + * + * \code{.c} + * const static struct foo_methods foo_funcs = { + * .bar = some_bar_implementation, + * }; + * + * struct foo *f = malloc(...); + * f->iface = SPA_INTERFACE_INIT("foo type", 0, foo_funcs, NULL); + * \endcode + * + */ +#define SPA_INTERFACE_INIT(_type,_version,_funcs,_data) \ + ((struct spa_interface){ (_type), (_version), SPA_CALLBACKS_INIT(_funcs,_data), }) + +/** + * Invoke method named \a method in the \a callbacks. + * The \a method_type defines the type of the method struct. + * Returns true if the method could be called, false otherwise. + */ +#define spa_callbacks_call(callbacks,type,method,vers,...) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + bool _res = SPA_CALLBACK_CHECK(_f,method,vers); \ + if (SPA_LIKELY(_res)) \ + _f->method((callbacks)->data, ## __VA_ARGS__); \ + _res; \ +}) + +/** + * True if the \a callbacks are of version \a vers, false otherwise + */ +#define spa_callback_version_min(callbacks,type,vers) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + SPA_CALLBACK_VERSION_MIN(_f,vers); \ +}) + +/** + * True if the \a callbacks contains \a method of version + * \a vers, false otherwise + */ +#define spa_callback_check(callbacks,type,method,vers) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + SPA_CALLBACK_CHECK(_f,method,vers); \ +}) + +/** + * Invoke method named \a method in the \a callbacks. + * The \a method_type defines the type of the method struct. + * + * The return value is stored in \a res. + */ +#define spa_callbacks_call_res(callbacks,type,res,method,vers,...) \ +({ \ + const type *_f = (const type *) (callbacks)->funcs; \ + if (SPA_LIKELY(SPA_CALLBACK_CHECK(_f,method,vers))) \ + res = _f->method((callbacks)->data, ## __VA_ARGS__); \ + res; \ +}) + +/** + * True if the \a iface's callbacks are of version \a vers, false otherwise + */ +#define spa_interface_callback_version_min(iface,method_type,vers) \ + spa_callback_version_min(&(iface)->cb, method_type, vers) + +/** + * True if the \a iface's callback \a method is of version \a vers + * and exists, false otherwise + */ +#define spa_interface_callback_check(iface,method_type,method,vers) \ + spa_callback_check(&(iface)->cb, method_type, method, vers) + +/** + * Invoke method named \a method in the callbacks on the given interface object. + * The \a method_type defines the type of the method struct, not the interface + * itself. + */ +#define spa_interface_call(iface,method_type,method,vers,...) \ + spa_callbacks_call(&(iface)->cb,method_type,method,vers,##__VA_ARGS__) + +/** + * Invoke method named \a method in the callbacks on the given interface object. + * The \a method_type defines the type of the method struct, not the interface + * itself. + * + * The return value is stored in \a res. + */ +#define spa_interface_call_res(iface,method_type,res,method,vers,...) \ + spa_callbacks_call_res(&(iface)->cb,method_type,res,method,vers,##__VA_ARGS__) + +/** + * \} + */ + +/** \defgroup spa_hooks Hooks + * + * A SPA Hook is a data structure to keep track of callbacks. It is similar to + * the \ref spa_interfaces and typically used where an implementation allows + * for multiple external callback functions. For example, an implementation may + * use a hook list to implement signals with each caller using a hook to + * register callbacks to be invoked on those signals. + * + * The below (pseudo)code is a minimal example outlining the use of hooks: + * \code{.c} + * // the public interface + * #define VERSION_BAR_EVENTS 0 // version of the vtable + * struct bar_events { + * uint32_t version; // NOTE: an integral member named `version` + * // must be present in the vtable + * void (*boom)(void *data, const char *msg); + * }; + * + * // private implementation + * struct party { + * struct spa_hook_list bar_list; + * }; + * + * void party_add_event_listener(struct party *p, struct spa_hook *listener, + * const struct bar_events *events, void *data) + * { + * spa_hook_list_append(&p->bar_list, listener, events, data); + * } + * + * static void party_on(struct party *p) + * { + * // NOTE: this is a macro, it evaluates to an integer, + * // which is the number of hooks called + * spa_hook_list_call(&p->list, + * struct bar_events, // vtable type + * boom, // function name + * 0, // hardcoded version, + * // usually the version in which `boom` + * // has been added to the vtable + * "party on, wayne" // function argument(s) + * ); + * } + * \endcode + * + * In the caller, the hooks can be used like this: + * \code{.c} + * static void boom_cb(void *data, const char *msg) { + * // data is userdata from main() + * printf("%s", msg); + * } + * + * static const struct bar_events events = { + * .version = VERSION_BAR_EVENTS, // version of the implemented interface + * .boom = boom_cb, + * }; + * + * void main(void) { + * void *userdata = whatever; + * struct spa_hook hook; + * struct party *p = start_the_party(); + * + * party_add_event_listener(p, &hook, &events, userdata); + * + * mainloop(); + * return 0; + * } + * + * \endcode + */ + +/** + * \addtogroup spa_hooks + * \{ + */ + +/** \struct spa_hook_list + * A list of hooks. This struct is primarily used by + * implementation that use multiple caller-provided \ref spa_hook. */ +struct spa_hook_list { + struct spa_list list; +}; + + +/** \struct spa_hook + * A hook, contains the structure with functions and the data passed + * to the functions. + * + * A hook should be treated as opaque by the caller. + */ +struct spa_hook { + struct spa_list link; + struct spa_callbacks cb; + /** callback and data for the hook list, private to the + * hook_list implementor */ + void (*removed) (struct spa_hook *hook); + void *priv; +}; + +/** Initialize a hook list to the empty list*/ +static inline void spa_hook_list_init(struct spa_hook_list *list) +{ + spa_list_init(&list->list); +} + +static inline bool spa_hook_list_is_empty(struct spa_hook_list *list) +{ + return spa_list_is_empty(&list->list); +} + +/** Append a hook. */ +static inline void spa_hook_list_append(struct spa_hook_list *list, + struct spa_hook *hook, + const void *funcs, void *data) +{ + spa_zero(*hook); + hook->cb = SPA_CALLBACKS_INIT(funcs, data); + spa_list_append(&list->list, &hook->link); +} + +/** Prepend a hook */ +static inline void spa_hook_list_prepend(struct spa_hook_list *list, + struct spa_hook *hook, + const void *funcs, void *data) +{ + spa_zero(*hook); + hook->cb = SPA_CALLBACKS_INIT(funcs, data); + spa_list_prepend(&list->list, &hook->link); +} + +/** Remove a hook */ +static inline void spa_hook_remove(struct spa_hook *hook) +{ + if (spa_list_is_initialized(&hook->link)) + spa_list_remove(&hook->link); + if (hook->removed) + hook->removed(hook); +} + +/** Remove all hooks from the list */ +static inline void spa_hook_list_clean(struct spa_hook_list *list) +{ + struct spa_hook *h; + spa_list_consume(h, &list->list, link) + spa_hook_remove(h); +} + +static inline void +spa_hook_list_isolate(struct spa_hook_list *list, + struct spa_hook_list *save, + struct spa_hook *hook, + const void *funcs, void *data) +{ + /* init save list and move hooks to it */ + spa_hook_list_init(save); + spa_list_insert_list(&save->list, &list->list); + /* init hooks and add single hook */ + spa_hook_list_init(list); + spa_hook_list_append(list, hook, funcs, data); +} + +static inline void +spa_hook_list_join(struct spa_hook_list *list, + struct spa_hook_list *save) +{ + spa_list_insert_list(&list->list, &save->list); +} + +#define spa_hook_list_call_simple(l,type,method,vers,...) \ +({ \ + struct spa_hook_list *_l = l; \ + struct spa_hook *_h, *_t; \ + spa_list_for_each_safe(_h, _t, &_l->list, link) \ + spa_callbacks_call(&_h->cb,type,method,vers, ## __VA_ARGS__); \ +}) + +/** Call all hooks in a list, starting from the given one and optionally stopping + * after calling the first non-NULL function, returns the number of methods + * called */ +#define spa_hook_list_do_call(l,start,type,method,vers,once,...) \ +({ \ + struct spa_hook_list *_list = l; \ + struct spa_list *_s = start ? (struct spa_list *)start : &_list->list; \ + struct spa_hook _cursor = { 0 }, *_ci; \ + int _count = 0; \ + spa_list_cursor_start(_cursor, _s, link); \ + spa_list_for_each_cursor(_ci, _cursor, &_list->list, link) { \ + if (spa_callbacks_call(&_ci->cb,type,method,vers, ## __VA_ARGS__)) { \ + _count++; \ + if (once) \ + break; \ + } \ + } \ + spa_list_cursor_end(_cursor, link); \ + _count; \ +}) + +/** + * Call the method named \a m for each element in list \a l. + * \a t specifies the type of the callback struct. + */ +#define spa_hook_list_call(l,t,m,v,...) spa_hook_list_do_call(l,NULL,t,m,v,false,##__VA_ARGS__) +/** + * Call the method named \a m for each element in list \a l, stopping after + * the first invocation. + * \a t specifies the type of the callback struct. + */ +#define spa_hook_list_call_once(l,t,m,v,...) spa_hook_list_do_call(l,NULL,t,m,v,true,##__VA_ARGS__) + +#define spa_hook_list_call_start(l,s,t,m,v,...) spa_hook_list_do_call(l,s,t,m,v,false,##__VA_ARGS__) +#define spa_hook_list_call_once_start(l,s,t,m,v,...) spa_hook_list_do_call(l,s,t,m,v,true,##__VA_ARGS__) + +/** + * \} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* SPA_HOOK_H */ diff --git a/spa/include/spa/utils/json-pod.h b/spa/include/spa/utils/json-pod.h new file mode 100644 index 0000000..34c7e08 --- /dev/null +++ b/spa/include/spa/utils/json-pod.h @@ -0,0 +1,177 @@ +/* Simple Plugin API + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_JSON_POD_H +#define SPA_UTILS_JSON_POD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +/** \defgroup spa_json_pod JSON to POD + * JSON to POD conversion + */ + +/** + * \addtogroup spa_json_pod + * \{ + */ + +static inline int spa_json_to_pod_part(struct spa_pod_builder *b, uint32_t flags, uint32_t id, + const struct spa_type_info *info, struct spa_json *iter, const char *value, int len) +{ + const struct spa_type_info *ti; + char key[256]; + struct spa_pod_frame f[1]; + struct spa_json it[1]; + int l, res; + const char *v; + uint32_t type; + + if (spa_json_is_object(value, len) && info != NULL) { + if ((ti = spa_debug_type_find(NULL, info->parent)) == NULL) + return -EINVAL; + + spa_pod_builder_push_object(b, &f[0], info->parent, id); + + spa_json_enter(iter, &it[0]); + while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) { + const struct spa_type_info *pi; + if ((l = spa_json_next(&it[0], &v)) <= 0) + break; + if ((pi = spa_debug_type_find_short(ti->values, key)) != NULL) + type = pi->type; + else if (!spa_atou32(key, &type, 0)) + continue; + spa_pod_builder_prop(b, type, 0); + if ((res = spa_json_to_pod_part(b, flags, id, pi, &it[0], v, l)) < 0) + return res; + } + spa_pod_builder_pop(b, &f[0]); + } + else if (spa_json_is_array(value, len)) { + if (info == NULL || info->parent == SPA_TYPE_Struct) { + spa_pod_builder_push_struct(b, &f[0]); + } else { + spa_pod_builder_push_array(b, &f[0]); + info = info->values; + } + spa_json_enter(iter, &it[0]); + while ((l = spa_json_next(&it[0], &v)) > 0) + if ((res = spa_json_to_pod_part(b, flags, id, info, &it[0], v, l)) < 0) + return res; + spa_pod_builder_pop(b, &f[0]); + } + else if (spa_json_is_float(value, len)) { + float val = 0.0f; + spa_json_parse_float(value, len, &val); + switch (info ? info->parent : (uint32_t)SPA_TYPE_Struct) { + case SPA_TYPE_Bool: + spa_pod_builder_bool(b, val >= 0.5f); + break; + case SPA_TYPE_Id: + spa_pod_builder_id(b, val); + break; + case SPA_TYPE_Int: + spa_pod_builder_int(b, val); + break; + case SPA_TYPE_Long: + spa_pod_builder_long(b, val); + break; + case SPA_TYPE_Struct: + if (spa_json_is_int(value, len)) + spa_pod_builder_int(b, val); + else + spa_pod_builder_float(b, val); + break; + case SPA_TYPE_Float: + spa_pod_builder_float(b, val); + break; + case SPA_TYPE_Double: + spa_pod_builder_double(b, val); + break; + default: + spa_pod_builder_none(b); + break; + } + } + else if (spa_json_is_bool(value, len)) { + bool val = false; + spa_json_parse_bool(value, len, &val); + spa_pod_builder_bool(b, val); + } + else if (spa_json_is_null(value, len)) { + spa_pod_builder_none(b); + } + else { + char *val = (char*)alloca(len+1); + spa_json_parse_stringn(value, len, val, len+1); + switch (info ? info->parent : (uint32_t)SPA_TYPE_Struct) { + case SPA_TYPE_Id: + if ((ti = spa_debug_type_find_short(info->values, val)) != NULL) + type = ti->type; + else if (!spa_atou32(val, &type, 0)) + return -EINVAL; + spa_pod_builder_id(b, type); + break; + case SPA_TYPE_Struct: + case SPA_TYPE_String: + spa_pod_builder_string(b, val); + break; + default: + spa_pod_builder_none(b); + break; + } + } + return 0; +} + +static inline int spa_json_to_pod(struct spa_pod_builder *b, uint32_t flags, + const struct spa_type_info *info, const char *value, int len) +{ + struct spa_json iter; + const char *val; + + spa_json_init(&iter, value, len); + if ((len = spa_json_next(&iter, &val)) <= 0) + return -EINVAL; + + return spa_json_to_pod_part(b, flags, info->type, info, &iter, val, len); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_POD_H */ diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h new file mode 100644 index 0000000..0f93149 --- /dev/null +++ b/spa/include/spa/utils/json.h @@ -0,0 +1,485 @@ +/* Simple Plugin API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_JSON_H +#define SPA_UTILS_JSON_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +#include +#include + +/** \defgroup spa_json JSON + * Relaxed JSON variant parsing + */ + +/** + * \addtogroup spa_json + * \{ + */ + +/* a simple JSON compatible tokenizer */ +struct spa_json { + const char *cur; + const char *end; + struct spa_json *parent; + uint32_t state; + uint32_t depth; +}; + +#define SPA_JSON_INIT(data,size) ((struct spa_json) { (data), (data)+(size), }) + +static inline void spa_json_init(struct spa_json * iter, const char *data, size_t size) +{ + *iter = SPA_JSON_INIT(data, size); +} +#define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), }) + +static inline void spa_json_enter(struct spa_json * iter, struct spa_json * sub) +{ + *sub = SPA_JSON_ENTER(iter); +} + +#define SPA_JSON_SAVE(iter) ((struct spa_json) { (iter)->cur, (iter)->end, }) + +/** Get the next token. \a value points to the token and the return value + * is the length. */ +static inline int spa_json_next(struct spa_json * iter, const char **value) +{ + int utf8_remain = 0; + enum { __NONE, __STRUCT, __BARE, __STRING, __UTF8, __ESC, __COMMENT }; + + *value = iter->cur; + for (; iter->cur < iter->end; iter->cur++) { + unsigned char cur = (unsigned char)*iter->cur; + again: + switch (iter->state) { + case __NONE: + iter->state = __STRUCT; + iter->depth = 0; + goto again; + case __STRUCT: + switch (cur) { + case '\0': case '\t': case ' ': case '\r': case '\n': case ':': case '=': case ',': + continue; + case '#': + iter->state = __COMMENT; + continue; + case '"': + *value = iter->cur; + iter->state = __STRING; + continue; + case '[': case '{': + *value = iter->cur; + if (++iter->depth > 1) + continue; + iter->cur++; + return 1; + case '}': case ']': + if (iter->depth == 0) { + if (iter->parent) + iter->parent->cur = iter->cur; + return 0; + } + --iter->depth; + continue; + default: + *value = iter->cur; + iter->state = __BARE; + } + continue; + case __BARE: + switch (cur) { + case '\t': case ' ': case '\r': case '\n': + case ':': case ',': case '=': case ']': case '}': + iter->state = __STRUCT; + if (iter->depth > 0) + goto again; + return iter->cur - *value; + } + continue; + case __STRING: + switch (cur) { + case '\\': + iter->state = __ESC; + continue; + case '"': + iter->state = __STRUCT; + if (iter->depth > 0) + continue; + return ++iter->cur - *value; + case 240 ... 247: + utf8_remain++; + SPA_FALLTHROUGH; + case 224 ... 239: + utf8_remain++; + SPA_FALLTHROUGH; + case 192 ... 223: + utf8_remain++; + iter->state = __UTF8; + continue; + default: + if (cur >= 32 && cur <= 126) + continue; + } + return -1; + case __UTF8: + switch (cur) { + case 128 ... 191: + if (--utf8_remain == 0) + iter->state = __STRING; + continue; + } + return -1; + case __ESC: + switch (cur) { + case '"': case '\\': case '/': case 'b': case 'f': + case 'n': case 'r': case 't': case 'u': + iter->state = __STRING; + continue; + } + return -1; + case __COMMENT: + switch (cur) { + case '\n': case '\r': + iter->state = __STRUCT; + } + } + + } + if (iter->depth != 0) + return -1; + if (iter->state != __STRUCT) { + iter->state = __STRUCT; + return iter->cur - *value; + } + return 0; +} + +static inline int spa_json_enter_container(struct spa_json *iter, struct spa_json *sub, char type) +{ + const char *value; + if (spa_json_next(iter, &value) <= 0 || *value != type) + return -1; + spa_json_enter(iter, sub); + return 1; +} + +static inline int spa_json_is_container(const char *val, int len) +{ + return len > 0 && (*val == '{' || *val == '['); +} + +static inline int spa_json_container_len(struct spa_json *iter, const char *value, int len) +{ + const char *val; + struct spa_json sub; + spa_json_enter(iter, &sub); + while (spa_json_next(&sub, &val) > 0); + return sub.cur + 1 - value; +} + +/* object */ +static inline int spa_json_is_object(const char *val, int len) +{ + return len > 0 && *val == '{'; +} +static inline int spa_json_enter_object(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '{'); +} + +/* array */ +static inline bool spa_json_is_array(const char *val, int len) +{ + return len > 0 && *val == '['; +} +static inline int spa_json_enter_array(struct spa_json *iter, struct spa_json *sub) +{ + return spa_json_enter_container(iter, sub, '['); +} + +/* null */ +static inline bool spa_json_is_null(const char *val, int len) +{ + return len == 4 && strncmp(val, "null", 4) == 0; +} + +/* float */ +static inline int spa_json_parse_float(const char *val, int len, float *result) +{ + char *end; + if (strspn(val, "+-0123456789.Ee") < (size_t)len) + return 0; + *result = spa_strtof(val, &end); + return len > 0 && end == val + len; +} + +static inline bool spa_json_is_float(const char *val, int len) +{ + float dummy; + return spa_json_parse_float(val, len, &dummy); +} +static inline int spa_json_get_float(struct spa_json *iter, float *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_float(value, len, res); +} + +static inline char *spa_json_format_float(char *str, int size, float val) +{ + if (SPA_UNLIKELY(!isnormal(val))) { + if (val == INFINITY) + val = FLT_MAX; + else if (val == -INFINITY) + val = FLT_MIN; + else + val = 0.0f; + } + return spa_dtoa(str, size, val); +} + +/* int */ +static inline int spa_json_parse_int(const char *val, int len, int *result) +{ + char *end; + *result = strtol(val, &end, 0); + return len > 0 && end == val + len; +} +static inline bool spa_json_is_int(const char *val, int len) +{ + int dummy; + return spa_json_parse_int(val, len, &dummy); +} +static inline int spa_json_get_int(struct spa_json *iter, int *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_int(value, len, res); +} + +/* bool */ +static inline bool spa_json_is_true(const char *val, int len) +{ + return len == 4 && strncmp(val, "true", 4) == 0; +} + +static inline bool spa_json_is_false(const char *val, int len) +{ + return len == 5 && strncmp(val, "false", 5) == 0; +} + +static inline bool spa_json_is_bool(const char *val, int len) +{ + return spa_json_is_true(val, len) || spa_json_is_false(val, len); +} + +static inline int spa_json_parse_bool(const char *val, int len, bool *result) +{ + if ((*result = spa_json_is_true(val, len))) + return 1; + if (!(*result = !spa_json_is_false(val, len))) + return 1; + return -1; +} +static inline int spa_json_get_bool(struct spa_json *iter, bool *res) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_bool(value, len, res); +} + +/* string */ +static inline bool spa_json_is_string(const char *val, int len) +{ + return len > 1 && *val == '"'; +} + +static inline int spa_json_parse_hex(const char *p, int num, uint32_t *res) +{ + int i; + *res = 0; + for (i = 0; i < num; i++) { + char v = p[i]; + if (v >= '0' && v <= '9') + v = v - '0'; + else if (v >= 'a' && v <= 'f') + v = v - 'a' + 10; + else if (v >= 'A' && v <= 'F') + v = v - 'A' + 10; + else + return -1; + *res = (*res << 4) | v; + } + return 1; +} + +static inline int spa_json_parse_stringn(const char *val, int len, char *result, int maxlen) +{ + const char *p; + if (maxlen <= len) + return -1; + if (!spa_json_is_string(val, len)) { + if (result != val) + strncpy(result, val, len); + result += len; + } else { + for (p = val+1; p < val + len; p++) { + if (*p == '\\') { + p++; + if (*p == 'n') + *result++ = '\n'; + else if (*p == 'r') + *result++ = '\r'; + else if (*p == 'b') + *result++ = '\b'; + else if (*p == 't') + *result++ = '\t'; + else if (*p == 'f') + *result++ = '\f'; + else if (*p == 'u') { + uint8_t prefix[] = { 0, 0xc0, 0xe0, 0xf0 }; + uint32_t idx, n, v, cp, enc[] = { 0x80, 0x800, 0x10000 }; + if (val + len - p < 5 || + spa_json_parse_hex(p+1, 4, &cp) < 0) { + *result++ = *p; + continue; + } + p += 4; + + if (cp >= 0xd800 && cp <= 0xdbff) { + if (val + len - p < 7 || + p[1] != '\\' || p[2] != 'u' || + spa_json_parse_hex(p+3, 4, &v) < 0 || + v < 0xdc00 || v > 0xdfff) + continue; + p += 6; + cp = 0x010000 | ((cp & 0x3ff) << 10) | (v & 0x3ff); + } else if (cp >= 0xdc00 && cp <= 0xdfff) + continue; + + for (idx = 0; idx < 3; idx++) + if (cp < enc[idx]) + break; + for (n = idx; n > 0; n--, cp >>= 6) + result[n] = (cp | 0x80) & 0xbf; + *result++ = (cp | prefix[idx]) & 0xff; + result += idx; + } else + *result++ = *p; + } else if (*p == '\"') { + break; + } else + *result++ = *p; + } + } + *result = '\0'; + return 1; +} + +static inline int spa_json_parse_string(const char *val, int len, char *result) +{ + return spa_json_parse_stringn(val, len, result, len+1); +} + +static inline int spa_json_get_string(struct spa_json *iter, char *res, int maxlen) +{ + const char *value; + int len; + if ((len = spa_json_next(iter, &value)) <= 0) + return -1; + return spa_json_parse_stringn(value, len, res, maxlen); +} + +static inline int spa_json_encode_string(char *str, int size, const char *val) +{ + int len = 0; + static const char hex[] = { "0123456789abcdef" }; +#define __PUT(c) { if (len < size) *str++ = c; len++; } + __PUT('"'); + while (*val) { + switch (*val) { + case '\n': + __PUT('\\'); __PUT('n'); + break; + case '\r': + __PUT('\\'); __PUT('r'); + break; + case '\b': + __PUT('\\'); __PUT('b'); + break; + case '\t': + __PUT('\\'); __PUT('t'); + break; + case '\f': + __PUT('\\'); __PUT('f'); + break; + case '\\': + case '"': + __PUT('\\'); __PUT(*val); + break; + default: + if (*val > 0 && *val < 0x20) { + __PUT('\\'); __PUT('u'); + __PUT('0'); __PUT('0'); + __PUT(hex[((*val)>>4)&0xf]); __PUT(hex[(*val)&0xf]); + } else { + __PUT(*val); + } + break; + } + val++; + } + __PUT('"'); + __PUT('\0'); +#undef __PUT + return len-1; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_H */ diff --git a/spa/include/spa/utils/keys.h b/spa/include/spa/utils/keys.h new file mode 100644 index 0000000..d7b3e29 --- /dev/null +++ b/spa/include/spa/utils/keys.h @@ -0,0 +1,151 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_KEYS_H +#define SPA_UTILS_KEYS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_keys Key Names + * Key names used by SPA plugins + */ + +/** + * \addtogroup spa_keys + * \{ + */ + +/** for objects */ +#define SPA_KEY_OBJECT_PATH "object.path" /**< a unique path to + * identity the object */ + +#define SPA_KEY_MEDIA_CLASS "media.class" /**< Media class + * Ex. "Audio/Device", + * "Video/Source",... */ +#define SPA_KEY_MEDIA_ROLE "media.role" /**< Role: Movie, Music, Camera, + * Screen, Communication, Game, + * Notification, DSP, Production, + * Accessibility, Test */ +/** keys for udev api */ +#define SPA_KEY_API_UDEV "api.udev" /**< key for the udev api */ +#define SPA_KEY_API_UDEV_MATCH "api.udev.match" /**< udev subsystem match */ + +/** keys for alsa api */ +#define SPA_KEY_API_ALSA "api.alsa" /**< key for the alsa api */ +#define SPA_KEY_API_ALSA_PATH "api.alsa.path" /**< alsa device path as can be + * used in snd_pcm_open() and + * snd_ctl_open(). */ +#define SPA_KEY_API_ALSA_CARD "api.alsa.card" /**< alsa card number */ +#define SPA_KEY_API_ALSA_USE_UCM "api.alsa.use-ucm" /**< if UCM should be used */ +#define SPA_KEY_API_ALSA_IGNORE_DB "api.alsa.ignore-dB" /**< if decibel info should be ignored */ +#define SPA_KEY_API_ALSA_OPEN_UCM "api.alsa.open.ucm" /**< if UCM should be opened card */ +#define SPA_KEY_API_ALSA_DISABLE_LONGNAME \ + "api.alsa.disable-longname" /**< if card long name should not be passed to MIDI port */ + +/** info from alsa card_info */ +#define SPA_KEY_API_ALSA_CARD_ID "api.alsa.card.id" /**< id from card_info */ +#define SPA_KEY_API_ALSA_CARD_COMPONENTS \ + "api.alsa.card.components" /**< components from card_info */ +#define SPA_KEY_API_ALSA_CARD_DRIVER "api.alsa.card.driver" /**< driver from card_info */ +#define SPA_KEY_API_ALSA_CARD_NAME "api.alsa.card.name" /**< name from card_info */ +#define SPA_KEY_API_ALSA_CARD_LONGNAME "api.alsa.card.longname" /**< longname from card_info */ +#define SPA_KEY_API_ALSA_CARD_MIXERNAME "api.alsa.card.mixername" /**< mixername from card_info */ + +/** info from alsa pcm_info */ +#define SPA_KEY_API_ALSA_PCM_ID "api.alsa.pcm.id" /**< id from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_CARD "api.alsa.pcm.card" /**< card from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_NAME "api.alsa.pcm.name" /**< name from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_SUBNAME "api.alsa.pcm.subname" /**< subdevice_name from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_STREAM "api.alsa.pcm.stream" /**< stream type from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_CLASS "api.alsa.pcm.class" /**< class from pcm_info as string */ +#define SPA_KEY_API_ALSA_PCM_DEVICE "api.alsa.pcm.device" /**< device from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_SUBDEVICE "api.alsa.pcm.subdevice" /**< subdevice from pcm_info */ +#define SPA_KEY_API_ALSA_PCM_SUBCLASS "api.alsa.pcm.subclass" /**< subclass from pcm_info as string */ +#define SPA_KEY_API_ALSA_PCM_SYNC_ID "api.alsa.pcm.sync-id" /**< sync id */ + +/** keys for v4l2 api */ +#define SPA_KEY_API_V4L2 "api.v4l2" /**< key for the v4l2 api */ +#define SPA_KEY_API_V4L2_PATH "api.v4l2.path" /**< v4l2 device path as can be + * used in open() */ + +/** keys for libcamera api */ +#define SPA_KEY_API_LIBCAMERA "api.libcamera" /**< key for the libcamera api */ +#define SPA_KEY_API_LIBCAMERA_PATH "api.libcamera.path" /**< libcamera device path as can be + * used in open() */ +#define SPA_KEY_API_LIBCAMERA_LOCATION "api.libcamera.location" /**< location of the camera: + * "front", "back" or "external" */ + +/** info from libcamera_capability */ +#define SPA_KEY_API_LIBCAMERA_CAP_DRIVER "api.libcamera.cap.driver" /**< driver from capbility */ +#define SPA_KEY_API_LIBCAMERA_CAP_CARD "api.libcamera.cap.card" /**< caps from capability */ +#define SPA_KEY_API_LIBCAMERA_CAP_BUS_INFO "api.libcamera.cap.bus_info"/**< bus_info from capability */ +#define SPA_KEY_API_LIBCAMERA_CAP_VERSION "api.libcamera.cap.version" /**< version from capability as %u.%u.%u */ +#define SPA_KEY_API_LIBCAMERA_CAP_CAPABILITIES \ + "api.libcamera.cap.capabilities" /**< capabilities from capability */ +#define SPA_KEY_API_LIBCAMERA_CAP_DEVICE_CAPS \ + "api.libcamera.cap.device-caps" /**< device_caps from capability */ +/** info from v4l2_capability */ +#define SPA_KEY_API_V4L2_CAP_DRIVER "api.v4l2.cap.driver" /**< driver from capbility */ +#define SPA_KEY_API_V4L2_CAP_CARD "api.v4l2.cap.card" /**< caps from capability */ +#define SPA_KEY_API_V4L2_CAP_BUS_INFO "api.v4l2.cap.bus_info" /**< bus_info from capability */ +#define SPA_KEY_API_V4L2_CAP_VERSION "api.v4l2.cap.version" /**< version from capability as %u.%u.%u */ +#define SPA_KEY_API_V4L2_CAP_CAPABILITIES \ + "api.v4l2.cap.capabilities" /**< capabilities from capability */ +#define SPA_KEY_API_V4L2_CAP_DEVICE_CAPS \ + "api.v4l2.cap.device-caps" /**< device_caps from capability */ + + +/** keys for bluez5 api */ +#define SPA_KEY_API_BLUEZ5 "api.bluez5" /**< key for the bluez5 api */ +#define SPA_KEY_API_BLUEZ5_PATH "api.bluez5.path" /**< a bluez5 path */ +#define SPA_KEY_API_BLUEZ5_DEVICE "api.bluez5.device" /**< an internal bluez5 device */ +#define SPA_KEY_API_BLUEZ5_CONNECTION "api.bluez5.connection" /**< bluez5 device connection status */ +#define SPA_KEY_API_BLUEZ5_TRANSPORT "api.bluez5.transport" /**< an internal bluez5 transport */ +#define SPA_KEY_API_BLUEZ5_PROFILE "api.bluez5.profile" /**< a bluetooth profile */ +#define SPA_KEY_API_BLUEZ5_ADDRESS "api.bluez5.address" /**< a bluetooth address */ +#define SPA_KEY_API_BLUEZ5_CODEC "api.bluez5.codec" /**< a bluetooth codec */ +#define SPA_KEY_API_BLUEZ5_CLASS "api.bluez5.class" /**< a bluetooth class */ +#define SPA_KEY_API_BLUEZ5_ICON "api.bluez5.icon" /**< a bluetooth icon */ +#define SPA_KEY_API_BLUEZ5_ROLE "api.bluez5.role" /**< "client" or "server" */ + +/** keys for jack api */ +#define SPA_KEY_API_JACK "api.jack" /**< key for the JACK api */ +#define SPA_KEY_API_JACK_SERVER "api.jack.server" /**< a jack server name */ +#define SPA_KEY_API_JACK_CLIENT "api.jack.client" /**< an internal jack client */ + +/** keys for glib api */ +#define SPA_KEY_API_GLIB_MAINLOOP "api.glib.mainloop" /**< whether glib mainloop runs + * in same thread as PW loop */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_KEYS_H */ diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h new file mode 100644 index 0000000..b57657a --- /dev/null +++ b/spa/include/spa/utils/list.h @@ -0,0 +1,166 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_LIST_H +#define SPA_LIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \defgroup spa_list List + * Doubly linked list data structure + */ + +/** + * \addtogroup spa_list List + * \{ + */ + +struct spa_list { + struct spa_list *next; + struct spa_list *prev; +}; + +#define SPA_LIST_INIT(list) ((struct spa_list){ (list), (list) }) + +static inline void spa_list_init(struct spa_list *list) +{ + *list = SPA_LIST_INIT(list); +} + +static inline int spa_list_is_initialized(struct spa_list *list) +{ + return !!list->prev; +} + +#define spa_list_is_empty(l) ((l)->next == (l)) + +static inline void spa_list_insert(struct spa_list *list, struct spa_list *elem) +{ + elem->prev = list; + elem->next = list->next; + list->next = elem; + elem->next->prev = elem; +} + +static inline void spa_list_insert_list(struct spa_list *list, struct spa_list *other) +{ + if (spa_list_is_empty(other)) + return; + other->next->prev = list; + other->prev->next = list->next; + list->next->prev = other->prev; + list->next = other->next; +} + +static inline void spa_list_remove(struct spa_list *elem) +{ + elem->prev->next = elem->next; + elem->next->prev = elem->prev; +} + +#define spa_list_first(head, type, member) \ + SPA_CONTAINER_OF((head)->next, type, member) + +#define spa_list_last(head, type, member) \ + SPA_CONTAINER_OF((head)->prev, type, member) + +#define spa_list_append(list, item) \ + spa_list_insert((list)->prev, item) + +#define spa_list_prepend(list, item) \ + spa_list_insert(list, item) + +#define spa_list_is_end(pos, head, member) \ + (&(pos)->member == (head)) + +#define spa_list_next(pos, member) \ + SPA_CONTAINER_OF((pos)->member.next, __typeof__(*(pos)), member) + +#define spa_list_prev(pos, member) \ + SPA_CONTAINER_OF((pos)->member.prev, __typeof__(*(pos)), member) + +#define spa_list_consume(pos, head, member) \ + for ((pos) = spa_list_first(head, __typeof__(*(pos)), member); \ + !spa_list_is_empty(head); \ + (pos) = spa_list_first(head, __typeof__(*(pos)), member)) + +#define spa_list_for_each_next(pos, head, curr, member) \ + for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member); \ + !spa_list_is_end(pos, head, member); \ + (pos) = spa_list_next(pos, member)) + +#define spa_list_for_each_prev(pos, head, curr, member) \ + for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member); \ + !spa_list_is_end(pos, head, member); \ + (pos) = spa_list_prev(pos, member)) + +#define spa_list_for_each(pos, head, member) \ + spa_list_for_each_next(pos, head, head, member) + +#define spa_list_for_each_reverse(pos, head, member) \ + spa_list_for_each_prev(pos, head, head, member) + +#define spa_list_for_each_safe_next(pos, tmp, head, curr, member) \ + for ((pos) = spa_list_first(curr, __typeof__(*(pos)), member); \ + (tmp) = spa_list_next(pos, member), \ + !spa_list_is_end(pos, head, member); \ + (pos) = (tmp)) + +#define spa_list_for_each_safe_prev(pos, tmp, head, curr, member) \ + for ((pos) = spa_list_last(curr, __typeof__(*(pos)), member); \ + (tmp) = spa_list_prev(pos, member), \ + !spa_list_is_end(pos, head, member); \ + (pos) = (tmp)) + +#define spa_list_for_each_safe(pos, tmp, head, member) \ + spa_list_for_each_safe_next(pos, tmp, head, head, member) + +#define spa_list_for_each_safe_reverse(pos, tmp, head, member) \ + spa_list_for_each_safe_prev(pos, tmp, head, head, member) + +#define spa_list_cursor_start(cursor, head, member) \ + spa_list_prepend(head, &(cursor).member) + +#define spa_list_for_each_cursor(pos, cursor, head, member) \ + for((pos) = spa_list_first(&(cursor).member, __typeof__(*(pos)), member); \ + spa_list_remove(&(pos)->member), \ + spa_list_append(&(cursor).member, &(pos)->member), \ + !spa_list_is_end(pos, head, member); \ + (pos) = spa_list_next(&(cursor), member)) + +#define spa_list_cursor_end(cursor, member) \ + spa_list_remove(&(cursor).member) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_LIST_H */ diff --git a/spa/include/spa/utils/names.h b/spa/include/spa/utils/names.h new file mode 100644 index 0000000..2ae04ed --- /dev/null +++ b/spa/include/spa/utils/names.h @@ -0,0 +1,164 @@ +/* Simple Plugin API + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_NAMES_H +#define SPA_UTILS_NAMES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup spa_names Factory Names + * SPA plugin factory names + */ + +/** + * \addtogroup spa_names + * \{ + */ + +/** for factory names */ +#define SPA_NAME_SUPPORT_CPU "support.cpu" /**< A CPU interface */ +#define SPA_NAME_SUPPORT_DBUS "support.dbus" /**< A DBUS interface */ +#define SPA_NAME_SUPPORT_LOG "support.log" /**< A Log interface */ +#define SPA_NAME_SUPPORT_LOOP "support.loop" /**< A Loop/LoopControl/LoopUtils + * interface */ +#define SPA_NAME_SUPPORT_SYSTEM "support.system" /**< A System interface */ + +#define SPA_NAME_SUPPORT_NODE_DRIVER "support.node.driver" /**< A dummy driver node */ + +/* control mixer */ +#define SPA_NAME_CONTROL_MIXER "control.mixer" /**< mixes control streams */ + +/* audio mixer */ +#define SPA_NAME_AUDIO_MIXER "audio.mixer" /**< mixes the raw audio on N input + * ports together on the output + * port */ +#define SPA_NAME_AUDIO_MIXER_DSP "audio.mixer.dsp" /**< mixes mono audio with fixed input + * and output buffer sizes. supported + * formats must include f32 and + * optionally f64 and s24_32 */ + +/** audio processing */ +#define SPA_NAME_AUDIO_PROCESS_FORMAT "audio.process.format" /**< processes raw audio from one format + * to another */ +#define SPA_NAME_AUDIO_PROCESS_CHANNELMIX \ + "audio.process.channelmix" /**< mixes raw audio channels and applies + * volume change. */ +#define SPA_NAME_AUDIO_PROCESS_RESAMPLE \ + "audio.process.resample" /**< resamples raw audio */ +#define SPA_NAME_AUDIO_PROCESS_DEINTERLEAVE \ + "audio.process.deinterleave" /**< deinterleave raw audio channels */ +#define SPA_NAME_AUDIO_PROCESS_INTERLEAVE \ + "audio.process.interleave" /**< interleave raw audio channels */ + + +/** audio convert combines some of the audio processing */ +#define SPA_NAME_AUDIO_CONVERT "audio.convert" /**< converts raw audio from one format + * to another. Must include at least + * format, channelmix and resample + * processing */ +#define SPA_NAME_AUDIO_ADAPT "audio.adapt" /**< combination of a node and an + * audio.convert. Does clock slaving */ + +#define SPA_NAME_AEC "audio.aec" /**< Echo canceling */ + +/** video processing */ +#define SPA_NAME_VIDEO_PROCESS_FORMAT "video.process.format" /**< processes raw video from one format + * to another */ +#define SPA_NAME_VIDEO_PROCESS_SCALE "video.process.scale" /**< scales raw video */ + +/** video convert combines some of the video processing */ +#define SPA_NAME_VIDEO_CONVERT "video.convert" /**< converts raw video from one format + * to another. Must include at least + * format and scaling */ +#define SPA_NAME_VIDEO_ADAPT "video.adapt" /**< combination of a node and a + * video.convert. */ +/** keys for alsa factory names */ +#define SPA_NAME_API_ALSA_ENUM_UDEV "api.alsa.enum.udev" /**< an alsa udev Device interface */ +#define SPA_NAME_API_ALSA_PCM_DEVICE "api.alsa.pcm.device" /**< an alsa Device interface */ +#define SPA_NAME_API_ALSA_PCM_SOURCE "api.alsa.pcm.source" /**< an alsa Node interface for + * capturing PCM */ +#define SPA_NAME_API_ALSA_PCM_SINK "api.alsa.pcm.sink" /**< an alsa Node interface for + * playback PCM */ +#define SPA_NAME_API_ALSA_SEQ_DEVICE "api.alsa.seq.device" /**< an alsa Midi device */ +#define SPA_NAME_API_ALSA_SEQ_SOURCE "api.alsa.seq.source" /**< an alsa Node interface for + * capture of midi */ +#define SPA_NAME_API_ALSA_SEQ_SINK "api.alsa.seq.sink" /**< an alsa Node interface for + * playback of midi */ +#define SPA_NAME_API_ALSA_SEQ_BRIDGE "api.alsa.seq.bridge" /**< an alsa Node interface for + * bridging midi ports */ +#define SPA_NAME_API_ALSA_ACP_DEVICE "api.alsa.acp.device" /**< an alsa ACP Device interface */ +#define SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK "api.alsa.compress.offload.sink" /**< an alsa Node interface for + * compressed audio */ + +/** keys for bluez5 factory names */ +#define SPA_NAME_API_BLUEZ5_ENUM_DBUS "api.bluez5.enum.dbus" /**< a dbus Device interface */ +#define SPA_NAME_API_BLUEZ5_DEVICE "api.bluez5.device" /**< a Device interface */ +#define SPA_NAME_API_BLUEZ5_MEDIA_SINK "api.bluez5.media.sink" /**< a playback Node interface for A2DP/BAP profiles */ +#define SPA_NAME_API_BLUEZ5_MEDIA_SOURCE "api.bluez5.media.source" /**< a capture Node interface for A2DP/BAP profiles */ +#define SPA_NAME_API_BLUEZ5_A2DP_SINK "api.bluez5.a2dp.sink" /**< alias for media.sink */ +#define SPA_NAME_API_BLUEZ5_A2DP_SOURCE "api.bluez5.a2dp.source" /**< alias for media.source */ +#define SPA_NAME_API_BLUEZ5_SCO_SINK "api.bluez5.sco.sink" /**< a playback Node interface for HSP/HFP profiles */ +#define SPA_NAME_API_BLUEZ5_SCO_SOURCE "api.bluez5.sco.source" /**< a capture Node interface for HSP/HFP profiles */ +#define SPA_NAME_API_BLUEZ5_MIDI_ENUM "api.bluez5.midi.enum" /**< a dbus midi Device interface */ +#define SPA_NAME_API_BLUEZ5_MIDI_NODE "api.bluez5.midi.node" /**< a midi Node interface */ + +/** keys for codec factory names */ +#define SPA_NAME_API_CODEC_BLUEZ5_MEDIA "api.codec.bluez5.media" /**< Bluez5 Media codec plugin */ + +/** keys for v4l2 factory names */ +#define SPA_NAME_API_V4L2_ENUM_UDEV "api.v4l2.enum.udev" /**< a v4l2 udev Device interface */ +#define SPA_NAME_API_V4L2_DEVICE "api.v4l2.device" /**< a v4l2 Device interface */ +#define SPA_NAME_API_V4L2_SOURCE "api.v4l2.source" /**< a v4l2 Node interface for + * capturing */ + +/** keys for libcamera factory names */ +#define SPA_NAME_API_LIBCAMERA_ENUM_CLIENT "api.libcamera.enum.client" /**< a libcamera client Device interface */ +#define SPA_NAME_API_LIBCAMERA_ENUM_MANAGER "api.libcamera.enum.manager" /**< a libcamera manager Device interface */ +#define SPA_NAME_API_LIBCAMERA_DEVICE "api.libcamera.device" /**< a libcamera Device interface */ +#define SPA_NAME_API_LIBCAMERA_SOURCE "api.libcamera.source" /**< a libcamera Node interface for + * capturing */ + +/** keys for jack factory names */ +#define SPA_NAME_API_JACK_DEVICE "api.jack.device" /**< a jack device. This is a + * client connected to a server */ +#define SPA_NAME_API_JACK_SOURCE "api.jack.source" /**< a jack source */ +#define SPA_NAME_API_JACK_SINK "api.jack.sink" /**< a jack sink */ + +/** keys for vulkan factory names */ +#define SPA_NAME_API_VULKAN_COMPUTE_SOURCE \ + "api.vulkan.compute.source" /**< a vulkan compute source. */ +#define SPA_NAME_API_VULKAN_COMPUTE_FILTER \ + "api.vulkan.compute.filter" /**< a vulkan compute filter. */ + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_NAMES_H */ diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h new file mode 100644 index 0000000..05a1ef9 --- /dev/null +++ b/spa/include/spa/utils/result.h @@ -0,0 +1,72 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_RESULT_H +#define SPA_UTILS_RESULT_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \defgroup spa_result Result handling + * Asynchronous result utilities + */ + +/** + * \addtogroup spa_result + * \{ + */ + +#include +#include + +#define SPA_ASYNC_BIT (1 << 30) +#define SPA_ASYNC_SEQ_MASK (SPA_ASYNC_BIT - 1) +#define SPA_ASYNC_MASK (~SPA_ASYNC_SEQ_MASK) + +#define SPA_RESULT_IS_OK(res) ((res) >= 0) +#define SPA_RESULT_IS_ERROR(res) ((res) < 0) +#define SPA_RESULT_IS_ASYNC(res) (((res) & SPA_ASYNC_MASK) == SPA_ASYNC_BIT) + +#define SPA_RESULT_ASYNC_SEQ(res) ((res) & SPA_ASYNC_SEQ_MASK) +#define SPA_RESULT_RETURN_ASYNC(seq) (SPA_ASYNC_BIT | SPA_RESULT_ASYNC_SEQ(seq)) + +#define spa_strerror(err) \ +({ \ + int _err = -(err); \ + if (SPA_RESULT_IS_ASYNC(err)) \ + _err = EINPROGRESS; \ + strerror(_err); \ +}) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_RESULT_H */ diff --git a/spa/include/spa/utils/ringbuffer.h b/spa/include/spa/utils/ringbuffer.h new file mode 100644 index 0000000..ed14939 --- /dev/null +++ b/spa/include/spa/utils/ringbuffer.h @@ -0,0 +1,188 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_RINGBUFFER_H +#define SPA_RINGBUFFER_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \defgroup spa_ringbuffer Ringbuffer + * Ring buffer implementation + */ + +/** + * \addtogroup spa_ringbuffer + * \{ + */ + +struct spa_ringbuffer; + +#include + +#include + +/** + * A ringbuffer type. + */ +struct spa_ringbuffer { + uint32_t readindex; /*< the current read index */ + uint32_t writeindex; /*< the current write index */ +}; + +#define SPA_RINGBUFFER_INIT() ((struct spa_ringbuffer) { 0, 0 }) + +/** + * Initialize a spa_ringbuffer with \a size. + * + * \param rbuf a spa_ringbuffer + */ +static inline void spa_ringbuffer_init(struct spa_ringbuffer *rbuf) +{ + *rbuf = SPA_RINGBUFFER_INIT(); +} + +/** + * Sets the pointers so that the ringbuffer contains \a size bytes. + * + * \param rbuf a spa_ringbuffer + * \param size the target size of \a rbuf + */ +static inline void spa_ringbuffer_set_avail(struct spa_ringbuffer *rbuf, uint32_t size) +{ + rbuf->readindex = 0; + rbuf->writeindex = size; +} + +/** + * Get the read index and available bytes for reading. + * + * \param rbuf a spa_ringbuffer + * \param index the value of readindex, should be taken modulo the size of the + * ringbuffer memory to get the offset in the ringbuffer memory + * \return number of available bytes to read. values < 0 mean + * there was an underrun. values > rbuf->size means there + * was an overrun. + */ +static inline int32_t spa_ringbuffer_get_read_index(struct spa_ringbuffer *rbuf, uint32_t *index) +{ + *index = __atomic_load_n(&rbuf->readindex, __ATOMIC_RELAXED); + return (int32_t) (__atomic_load_n(&rbuf->writeindex, __ATOMIC_ACQUIRE) - *index); +} + +/** + * Read \a len bytes from \a rbuf starting \a offset. \a offset must be taken + * modulo \a size and len should be smaller than \a size. + * + * \param rbuf a struct \ref spa_ringbuffer + * \param buffer memory to read from + * \param size the size of \a buffer + * \param offset offset in \a buffer to read from + * \param data destination memory + * \param len number of bytes to read + */ +static inline void +spa_ringbuffer_read_data(struct spa_ringbuffer *rbuf, + const void *buffer, uint32_t size, + uint32_t offset, void *data, uint32_t len) +{ + uint32_t l0 = SPA_MIN(len, size - offset), l1 = len - l0; + spa_memcpy(data, SPA_PTROFF(buffer, offset, void), l0); + if (SPA_UNLIKELY(l1 > 0)) + spa_memcpy(SPA_PTROFF(data, l0, void), buffer, l1); +} + +/** + * Update the read pointer to \a index. + * + * \param rbuf a spa_ringbuffer + * \param index new index + */ +static inline void spa_ringbuffer_read_update(struct spa_ringbuffer *rbuf, int32_t index) +{ + __atomic_store_n(&rbuf->readindex, index, __ATOMIC_RELEASE); +} + +/** + * Get the write index and the number of bytes inside the ringbuffer. + * + * \param rbuf a spa_ringbuffer + * \param index the value of writeindex, should be taken modulo the size of the + * ringbuffer memory to get the offset in the ringbuffer memory + * \return the fill level of \a rbuf. values < 0 mean + * there was an underrun. values > rbuf->size means there + * was an overrun. Subtract from the buffer size to get + * the number of bytes available for writing. + */ +static inline int32_t spa_ringbuffer_get_write_index(struct spa_ringbuffer *rbuf, uint32_t *index) +{ + *index = __atomic_load_n(&rbuf->writeindex, __ATOMIC_RELAXED); + return (int32_t) (*index - __atomic_load_n(&rbuf->readindex, __ATOMIC_ACQUIRE)); +} + +/** + * Write \a len bytes to \a buffer starting \a offset. \a offset must be taken + * modulo \a size and len should be smaller than \a size. + * + * \param rbuf a spa_ringbuffer + * \param buffer memory to write to + * \param size the size of \a buffer + * \param offset offset in \a buffer to write to + * \param data source memory + * \param len number of bytes to write + */ +static inline void +spa_ringbuffer_write_data(struct spa_ringbuffer *rbuf, + void *buffer, uint32_t size, + uint32_t offset, const void *data, uint32_t len) +{ + uint32_t l0 = SPA_MIN(len, size - offset), l1 = len - l0; + spa_memcpy(SPA_PTROFF(buffer, offset, void), data, l0); + if (SPA_UNLIKELY(l1 > 0)) + spa_memcpy(buffer, SPA_PTROFF(data, l0, void), l1); +} + +/** + * Update the write pointer to \a index + * + * \param rbuf a spa_ringbuffer + * \param index new index + */ +static inline void spa_ringbuffer_write_update(struct spa_ringbuffer *rbuf, int32_t index) +{ + __atomic_store_n(&rbuf->writeindex, index, __ATOMIC_RELEASE); +} + +/** + * \} + */ + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_RINGBUFFER_H */ diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h new file mode 100644 index 0000000..787a116 --- /dev/null +++ b/spa/include/spa/utils/string.h @@ -0,0 +1,414 @@ +/* Simple Plugin API + * + * Copyright © 2021 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_UTILS_STRING_H +#define SPA_UTILS_STRING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#include + +/** + * \defgroup spa_string String handling + * String handling utilities + */ + +/** + * \addtogroup spa_string + * \{ + */ + +/** + * \return true if the two strings are equal, false otherwise + * + * If both \a a and \a b are NULL, the two are considered equal. + * + */ +static inline bool spa_streq(const char *s1, const char *s2) +{ + return SPA_LIKELY(s1 && s2) ? strcmp(s1, s2) == 0 : s1 == s2; +} + +/** + * \return true if the two strings are equal, false otherwise + * + * If both \a a and \a b are NULL, the two are considered equal. + */ +static inline bool spa_strneq(const char *s1, const char *s2, size_t len) +{ + return SPA_LIKELY(s1 && s2) ? strncmp(s1, s2, len) == 0 : s1 == s2; +} + + +/** + * \return true if \a s starts with the \a prefix or false otherwise. + * A \a s is NULL, it never starts with the given \a prefix. A \a prefix of + * NULL is a bug in the caller. + */ +static inline bool spa_strstartswith(const char *s, const char *prefix) +{ + if (SPA_UNLIKELY(s == NULL)) + return false; + + spa_assert_se(prefix); + + return strncmp(s, prefix, strlen(prefix)) == 0; +} + + +/** + * \return true if \a s ends with the \a suffix or false otherwise. + * A \a s is NULL, it never ends with the given \a suffix. A \a suffix of + * NULL is a bug in the caller. + */ +static inline bool spa_strendswith(const char *s, const char *suffix) +{ + size_t l1, l2; + + if (SPA_UNLIKELY(s == NULL)) + return false; + + spa_assert_se(suffix); + + l1 = strlen(s); + l2 = strlen(suffix); + return l1 >= l2 && spa_streq(s + l1 - l2, suffix); +} + +/** + * Convert \a str to an int32_t with the given \a base and store the + * result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atoi32(const char *str, int32_t *val, int base) +{ + char *endptr; + long v; + + if (!str || *str =='\0') + return false; + + errno = 0; + v = strtol(str, &endptr, base); + if (errno != 0 || *endptr != '\0') + return false; + + if (v != (int32_t)v) + return false; + + *val = v; + return true; +} + +/** + * Convert \a str to an uint32_t with the given \a base and store the + * result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atou32(const char *str, uint32_t *val, int base) +{ + char *endptr; + unsigned long long v; + + if (!str || *str =='\0') + return false; + + errno = 0; + v = strtoull(str, &endptr, base); + if (errno != 0 || *endptr != '\0') + return false; + + if (v != (uint32_t)v) + return false; + + *val = v; + return true; +} + +/** + * Convert \a str to an int64_t with the given \a base and store the + * result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atoi64(const char *str, int64_t *val, int base) +{ + char *endptr; + long long v; + + if (!str || *str =='\0') + return false; + + errno = 0; + v = strtoll(str, &endptr, base); + if (errno != 0 || *endptr != '\0') + return false; + + *val = v; + return true; +} + +/** + * Convert \a str to an uint64_t with the given \a base and store the + * result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atou64(const char *str, uint64_t *val, int base) +{ + char *endptr; + unsigned long long v; + + if (!str || *str =='\0') + return false; + + errno = 0; + v = strtoull(str, &endptr, base); + if (errno != 0 || *endptr != '\0') + return false; + + *val = v; + return true; +} + +/** + * Convert \a str to a boolean. Allowed boolean values are "true" and a + * literal "1", anything else is false. + * + * \return true on success, false otherwise + */ +static inline bool spa_atob(const char *str) +{ + return spa_streq(str, "true") || spa_streq(str, "1"); +} + +/** + * "Safe" version of vsnprintf. Exactly the same as vsnprintf but the + * returned value is clipped to `size - 1` and a negative or zero size + * will abort() the program. + * + * \return The number of bytes printed, capped to `size-1`, or a negative + * number on error. + */ +SPA_PRINTF_FUNC(3, 0) +static inline int spa_vscnprintf(char *buffer, size_t size, const char *format, va_list args) +{ + int r; + + spa_assert_se((ssize_t)size > 0); + + r = vsnprintf(buffer, size, format, args); + if (SPA_UNLIKELY(r < 0)) + buffer[0] = '\0'; + if (SPA_LIKELY(r < (ssize_t)size)) + return r; + return size - 1; +} + +/** + * "Safe" version of snprintf. Exactly the same as snprintf but the + * returned value is clipped to `size - 1` and a negative or zero size + * will abort() the program. + * + * \return The number of bytes printed, capped to `size-1`, or a negative + * number on error. + */ +SPA_PRINTF_FUNC(3, 4) +static inline int spa_scnprintf(char *buffer, size_t size, const char *format, ...) +{ + int r; + va_list args; + + va_start(args, format); + r = spa_vscnprintf(buffer, size, format, args); + va_end(args); + + return r; +} + +/** + * Convert \a str to a float in the C locale. + * + * If \a endptr is not NULL, a pointer to the character after the last character + * used in the conversion is stored in the location referenced by endptr. + * + * \return the result float. + */ +static inline float spa_strtof(const char *str, char **endptr) +{ +#ifndef __LOCALE_C_ONLY + static locale_t locale = NULL; + locale_t prev; +#endif + float v; +#ifndef __LOCALE_C_ONLY + if (SPA_UNLIKELY(locale == NULL)) + locale = newlocale(LC_ALL_MASK, "C", NULL); + prev = uselocale(locale); +#endif + v = strtof(str, endptr); +#ifndef __LOCALE_C_ONLY + uselocale(prev); +#endif + return v; +} + +/** + * Convert \a str to a float and store the result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atof(const char *str, float *val) +{ + char *endptr; + float v; + + if (!str || *str =='\0') + return false; + errno = 0; + v = spa_strtof(str, &endptr); + if (errno != 0 || *endptr != '\0') + return false; + + *val = v; + return true; +} + +/** + * Convert \a str to a double in the C locale. + * + * If \a endptr is not NULL, a pointer to the character after the last character + * used in the conversion is stored in the location referenced by endptr. + * + * \return the result float. + */ +static inline double spa_strtod(const char *str, char **endptr) +{ +#ifndef __LOCALE_C_ONLY + static locale_t locale = NULL; + locale_t prev; +#endif + double v; +#ifndef __LOCALE_C_ONLY + if (SPA_UNLIKELY(locale == NULL)) + locale = newlocale(LC_ALL_MASK, "C", NULL); + prev = uselocale(locale); +#endif + v = strtod(str, endptr); +#ifndef __LOCALE_C_ONLY + uselocale(prev); +#endif + return v; +} + +/** + * Convert \a str to a double and store the result in \a val. + * + * On failure, the value of \a val is unmodified. + * + * \return true on success, false otherwise + */ +static inline bool spa_atod(const char *str, double *val) +{ + char *endptr; + double v; + + if (!str || *str =='\0') + return false; + + errno = 0; + v = spa_strtod(str, &endptr); + if (errno != 0 || *endptr != '\0') + return false; + + *val = v; + return true; +} + +static inline char *spa_dtoa(char *str, size_t size, double val) +{ + int i, l; + l = spa_scnprintf(str, size, "%f", val); + for (i = 0; i < l; i++) + if (str[i] == ',') + str[i] = '.'; + return str; +} + +struct spa_strbuf { + char *buffer; + size_t maxsize; + size_t pos; +}; + +static inline void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t maxsize) +{ + buf->buffer = buffer; + buf->maxsize = maxsize; + buf->pos = 0; +} + +SPA_PRINTF_FUNC(2, 3) +static inline int spa_strbuf_append(struct spa_strbuf *buf, const char *fmt, ...) +{ + size_t remain = buf->maxsize - buf->pos; + ssize_t written; + va_list args; + va_start(args, fmt); + written = vsnprintf(&buf->buffer[buf->pos], remain, fmt, args); + va_end(args); + if (written > 0) + buf->pos += SPA_MIN(remain, (size_t)written); + return written; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_STRING_H */ diff --git a/spa/include/spa/utils/type-info.h b/spa/include/spa/utils/type-info.h new file mode 100644 index 0000000..643a7ac --- /dev/null +++ b/spa/include/spa/utils/type-info.h @@ -0,0 +1,118 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_TYPE_INFO_H +#define SPA_TYPE_INFO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** + * \addtogroup spa_types + * \{ + */ + +#ifndef SPA_TYPE_ROOT +#define SPA_TYPE_ROOT spa_types +#endif + +static inline bool spa_type_is_a(const char *type, const char *parent) +{ + return type != NULL && parent != NULL && strncmp(type, parent, strlen(parent)) == 0; +} + +#include +#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 }, + + { 0, 0, NULL, NULL } +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_TYPE_INFO_H */ diff --git a/spa/include/spa/utils/type.h b/spa/include/spa/utils/type.h new file mode 100644 index 0000000..31623c3 --- /dev/null +++ b/spa/include/spa/utils/type.h @@ -0,0 +1,153 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_TYPE_H +#define SPA_TYPE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** \defgroup spa_types Types + * Data type information enumerations + */ + +/** + * \addtogroup spa_types + * \{ + */ + +enum { + /* Basic types */ + SPA_TYPE_START = 0x00000, + SPA_TYPE_None, + SPA_TYPE_Bool, + SPA_TYPE_Id, + SPA_TYPE_Int, + SPA_TYPE_Long, + SPA_TYPE_Float, + SPA_TYPE_Double, + SPA_TYPE_String, + SPA_TYPE_Bytes, + SPA_TYPE_Rectangle, + SPA_TYPE_Fraction, + SPA_TYPE_Bitmap, + SPA_TYPE_Array, + SPA_TYPE_Struct, + SPA_TYPE_Object, + SPA_TYPE_Sequence, + SPA_TYPE_Pointer, + SPA_TYPE_Fd, + SPA_TYPE_Choice, + SPA_TYPE_Pod, + _SPA_TYPE_LAST, /**< not part of ABI */ + + /* Pointers */ + SPA_TYPE_POINTER_START = 0x10000, + SPA_TYPE_POINTER_Buffer, + SPA_TYPE_POINTER_Meta, + SPA_TYPE_POINTER_Dict, + _SPA_TYPE_POINTER_LAST, /**< not part of ABI */ + + /* Events */ + SPA_TYPE_EVENT_START = 0x20000, + SPA_TYPE_EVENT_Device, + SPA_TYPE_EVENT_Node, + _SPA_TYPE_EVENT_LAST, /**< not part of ABI */ + + /* Commands */ + SPA_TYPE_COMMAND_START = 0x30000, + SPA_TYPE_COMMAND_Device, + SPA_TYPE_COMMAND_Node, + _SPA_TYPE_COMMAND_LAST, /**< not part of ABI */ + + /* Objects */ + SPA_TYPE_OBJECT_START = 0x40000, + SPA_TYPE_OBJECT_PropInfo, + SPA_TYPE_OBJECT_Props, + SPA_TYPE_OBJECT_Format, + SPA_TYPE_OBJECT_ParamBuffers, + SPA_TYPE_OBJECT_ParamMeta, + SPA_TYPE_OBJECT_ParamIO, + SPA_TYPE_OBJECT_ParamProfile, + SPA_TYPE_OBJECT_ParamPortConfig, + SPA_TYPE_OBJECT_ParamRoute, + SPA_TYPE_OBJECT_Profiler, + SPA_TYPE_OBJECT_ParamLatency, + SPA_TYPE_OBJECT_ParamProcessLatency, + _SPA_TYPE_OBJECT_LAST, /**< not part of ABI */ + + /* vendor extensions */ + SPA_TYPE_VENDOR_PipeWire = 0x02000000, + + SPA_TYPE_VENDOR_Other = 0x7f000000, +}; + +#define SPA_TYPE_INFO_BASE "Spa:" + +#define SPA_TYPE_INFO_Flags SPA_TYPE_INFO_BASE "Flags" +#define SPA_TYPE_INFO_FLAGS_BASE SPA_TYPE_INFO_Flags ":" + +#define SPA_TYPE_INFO_Enum SPA_TYPE_INFO_BASE "Enum" +#define SPA_TYPE_INFO_ENUM_BASE SPA_TYPE_INFO_Enum ":" + +#define SPA_TYPE_INFO_Pod SPA_TYPE_INFO_BASE "Pod" +#define SPA_TYPE_INFO_POD_BASE SPA_TYPE_INFO_Pod ":" + +#define SPA_TYPE_INFO_Struct SPA_TYPE_INFO_POD_BASE "Struct" +#define SPA_TYPE_INFO_STRUCT_BASE SPA_TYPE_INFO_Struct ":" + +#define SPA_TYPE_INFO_Object SPA_TYPE_INFO_POD_BASE "Object" +#define SPA_TYPE_INFO_OBJECT_BASE SPA_TYPE_INFO_Object ":" + +#define SPA_TYPE_INFO_Pointer SPA_TYPE_INFO_BASE "Pointer" +#define SPA_TYPE_INFO_POINTER_BASE SPA_TYPE_INFO_Pointer ":" + +#define SPA_TYPE_INFO_Interface SPA_TYPE_INFO_POINTER_BASE "Interface" +#define SPA_TYPE_INFO_INTERFACE_BASE SPA_TYPE_INFO_Interface ":" + +#define SPA_TYPE_INFO_Event SPA_TYPE_INFO_OBJECT_BASE "Event" +#define SPA_TYPE_INFO_EVENT_BASE SPA_TYPE_INFO_Event ":" + +#define SPA_TYPE_INFO_Command SPA_TYPE_INFO_OBJECT_BASE "Command" +#define SPA_TYPE_INFO_COMMAND_BASE SPA_TYPE_INFO_Command ":" + +struct spa_type_info { + uint32_t type; + uint32_t parent; + const char *name; + const struct spa_type_info *values; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_TYPE_H */ diff --git a/spa/meson.build b/spa/meson.build new file mode 100644 index 0000000..995fbf8 --- /dev/null +++ b/spa/meson.build @@ -0,0 +1,101 @@ +#project('spa', 'c') + +#cc = meson.get_compiler('c') +#dl_lib = cc.find_library('dl', required : false) +#pthread_lib = dependencies('threads') +#mathlib = cc.find_library('m', required : false) + +spa_dep = declare_dependency( + include_directories : [ + include_directories('include'), + ], + dependencies : [atomic_dep], + version : spaversion, + variables : { + 'plugindir' : meson.current_build_dir() / 'plugins', + 'datadir' : meson.current_source_dir() / 'plugins', + }, +) + +meson.override_dependency('lib@0@'.format(spa_name), spa_dep) + +pkgconfig.generate(filebase : 'lib@0@'.format(spa_name), + name : 'libspa', + subdirs : spa_name, + description : 'Simple Plugin API', + version : spaversion, + extra_cflags : '-D_REENTRANT', + variables : ['plugindir=${libdir}/@0@'.format(spa_name)], + uninstalled_variables : ['plugindir=${prefix}/spa/plugins'], +) + +subdir('include') + +if get_option('spa-plugins').allowed() + udevrulesdir = get_option('udevrulesdir') + if udevrulesdir == '' + # absolute path, otherwise meson prepends the prefix + udevrulesdir = '/lib/udev/rules.d' + endif + + # plugin-specific dependencies + alsa_dep = dependency('alsa', required: get_option('alsa')) + summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend') + bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5')) + gio_dep = dependency('gio-2.0', required : get_option('bluez5')) + gio_unix_dep = dependency('gio-unix-2.0', required : get_option('bluez5')) + bluez_deps_found = bluez_dep.found() and gio_dep.found() and gio_unix_dep.found() + summary({'Bluetooth audio': bluez_deps_found}, bool_yn: true, section: 'Backend') + if bluez_deps_found + sbc_dep = dependency('sbc', required: get_option('bluez5')) + summary({'SBC': sbc_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + ldac_dep = dependency('ldacBT-enc', required : get_option('bluez5-codec-ldac')) + summary({'LDAC': ldac_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + ldac_abr_dep = dependency('ldacBT-abr', required : get_option('bluez5-codec-ldac')) + summary({'LDAC ABR': ldac_abr_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + aptx_dep = dependency('libfreeaptx', required : get_option('bluez5-codec-aptx')) + summary({'aptX': aptx_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + fdk_aac_dep = dependency('fdk-aac', required : get_option('bluez5-codec-aac')) + summary({'AAC': fdk_aac_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + lc3plus_dep = dependency('lc3plus', required : false) + if not lc3plus_dep.found() + lc3plus_lc3plus_h_dep = cc.find_library('LC3plus', has_headers: ['lc3plus.h'], required : get_option('bluez5-codec-lc3plus')) + if lc3plus_lc3plus_h_dep.found() + lc3plus_dep = declare_dependency(compile_args : '-DHAVE_LC3PLUS_H', dependencies : [ lc3plus_lc3plus_h_dep ]) + endif + endif + summary({'LC3plus': lc3plus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + opus_dep = dependency('opus', required : get_option('bluez5-codec-opus')) + summary({'Opus': opus_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + lc3_dep = dependency('lc3', required : get_option('bluez5-codec-lc3')) + summary({'LC3': lc3_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed() + mm_dep = dependency('ModemManager', version : '>= 1.10.0', required : get_option('bluez5-backend-native-mm')) + summary({'ModemManager': mm_dep.found()}, bool_yn: true, section: 'Bluetooth backends') + endif + endif + jack_dep = dependency('jack', version : '>= 1.9.10', required: get_option('jack')) + summary({'JACK2': jack_dep.found()}, bool_yn: true, section: 'Backend') + vulkan_dep = dependency('vulkan', disabler : true, version : '>= 1.1.69', required: get_option('vulkan')) + vulkan_headers = cc.has_header('vulkan/vulkan.h', dependencies : vulkan_dep) + #summary({'Vulkan': vulkan_headers}, bool_yn: true, section: 'Misc dependencies') + + libcamera_dep = dependency('libcamera', required: get_option('libcamera')) + summary({'libcamera': libcamera_dep.found()}, bool_yn: true, section: 'Backend') + + tinycompress_dep = cc.find_library('tinycompress', has_headers: ['tinycompress/tinycompress.h' ], required: get_option('compress-offload')) + summary({'Compress-Offload': tinycompress_dep.found()}, bool_yn: true, section: 'Backend') + cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', tinycompress_dep.found()) + + # common dependencies + libudev_dep = dependency('libudev', required: alsa_dep.found() or get_option('udev').enabled() or get_option('v4l2').enabled()) + summary({'Udev': libudev_dep.found()}, bool_yn: true, section: 'Backend') + + subdir('plugins') +endif + +subdir('tools') +subdir('tests') +if get_option('examples').allowed() + subdir('examples') +endif diff --git a/spa/plugins/aec/aec-null.c b/spa/plugins/aec/aec-null.c new file mode 100644 index 0000000..b8d5b85 --- /dev/null +++ b/spa/plugins/aec/aec-null.c @@ -0,0 +1,175 @@ +/* PipeWire + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF 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 + +struct impl { + struct spa_handle handle; + struct spa_audio_aec aec; + struct spa_log *log; + + struct spa_hook_list hooks_list; + + uint32_t channels; +}; + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.aec.null"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +static int null_init(void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info) +{ + struct impl *impl = object; + impl->channels = info->channels; + return 0; +} + +static int null_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples) +{ + struct impl *impl = object; + uint32_t i; + for (i = 0; i < impl->channels; i++) + memcpy(out[i], rec[i], n_samples * sizeof(float)); + return 0; +} + +static const struct spa_audio_aec_methods impl_aec = { + SPA_VERSION_AUDIO_AEC, + .init = null_init, + .run = null_run, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + struct impl *impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_AUDIO_AEC)) + *interface = &impl->aec; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + spa_return_val_if_fail(handle != NULL, -EINVAL); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + struct impl *impl = (struct impl *) handle; + + impl->aec.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_AUDIO_AEC, + SPA_VERSION_AUDIO_AEC, + &impl_aec, impl); + impl->aec.name = "null"; + impl->aec.info = NULL; + impl->aec.latency = NULL; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(impl->log, &log_topic); + + spa_hook_list_init(&impl->hooks_list); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_AUDIO_AEC,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_handle_factory spa_aec_null_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_AEC, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_aec_null_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp new file mode 100644 index 0000000..19a506e --- /dev/null +++ b/spa/plugins/aec/aec-webrtc.cpp @@ -0,0 +1,276 @@ +/* PipeWire + * + * Copyright © 2021 Wim Taymans + * © 2021 Arun Raghavan + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF 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 +#include +#include + +#include +#include +#include + +struct impl_data { + struct spa_handle handle; + struct spa_audio_aec aec; + + struct spa_log *log; + std::unique_ptr apm; + spa_audio_info_raw info; + std::unique_ptr play_buffer, rec_buffer, out_buffer; +}; + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.eac.webrtc"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +static bool webrtc_get_spa_bool(const struct spa_dict *args, const char *key, bool default_value) +{ + if (auto str = spa_dict_lookup(args, key)) + return spa_atob(str); + + return default_value; +} + +static int webrtc_init(void *object, const struct spa_dict *args, const struct spa_audio_info_raw *info) +{ + auto impl = static_cast(object); + + bool extended_filter = webrtc_get_spa_bool(args, "webrtc.extended_filter", true); + bool delay_agnostic = webrtc_get_spa_bool(args, "webrtc.delay_agnostic", true); + bool high_pass_filter = webrtc_get_spa_bool(args, "webrtc.high_pass_filter", true); + bool noise_suppression = webrtc_get_spa_bool(args, "webrtc.noise_suppression", true); + bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true); + + // Note: AGC seems to mess up with Agnostic Delay Detection, especially with speech, + // result in very poor performance, disable by default + bool gain_control = webrtc_get_spa_bool(args, "webrtc.gain_control", false); + + // Disable experimental flags by default + bool experimental_agc = webrtc_get_spa_bool(args, "webrtc.experimental_agc", false); + bool experimental_ns = webrtc_get_spa_bool(args, "webrtc.experimental_ns", false); + + // FIXME: Intelligibility enhancer is not currently supported + // This filter will modify playback buffer (when calling ProcessReverseStream), but now + // playback buffer modifications are discarded. + + webrtc::Config config; + config.Set(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)); + + webrtc::ProcessingConfig pconfig = {{ + webrtc::StreamConfig(info->rate, info->channels, false), /* input stream */ + webrtc::StreamConfig(info->rate, info->channels, false), /* output stream */ + webrtc::StreamConfig(info->rate, info->channels, false), /* reverse input stream */ + webrtc::StreamConfig(info->rate, info->channels, false), /* reverse output stream */ + }}; + + auto apm = std::unique_ptr(webrtc::AudioProcessing::Create(config)); + if (apm->Initialize(pconfig) != webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Error initialising webrtc audio processing module"); + return -1; + } + + apm->high_pass_filter()->Enable(high_pass_filter); + // Always disable drift compensation since PipeWire will already do + // drift compensation on all sinks and sources linked to this echo-canceler + apm->echo_cancellation()->enable_drift_compensation(false); + apm->echo_cancellation()->Enable(true); + // TODO: wire up supression levels to args + apm->echo_cancellation()->set_suppression_level(webrtc::EchoCancellation::kHighSuppression); + apm->noise_suppression()->set_level(webrtc::NoiseSuppression::kHigh); + apm->noise_suppression()->Enable(noise_suppression); + apm->voice_detection()->Enable(voice_detection); + // TODO: wire up AGC parameters to args + apm->gain_control()->set_analog_level_limits(0, 255); + apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveDigital); + apm->gain_control()->Enable(gain_control); + impl->apm = std::move(apm); + impl->info = *info; + impl->play_buffer = std::make_unique(info->channels); + impl->rec_buffer = std::make_unique(info->channels); + impl->out_buffer = std::make_unique(info->channels); + return 0; +} + +static int webrtc_run(void *object, const float *rec[], const float *play[], float *out[], uint32_t n_samples) +{ + auto impl = static_cast(object); + webrtc::StreamConfig config = + webrtc::StreamConfig(impl->info.rate, impl->info.channels, false); + unsigned int num_blocks = n_samples * 1000 / impl->info.rate / 10; + + if (n_samples * 1000 / impl->info.rate % 10 != 0) { + spa_log_error(impl->log, "Buffers must be multiples of 10ms in length (currently %u samples)", n_samples); + return -1; + } + + for (size_t i = 0; i < num_blocks; i ++) { + for (size_t j = 0; j < impl->info.channels; j++) { + impl->play_buffer[j] = const_cast(play[j]) + config.num_frames() * i; + impl->rec_buffer[j] = const_cast(rec[j]) + config.num_frames() * i; + impl->out_buffer[j] = out[j] + config.num_frames() * i; + } + /* FIXME: ProcessReverseStream may change the playback buffer, in which + * case we should use that, if we ever expose the intelligibility + * enhancer */ + if (impl->apm->ProcessReverseStream(impl->play_buffer.get(), config, config, impl->play_buffer.get()) != + webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Processing reverse stream failed"); + } + + // Extra delay introduced by multiple frames + impl->apm->set_stream_delay_ms((num_blocks - 1) * 10); + + if (impl->apm->ProcessStream(impl->rec_buffer.get(), config, config, impl->out_buffer.get()) != + webrtc::AudioProcessing::kNoError) { + spa_log_error(impl->log, "Processing stream failed"); + } + } + + return 0; +} + +static const struct spa_audio_aec_methods impl_aec = { + SPA_VERSION_AUDIO_AEC_METHODS, + .add_listener = NULL, + .init = webrtc_init, + .run = webrtc_run, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + auto impl = reinterpret_cast(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, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_aec_webrtc_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/aec/meson.build b/spa/plugins/aec/meson.build new file mode 100644 index 0000000..2b1a2c0 --- /dev/null +++ b/spa/plugins/aec/meson.build @@ -0,0 +1,16 @@ +aec_null = shared_library('spa-aec-null', + [ 'aec-null.c' ], + include_directories : [ configinc ], + dependencies : [ spa_dep ], + install : true, + install_dir : spa_plugindir / 'aec') + +if webrtc_dep.found() + aec_webrtc = shared_library('spa-aec-webrtc', + [ 'aec-webrtc.cpp' ], + include_directories : [ configinc ], + dependencies : [ spa_dep, webrtc_dep ], + install : true, + install_dir : spa_plugindir / 'aec') +endif + diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules new file mode 100644 index 0000000..54e0244 --- /dev/null +++ b/spa/plugins/alsa/90-pipewire-alsa.rules @@ -0,0 +1,214 @@ +# do not edit this file, it will be overwritten on update + +# This file is part of PipeWire adapted from PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +SUBSYSTEM!="sound", GOTO="pipewire_end" +ACTION!="change", GOTO="pipewire_end" +KERNEL!="card*", GOTO="pipewire_end" +SUBSYSTEMS=="usb", GOTO="pipewire_check_usb" +SUBSYSTEMS=="pci", GOTO="pipewire_check_pci" +SUBSYSTEMS=="firewire", GOTO="pipewire_firewire_quirk" + +SUBSYSTEMS=="platform", DRIVERS=="thinkpad_acpi", ENV{ACP_IGNORE}="1" + +# Force enable speaker and internal mic for some laptops +# This should only be necessary for kernels 3.3, 3.4 and 3.5 (as they are lacking the phantom jack kctls). +# Acer AOA150 +ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x015b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Acer Aspire 4810TZ +ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x022a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Packard bell dot m/a +ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x028c", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Acer Aspire 1810TZ +ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x029b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Acer AOD260 and AO532h +ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x0349", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell MXC051 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01b5", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Inspiron 6400 and E1505 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01bd", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Latitude D620 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01c2", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Latitude D820 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01cc", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Latitude D520 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Latitude D420 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d6", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Inspiron 1525 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x022f", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Inspiron 1011 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x02f4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell XPS 14 (L401X) +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0468", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell XPS 15 (L501X) +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x046e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell XPS 15 (L502X) +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x050e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Dell Inspiron 3420 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0553", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Inspiron 3520 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0555", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Vostro 2420 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0556", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Vostro 2520 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0558", ENV{ACP_PROFILE_SET}="force-speaker.conf" +# Dell Inspiron One 2020 +ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0579", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Asus 904HA (1000H) +ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x831a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Asus T101MT +ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x83ce", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Sony Vaio VGN-SR21M +ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9033", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Sony Vaio VPC-W115XG +ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9064", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Fujitsu Lifebook S7110 +ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1397", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Fujitsu Lifebook A530 +ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1531", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Toshiba A200 +ATTRS{subsystem_vendor}=="0x1179", ATTRS{subsystem_device}=="0xff00", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# MSI X360 +ATTRS{subsystem_vendor}=="0x1462", ATTRS{subsystem_device}=="0x1053", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf" +# Lenovo 3000 Y410 +ATTRS{subsystem_vendor}=="0x17aa", ATTRS{subsystem_device}=="0x384e", ENV{ACP_PROFILE_SET}="force-speaker.conf" + +GOTO="pipewire_end" + +LABEL="pipewire_check_usb" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1978", ENV{ACP_PROFILE_SET}="native-instruments-audio8dj.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="0839", ENV{ACP_PROFILE_SET}="native-instruments-audio4dj.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="baff", ENV{ACP_PROFILE_SET}="native-instruments-traktorkontrol-s4.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="4711", ENV{ACP_PROFILE_SET}="native-instruments-korecontroller.conf" + +# This ID 17cc:041c is verified for the older Audio 2 DJ model (pre-2014 ish). +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041c", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041d", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf" + +# There appear to be two IDs in use for Traktor Audio 6 (or maybe 17cc:1011 +# is just incorrect - 17cc:1010 has been verified to be correct at least +# for some hardware). +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1010", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1011", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf" + + +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1001", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf" +# This entry is for the Komplete Audio 6 MK2, which has a different ID, but is functionally identical to the Komplete Audio 6. +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1870", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf" +ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1021", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio10.conf" +ATTRS{idVendor}=="0763", ATTRS{idProduct}=="2012", ENV{ACP_PROFILE_SET}="maudio-fasttrack-pro.conf" +ATTRS{idVendor}=="045e", ATTRS{idProduct}=="02bb", ENV{ACP_PROFILE_SET}="kinect-audio.conf" +ATTRS{idVendor}=="041e", ATTRS{idProduct}=="322c", ENV{ACP_PROFILE_SET}="sb-omni-surround-5.1.conf" +ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="4014", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf" +ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="402e", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf" +ATTRS{idVendor}=="08bb", ATTRS{idProduct}=="2902", ENV{ACP_PROFILE_SET}="texas-instruments-pcm2902.conf" +ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0269", ENV{ACP_PROFILE_SET}="hp-tbt-dock-120w-g2.conf" +ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0567", ENV{ACP_PROFILE_SET}="hp-tbt-dock-audio-module.conf" + +# ID 1038:12ad is for the 2018 refresh of the Arctis 7. +# ID 1038:1294 is for Arctis Pro Wireless (which works with the Arctis 7 configuration). +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1260", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12ad", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1294", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1730", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 1038:1282 is for SteelSeries GameDAC +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1282", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 1038:12c4 is for Arctis 9 +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12c4", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# Lucidsound LS31 +ATTRS{idVendor}=="2f12", ATTRS{idProduct}=="0109", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 9886:002c is for the Astro A50 Gen4 +ATTRS{idVendor}=="9886", ATTRS{idProduct}=="002c", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 9886:0045 is for the Astro A20 Gen2 +ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0045", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 1532:0520 is for the Razer Kraken Tournament Edition +ATTRS{idVendor}=="1532", ATTRS{idProduct}=="0520", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" + + +# ID 1038:1250 is for the Arctis 5 +# ID 1037:12aa is for the Arctis 5 2019 +# ID 1038:1252 is for the Arctis Pro 2019 edition +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1250", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf" +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12aa", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf" +ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1252", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf" + +ATTRS{idVendor}=="147a", ATTRS{idProduct}=="e055", ENV{ACP_PROFILE_SET}="cmedia-high-speed-true-hdaudio.conf" + +# HyperX Cloud Orbit S has three modes. Each mode has a separate product ID. +# ID_SERIAL for this device is the device name + mode repeated three times. +# ID_SERIAL is used for the ID_ID property, and the ID_ID property is used in +# the card name in PulseAudio. The resulting card name is too long for the name +# length limit, so we set a more sensible ID_ID here (the same as the default +# ID_ID, but without repetition in the serial part). +ATTRS{idVendor}=="0951", ATTRS{idProduct}=="16ff", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_2Ch-$env{ID_USB_INTERFACE_NUM}" +ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1702", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_Hi-Res_2Ch-$env{ID_USB_INTERFACE_NUM}" +ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1703", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_3D_8Ch-$env{ID_USB_INTERFACE_NUM}" + +# OnePlus Type-C Bullets (ED117) +ATTRS{idVendor}=="2a70", ATTRS{idProduct}=="1881", ENV{ACP_PROFILE_SET}="simple-headphones-mic.conf" + +# ID 1395:005e is for Sennheiser GSX 1000 +# ID 1395:00a0 is for Sennheiser GSX 1000 +# ID 1395:00b1 is for Sennheiser GSX 1000 v2 +# ID 1395:005f is for Sennheiser GSX 1200 +# ID 1395:00a1 is for Sennheiser GSX 1200 +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005e", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf" +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a0", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf" +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00b1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf" +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005f", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf" +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf" + +# Sennheiser GSA 70 wireless USB dongle for GSP 670 +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0089", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# EPOS GSA 70 wireless USB dongle for GSP 670 (Sennheiser GSA 70 with updated firmware) +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0300", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# Sennheiser GSP 670 USB headset +ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" + +# Audioengine HD3 powered speakers support IEC958 but don't actually +# have any digital outputs. +ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf" + +# Asus Xonar SE +ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="189d", ENV{ACP_PROFILE_SET}="asus-xonar-se.conf" + +GOTO="pipewire_end" + +LABEL="pipewire_check_pci" + +# Creative SoundBlaster Audigy-based cards +# EMU10k2/CA0100/CA0102/CA10200 +ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0004", ENV{ACP_PROFILE_SET}="audigy.conf" +# CA0108/CA10300 +ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0008", ENV{ACP_PROFILE_SET}="audigy.conf" + +GOTO="pipewire_end" + +LABEL="pipewire_firewire_quirk" + +# Focusrite Saffire Pro 10 i/o and Pro 26 i/o have a quirk to disappear from +# IEEE 1394 bus when breaking connections. This brings an issue for PulseAudio +# to continue the routine for ever that: +# - detecting sound card +# - starting PCM substream +# - stopping the PCM substream +# - the card disappears +# - the card appears +# In detail, see: https://bugzilla.kernel.org/show_bug.cgi?id=199365 +ATTRS{vendor}=="0x00130e", ATTRS{model}=="0x00000[36]", ATTRS{units}=="0x00a02d:0x010001", ENV{ACP_IGNORE}="1" + +LABEL="pipewire_end" diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c new file mode 100644 index 0000000..0fc92a4 --- /dev/null +++ b/spa/plugins/alsa/acp-tool.c @@ -0,0 +1,786 @@ +/* ALSA card profile test + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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) +{ + 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 = 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.2); +} + +static int cmd_dec_volume(struct data *data, const struct command *cmd, int argc, char *argv[]) +{ + return adjust_volume(data, cmd, argc, argv, -0.2); +} + +static int cmd_get_mute(struct data *data, const struct command *cmd, int argc, char *argv[]) +{ + struct acp_card *card = data->card; + uint32_t dev_id; + bool mute; + + if (argc < 2) { + fprintf(stderr, "arguments: 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; + + acp_set_log_func(log_func, data); + acp_set_log_level(data->verbose); + + items[n_items++] = ACP_DICT_ITEM_INIT("use-ucm", "true"); + items[n_items++] = ACP_DICT_ITEM_INIT("verbose", data->verbose ? "true" : "false"); + if (data->properties != NULL) { + char *p = data->properties, *e, f; + + while (*p) { + const char *k, *v; + + if ((e = strchr(p, '=')) == NULL) + break; + *e = '\0'; + k = p; + p = e+1; + + if (*p == '\"') { + p++; + f = '\"'; + } else { + f = ' '; + } + if ((e = strchr(p, f)) == NULL && + (e = strchr(p, '\0')) == NULL) + break; + *e = '\0'; + v = p; + p = e+1; + items[n_items++] = ACP_DICT_ITEM_INIT(k, v); + if (n_items == 64) + break; + } + } + props = ACP_DICT_INIT(items, n_items); + + data->card = acp_card_new(data->card_index, &props); + if (data->card == NULL) + return -errno; + return 0; +} + +static int do_prompt(struct data *data) +{ + struct pollfd *pfds; + int err, count; + + acp_card_add_listener(data->card, &card_events, data); + + count = acp_card_poll_descriptors_count(data->card); + if (count == 0) + fprintf(stderr, "card has no events\n"); + + count++; + pfds = alloca(sizeof(struct pollfd) * count); + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + + print_card(data, data->card, 0, 0); + + fprintf(stderr, "type 'help' for usage.\n"); + show_prompt(data); + + while (!data->quit) { + unsigned short revents; + + err = acp_card_poll_descriptors(data->card, &pfds[1], count-1); + if (err < 0) + return err; + + err = poll(pfds, (unsigned int) count, -1); + if (err < 0) + return -errno; + + if (pfds[0].revents & POLLIN) { + if ((err = handle_input(data)) < 0) { + if (err == -EPIPE) + break; + } + } + + if (count < 2) + continue; + + err = acp_card_poll_descriptors_revents(data->card, &pfds[1], count-1, &revents); + if (err < 0) + return err; + + if (revents) + acp_card_handle_events(data->card); + } + return 0; +} + +#define OPTIONS "hvc:p:" +static const struct option long_options[] = { + { "help", no_argument, NULL, 'h'}, + { "verbose", no_argument, NULL, 'v'}, + + { "card", required_argument, NULL, 'c' }, + { "properties", required_argument, NULL, 'p' }, + + { NULL, 0, NULL, 0 } +}; + +static void show_usage(struct data *data, const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, "%s [options] [COMMAND]\n", name); + fprintf(fp, + " -h, --help Show this help\n" + " -v --verbose Be verbose\n" + " -c --card Card number\n" + " -p --properties Extra properties:\n" + " 'key=value ... '\n" + "\n"); + cmd_help(data, NULL, 0, NULL); +} + +int main(int argc, char *argv[]) +{ + int c, res; + int longopt_index = 0, ret; + struct data data = { 0, }; + + data.verbose = 1; + + while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) { + switch (c) { + case 'h': + show_usage(&data, argv[0], false); + return EXIT_SUCCESS; + case 'v': + data.verbose++; + break; + case 'c': + ret = atoi(optarg); + if (ret < 0) { + fprintf(stderr, "error: bad card %s\n", optarg); + goto error_usage; + } + data.card_index = ret; + break; + case 'p': + data.properties = strdup(optarg); + break; + default: + fprintf(stderr, "error: unknown option '%c'\n", c); + goto error_usage; + } + } + + if ((res = do_probe(&data)) < 0) { + fprintf(stderr, "failed to probe card: %s\n", strerror(-res)); + return res; + } + + if (optind < argc) + run_command(&data, argc - optind, &argv[optind]); + else + do_prompt(&data); + + if (data.card) + acp_card_destroy(data.card); + + free(data.properties); + + return 0; + +error_usage: + show_usage(&data, argv[0], true); + return EXIT_FAILURE; +} diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c new file mode 100644 index 0000000..f9985b4 --- /dev/null +++ b/spa/plugins/alsa/acp/acp.c @@ -0,0 +1,1983 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "acp.h" +#include "alsa-mixer.h" +#include "alsa-ucm.h" + +#include + +int _acp_log_level = 1; +acp_log_func _acp_log_func; +void *_acp_log_data; + +struct spa_i18n *acp_i18n; + +#define DEFAULT_RATE 48000 + +#define VOLUME_ACCURACY (PA_VOLUME_NORM/100) /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */ + +static const uint32_t channel_table[PA_CHANNEL_POSITION_MAX] = { + [PA_CHANNEL_POSITION_MONO] = ACP_CHANNEL_MONO, + + [PA_CHANNEL_POSITION_FRONT_LEFT] = ACP_CHANNEL_FL, + [PA_CHANNEL_POSITION_FRONT_RIGHT] = ACP_CHANNEL_FR, + [PA_CHANNEL_POSITION_FRONT_CENTER] = ACP_CHANNEL_FC, + + [PA_CHANNEL_POSITION_REAR_CENTER] = ACP_CHANNEL_RC, + [PA_CHANNEL_POSITION_REAR_LEFT] = ACP_CHANNEL_RL, + [PA_CHANNEL_POSITION_REAR_RIGHT] = ACP_CHANNEL_RR, + + [PA_CHANNEL_POSITION_LFE] = ACP_CHANNEL_LFE, + [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = ACP_CHANNEL_FLC, + [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = ACP_CHANNEL_FRC, + + [PA_CHANNEL_POSITION_SIDE_LEFT] = ACP_CHANNEL_SL, + [PA_CHANNEL_POSITION_SIDE_RIGHT] = ACP_CHANNEL_SR, + + [PA_CHANNEL_POSITION_AUX0] = ACP_CHANNEL_START_Aux + 0, + [PA_CHANNEL_POSITION_AUX1] = ACP_CHANNEL_START_Aux + 1, + [PA_CHANNEL_POSITION_AUX2] = ACP_CHANNEL_START_Aux + 2, + [PA_CHANNEL_POSITION_AUX3] = ACP_CHANNEL_START_Aux + 3, + [PA_CHANNEL_POSITION_AUX4] = ACP_CHANNEL_START_Aux + 4, + [PA_CHANNEL_POSITION_AUX5] = ACP_CHANNEL_START_Aux + 5, + [PA_CHANNEL_POSITION_AUX6] = ACP_CHANNEL_START_Aux + 6, + [PA_CHANNEL_POSITION_AUX7] = ACP_CHANNEL_START_Aux + 7, + [PA_CHANNEL_POSITION_AUX8] = ACP_CHANNEL_START_Aux + 8, + [PA_CHANNEL_POSITION_AUX9] = ACP_CHANNEL_START_Aux + 9, + [PA_CHANNEL_POSITION_AUX10] = ACP_CHANNEL_START_Aux + 10, + [PA_CHANNEL_POSITION_AUX11] = ACP_CHANNEL_START_Aux + 11, + [PA_CHANNEL_POSITION_AUX12] = ACP_CHANNEL_START_Aux + 12, + [PA_CHANNEL_POSITION_AUX13] = ACP_CHANNEL_START_Aux + 13, + [PA_CHANNEL_POSITION_AUX14] = ACP_CHANNEL_START_Aux + 14, + [PA_CHANNEL_POSITION_AUX15] = ACP_CHANNEL_START_Aux + 15, + [PA_CHANNEL_POSITION_AUX16] = ACP_CHANNEL_START_Aux + 16, + [PA_CHANNEL_POSITION_AUX17] = ACP_CHANNEL_START_Aux + 17, + [PA_CHANNEL_POSITION_AUX18] = ACP_CHANNEL_START_Aux + 18, + [PA_CHANNEL_POSITION_AUX19] = ACP_CHANNEL_START_Aux + 19, + [PA_CHANNEL_POSITION_AUX20] = ACP_CHANNEL_START_Aux + 20, + [PA_CHANNEL_POSITION_AUX21] = ACP_CHANNEL_START_Aux + 21, + [PA_CHANNEL_POSITION_AUX22] = ACP_CHANNEL_START_Aux + 22, + [PA_CHANNEL_POSITION_AUX23] = ACP_CHANNEL_START_Aux + 23, + [PA_CHANNEL_POSITION_AUX24] = ACP_CHANNEL_START_Aux + 24, + [PA_CHANNEL_POSITION_AUX25] = ACP_CHANNEL_START_Aux + 25, + [PA_CHANNEL_POSITION_AUX26] = ACP_CHANNEL_START_Aux + 26, + [PA_CHANNEL_POSITION_AUX27] = ACP_CHANNEL_START_Aux + 27, + [PA_CHANNEL_POSITION_AUX28] = ACP_CHANNEL_START_Aux + 28, + [PA_CHANNEL_POSITION_AUX29] = ACP_CHANNEL_START_Aux + 29, + [PA_CHANNEL_POSITION_AUX30] = ACP_CHANNEL_START_Aux + 30, + [PA_CHANNEL_POSITION_AUX31] = ACP_CHANNEL_START_Aux + 31, + + [PA_CHANNEL_POSITION_TOP_CENTER] = ACP_CHANNEL_TC, + + [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = ACP_CHANNEL_TFL, + [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = ACP_CHANNEL_TFR, + [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = ACP_CHANNEL_TFC, + + [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = ACP_CHANNEL_TRL, + [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = ACP_CHANNEL_TRR, + [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = ACP_CHANNEL_TRC, +}; + +static const char *channel_names[] = { + [ACP_CHANNEL_UNKNOWN] = "UNK", + [ACP_CHANNEL_NA] = "NA", + [ACP_CHANNEL_MONO] = "MONO", + [ACP_CHANNEL_FL] = "FL", + [ACP_CHANNEL_FR] = "FR", + [ACP_CHANNEL_FC] = "FC", + [ACP_CHANNEL_LFE] = "LFE", + [ACP_CHANNEL_SL] = "SL", + [ACP_CHANNEL_SR] = "SR", + [ACP_CHANNEL_FLC] = "FLC", + [ACP_CHANNEL_FRC] = "FRC", + [ACP_CHANNEL_RC] = "RC", + [ACP_CHANNEL_RL] = "RL", + [ACP_CHANNEL_RR] = "RR", + [ACP_CHANNEL_TC] = "TC", + [ACP_CHANNEL_TFL] = "TFL", + [ACP_CHANNEL_TFC] = "TFC", + [ACP_CHANNEL_TFR] = "TFR", + [ACP_CHANNEL_TRL] = "TRL", + [ACP_CHANNEL_TRC] = "TRC", + [ACP_CHANNEL_TRR] = "TRR", + [ACP_CHANNEL_RLC] = "RLC", + [ACP_CHANNEL_RRC] = "RRC", + [ACP_CHANNEL_FLW] = "FLW", + [ACP_CHANNEL_FRW] = "FRW", + [ACP_CHANNEL_LFE2] = "LFE2", + [ACP_CHANNEL_FLH] = "FLH", + [ACP_CHANNEL_FCH] = "FCH", + [ACP_CHANNEL_FRH] = "FRH", + [ACP_CHANNEL_TFLC] = "TFLC", + [ACP_CHANNEL_TFRC] = "TFRC", + [ACP_CHANNEL_TSL] = "TSL", + [ACP_CHANNEL_TSR] = "TSR", + [ACP_CHANNEL_LLFE] = "LLFE", + [ACP_CHANNEL_RLFE] = "RLFE", + [ACP_CHANNEL_BC] = "BC", + [ACP_CHANNEL_BLC] = "BLC", + [ACP_CHANNEL_BRC] = "BRC", +}; + +#define ACP_N_ELEMENTS(arr) (sizeof(arr) / sizeof((arr)[0])) + +static inline uint32_t channel_pa2acp(pa_channel_position_t channel) +{ + if (channel < 0 || (size_t)channel >= ACP_N_ELEMENTS(channel_table)) + return ACP_CHANNEL_UNKNOWN; + return channel_table[channel]; +} + +char *acp_channel_str(char *buf, size_t len, enum acp_channel ch) +{ + if (ch >= ACP_CHANNEL_START_Aux && ch <= ACP_CHANNEL_LAST_Aux) { + snprintf(buf, len, "AUX%d", ch - ACP_CHANNEL_START_Aux); + } else if (ch >= ACP_CHANNEL_UNKNOWN && ch <= ACP_CHANNEL_BRC) { + snprintf(buf, len, "%s", channel_names[ch]); + } else { + snprintf(buf, len, "UNK"); + } + return buf; +} + + +const char *acp_available_str(enum acp_available status) +{ + switch (status) { + case ACP_AVAILABLE_UNKNOWN: + return "unknown"; + case ACP_AVAILABLE_NO: + return "no"; + case ACP_AVAILABLE_YES: + return "yes"; + } + return "error"; +} + +const char *acp_direction_str(enum acp_direction direction) +{ + switch (direction) { + case ACP_DIRECTION_CAPTURE: + return "capture"; + case ACP_DIRECTION_PLAYBACK: + return "playback"; + } + return "error"; +} + +static void port_free(void *data) +{ + pa_device_port *dp = data; + pa_dynarray_clear(&dp->devices); + pa_dynarray_clear(&dp->prof); + pa_device_port_free(dp); +} + +static void device_free(void *data) +{ + pa_alsa_device *dev = data; + pa_dynarray_clear(&dev->port_array); + pa_proplist_free(dev->proplist); + pa_hashmap_free(dev->ports); +} + +static inline void channelmap_to_acp(pa_channel_map *m, uint32_t *map) +{ + uint32_t i, j; + for (i = 0; i < m->channels; i++) { + map[i] = channel_pa2acp(m->map[i]); + for (j = 0; j < i; j++) { + if (map[i] == map[j]) + map[i] += 32; + } + + } +} + +static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t direction, + pa_alsa_mapping *m, uint32_t index) +{ + char **d; + + dev->card = impl; + dev->mapping = m; + dev->device.index = index; + dev->device.name = m->name; + dev->device.description = m->description; + dev->device.priority = m->priority; + dev->device.device_strings = (const char **)m->device_strings; + dev->device.format.format_mask = m->sample_spec.format; + dev->device.format.rate_mask = m->sample_spec.rate; + dev->device.format.channels = m->channel_map.channels; + pa_cvolume_reset(&dev->real_volume, dev->device.format.channels); + pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels); + channelmap_to_acp(&m->channel_map, dev->device.format.map); + dev->direction = direction; + dev->proplist = pa_proplist_new(); + pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->proplist); + if (direction == PA_ALSA_DIRECTION_OUTPUT) { + dev->mixer_path_set = m->output_path_set; + dev->pcm_handle = m->output_pcm; + dev->device.direction = ACP_DIRECTION_PLAYBACK; + pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->output_proplist); + } else { + dev->mixer_path_set = m->input_path_set; + dev->pcm_handle = m->input_pcm; + dev->device.direction = ACP_DIRECTION_CAPTURE; + pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist); + } + pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name); + pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description); + pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index); + pa_proplist_as_dict(dev->proplist, &dev->device.props); + + dev->ports = pa_hashmap_new(pa_idxset_string_hash_func, + pa_idxset_string_compare_func); + if (m->ucm_context.ucm) { + dev->ucm_context = &m->ucm_context; + if (impl->ucm.alib_prefix != NULL) { + for (d = m->device_strings; *d; d++) { + if (pa_startswith(*d, impl->ucm.alib_prefix)) { + size_t plen = strlen(impl->ucm.alib_prefix); + size_t len = strlen(*d); + memmove(*d, (*d) + plen, len - plen + 1); + dev->device.flags |= ACP_DEVICE_UCM_DEVICE; + } + } + } + } + for (d = m->device_strings; *d; d++) { + if (pa_startswith(*d, "iec958") || + pa_startswith(*d, "hdmi")) + dev->device.flags |= ACP_DEVICE_IEC958; + } + pa_dynarray_init(&dev->port_array, NULL); +} + +static int compare_profile(const void *a, const void *b) +{ + const pa_hashmap_item *i1 = a; + const pa_hashmap_item *i2 = b; + const pa_alsa_profile *p1, *p2; + if (i1->key == NULL || i2->key == NULL) + return 0; + p1 = i1->value; + p2 = i2->value; + if (p1->profile.priority == 0 || p2->profile.priority == 0) + return 0; + return p2->profile.priority - p1->profile.priority; +} + +static void profile_free(void *data) +{ + pa_alsa_profile *ap = data; + pa_dynarray_clear(&ap->out.devices); + if (ap->profile.flags & ACP_PROFILE_OFF) { + free(ap->name); + free(ap->description); + free(ap); + } +} + +static int add_pro_profile(pa_card *impl, uint32_t index) +{ + snd_ctl_t *ctl_hndl; + int err, dev, count = 0; + pa_alsa_profile *ap; + pa_alsa_profile_set *ps = impl->profile_set; + pa_alsa_mapping *m; + char *device; + snd_pcm_info_t *pcminfo; + pa_sample_spec ss; + snd_pcm_uframes_t try_period_size, try_buffer_size; + + ss.format = PA_SAMPLE_S32LE; + ss.rate = impl->rate; + ss.channels = 64; + + ap = pa_xnew0(pa_alsa_profile, 1); + ap->profile_set = ps; + ap->profile.name = ap->name = pa_xstrdup("pro-audio"); + ap->profile.description = ap->description = pa_xstrdup(_("Pro Audio")); + ap->profile.available = ACP_AVAILABLE_YES; + ap->profile.flags = ACP_PROFILE_PRO; + ap->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + ap->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + pa_hashmap_put(ps->profiles, ap->name, ap); + + ap->output_name = pa_xstrdup("pro-output"); + ap->input_name = pa_xstrdup("pro-input"); + ap->priority = 1; + + pa_assert_se(asprintf(&device, "hw:%d", index) >= 0); + + if ((err = snd_ctl_open(&ctl_hndl, device, 0)) < 0) { + pa_log_error("can't open control for card %s: %s", + device, snd_strerror(err)); + free(device); + return err; + } + free(device); + + snd_pcm_info_alloca(&pcminfo); + + dev = -1; + while (1) { + char desc[128], devstr[128], *name; + + if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) { + pa_log_error("error iterating devices: %s", snd_strerror(err)); + break; + } + if (dev < 0) + break; + + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + + snprintf(devstr, sizeof(devstr), "hw:%d,%d", index, dev); + if (count++ == 0) + snprintf(desc, sizeof(desc), "Pro"); + else + snprintf(desc, sizeof(desc), "Pro %d", dev); + + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK); + if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) { + if (err != -ENOENT) + pa_log_error("error pcm info: %s", snd_strerror(err)); + } + if (err >= 0) { + pa_assert_se(asprintf(&name, "Mapping pro-output-%d", dev) >= 0); + m = pa_alsa_mapping_get(ps, name); + m->description = pa_xstrdup(desc); + m->device_strings = pa_split_spaces_strv(devstr); + + try_period_size = 1024; + try_buffer_size = 1024 * 64; + m->sample_spec = ss; + + if ((m->output_pcm = pa_alsa_open_by_template(m->device_strings, + devstr, NULL, &m->sample_spec, + &m->channel_map, SND_PCM_STREAM_PLAYBACK, + &try_period_size, &try_buffer_size, + 0, NULL, NULL, false))) { + pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); + pa_proplist_setf(m->output_proplist, "clock.name", "api.alsa.%u", index); + pa_alsa_close(&m->output_pcm); + m->supported = true; + pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX); + } + pa_idxset_put(ap->output_mappings, m, NULL); + free(name); + } + + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); + if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) { + if (err != -ENOENT) + pa_log_error("error pcm info: %s", snd_strerror(err)); + } + if (err >= 0) { + pa_assert_se(asprintf(&name, "Mapping pro-input-%d", dev) >= 0); + m = pa_alsa_mapping_get(ps, name); + m->description = pa_xstrdup(desc); + m->device_strings = pa_split_spaces_strv(devstr); + + try_period_size = 1024; + try_buffer_size = 1024 * 64; + m->sample_spec = ss; + + if ((m->input_pcm = pa_alsa_open_by_template(m->device_strings, + devstr, NULL, &m->sample_spec, + &m->channel_map, SND_PCM_STREAM_CAPTURE, + &try_period_size, &try_buffer_size, + 0, NULL, NULL, false))) { + pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); + pa_proplist_setf(m->input_proplist, "clock.name", "api.alsa.%u", index); + pa_alsa_close(&m->input_pcm); + m->supported = true; + pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX); + } + pa_idxset_put(ap->input_mappings, m, NULL); + free(name); + } + } + snd_ctl_close(ctl_hndl); + + return 0; +} + + +static void add_profiles(pa_card *impl) +{ + pa_alsa_profile *ap; + void *state; + struct acp_card_profile *cp; + pa_device_port *dp; + pa_alsa_device *dev; + int n_profiles, n_ports, n_devices; + uint32_t idx; + + n_devices = 0; + pa_dynarray_init(&impl->out.devices, device_free); + + ap = pa_xnew0(pa_alsa_profile, 1); + ap->profile.name = ap->name = pa_xstrdup("off"); + ap->profile.description = ap->description = pa_xstrdup(_("Off")); + ap->profile.available = ACP_AVAILABLE_YES; + ap->profile.flags = ACP_PROFILE_OFF; + pa_hashmap_put(impl->profiles, ap->name, ap); + + add_pro_profile(impl, impl->card.index); + + PA_HASHMAP_FOREACH(ap, impl->profile_set->profiles, state) { + pa_alsa_mapping *m; + + cp = &ap->profile; + cp->name = ap->name; + cp->description = ap->description; + cp->priority = ap->priority ? ap->priority : 1; + + pa_dynarray_init(&ap->out.devices, NULL); + + if (ap->output_mappings) { + PA_IDXSET_FOREACH(m, ap->output_mappings, idx) { + dev = &m->output; + if (dev->mapping == NULL) { + init_device(impl, dev, PA_ALSA_DIRECTION_OUTPUT, m, n_devices++); + pa_dynarray_append(&impl->out.devices, dev); + } + if (impl->use_ucm) { + if (m->ucm_context.ucm_devices) { + pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context, + true, impl->ports, ap, NULL); + pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context, + true, impl, dev->pcm_handle, impl->profile_set->ignore_dB); + } + } + else + pa_alsa_path_set_add_ports(m->output_path_set, ap, impl->ports, + dev->ports, NULL); + + pa_dynarray_append(&ap->out.devices, dev); + } + } + + if (ap->input_mappings) { + PA_IDXSET_FOREACH(m, ap->input_mappings, idx) { + dev = &m->input; + if (dev->mapping == NULL) { + init_device(impl, dev, PA_ALSA_DIRECTION_INPUT, m, n_devices++); + pa_dynarray_append(&impl->out.devices, dev); + } + + if (impl->use_ucm) { + if (m->ucm_context.ucm_devices) { + pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context, + false, impl->ports, ap, NULL); + pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context, + false, impl, dev->pcm_handle, impl->profile_set->ignore_dB); + } + } else + pa_alsa_path_set_add_ports(m->input_path_set, ap, impl->ports, + dev->ports, NULL); + + pa_dynarray_append(&ap->out.devices, dev); + } + } + cp->n_devices = pa_dynarray_size(&ap->out.devices); + cp->devices = ap->out.devices.array.data; + pa_hashmap_put(impl->profiles, ap->name, cp); + } + + pa_dynarray_init(&impl->out.ports, NULL); + n_ports = 0; + PA_HASHMAP_FOREACH(dp, impl->ports, state) { + void *state2; + dp->card = impl; + dp->port.index = n_ports++; + dp->port.priority = dp->priority; + pa_dynarray_init(&dp->prof, NULL); + pa_dynarray_init(&dp->devices, NULL); + n_profiles = 0; + PA_HASHMAP_FOREACH(cp, dp->profiles, state2) { + pa_dynarray_append(&dp->prof, cp); + n_profiles++; + } + dp->port.n_profiles = n_profiles; + dp->port.profiles = dp->prof.array.data; + + pa_proplist_setf(dp->proplist, "card.profile.port", "%u", dp->port.index); + pa_proplist_as_dict(dp->proplist, &dp->port.props); + pa_dynarray_append(&impl->out.ports, dp); + } + PA_DYNARRAY_FOREACH(dev, &impl->out.devices, idx) { + PA_HASHMAP_FOREACH(dp, dev->ports, state) { + pa_dynarray_append(&dev->port_array, dp); + pa_dynarray_append(&dp->devices, dev); + } + dev->device.ports = dev->port_array.array.data; + dev->device.n_ports = pa_dynarray_size(&dev->port_array); + } + PA_HASHMAP_FOREACH(dp, impl->ports, state) { + dp->port.devices = dp->devices.array.data; + dp->port.n_devices = pa_dynarray_size(&dp->devices); + } + + pa_hashmap_sort(impl->profiles, compare_profile); + + n_profiles = 0; + pa_dynarray_init(&impl->out.profiles, NULL); + PA_HASHMAP_FOREACH(cp, impl->profiles, state) { + cp->index = n_profiles++; + pa_dynarray_append(&impl->out.profiles, cp); + } +} + +static pa_available_t calc_port_state(pa_device_port *p, pa_card *impl) +{ + void *state; + pa_alsa_jack *jack; + pa_available_t pa = PA_AVAILABLE_UNKNOWN; + pa_device_port *port; + + PA_HASHMAP_FOREACH(jack, impl->jacks, state) { + pa_available_t cpa; + + if (impl->use_ucm) + port = pa_hashmap_get(impl->ports, jack->name); + else { + if (jack->path) + port = jack->path->port; + else + continue; + } + + if (p != port) + continue; + + cpa = jack->plugged_in ? jack->state_plugged : jack->state_unplugged; + + if (cpa == PA_AVAILABLE_NO) { + /* If a plugged-in jack causes the availability to go to NO, it + * should override all other availability information (like a + * blacklist) so set and bail */ + if (jack->plugged_in) { + pa = cpa; + break; + } + + /* If the current availability is unknown go the more precise no, + * but otherwise don't change state */ + if (pa == PA_AVAILABLE_UNKNOWN) + pa = cpa; + } else if (cpa == PA_AVAILABLE_YES) { + /* Output is available through at least one jack, so go to that + * level of availability. We still need to continue iterating through + * the jacks in case a jack is plugged in that forces the state to no + */ + pa = cpa; + } + } + return pa; +} + +static void profile_set_available(pa_card *impl, uint32_t index, + enum acp_available status, bool emit) +{ + struct acp_card_profile *p = impl->card.profiles[index]; + enum acp_available old = p->available; + + if (old != status) + pa_log_info("Profile %s available %s -> %s", p->name, + acp_available_str(old), acp_available_str(status)); + + p->available = status; + + if (emit && impl->events && impl->events->profile_available) + impl->events->profile_available(impl->user_data, index, + old, status); +} + +struct temp_port_avail { + pa_device_port *port; + pa_available_t avail; +}; + +static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask) +{ + pa_card *impl = snd_mixer_elem_get_callback_private(melem); + snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem); + snd_ctl_elem_value_t *elem_value; + bool plugged_in, any_input_port_available; + void *state; + pa_alsa_jack *jack; + struct temp_port_avail *tp, *tports; + pa_alsa_profile *profile; + enum acp_available active_available = ACP_AVAILABLE_UNKNOWN; + size_t size; + +#if 0 + /* Changing the jack state may cause a port change, and a port change will + * make the sink or source change the mixer settings. If there are multiple + * users having pulseaudio running, the mixer changes done by inactive + * users may mess up the volume settings for the active users, because when + * the inactive users change the mixer settings, those changes are picked + * up by the active user's pulseaudio instance and the changes are + * interpreted as if the active user changed the settings manually e.g. + * with alsamixer. Even single-user systems suffer from this, because gdm + * runs its own pulseaudio instance. + * + * We rerun this function when being unsuspended to catch up on jack state + * changes */ + if (u->card->suspend_cause & PA_SUSPEND_SESSION) + return 0; +#endif + + if (mask == SND_CTL_EVENT_MASK_REMOVE) + return 0; + + snd_ctl_elem_value_alloca(&elem_value); + if (snd_hctl_elem_read(elem, elem_value) < 0) { + pa_log_warn("Failed to read jack detection from '%s'", pa_strnull(snd_hctl_elem_get_name(elem))); + return 0; + } + + plugged_in = !!snd_ctl_elem_value_get_boolean(elem_value, 0); + + pa_log_debug("Jack '%s' is now %s", pa_strnull(snd_hctl_elem_get_name(elem)), + plugged_in ? "plugged in" : "unplugged"); + + size = sizeof(struct temp_port_avail) * (pa_hashmap_size(impl->jacks)+1); + tports = tp = alloca(size); + memset(tports, 0, size); + + PA_HASHMAP_FOREACH(jack, impl->jacks, state) + if (jack->melem == melem) { + pa_alsa_jack_set_plugged_in(jack, plugged_in); + + if (impl->use_ucm) { + /* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack + * state to port availability. */ + continue; + } + + /* When not using UCM, we have to do the jack state -> port + * availability mapping ourselves. */ + pa_assert_se(tp->port = jack->path->port); + tp->avail = calc_port_state(tp->port, impl); + tp++; + } + + /* Report available ports before unavailable ones: in case port 1 + * becomes available when port 2 becomes unavailable, + * this prevents an unnecessary switch port 1 -> port 3 -> port 2 */ + + for (tp = tports; tp->port; tp++) + if (tp->avail != PA_AVAILABLE_NO) + pa_device_port_set_available(tp->port, tp->avail); + for (tp = tports; tp->port; tp++) + if (tp->avail == PA_AVAILABLE_NO) + pa_device_port_set_available(tp->port, tp->avail); + + for (tp = tports; tp->port; tp++) { + pa_alsa_port_data *data; + + data = PA_DEVICE_PORT_DATA(tp->port); + + if (!data->suspend_when_unavailable) + continue; + +#if 0 + pa_sink *sink; + uint32_t idx; + PA_IDXSET_FOREACH(sink, u->core->sinks, idx) { + if (sink->active_port == tp->port) + pa_sink_suspend(sink, tp->avail == PA_AVAILABLE_NO, PA_SUSPEND_UNAVAILABLE); + } +#endif + } + + /* Update profile availabilities. Ideally we would mark all profiles + * unavailable that contain unavailable devices. We can't currently do that + * in all cases, because if there are multiple sinks in a profile, and the + * profile contains a mix of available and unavailable ports, we don't know + * how the ports are distributed between the different sinks. It's possible + * that some sinks contain only unavailable ports, in which case we should + * mark the profile as unavailable, but it's also possible that all sinks + * contain at least one available port, in which case we should mark the + * profile as available. Until the data structures are improved so that we + * can distinguish between these two cases, we mark the problematic cases + * as available (well, "unknown" to be precise, but there's little + * practical difference). + * + * When all output ports are unavailable, we know that all sinks are + * unavailable, and therefore the profile is marked unavailable as well. + * The same applies to input ports as well, of course. + * + * If there are no output ports at all, but the profile contains at least + * one sink, then the output is considered to be available. */ + if (impl->card.active_profile_index != ACP_INVALID_INDEX) + active_available = impl->card.profiles[impl->card.active_profile_index]->available; + + /* First round - detect, if we have any input port available. + If the hardware can report the state for all I/O jacks, only speakers + may be plugged in. */ + any_input_port_available = false; + PA_HASHMAP_FOREACH(profile, impl->profiles, state) { + pa_device_port *port; + void *state2; + + if (profile->profile.flags & ACP_PROFILE_OFF) + continue; + + PA_HASHMAP_FOREACH(port, impl->ports, state2) { + if (!pa_hashmap_get(port->profiles, profile->profile.name)) + continue; + + if (port->port.direction == ACP_DIRECTION_CAPTURE && + port->port.available != ACP_AVAILABLE_NO) { + any_input_port_available = true; + goto input_port_found; + } + } + } +input_port_found: + + /* Second round */ + PA_HASHMAP_FOREACH(profile, impl->profiles, state) { + pa_device_port *port; + void *state2; + bool has_input_port = false; + bool has_output_port = false; + bool found_available_input_port = false; + bool found_available_output_port = false; + enum acp_available available = ACP_AVAILABLE_UNKNOWN; + + if (profile->profile.flags & ACP_PROFILE_OFF) + continue; + + PA_HASHMAP_FOREACH(port, impl->ports, state2) { + if (!pa_hashmap_get(port->profiles, profile->profile.name)) + continue; + + if (port->port.direction == ACP_DIRECTION_CAPTURE) { + has_input_port = true; + if (port->port.available != ACP_AVAILABLE_NO) + found_available_input_port = true; + } else { + has_output_port = true; + if (port->port.available != ACP_AVAILABLE_NO) + found_available_output_port = true; + } + } + + if ((has_input_port && !found_available_input_port) || + (has_output_port && !found_available_output_port)) + available = ACP_AVAILABLE_NO; + + if (has_input_port && !has_output_port && found_available_input_port) + available = ACP_AVAILABLE_YES; + if (has_output_port && (!has_input_port || !any_input_port_available) && found_available_output_port) + available = ACP_AVAILABLE_YES; + if (has_output_port && has_input_port && found_available_output_port && found_available_input_port) + available = ACP_AVAILABLE_YES; + + /* We want to update the active profile's status last, so logic that + * may change the active profile based on profile availability status + * has an updated view of all profiles' availabilities. */ + if (profile->profile.index == impl->card.active_profile_index) + active_available = available; + else + profile_set_available(impl, profile->profile.index, available, false); + } + + if (impl->card.active_profile_index != ACP_INVALID_INDEX) + profile_set_available(impl, impl->card.active_profile_index, active_available, true); + + return 0; +} + +static void init_jacks(pa_card *impl) +{ + void *state; + pa_alsa_path* path; + pa_alsa_jack* jack; + char buf[64]; + + impl->jacks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (impl->use_ucm) { + PA_LLIST_FOREACH(jack, impl->ucm.jacks) + if (jack->has_control) + pa_hashmap_put(impl->jacks, jack, jack); + } else { + /* See if we have any jacks */ + if (impl->profile_set->output_paths) + PA_HASHMAP_FOREACH(path, impl->profile_set->output_paths, state) + PA_LLIST_FOREACH(jack, path->jacks) + if (jack->has_control) + pa_hashmap_put(impl->jacks, jack, jack); + + if (impl->profile_set->input_paths) + PA_HASHMAP_FOREACH(path, impl->profile_set->input_paths, state) + PA_LLIST_FOREACH(jack, path->jacks) + if (jack->has_control) + pa_hashmap_put(impl->jacks, jack, jack); + } + + pa_log_debug("Found %d jacks.", pa_hashmap_size(impl->jacks)); + + if (pa_hashmap_size(impl->jacks) == 0) + return; + + PA_HASHMAP_FOREACH(jack, impl->jacks, state) { + if (!jack->mixer_device_name) { + jack->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, false); + if (!jack->mixer_handle) { + pa_log("Failed to open mixer for card %d for jack detection", impl->card.index); + continue; + } + } else { + jack->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, jack->mixer_device_name, false); + if (!jack->mixer_handle) { + pa_log("Failed to open mixer '%s' for jack detection", jack->mixer_device_name); + continue; + } + } + + pa_alsa_mixer_use_for_poll(impl->ucm.mixers, jack->mixer_handle); + jack->melem = pa_alsa_mixer_find_card(jack->mixer_handle, &jack->alsa_id, 0); + if (!jack->melem) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &jack->alsa_id); + pa_log_warn("Jack '%s' seems to have disappeared.", buf); + pa_alsa_jack_set_has_control(jack, false); + continue; + } + snd_mixer_elem_set_callback(jack->melem, report_jack_state); + snd_mixer_elem_set_callback_private(jack->melem, impl); + report_jack_state(jack->melem, 0); + } +} +static pa_device_port* find_port_with_eld_device(pa_card *impl, int device) +{ + void *state; + pa_device_port *p; + + if (impl->use_ucm) { + PA_HASHMAP_FOREACH(p, impl->ports, state) { + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(p); + pa_assert(data->eld_mixer_device_name); + if (device == data->eld_device) + return p; + } + } else { + PA_HASHMAP_FOREACH(p, impl->ports, state) { + pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(p); + pa_assert(data->path); + if (device == data->path->eld_device) + return p; + } + } + return NULL; +} + +static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) +{ + pa_card *impl = snd_mixer_elem_get_callback_private(melem); + snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem); + int device = snd_hctl_elem_get_device(elem); + const char *old_monitor_name; + pa_device_port *p; + pa_hdmi_eld eld; + bool changed = false; + + if (mask == SND_CTL_EVENT_MASK_REMOVE) + return 0; + + p = find_port_with_eld_device(impl, device); + if (p == NULL) { + pa_log_error("Invalid device changed in ALSA: %d", device); + return 0; + } + + if (pa_alsa_get_hdmi_eld(elem, &eld) < 0) + memset(&eld, 0, sizeof(eld)); + + old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME); + if (eld.monitor_name[0] == '\0') { + changed |= old_monitor_name != NULL; + pa_proplist_unset(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME); + } else { + changed |= (old_monitor_name == NULL) || (!spa_streq(old_monitor_name, eld.monitor_name)); + pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name); + } + pa_proplist_as_dict(p->proplist, &p->port.props); + + if (changed && mask != 0 && impl->events && impl->events->props_changed) + impl->events->props_changed(impl->user_data); + return 0; +} + +static void init_eld_ctls(pa_card *impl) +{ + void *state; + pa_device_port *port; + + /* The code in this function expects ports to have a pa_alsa_port_data + * struct as their data, but in UCM mode ports don't have any data. Hence, + * the ELD controls can't currently be used in UCM mode. */ + PA_HASHMAP_FOREACH(port, impl->ports, state) { + snd_mixer_t *mixer_handle; + snd_mixer_elem_t* melem; + int device; + + if (impl->use_ucm) { + pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port); + device = data->eld_device; + if (device < 0 || !data->eld_mixer_device_name) + continue; + + mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, data->eld_mixer_device_name, true); + } else { + pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(port); + + pa_assert(data->path); + + device = data->path->eld_device; + if (device < 0) + continue; + + mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true); + } + + if (!mixer_handle) + continue; + + melem = pa_alsa_mixer_find_pcm(mixer_handle, "ELD", device); + if (melem) { + pa_alsa_mixer_use_for_poll(impl->ucm.mixers, mixer_handle); + snd_mixer_elem_set_callback(melem, hdmi_eld_changed); + snd_mixer_elem_set_callback_private(melem, impl); + hdmi_eld_changed(melem, 0); + pa_log_info("ELD device found for port %s (%d).", port->port.name, device); + } + else + pa_log_debug("No ELD device found for port %s (%d).", port->port.name, device); + } +} + +uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name) +{ + uint32_t i; + uint32_t best, best2, off; + struct acp_card_profile **profiles = card->profiles; + + best = best2 = ACP_INVALID_INDEX; + off = 0; + + for (i = 0; i < card->n_profiles; i++) { + struct acp_card_profile *p = profiles[i]; + + if (name) { + if (spa_streq(name, p->name)) + best = i; + } else if (p->flags & ACP_PROFILE_OFF) { + off = i; + } else if (p->available == ACP_AVAILABLE_YES) { + if (best == ACP_INVALID_INDEX || p->priority > profiles[best]->priority) + best = i; + } else if (p->available != ACP_AVAILABLE_NO) { + if (best2 == ACP_INVALID_INDEX || p->priority > profiles[best2]->priority) + best2 = i; + } + } + if (best == ACP_INVALID_INDEX) + best = best2; + if (best == ACP_INVALID_INDEX) + best = off; + return best; +} + +static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, bool ignore_dB) +{ + const char *mdev; + pa_alsa_mapping *mapping = dev->mapping; + + if (!mapping && !element) + return; + + if (!element && mapping && pa_alsa_path_set_is_empty(dev->mixer_path_set)) + return; + + mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device"); + if (mdev) { + dev->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, mdev, true); + } else { + dev->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true); + } + if (!dev->mixer_handle) { + pa_log_info("Failed to find a working mixer device."); + return; + } + + if (element) { + if (!(dev->mixer_path = pa_alsa_path_synthesize(element, dev->direction))) + goto fail; + + if (pa_alsa_path_probe(dev->mixer_path, NULL, dev->mixer_handle, ignore_dB) < 0) + goto fail; + + pa_log_debug("Probed mixer path %s:", dev->mixer_path->name); + pa_alsa_path_dump(dev->mixer_path); + } + return; + +fail: + if (dev->mixer_path) { + pa_alsa_path_free(dev->mixer_path); + dev->mixer_path = NULL; + } + dev->mixer_handle = NULL; +} + +static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask) +{ + pa_alsa_device *dev = snd_mixer_elem_get_callback_private(elem); + + if (mask == SND_CTL_EVENT_MASK_REMOVE) + return 0; + + pa_log_info("%p mixer changed %d", dev, mask); + + if (mask & SND_CTL_EVENT_MASK_VALUE) { + if (dev->read_volume) + dev->read_volume(dev); + if (dev->read_mute) + dev->read_mute(dev); + } + return 0; +} + +static int read_volume(pa_alsa_device *dev) +{ + pa_card *impl = dev->card; + pa_cvolume r; + uint32_t i; + int res; + + if (!dev->mixer_handle) + return 0; + + if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0) + return res; + + /* Shift down by the base volume, so that 0dB becomes maximum volume */ + pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); + + if (pa_cvolume_equal(&dev->hardware_volume, &r)) + return 0; + + dev->real_volume = dev->hardware_volume = r; + + pa_log_info("New hardware volume: min:%d max:%d", + pa_cvolume_min(&r), pa_cvolume_max(&r)); + + for (i = 0; i < r.channels; i++) + pa_log_debug(" %d: %d", i, r.values[i]); + + pa_cvolume_reset(&dev->soft_volume, r.channels); + + if (impl->events && impl->events->volume_changed) + impl->events->volume_changed(impl->user_data, &dev->device); + + return 0; +} + +static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) +{ + pa_cvolume r; + + dev->real_volume = *v; + + if (!dev->mixer_handle) + return; + + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); + + if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, true) < 0) + return; + + /* Shift down by the base volume, so that 0dB becomes maximum volume */ + pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); + + dev->hardware_volume = r; + + if (dev->mixer_path->has_dB) { + pa_cvolume new_soft_volume; + bool accurate_enough; + + /* Match exactly what the user requested by software */ + pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume); + + /* If the adjustment to do in software is only minimal we + * can skip it. That saves us CPU at the expense of a bit of + * accuracy */ + accurate_enough = + (pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) && + (pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY)); + + pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume)); + pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume)); + pa_log_debug("Calculated software volume: %d (accurate-enough=%s)", + pa_cvolume_max(&new_soft_volume), + pa_yes_no(accurate_enough)); + + if (accurate_enough) + pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels); + + dev->soft_volume = new_soft_volume; + } else { + pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r)); + /* We can't match exactly what the user requested, hence let's + * at least tell the user about it */ + dev->real_volume = r; + } +} + +static int read_mute(pa_alsa_device *dev) +{ + pa_card *impl = dev->card; + bool mute; + int res; + + if (!dev->mixer_handle) + return 0; + + if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0) + return res; + + if (mute == dev->muted) + return 0; + + dev->muted = mute; + pa_log_info("New hardware muted: %d", mute); + + if (impl->events && impl->events->mute_changed) + impl->events->mute_changed(impl->user_data, &dev->device); + + return 0; +} + +static void set_mute(pa_alsa_device *dev, bool mute) +{ + dev->muted = mute; + + if (!dev->mixer_handle) + return; + + pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute); +} + +static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) +{ + pa_assert(dev); + + if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) { + dev->read_volume = NULL; + dev->set_volume = NULL; + pa_log_info("Driver does not support hardware volume control, " + "falling back to software volume control."); + dev->base_volume = PA_VOLUME_NORM; + dev->n_volume_steps = PA_VOLUME_NORM+1; + dev->device.flags &= ~ACP_DEVICE_HW_VOLUME; + } else { + dev->read_volume = read_volume; + dev->set_volume = set_volume; + dev->device.flags |= ACP_DEVICE_HW_VOLUME; + +#if 0 + if (u->mixer_path->has_dB && u->deferred_volume) { + pa_sink_set_write_volume_callback(u->sink, sink_write_volume_cb); + pa_log_info("Successfully enabled deferred volume."); + } else + pa_sink_set_write_volume_callback(u->sink, NULL); +#endif + + if (dev->mixer_path->has_dB) { + dev->decibel_volume = true; + pa_log_info("Hardware volume ranges from %0.2f dB to %0.2f dB.", + dev->mixer_path->min_dB, dev->mixer_path->max_dB); + + dev->base_volume = pa_sw_volume_from_dB(-dev->mixer_path->max_dB); + dev->n_volume_steps = PA_VOLUME_NORM+1; + + pa_log_info("Fixing base volume to %0.2f dB", pa_sw_volume_to_dB(dev->base_volume)); + } else { + dev->decibel_volume = false; + pa_log_info("Hardware volume ranges from %li to %li.", + dev->mixer_path->min_volume, dev->mixer_path->max_volume); + dev->base_volume = PA_VOLUME_NORM; + dev->n_volume_steps = dev->mixer_path->max_volume - dev->mixer_path->min_volume + 1; + } + pa_log_info("Using hardware volume control. Hardware dB scale %s.", + dev->mixer_path->has_dB ? "supported" : "not supported"); + } + dev->device.base_volume = pa_sw_volume_to_linear(dev->base_volume); + dev->device.volume_step = 1.0f / dev->n_volume_steps; + + if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_mute) { + dev->read_mute = NULL; + dev->set_mute = NULL; + pa_log_info("Driver does not support hardware mute control, falling back to software mute control."); + dev->device.flags &= ~ACP_DEVICE_HW_MUTE; + } else { + dev->read_mute = read_mute; + dev->set_mute = set_mute; + pa_log_info("Using hardware mute control."); + dev->device.flags |= ACP_DEVICE_HW_MUTE; + } +} + + +static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB) +{ + int res; + bool need_mixer_callback = false; + + /* This code is before the u->mixer_handle check, because if the UCM + * configuration doesn't specify volume or mute controls, u->mixer_handle + * will be NULL, but the UCM device enable sequence will still need to be + * executed. */ + if (dev->active_port && dev->ucm_context) { + if ((res = pa_alsa_ucm_set_port(dev->ucm_context, dev->active_port, + dev->direction == PA_ALSA_DIRECTION_OUTPUT)) < 0) + return res; + } + + if (!dev->mixer_handle) + return 0; + + if (dev->active_port) { + if (!impl->use_ucm) { + pa_alsa_port_data *data; + + /* We have a list of supported paths, so let's activate the + * one that has been chosen as active */ + data = PA_DEVICE_PORT_DATA(dev->active_port); + dev->mixer_path = data->path; + + pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted); + } else { + pa_alsa_ucm_port_data *data; + + data = PA_DEVICE_PORT_DATA(dev->active_port); + + /* Now activate volume controls, if any */ + if (data->path) { + dev->mixer_path = data->path; + pa_alsa_path_select(dev->mixer_path, NULL, dev->mixer_handle, dev->muted); + } + } + } else { + if (!dev->mixer_path && dev->mixer_path_set) + dev->mixer_path = pa_hashmap_first(dev->mixer_path_set->paths); + + if (dev->mixer_path) { + /* Hmm, we have only a single path, then let's activate it */ + pa_alsa_path_select(dev->mixer_path, dev->mixer_path->settings, + dev->mixer_handle, dev->muted); + } else + return 0; + } + + mixer_volume_init(impl, dev); + + /* Will we need to register callbacks? */ + if (dev->mixer_path_set && dev->mixer_path_set->paths) { + pa_alsa_path *p; + void *state; + + PA_HASHMAP_FOREACH(p, dev->mixer_path_set->paths, state) { + if (p->has_volume || p->has_mute) + need_mixer_callback = true; + } + } + else if (dev->mixer_path) + need_mixer_callback = dev->mixer_path->has_volume || dev->mixer_path->has_mute; + + if (!impl->soft_mixer && need_mixer_callback) { + pa_alsa_mixer_use_for_poll(impl->ucm.mixers, dev->mixer_handle); + if (dev->mixer_path_set) + pa_alsa_path_set_set_callback(dev->mixer_path_set, dev->mixer_handle, mixer_callback, dev); + else + pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev); + } + return 0; +} + +static int device_disable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev) +{ + dev->device.flags &= ~ACP_DEVICE_ACTIVE; + if (dev->active_port) { + dev->active_port->port.flags &= ~ACP_PORT_ACTIVE; + dev->active_port = NULL; + } + return 0; +} + +static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev) +{ + const char *mod_name; + uint32_t i, port_index; + int res; + + if (impl->use_ucm && + (mod_name = pa_proplist_gets(mapping->proplist, PA_ALSA_PROP_UCM_MODIFIER))) { + if (snd_use_case_set(impl->ucm.ucm_mgr, "_enamod", mod_name) < 0) + pa_log("Failed to enable ucm modifier %s", mod_name); + else + pa_log_debug("Enabled ucm modifier %s", mod_name); + } + + pa_log_info("Device: %s mapping '%s' (%s).", dev->device.description, + mapping->description, mapping->name); + + dev->device.flags |= ACP_DEVICE_ACTIVE; + + find_mixer(impl, dev, NULL, impl->ignore_dB); + + /* Synchronize priority values, as it may have changed when setting the profile */ + for (i = 0; i < impl->card.n_ports; i++) { + pa_device_port *p = (pa_device_port *)impl->card.ports[i]; + p->port.priority = p->priority; + } + + if (impl->auto_port) + port_index = acp_device_find_best_port_index(&dev->device, NULL); + else + port_index = ACP_INVALID_INDEX; + + if (port_index == ACP_INVALID_INDEX) + dev->active_port = NULL; + else + dev->active_port = (pa_device_port*)impl->card.ports[port_index]; + + if (dev->active_port) + dev->active_port->port.flags |= ACP_PORT_ACTIVE; + + if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0) + return res; + + if (dev->read_volume) + dev->read_volume(dev); + else { + pa_cvolume_reset(&dev->real_volume, dev->device.format.channels); + pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels); + } + if (dev->read_mute) + dev->read_mute(dev); + else + dev->muted = false; + + return 0; +} + +int acp_card_set_profile(struct acp_card *card, uint32_t new_index, uint32_t flags) +{ + pa_card *impl = (pa_card *)card; + pa_alsa_mapping *am; + uint32_t old_index = impl->card.active_profile_index; + struct acp_card_profile **profiles = card->profiles; + pa_alsa_profile *op, *np; + uint32_t idx; + int res; + + if (new_index >= card->n_profiles) + return -EINVAL; + + op = old_index != ACP_INVALID_INDEX ? (pa_alsa_profile*)profiles[old_index] : NULL; + np = (pa_alsa_profile*)profiles[new_index]; + + if (op == np) + return 0; + + pa_log_info("activate profile: %s (%d)", np->profile.name, new_index); + + if (op && op->output_mappings) { + PA_IDXSET_FOREACH(am, op->output_mappings, idx) { + if (np->output_mappings && + pa_idxset_get_by_data(np->output_mappings, am, NULL)) + continue; + + device_disable(impl, am, &am->output); + } + } + if (op && op->input_mappings) { + PA_IDXSET_FOREACH(am, op->input_mappings, idx) { + if (np->input_mappings && + pa_idxset_get_by_data(np->input_mappings, am, NULL)) + continue; + + device_disable(impl, am, &am->input); + } + } + + /* if UCM is available for this card then update the verb */ + if (impl->use_ucm && !(np->profile.flags & ACP_PROFILE_PRO)) { + if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl, + np->profile.flags & ACP_PROFILE_OFF ? NULL : np->profile.name, + op ? op->profile.name : NULL)) < 0) { + return res; + } + } + + if (np->output_mappings) { + PA_IDXSET_FOREACH(am, np->output_mappings, idx) { + if (impl->use_ucm) { + /* Update ports priorities */ + if (am->ucm_context.ucm_devices) { + pa_alsa_ucm_add_ports_combination(am->output.ports, &am->ucm_context, + true, impl->ports, np, NULL); + } + } + device_enable(impl, am, &am->output); + } + } + + if (np->input_mappings) { + PA_IDXSET_FOREACH(am, np->input_mappings, idx) { + if (impl->use_ucm) { + /* Update ports priorities */ + if (am->ucm_context.ucm_devices) { + pa_alsa_ucm_add_ports_combination(am->input.ports, &am->ucm_context, + false, impl->ports, np, NULL); + } + } + device_enable(impl, am, &am->input); + } + } + if (op) + op->profile.flags &= ~(ACP_PROFILE_ACTIVE | ACP_PROFILE_SAVE); + np->profile.flags |= ACP_PROFILE_ACTIVE | flags; + impl->card.active_profile_index = new_index; + + if (impl->events && impl->events->profile_changed) + impl->events->profile_changed(impl->user_data, old_index, + new_index); + return 0; +} + +static void prune_singleton_availability_groups(pa_hashmap *ports) { + pa_device_port *p; + pa_hashmap *group_counts; + void *state, *count; + const char *group; + + /* Collect groups and erase those that don't have more than 1 path */ + group_counts = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + PA_HASHMAP_FOREACH(p, ports, state) { + if (p->availability_group) { + count = pa_hashmap_get(group_counts, p->availability_group); + pa_hashmap_remove(group_counts, p->availability_group); + pa_hashmap_put(group_counts, p->availability_group, PA_UINT_TO_PTR(PA_PTR_TO_UINT(count) + 1)); + } + } + + /* Now we have an availability_group -> count map, let's drop all groups + * that have only one member */ + PA_HASHMAP_FOREACH_KV(group, count, group_counts, state) { + if (count == PA_UINT_TO_PTR(1)) + pa_hashmap_remove(group_counts, group); + } + + PA_HASHMAP_FOREACH(p, ports, state) { + if (p->availability_group && !pa_hashmap_get(group_counts, p->availability_group)) { + pa_log_debug("Pruned singleton availability group %s from port %s", p->availability_group, p->name); + pa_xfree(p->availability_group); + p->availability_group = NULL; + } + } + + pa_hashmap_free(group_counts); +} + +static const char *acp_dict_lookup(const struct acp_dict *dict, const char *key) +{ + const struct acp_dict_item *it; + acp_dict_for_each(it, dict) { + if (spa_streq(key, it->key)) + return it->value; + } + return NULL; +} + +struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) +{ + pa_card *impl; + struct acp_card *card; + const char *s, *profile_set = NULL, *profile = NULL; + char device_id[16]; + uint32_t profile_index; + int res; + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + pa_alsa_refcnt_inc(); + + snprintf(device_id, sizeof(device_id), "%d", index); + + impl->proplist = pa_proplist_new_dict(props); + + card = &impl->card; + card->index = index; + card->active_profile_index = ACP_INVALID_INDEX; + + impl->use_ucm = true; + impl->auto_profile = true; + impl->auto_port = true; + impl->ignore_dB = false; + impl->rate = DEFAULT_RATE; + + if (props) { + if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL) + impl->use_ucm = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.alsa.soft-mixer")) != NULL) + impl->soft_mixer = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL) + impl->ignore_dB = spa_atob(s); + if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL) + profile_set = s; + if ((s = acp_dict_lookup(props, "device.profile")) != NULL) + profile = s; + if ((s = acp_dict_lookup(props, "api.acp.auto-profile")) != NULL) + impl->auto_profile = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.acp.auto-port")) != NULL) + impl->auto_port = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.acp.probe-rate")) != NULL) + impl->rate = atoi(s); + } + + impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE; + impl->ucm.default_sample_spec.rate = impl->rate; + impl->ucm.default_sample_spec.channels = 2; + pa_channel_map_init_extend(&impl->ucm.default_channel_map, + impl->ucm.default_sample_spec.channels, PA_CHANNEL_MAP_ALSA); + impl->ucm.default_n_fragments = 4; + impl->ucm.default_fragment_size_msec = 25; + + impl->ucm.mixers = pa_hashmap_new_full(pa_idxset_string_hash_func, + pa_idxset_string_compare_func, + pa_xfree, (pa_free_cb_t) pa_alsa_mixer_free); + impl->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, + pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) profile_free); + impl->ports = pa_hashmap_new_full(pa_idxset_string_hash_func, + pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) port_free); + + snd_config_update_free_global(); + + res = impl->use_ucm ? pa_alsa_ucm_query_profiles(&impl->ucm, card->index) : -1; + if (res == -PA_ALSA_ERR_UCM_LINKED) { + res = -ENOENT; + goto error; + } + if (res == 0) { + pa_log_info("Found UCM profiles"); + impl->profile_set = pa_alsa_ucm_add_profile_set(&impl->ucm, &impl->ucm.default_channel_map); + } else { + impl->use_ucm = false; + impl->profile_set = pa_alsa_profile_set_new(profile_set, &impl->ucm.default_channel_map); + } + if (impl->profile_set == NULL) { + res = -ENOTSUP; + goto error; + } + + impl->profile_set->ignore_dB = impl->ignore_dB; + + pa_alsa_profile_set_probe(impl->profile_set, impl->ucm.mixers, + device_id, + &impl->ucm.default_sample_spec, + impl->ucm.default_n_fragments, + impl->ucm.default_fragment_size_msec); + + pa_alsa_init_proplist_card(NULL, impl->proplist, impl->card.index); + pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_STRING, device_id); + pa_alsa_init_description(impl->proplist, NULL); + + add_profiles(impl); + prune_singleton_availability_groups(impl->ports); + + card->n_profiles = pa_dynarray_size(&impl->out.profiles); + card->profiles = impl->out.profiles.array.data; + + card->n_ports = pa_dynarray_size(&impl->out.ports); + card->ports = impl->out.ports.array.data; + + card->n_devices = pa_dynarray_size(&impl->out.devices); + card->devices = impl->out.devices.array.data; + + pa_proplist_as_dict(impl->proplist, &card->props); + + init_jacks(impl); + + if (!impl->auto_profile && profile == NULL) + profile = "off"; + + profile_index = acp_card_find_best_profile_index(&impl->card, profile); + acp_card_set_profile(&impl->card, profile_index, 0); + + init_eld_ctls(impl); + + return &impl->card; +error: + pa_alsa_refcnt_dec(); + free(impl); + errno = -res; + return NULL; +} + +void acp_card_add_listener(struct acp_card *card, + const struct acp_card_events *events, void *user_data) +{ + pa_card *impl = (pa_card *)card; + impl->events = events; + impl->user_data = user_data; +} + +void acp_card_destroy(struct acp_card *card) +{ + pa_card *impl = (pa_card *)card; + if (impl->profiles) + pa_hashmap_free(impl->profiles); + if (impl->ports) + pa_hashmap_free(impl->ports); + pa_dynarray_clear(&impl->out.devices); + pa_dynarray_clear(&impl->out.profiles); + pa_dynarray_clear(&impl->out.ports); + if (impl->ucm.mixers) + pa_hashmap_free(impl->ucm.mixers); + if (impl->jacks) + pa_hashmap_free(impl->jacks); + if (impl->profile_set) + pa_alsa_profile_set_free(impl->profile_set); + pa_alsa_ucm_free(&impl->ucm); + pa_proplist_free(impl->proplist); + pa_alsa_refcnt_dec(); + free(impl); +} + +int acp_card_poll_descriptors_count(struct acp_card *card) +{ + pa_card *impl = (pa_card *)card; + void *state; + pa_alsa_mixer *pm; + int n, count = 0; + + PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) { + if (!pm->used_for_poll) + continue; + n = snd_mixer_poll_descriptors_count(pm->mixer_handle); + if (n < 0) + return n; + count += n; + } + return count; +} + +int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space) +{ + pa_card *impl = (pa_card *)card; + void *state; + pa_alsa_mixer *pm; + int n, count = 0; + + PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) { + if (!pm->used_for_poll) + continue; + + n = snd_mixer_poll_descriptors(pm->mixer_handle, pfds, space); + if (n < 0) + return n; + if (space >= (unsigned int) n) { + count += n; + space -= n; + pfds += n; + } else + space = 0; + } + return count; +} + +int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds, + unsigned int nfds, unsigned short *revents) +{ + unsigned int idx; + unsigned short res; + if (nfds == 0) + return -EINVAL; + res = 0; + for (idx = 0; idx < nfds; idx++, pfds++) + res |= pfds->revents & (POLLIN|POLLERR|POLLNVAL); + *revents = res; + return 0; +} + +int acp_card_handle_events(struct acp_card *card) +{ + pa_card *impl = (pa_card *)card; + void *state; + pa_alsa_mixer *pm; + int n, count = 0; + + PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) { + if (!pm->used_for_poll) + continue; + + n = snd_mixer_handle_events(pm->mixer_handle); + if (n < 0) + return n; + count += n; + } + return count; +} + +static void sync_mixer(pa_alsa_device *d, pa_device_port *port) +{ + pa_alsa_setting *setting = NULL; + + if (!d->mixer_path) + return; + + /* port may be NULL, because if we use a synthesized mixer path, then the + * sink has no ports. */ + if (port && !d->ucm_context) { + pa_alsa_port_data *data; + data = PA_DEVICE_PORT_DATA(port); + setting = data->setting; + } + + if (d->mixer_handle) + pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted); + + if (d->set_mute) + d->set_mute(d, d->muted); + if (d->set_volume) + d->set_volume(d, &d->real_volume); +} + + +uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name) +{ + uint32_t i; + uint32_t best, best2, best3; + struct acp_port **ports = dev->ports; + + best = best2 = best3 = ACP_INVALID_INDEX; + + for (i = 0; i < dev->n_ports; i++) { + struct acp_port *p = ports[i]; + + if (name) { + if (spa_streq(name, p->name)) + best = i; + } else if (p->available == ACP_AVAILABLE_YES) { + if (best == ACP_INVALID_INDEX || p->priority > ports[best]->priority) + best = i; + } else if (p->available != ACP_AVAILABLE_NO) { + if (best2 == ACP_INVALID_INDEX || p->priority > ports[best2]->priority) + best2 = i; + } else { + if (best3 == ACP_INVALID_INDEX || p->priority > ports[best3]->priority) + best3 = i; + } + } + if (best == ACP_INVALID_INDEX) + best = best2; + if (best == ACP_INVALID_INDEX) + best = best3; + if (best == ACP_INVALID_INDEX) + best = 0; + if (best < dev->n_ports) + return ports[best]->index; + else + return ACP_INVALID_INDEX; +} + +int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + pa_card *impl = d->card; + pa_device_port *p, *old = d->active_port; + int res; + + if (port_index >= impl->card.n_ports) + return -EINVAL; + + p = (pa_device_port*)impl->card.ports[port_index]; + if (!pa_hashmap_get(d->ports, p->name)) + return -EINVAL; + + p->port.flags = ACP_PORT_ACTIVE | flags; + if (p == old) + return 0; + if (old) + old->port.flags &= ~(ACP_PORT_ACTIVE | ACP_PORT_SAVE); + d->active_port = p; + + if (impl->use_ucm) { + pa_alsa_ucm_port_data *data; + + data = PA_DEVICE_PORT_DATA(p); + d->mixer_path = data->path; + mixer_volume_init(impl, d); + + sync_mixer(d, p); + res = pa_alsa_ucm_set_port(d->ucm_context, p, + dev->direction == ACP_DIRECTION_PLAYBACK); + } else { + pa_alsa_port_data *data; + + data = PA_DEVICE_PORT_DATA(p); + d->mixer_path = data->path; + mixer_volume_init(impl, d); + + sync_mixer(d, p); + res = 0; +#if 0 + if (data->suspend_when_unavailable && p->available == PA_AVAILABLE_NO) + pa_sink_suspend(s, true, PA_SUSPEND_UNAVAILABLE); + else + pa_sink_suspend(s, false, PA_SUSPEND_UNAVAILABLE); +#endif + } + if (impl->events && impl->events->port_changed) + impl->events->port_changed(impl->user_data, + old ? old->port.index : 0, p->port.index); + return res; +} + +int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + pa_card *impl = d->card; + uint32_t i; + pa_cvolume v, old_volume; + + if (n_volume == 0) + return -EINVAL; + + old_volume = d->real_volume; + + v.channels = d->mapping->channel_map.channels; + for (i = 0; i < v.channels; i++) + v.values[i] = pa_sw_volume_from_linear(volume[i % n_volume]); + + pa_log_info("Set %s volume: min:%d max:%d", + d->set_volume ? "hardware" : "software", + pa_cvolume_min(&v), pa_cvolume_max(&v)); + + for (i = 0; i < v.channels; i++) + pa_log_debug(" %d: %d", i, v.values[i]); + + if (d->set_volume) { + d->set_volume(d, &v); + } else { + d->real_volume = v; + d->soft_volume = v; + } + if (!pa_cvolume_equal(&d->real_volume, &old_volume)) + if (impl->events && impl->events->volume_changed) + impl->events->volume_changed(impl->user_data, dev); + return 0; +} + +static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume) +{ + uint32_t i; + if (v->channels == 0) + return -EIO; + for (i = 0; i < n_volume; i++) + volume[i] = pa_sw_volume_to_linear(v->values[i % v->channels]); + return 0; +} + +int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + return get_volume(&d->soft_volume, volume, n_volume); +} + +int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + return get_volume(&d->real_volume, volume, n_volume); +} + +int acp_device_set_mute(struct acp_device *dev, bool mute) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + pa_card *impl = d->card; + bool old_muted = d->muted; + + if (old_muted == mute) + return 0; + + pa_log_info("Set %s mute: %d", d->set_mute ? "hardware" : "software", mute); + + if (d->set_mute) { + d->set_mute(d, mute); + } else { + d->muted = mute; + } + if (old_muted != mute) + if (impl->events && impl->events->mute_changed) + impl->events->mute_changed(impl->user_data, dev); + + return 0; +} + +int acp_device_get_mute(struct acp_device *dev, bool *mute) +{ + pa_alsa_device *d = (pa_alsa_device*)dev; + *mute = d->muted; + return 0; +} + +void acp_set_log_func(acp_log_func func, void *data) +{ + _acp_log_func = func; + _acp_log_data = data; +} +void acp_set_log_level(int level) +{ + _acp_log_level = level; +} diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h new file mode 100644 index 0000000..3fed6f6 --- /dev/null +++ b/spa/plugins/alsa/acp/acp.h @@ -0,0 +1,308 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef ACP_H +#define ACP_H + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#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)++) + +enum acp_direction { + ACP_DIRECTION_PLAYBACK = 1, + ACP_DIRECTION_CAPTURE = 2 +}; + +const char *acp_direction_str(enum acp_direction direction); + +enum acp_available { + ACP_AVAILABLE_UNKNOWN = 0, + ACP_AVAILABLE_NO = 1, + ACP_AVAILABLE_YES = 2 +}; + +const char *acp_available_str(enum acp_available status); + +#define ACP_KEY_PORT_TYPE "port.type" /**< a Port type, like "aux", "speaker", ... */ +#define ACP_KEY_PORT_AVAILABILITY_GROUP "port.availability-group" + /**< An identifier for the group of ports that share their availability status with + * each other. This is meant especially for handling cases where one 3.5 mm connector + * is used for headphones, headsets and microphones, and the hardware can only tell + * that something was plugged in but not what exactly. In this situation the ports for + * all those devices share their availability status, and ACP can't tell which + * one is actually plugged in, and some application may ask the user what was plugged + * in. Such applications should get a list of all card ports and compare their + * `available_group` fields. Ports that have the same group are those that need + * input from the user to determine which device was plugged in. The application should + * then activate the user-chosen port. + * + * May be NULL, in which case the port is not part of any availability group (which is + * the same as having a group with only one member). + * + * The group identifier must be treated as an opaque identifier. The string may look + * like an ALSA control name, but applications must not assume any such relationship. + * The group naming scheme can change without a warning. + */ + +struct acp_device; + +struct acp_card_events { +#define ACP_VERSION_CARD_EVENTS 0 + uint32_t version; + + void (*destroy) (void *data); + + void (*props_changed) (void *data); + + void (*profile_changed) (void *data, uint32_t old_index, uint32_t new_index); + + void (*profile_available) (void *data, uint32_t index, + enum acp_available old, enum acp_available available); + + void (*port_changed) (void *data, uint32_t old_index, uint32_t new_index); + + void (*port_available) (void *data, uint32_t index, + enum acp_available old, enum acp_available available); + + void (*volume_changed) (void *data, struct acp_device *dev); + void (*mute_changed) (void *data, struct acp_device *dev); +}; + +struct acp_port { + uint32_t index; /**< unique index for this port */ +#define ACP_PORT_ACTIVE (1<<0) +#define ACP_PORT_SAVE (1<<1) /* if the port needs saving */ + uint32_t flags; /**< extra port flags */ + + const char *name; /**< Name of this port */ + const char *description; /**< Description of this port */ + uint32_t priority; /**< The higher this value is, the more useful this port is as a default. */ + enum acp_direction direction; + enum acp_available available; /**< A flags (see #acp_port_available), indicating availability status of this port. */ + struct acp_dict props; /**< extra port properties */ + + uint32_t n_profiles; /**< number of elements in profiles array */ + struct acp_card_profile **profiles; /**< array of profiles for this port */ + + uint32_t n_devices; /**< number of elements in devices array */ + struct acp_device **devices; /**< array of devices */ +}; + +struct acp_device { + uint32_t index; +#define ACP_DEVICE_ACTIVE (1<<0) +#define ACP_DEVICE_HW_VOLUME (1<<1) +#define ACP_DEVICE_HW_MUTE (1<<2) +#define ACP_DEVICE_UCM_DEVICE (1<<3) +#define ACP_DEVICE_IEC958 (1<<4) + uint32_t flags; + + const char *name; + const char *description; + uint32_t priority; + enum acp_direction direction; + struct acp_dict props; + + const char **device_strings; + struct acp_format format; + + float base_volume; + float volume_step; + + uint32_t n_ports; + struct acp_port **ports; + + int64_t latency_ns; + uint32_t codecs[32]; + uint32_t n_codecs; +}; + +struct acp_card_profile { + uint32_t index; +#define ACP_PROFILE_ACTIVE (1<<0) +#define ACP_PROFILE_OFF (1<<1) /* the Off profile */ +#define ACP_PROFILE_SAVE (1<<2) /* if the profile needs saving */ +#define ACP_PROFILE_PRO (1<<3) /* the Pro profile */ + uint32_t flags; + + const char *name; + const char *description; + uint32_t priority; + enum acp_available available; + struct acp_dict props; + + uint32_t n_devices; + struct acp_device **devices; +}; + +struct acp_card { + uint32_t index; + uint32_t flags; + + struct acp_dict props; + + uint32_t n_profiles; + uint32_t active_profile_index; + struct acp_card_profile **profiles; + + uint32_t n_devices; + struct acp_device **devices; + + uint32_t n_ports; + struct acp_port **ports; + uint32_t preferred_input_port_index; + uint32_t preferred_output_port_index; +}; + +struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props); + +void acp_card_add_listener(struct acp_card *card, + const struct acp_card_events *events, void *user_data); + +void acp_card_destroy(struct acp_card *card); + +int acp_card_poll_descriptors_count(struct acp_card *card); +int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space); +int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds, + unsigned int nfds, unsigned short *revents); +int acp_card_handle_events(struct acp_card *card); + +uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name); +int acp_card_set_profile(struct acp_card *card, uint32_t profile_index, uint32_t flags); + +uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name); +int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags); + +int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume); +int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume); +int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume); +int acp_device_set_mute(struct acp_device *dev, bool mute); +int acp_device_get_mute(struct acp_device *dev, bool *mute); + +typedef void (*acp_log_func) (void *data, + int level, const char *file, int line, const char *func, + const char *fmt, va_list arg) ACP_PRINTF_FUNC(6,0); + +void acp_set_log_func(acp_log_func, void *data); +void acp_set_log_level(int level); + +#ifdef __cplusplus +} +#endif + +#endif /* ACP_H */ diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c new file mode 100644 index 0000000..86425fd --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-mixer.c @@ -0,0 +1,5398 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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 = e->max_dB * 100; + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + /* If we call set_playback_volume() without checking first + * if the channel is available, ALSA behaves very + * strangely and doesn't fail the call */ + if (snd_mixer_selem_has_playback_channel(me, c)) { + rounding = +1; + if (e->db_fix) { + if (write_to_hw) + r = snd_mixer_selem_set_playback_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding)); + else { + decibel_fix_get_step(e->db_fix, &value, rounding); + r = 0; + } + + } else { + if (write_to_hw) { + if (deferred_volume) { + if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_OUTPUT, &value)) >= 0) + r = snd_mixer_selem_set_playback_dB(me, c, value, 0); + } else { + if ((r = snd_mixer_selem_set_playback_dB(me, c, value, rounding)) >= 0) + r = snd_mixer_selem_get_playback_dB(me, c, &value); + } + } else { + long alsa_val; + if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value, rounding, &alsa_val)) >= 0) + r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value); + } + } + } else + r = -1; + } else { + if (snd_mixer_selem_has_capture_channel(me, c)) { + rounding = -1; + if (e->db_fix) { + if (write_to_hw) + r = snd_mixer_selem_set_capture_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding)); + else { + decibel_fix_get_step(e->db_fix, &value, rounding); + r = 0; + } + + } else { + if (write_to_hw) { + if (deferred_volume) { + if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_INPUT, &value)) >= 0) + r = snd_mixer_selem_set_capture_dB(me, c, value, 0); + } else { + if ((r = snd_mixer_selem_set_capture_dB(me, c, value, rounding)) >= 0) + r = snd_mixer_selem_get_capture_dB(me, c, &value); + } + } else { + long alsa_val; + if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value, rounding, &alsa_val)) >= 0) + r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value); + } + } + } else + r = -1; + } + + if (r < 0) + continue; + + f = from_alsa_dB(value); + + } else { + long value; + + value = to_alsa_volume(f, e->min_volume, e->max_volume); + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + if (snd_mixer_selem_has_playback_channel(me, c)) { + if ((r = snd_mixer_selem_set_playback_volume(me, c, value)) >= 0) + r = snd_mixer_selem_get_playback_volume(me, c, &value); + } else + r = -1; + } else { + if (snd_mixer_selem_has_capture_channel(me, c)) { + if ((r = snd_mixer_selem_set_capture_volume(me, c, value)) >= 0) + r = snd_mixer_selem_get_capture_volume(me, c, &value); + } else + r = -1; + } + + if (r < 0) + continue; + + f = from_alsa_volume(value, e->min_volume, e->max_volume); + } + + for (k = 0; k < cm->channels; k++) + if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k])) + if (rv.values[k] < f) + rv.values[k] = f; + + mask |= e->masks[c][e->n_channels-1]; + } + + for (k = 0; k < cm->channels; k++) + if (!(mask & PA_CHANNEL_POSITION_MASK(cm->map[k]))) + rv.values[k] = PA_VOLUME_NORM; + + *v = rv; + return 0; +} + +int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw) { + + pa_alsa_element *e; + pa_cvolume rv; + + pa_assert(m); + pa_assert(p); + pa_assert(cm); + pa_assert(v); + pa_assert(pa_cvolume_compatible_with_channel_map(v, cm)); + + if (!p->has_volume) + return -1; + + rv = *v; /* Remaining adjustment */ + pa_cvolume_reset(v, cm->channels); /* Adjustment done */ + + PA_LLIST_FOREACH(e, p->elements) { + pa_cvolume ev; + + if (e->volume_use != PA_ALSA_VOLUME_MERGE) + continue; + + pa_assert(!p->has_dB || e->has_dB); + + ev = rv; + if (element_set_volume(e, m, cm, &ev, deferred_volume, write_to_hw) < 0) + return -1; + + if (!p->has_dB) { + *v = ev; + return 0; + } + + pa_sw_cvolume_multiply(v, v, &ev); + pa_sw_cvolume_divide(&rv, &rv, &ev); + } + + return 0; +} + +static int element_set_switch(pa_alsa_element *e, snd_mixer_t *m, bool b) { + snd_mixer_elem_t *me; + snd_mixer_selem_id_t *sid; + char buf[64]; + int r; + + pa_assert(m); + pa_assert(e); + + SELEM_INIT(sid, &e->alsa_id); + if (!(me = snd_mixer_find_selem(m, sid))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Element %s seems to have disappeared.", buf); + return -1; + } + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_set_playback_switch_all(me, b); + else + r = snd_mixer_selem_set_capture_switch_all(me, b); + + if (r < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno)); + } + + return r; +} + +int pa_alsa_path_set_mute(pa_alsa_path *p, snd_mixer_t *m, bool muted) { + pa_alsa_element *e; + + pa_assert(m); + pa_assert(p); + + if (!p->has_mute) + return -1; + + PA_LLIST_FOREACH(e, p->elements) { + + if (e->switch_use != PA_ALSA_SWITCH_MUTE) + continue; + + if (element_set_switch(e, m, !muted) < 0) + return -1; + } + + return 0; +} + +/* Depending on whether e->volume_use is _OFF, _ZERO or _CONSTANT, this + * function sets all channels of the volume element to e->min_volume, 0 dB or + * e->constant_volume. */ +static int element_set_constant_volume(pa_alsa_element *e, snd_mixer_t *m) { + snd_mixer_elem_t *me = NULL; + snd_mixer_selem_id_t *sid = NULL; + int r = 0; + long volume = -1; + bool volume_set = false; + char buf[64]; + + pa_assert(m); + pa_assert(e); + + SELEM_INIT(sid, &e->alsa_id); + if (!(me = snd_mixer_find_selem(m, sid))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Element %s seems to have disappeared.", buf); + return -1; + } + + switch (e->volume_use) { + case PA_ALSA_VOLUME_OFF: + volume = e->min_volume; + volume_set = true; + break; + + case PA_ALSA_VOLUME_ZERO: + if (e->db_fix) { + long dB = 0; + + volume = decibel_fix_get_step(e->db_fix, &dB, (e->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1)); + volume_set = true; + } + break; + + case PA_ALSA_VOLUME_CONSTANT: + volume = e->constant_volume; + volume_set = true; + break; + + default: + pa_assert_not_reached(); + } + + if (volume_set) { + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_set_playback_volume_all(me, volume); + else + r = snd_mixer_selem_set_capture_volume_all(me, volume); + } else { + pa_assert(e->volume_use == PA_ALSA_VOLUME_ZERO); + pa_assert(!e->db_fix); + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_set_playback_dB_all(me, 0, +1); + else + r = snd_mixer_selem_set_capture_dB_all(me, 0, -1); + } + + if (r < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to set volume of %s: %s", buf, pa_alsa_strerror(errno)); + } + + return r; +} + +int pa_alsa_path_select(pa_alsa_path *p, pa_alsa_setting *s, snd_mixer_t *m, bool device_is_muted) { + pa_alsa_element *e; + int r = 0; + + pa_assert(m); + pa_assert(p); + + pa_log_info("Activating path %s", p->name); + pa_alsa_path_dump(p); + + /* First turn on hw mute if available, to avoid noise + * when setting the mixer controls. */ + if (p->mute_during_activation) { + PA_LLIST_FOREACH(e, p->elements) { + if (e->switch_use == PA_ALSA_SWITCH_MUTE) + /* If the muting fails here, that's not a critical problem for + * selecting a path, so we ignore the return value. + * element_set_switch() will print a warning anyway, so this + * won't be a silent failure either. */ + (void) element_set_switch(e, m, false); + } + } + + PA_LLIST_FOREACH(e, p->elements) { + + switch (e->switch_use) { + case PA_ALSA_SWITCH_OFF: + r = element_set_switch(e, m, false); + break; + + case PA_ALSA_SWITCH_ON: + r = element_set_switch(e, m, true); + break; + + case PA_ALSA_SWITCH_MUTE: + case PA_ALSA_SWITCH_IGNORE: + case PA_ALSA_SWITCH_SELECT: + r = 0; + break; + } + + if (r < 0) + return -1; + + switch (e->volume_use) { + case PA_ALSA_VOLUME_OFF: + case PA_ALSA_VOLUME_ZERO: + case PA_ALSA_VOLUME_CONSTANT: + r = element_set_constant_volume(e, m); + break; + + case PA_ALSA_VOLUME_MERGE: + case PA_ALSA_VOLUME_IGNORE: + r = 0; + break; + } + + if (r < 0) + return -1; + } + + if (s) + setting_select(s, m); + + /* Finally restore hw mute to the device mute status. */ + if (p->mute_during_activation) { + PA_LLIST_FOREACH(e, p->elements) { + if (e->switch_use == PA_ALSA_SWITCH_MUTE) { + if (element_set_switch(e, m, !device_is_muted) < 0) + return -1; + } + } + } + + return 0; +} + +static int check_required(pa_alsa_element *e, snd_mixer_elem_t *me) { + bool has_switch; + bool has_enumeration; + bool has_volume; + + pa_assert(e); + pa_assert(me); + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + has_switch = + snd_mixer_selem_has_playback_switch(me) || + (e->direction_try_other && snd_mixer_selem_has_capture_switch(me)); + } else { + has_switch = + snd_mixer_selem_has_capture_switch(me) || + (e->direction_try_other && snd_mixer_selem_has_playback_switch(me)); + } + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + has_volume = + snd_mixer_selem_has_playback_volume(me) || + (e->direction_try_other && snd_mixer_selem_has_capture_volume(me)); + } else { + has_volume = + snd_mixer_selem_has_capture_volume(me) || + (e->direction_try_other && snd_mixer_selem_has_playback_volume(me)); + } + + has_enumeration = snd_mixer_selem_is_enumerated(me); + + if ((e->required == PA_ALSA_REQUIRED_SWITCH && !has_switch) || + (e->required == PA_ALSA_REQUIRED_VOLUME && !has_volume) || + (e->required == PA_ALSA_REQUIRED_ENUMERATION && !has_enumeration)) + return -1; + + if (e->required == PA_ALSA_REQUIRED_ANY && !(has_switch || has_volume || has_enumeration)) + return -1; + + if ((e->required_absent == PA_ALSA_REQUIRED_SWITCH && has_switch) || + (e->required_absent == PA_ALSA_REQUIRED_VOLUME && has_volume) || + (e->required_absent == PA_ALSA_REQUIRED_ENUMERATION && has_enumeration)) + return -1; + + if (e->required_absent == PA_ALSA_REQUIRED_ANY && (has_switch || has_volume || has_enumeration)) + return -1; + + if (e->required_any != PA_ALSA_REQUIRED_IGNORE) { + switch (e->required_any) { + case PA_ALSA_REQUIRED_VOLUME: + e->path->req_any_present |= (e->volume_use != PA_ALSA_VOLUME_IGNORE); + break; + case PA_ALSA_REQUIRED_SWITCH: + e->path->req_any_present |= (e->switch_use != PA_ALSA_SWITCH_IGNORE); + break; + case PA_ALSA_REQUIRED_ENUMERATION: + e->path->req_any_present |= (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE); + break; + case PA_ALSA_REQUIRED_ANY: + e->path->req_any_present |= + (e->volume_use != PA_ALSA_VOLUME_IGNORE) || + (e->switch_use != PA_ALSA_SWITCH_IGNORE) || + (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE); + break; + default: + pa_assert_not_reached(); + } + } + + if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) { + pa_alsa_option *o; + PA_LLIST_FOREACH(o, e->options) { + e->path->req_any_present |= (o->required_any != PA_ALSA_REQUIRED_IGNORE) && + (o->alsa_idx >= 0); + if (o->required != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx < 0) + return -1; + if (o->required_absent != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx >= 0) + return -1; + } + } + + return 0; +} + +static int element_ask_vol_dB(snd_mixer_elem_t *me, pa_alsa_direction_t dir, long value, long *dBvalue) { + if (dir == PA_ALSA_DIRECTION_OUTPUT) + return snd_mixer_selem_ask_playback_vol_dB(me, value, dBvalue); + else + return snd_mixer_selem_ask_capture_vol_dB(me, value, dBvalue); +} + +static bool element_probe_volume(pa_alsa_element *e, snd_mixer_elem_t *me) { + + long min_dB = 0, max_dB = 0; + int r; + bool is_mono; + pa_channel_position_t p; + char buf[64]; + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + if (!snd_mixer_selem_has_playback_volume(me)) { + if (e->direction_try_other && snd_mixer_selem_has_capture_volume(me)) + e->direction = PA_ALSA_DIRECTION_INPUT; + else + return false; + } + } else { + if (!snd_mixer_selem_has_capture_volume(me)) { + if (e->direction_try_other && snd_mixer_selem_has_playback_volume(me)) + e->direction = PA_ALSA_DIRECTION_OUTPUT; + else + return false; + } + } + + e->direction_try_other = false; + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_get_playback_volume_range(me, &e->min_volume, &e->max_volume); + else + r = snd_mixer_selem_get_capture_volume_range(me, &e->min_volume, &e->max_volume); + + if (r < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to get volume range of %s: %s", buf, pa_alsa_strerror(r)); + return false; + } + + if (e->min_volume >= e->max_volume) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Your kernel driver is broken for element %s: it reports a volume range from %li to %li which makes no sense.", + buf, e->min_volume, e->max_volume); + return false; + } + if (e->volume_use == PA_ALSA_VOLUME_CONSTANT && (e->min_volume > e->constant_volume || e->max_volume < e->constant_volume)) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Constant volume %li configured for element %s, but the available range is from %li to %li.", + e->constant_volume, buf, e->min_volume, e->max_volume); + return false; + } + + + if (e->db_fix && ((e->min_volume > e->db_fix->min_step) || (e->max_volume < e->db_fix->max_step))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("The step range of the decibel fix for element %s (%li-%li) doesn't fit to the " + "real hardware range (%li-%li). Disabling the decibel fix.", buf, + e->db_fix->min_step, e->db_fix->max_step, e->min_volume, e->max_volume); + + decibel_fix_free(e->db_fix); + e->db_fix = NULL; + } + + if (e->db_fix) { + e->has_dB = true; + e->min_volume = e->db_fix->min_step; + e->max_volume = e->db_fix->max_step; + min_dB = e->db_fix->db_values[0]; + max_dB = e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step]; + } else if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + e->has_dB = snd_mixer_selem_get_playback_dB_range(me, &min_dB, &max_dB) >= 0; + else + e->has_dB = snd_mixer_selem_get_capture_dB_range(me, &min_dB, &max_dB) >= 0; + + /* Assume decibel data to be incorrect if max_dB is negative. */ + if (e->has_dB && max_dB < 0 && !e->db_fix) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("The decibel volume range for element %s (%li dB - %li dB) has negative maximum. " + "Disabling the decibel range.", buf, min_dB, max_dB); + e->has_dB = false; + } + + /* Check that the kernel driver returns consistent limits with + * both _get_*_dB_range() and _ask_*_vol_dB(). */ + if (e->has_dB && !e->db_fix) { + long min_dB_checked = 0; + long max_dB_checked = 0; + + if (element_ask_vol_dB(me, e->direction, e->min_volume, &min_dB_checked) < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->min_volume); + return false; + } + + if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB_checked) < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->max_volume); + return false; + } + + if (min_dB != min_dB_checked || max_dB != max_dB_checked) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Your kernel driver is broken: the reported dB range for %s (from %0.2f dB to %0.2f dB) " + "doesn't match the dB values at minimum and maximum volume levels: %0.2f dB at level %li, " + "%0.2f dB at level %li.", buf, min_dB / 100.0, max_dB / 100.0, + min_dB_checked / 100.0, e->min_volume, max_dB_checked / 100.0, e->max_volume); + return false; + } + } + + if (e->has_dB) { + e->min_dB = ((double) min_dB) / 100.0; + e->max_dB = ((double) max_dB) / 100.0; + + if (min_dB >= max_dB) { + pa_assert(!e->db_fix); + pa_log_warn("Your kernel driver is broken: it reports a volume range from %0.2f dB to %0.2f dB which makes no sense.", + e->min_dB, e->max_dB); + e->has_dB = false; + } + } + + if (e->volume_limit >= 0) { + if (e->volume_limit <= e->min_volume || e->volume_limit > e->max_volume) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Volume limit for element %s of path %s is invalid: %li isn't within the valid range " + "%li-%li. The volume limit is ignored.", + buf, e->path->name, e->volume_limit, e->min_volume + 1, e->max_volume); + } else { + e->max_volume = e->volume_limit; + + if (e->has_dB) { + if (e->db_fix) { + e->db_fix->max_step = e->max_volume; + e->max_dB = ((double) e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step]) / 100.0; + } else if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB) < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to get dB value of %s: %s", buf, pa_alsa_strerror(r)); + e->has_dB = false; + } else + e->max_dB = ((double) max_dB) / 100.0; + } + } + } + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + is_mono = snd_mixer_selem_is_playback_mono(me) > 0; + else + is_mono = snd_mixer_selem_is_capture_mono(me) > 0; + + if (is_mono) { + e->n_channels = 1; + + if ((e->override_map & (1 << (e->n_channels-1))) && e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] == 0) { + pa_log_warn("Override map for mono element %s is invalid, ignoring override map", e->path->name); + e->override_map &= ~(1 << (e->n_channels-1)); + } + if (!(e->override_map & (1 << (e->n_channels-1)))) { + for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) { + if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN) + continue; + e->masks[alsa_channel_ids[p]][e->n_channels-1] = 0; + } + e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] = PA_CHANNEL_POSITION_MASK_ALL; + } + e->merged_mask = e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1]; + return true; + } + + e->n_channels = 0; + for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) { + if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN) + continue; + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + e->n_channels += snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0; + else + e->n_channels += snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0; + } + + if (e->n_channels <= 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Volume element %s with no channels?", buf); + return false; + } else if (e->n_channels > POSITION_MASK_CHANNELS) { + /* FIXME: In some places code like this is used: + * + * e->masks[alsa_channel_ids[p]][e->n_channels-1] + * + * The definition of e->masks is + * + * pa_channel_position_mask_t masks[SND_MIXER_SCHN_LAST + 1][POSITION_MASK_CHANNELS]; + * + * Since the array size is fixed at POSITION_MASK_CHANNELS, we obviously + * don't support elements with more than POSITION_MASK_CHANNELS + * channels... */ + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Volume element %s has %u channels. That's too much! I can't handle that!", buf, e->n_channels); + return false; + } + +retry: + if (!(e->override_map & (1 << (e->n_channels-1)))) { + for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) { + bool has_channel; + + if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN) + continue; + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + has_channel = snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0; + else + has_channel = snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0; + + e->masks[alsa_channel_ids[p]][e->n_channels-1] = has_channel ? PA_CHANNEL_POSITION_MASK(p) : 0; + } + } + + e->merged_mask = 0; + for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) { + if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN) + continue; + + e->merged_mask |= e->masks[alsa_channel_ids[p]][e->n_channels-1]; + } + + if (e->merged_mask == 0) { + if (!(e->override_map & (1 << (e->n_channels-1)))) { + pa_log_warn("Channel map for element %s is invalid", e->path->name); + return false; + } + pa_log_warn("Override map for element %s has empty result, ignoring override map", e->path->name); + e->override_map &= ~(1 << (e->n_channels-1)); + goto retry; + } + + return true; +} + +static int element_probe(pa_alsa_element *e, snd_mixer_t *m) { + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *me; + + pa_assert(m); + pa_assert(e); + pa_assert(e->path); + + SELEM_INIT(sid, &e->alsa_id); + + if (!(me = snd_mixer_find_selem(m, sid))) { + + if (e->required != PA_ALSA_REQUIRED_IGNORE) + return -1; + + e->switch_use = PA_ALSA_SWITCH_IGNORE; + e->volume_use = PA_ALSA_VOLUME_IGNORE; + e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE; + + return 0; + } + + if (e->switch_use != PA_ALSA_SWITCH_IGNORE) { + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) { + + if (!snd_mixer_selem_has_playback_switch(me)) { + if (e->direction_try_other && snd_mixer_selem_has_capture_switch(me)) + e->direction = PA_ALSA_DIRECTION_INPUT; + else + e->switch_use = PA_ALSA_SWITCH_IGNORE; + } + + } else { + + if (!snd_mixer_selem_has_capture_switch(me)) { + if (e->direction_try_other && snd_mixer_selem_has_playback_switch(me)) + e->direction = PA_ALSA_DIRECTION_OUTPUT; + else + e->switch_use = PA_ALSA_SWITCH_IGNORE; + } + } + + if (e->switch_use != PA_ALSA_SWITCH_IGNORE) + e->direction_try_other = false; + } + + if (!element_probe_volume(e, me)) + e->volume_use = PA_ALSA_VOLUME_IGNORE; + + if (e->switch_use == PA_ALSA_SWITCH_SELECT) { + pa_alsa_option *o; + + PA_LLIST_FOREACH(o, e->options) + o->alsa_idx = pa_streq(o->alsa_name, "on") ? 1 : 0; + } else if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) { + int n; + pa_alsa_option *o; + + if ((n = snd_mixer_selem_get_enum_items(me)) < 0) { + pa_log("snd_mixer_selem_get_enum_items() failed: %s", pa_alsa_strerror(n)); + return -1; + } + + PA_LLIST_FOREACH(o, e->options) { + int i; + + for (i = 0; i < n; i++) { + char buf[128]; + + if (snd_mixer_selem_get_enum_item_name(me, i, sizeof(buf), buf) < 0) + continue; + + if (!pa_streq(buf, o->alsa_name)) + continue; + + o->alsa_idx = i; + } + } + } + + if (check_required(e, me) < 0) + return -1; + + return 0; +} + +static int jack_probe(pa_alsa_jack *j, pa_alsa_mapping *mapping, snd_mixer_t *m) { + bool has_control; + + pa_assert(j); + pa_assert(j->path); + + if (j->append_pcm_to_name) { + char *new_name; + + if (!mapping) { + /* This could also be an assertion, because this should never + * happen. At the time of writing, mapping can only be NULL when + * module-alsa-sink/source synthesizes a path, and those + * synthesized paths never have any jacks, so jack_probe() should + * never be called with a NULL mapping. */ + pa_log("Jack %s: append_pcm_to_name is set, but mapping is NULL. Can't use this jack.", j->name); + return -1; + } + + new_name = pa_sprintf_malloc("%s,pcm=%i Jack", j->name, mapping->hw_device_index); + pa_xfree(j->alsa_id.name); + j->alsa_id.name = new_name; + j->append_pcm_to_name = false; + } + + has_control = pa_alsa_mixer_find_card(m, &j->alsa_id, 0) != NULL; + pa_alsa_jack_set_has_control(j, has_control); + + if (j->has_control) { + if (j->required_absent != PA_ALSA_REQUIRED_IGNORE) + return -1; + if (j->required_any != PA_ALSA_REQUIRED_IGNORE) + j->path->req_any_present = true; + } else { + if (j->required != PA_ALSA_REQUIRED_IGNORE) + return -1; + } + + return 0; +} + +pa_alsa_element * pa_alsa_element_get(pa_alsa_path *p, const char *section, bool prefixed) { + pa_alsa_element *e; + char *name; + int index; + + pa_assert(p); + pa_assert(section); + + if (prefixed) { + if (!pa_startswith(section, "Element ")) + return NULL; + + section += 8; + } + + /* This is not an element section, but an enum section? */ + if (strchr(section, ':')) + return NULL; + + name = alloca(strlen(section) + 1); + if (alsa_id_decode(section, name, &index)) + return NULL; + + if (p->last_element && pa_streq(p->last_element->alsa_id.name, name) && + p->last_element->alsa_id.index == index) + return p->last_element; + + PA_LLIST_FOREACH(e, p->elements) + if (pa_streq(e->alsa_id.name, name) && e->alsa_id.index == index) + goto finish; + + e = pa_xnew0(pa_alsa_element, 1); + e->path = p; + e->alsa_id.name = pa_xstrdup(name); + e->alsa_id.index = index; + e->direction = p->direction; + e->volume_limit = -1; + + PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e); + +finish: + p->last_element = e; + return e; +} + +static pa_alsa_jack* jack_get(pa_alsa_path *p, const char *section) { + pa_alsa_jack *j; + char *name; + int index; + + if (!pa_startswith(section, "Jack ")) + return NULL; + section += 5; + + name = alloca(strlen(section) + 1); + if (alsa_id_decode(section, name, &index)) + return NULL; + + if (p->last_jack && pa_streq(p->last_jack->name, name) && + p->last_jack->alsa_id.index == index) + return p->last_jack; + + PA_LLIST_FOREACH(j, p->jacks) + if (pa_streq(j->name, name) && j->alsa_id.index == index) + goto finish; + + j = pa_alsa_jack_new(p, NULL, name, index); + PA_LLIST_INSERT_AFTER(pa_alsa_jack, p->jacks, p->last_jack, j); + +finish: + p->last_jack = j; + return j; +} + +static pa_alsa_option* option_get(pa_alsa_path *p, const char *section) { + char *en, *name; + const char *on; + pa_alsa_option *o; + pa_alsa_element *e; + size_t len; + int index; + + if (!pa_startswith(section, "Option ")) + return NULL; + + section += 7; + + /* This is not an enum section, but an element section? */ + if (!(on = strchr(section, ':'))) + return NULL; + + len = on - section; + en = alloca(len + 1); + strncpy(en, section, len); + en[len] = '\0'; + + name = alloca(strlen(en) + 1); + if (alsa_id_decode(en, name, &index)) + return NULL; + + on++; + + if (p->last_option && + pa_streq(p->last_option->element->alsa_id.name, name) && + p->last_option->element->alsa_id.index == index && + pa_streq(p->last_option->alsa_name, on)) { + return p->last_option; + } + + pa_assert_se(e = pa_alsa_element_get(p, en, false)); + + PA_LLIST_FOREACH(o, e->options) + if (pa_streq(o->alsa_name, on)) + goto finish; + + o = pa_xnew0(pa_alsa_option, 1); + o->element = e; + o->alsa_name = pa_xstrdup(on); + o->alsa_idx = -1; + + if (p->last_option && p->last_option->element == e) + PA_LLIST_INSERT_AFTER(pa_alsa_option, e->options, p->last_option, o); + else + PA_LLIST_PREPEND(pa_alsa_option, e->options, o); + +finish: + p->last_option = o; + return o; +} + +static int element_parse_switch(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Switch makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "ignore")) + e->switch_use = PA_ALSA_SWITCH_IGNORE; + else if (pa_streq(state->rvalue, "mute")) + e->switch_use = PA_ALSA_SWITCH_MUTE; + else if (pa_streq(state->rvalue, "off")) + e->switch_use = PA_ALSA_SWITCH_OFF; + else if (pa_streq(state->rvalue, "on")) + e->switch_use = PA_ALSA_SWITCH_ON; + else if (pa_streq(state->rvalue, "select")) + e->switch_use = PA_ALSA_SWITCH_SELECT; + else { + pa_log("[%s:%u] Switch invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int element_parse_volume(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Volume makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "ignore")) + e->volume_use = PA_ALSA_VOLUME_IGNORE; + else if (pa_streq(state->rvalue, "merge")) + e->volume_use = PA_ALSA_VOLUME_MERGE; + else if (pa_streq(state->rvalue, "off")) + e->volume_use = PA_ALSA_VOLUME_OFF; + else if (pa_streq(state->rvalue, "zero")) + e->volume_use = PA_ALSA_VOLUME_ZERO; + else { + uint32_t constant; + + if (pa_atou(state->rvalue, &constant) >= 0) { + e->volume_use = PA_ALSA_VOLUME_CONSTANT; + e->constant_volume = constant; + } else { + pa_log("[%s:%u] Volume invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + } + + return 0; +} + +static int element_parse_enumeration(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Enumeration makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "ignore")) + e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE; + else if (pa_streq(state->rvalue, "select")) + e->enumeration_use = PA_ALSA_ENUMERATION_SELECT; + else { + pa_log("[%s:%u] Enumeration invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int parse_type(pa_config_parser_state *state) { + struct device_port_types { + const char *name; + pa_device_port_type_t type; + } device_port_types[] = { + { "unknown", PA_DEVICE_PORT_TYPE_UNKNOWN }, + { "aux", PA_DEVICE_PORT_TYPE_AUX }, + { "speaker", PA_DEVICE_PORT_TYPE_SPEAKER }, + { "headphones", PA_DEVICE_PORT_TYPE_HEADPHONES }, + { "line", PA_DEVICE_PORT_TYPE_LINE }, + { "mic", PA_DEVICE_PORT_TYPE_MIC }, + { "headset", PA_DEVICE_PORT_TYPE_HEADSET }, + { "handset", PA_DEVICE_PORT_TYPE_HANDSET }, + { "earpiece", PA_DEVICE_PORT_TYPE_EARPIECE }, + { "spdif", PA_DEVICE_PORT_TYPE_SPDIF }, + { "hdmi", PA_DEVICE_PORT_TYPE_HDMI }, + { "tv", PA_DEVICE_PORT_TYPE_TV }, + { "radio", PA_DEVICE_PORT_TYPE_RADIO }, + { "video", PA_DEVICE_PORT_TYPE_VIDEO }, + { "usb", PA_DEVICE_PORT_TYPE_USB }, + { "bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH }, + { "portable", PA_DEVICE_PORT_TYPE_PORTABLE }, + { "handsfree", PA_DEVICE_PORT_TYPE_HANDSFREE }, + { "car", PA_DEVICE_PORT_TYPE_CAR }, + { "hifi", PA_DEVICE_PORT_TYPE_HIFI }, + { "phone", PA_DEVICE_PORT_TYPE_PHONE }, + { "network", PA_DEVICE_PORT_TYPE_NETWORK }, + { "analog", PA_DEVICE_PORT_TYPE_ANALOG }, + }; + pa_alsa_path *path; + unsigned int idx; + + path = state->userdata; + + for (idx = 0; idx < PA_ELEMENTSOF(device_port_types); idx++) + if (pa_streq(state->rvalue, device_port_types[idx].name)) { + path->device_port_type = device_port_types[idx].type; + return 0; + } + + pa_log("[%s:%u] Invalid value for option 'type': %s", state->filename, state->lineno, state->rvalue); + return -1; +} + +static int parse_eld_device(pa_config_parser_state *state) { + pa_alsa_path *path; + uint32_t eld_device; + + path = state->userdata; + + if (pa_atou(state->rvalue, &eld_device) >= 0) { + path->autodetect_eld_device = false; + path->eld_device = eld_device; + return 0; + } + + if (pa_streq(state->rvalue, "auto")) { + path->autodetect_eld_device = true; + path->eld_device = -1; + return 0; + } + + pa_log("[%s:%u] Invalid value for option 'eld-device': %s", state->filename, state->lineno, state->rvalue); + return -1; +} + +static int option_parse_priority(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_option *o; + uint32_t prio; + + pa_assert(state); + + p = state->userdata; + + if (!(o = option_get(p, state->section))) { + pa_log("[%s:%u] Priority makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_atou(state->rvalue, &prio) < 0) { + pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + o->priority = prio; + return 0; +} + +static int option_parse_name(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_option *o; + + pa_assert(state); + + p = state->userdata; + + if (!(o = option_get(p, state->section))) { + pa_log("[%s:%u] Name makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + pa_xfree(o->name); + o->name = pa_xstrdup(state->rvalue); + + return 0; +} + +static int element_parse_required(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + pa_alsa_option *o; + pa_alsa_jack *j; + pa_alsa_required_t req; + + pa_assert(state); + + p = state->userdata; + + e = pa_alsa_element_get(p, state->section, true); + o = option_get(p, state->section); + j = jack_get(p, state->section); + if (!e && !o && !j) { + pa_log("[%s:%u] Required makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "ignore")) + req = PA_ALSA_REQUIRED_IGNORE; + else if (pa_streq(state->rvalue, "switch") && e) + req = PA_ALSA_REQUIRED_SWITCH; + else if (pa_streq(state->rvalue, "volume") && e) + req = PA_ALSA_REQUIRED_VOLUME; + else if (pa_streq(state->rvalue, "enumeration")) + req = PA_ALSA_REQUIRED_ENUMERATION; + else if (pa_streq(state->rvalue, "any")) + req = PA_ALSA_REQUIRED_ANY; + else { + pa_log("[%s:%u] Required invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->lvalue, "required-absent")) { + if (e) + e->required_absent = req; + if (o) + o->required_absent = req; + if (j) + j->required_absent = req; + } + else if (pa_streq(state->lvalue, "required-any")) { + if (e) { + e->required_any = req; + e->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE); + } + if (o) { + o->required_any = req; + o->element->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE); + } + if (j) { + j->required_any = req; + j->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE); + } + + } + else { + if (e) + e->required = req; + if (o) + o->required = req; + if (j) + j->required = req; + } + + return 0; +} + +static int element_parse_direction(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "playback")) + e->direction = PA_ALSA_DIRECTION_OUTPUT; + else if (pa_streq(state->rvalue, "capture")) + e->direction = PA_ALSA_DIRECTION_INPUT; + else { + pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int element_parse_direction_try_other(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + int yes; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if ((yes = pa_parse_boolean(state->rvalue)) < 0) { + pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + e->direction_try_other = !!yes; + return 0; +} + +static int element_parse_volume_limit(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + long volume_limit; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] volume-limit makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_atol(state->rvalue, &volume_limit) < 0 || volume_limit < 0) { + pa_log("[%s:%u] Invalid value for volume-limit", state->filename, state->lineno); + return -1; + } + + e->volume_limit = volume_limit; + return 0; +} + +static unsigned int parse_channel_position(const char *m) +{ + pa_channel_position_t p; + + if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID) + return SND_MIXER_SCHN_UNKNOWN; + + return alsa_channel_ids[p]; +} + +static pa_channel_position_mask_t parse_mask(const char *m) { + pa_channel_position_mask_t v; + + if (pa_streq(m, "all-left")) + v = PA_CHANNEL_POSITION_MASK_LEFT; + else if (pa_streq(m, "all-right")) + v = PA_CHANNEL_POSITION_MASK_RIGHT; + else if (pa_streq(m, "all-center")) + v = PA_CHANNEL_POSITION_MASK_CENTER; + else if (pa_streq(m, "all-front")) + v = PA_CHANNEL_POSITION_MASK_FRONT; + else if (pa_streq(m, "all-rear")) + v = PA_CHANNEL_POSITION_MASK_REAR; + else if (pa_streq(m, "all-side")) + v = PA_CHANNEL_POSITION_MASK_SIDE_OR_TOP_CENTER; + else if (pa_streq(m, "all-top")) + v = PA_CHANNEL_POSITION_MASK_TOP; + else if (pa_streq(m, "all-no-lfe")) + v = PA_CHANNEL_POSITION_MASK_ALL ^ PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_LFE); + else if (pa_streq(m, "all")) + v = PA_CHANNEL_POSITION_MASK_ALL; + else { + pa_channel_position_t p; + + if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID) + return 0; + + v = PA_CHANNEL_POSITION_MASK(p); + } + + return v; +} + +static int element_parse_override_map(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_element *e; + const char *split_state = NULL; + char *s; + unsigned i = 0; + int channel_count = 0; + char *n; + + pa_assert(state); + + p = state->userdata; + + if (!(e = pa_alsa_element_get(p, state->section, true))) { + pa_log("[%s:%u] Override map makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + s = strstr(state->lvalue, "."); + if (s) { + pa_atoi(s + 1, &channel_count); + if (channel_count < 1 || channel_count > POSITION_MASK_CHANNELS) { + pa_log("[%s:%u] Override map index '%s' invalid in '%s'", state->filename, state->lineno, state->lvalue, state->section); + return 0; + } + } else { + pa_log("[%s:%u] Invalid override map syntax '%s' in '%s'", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + while ((n = pa_split(state->rvalue, ",", &split_state))) { + pa_channel_position_mask_t m; + snd_mixer_selem_channel_id_t channel_position; + + if (i >= (unsigned)channel_count) { + pa_log("[%s:%u] Invalid override map size (>%d) in '%s'", state->filename, state->lineno, channel_count, state->section); + pa_xfree(n); + return -1; + } + channel_position = alsa_channel_positions[i]; + + if (!*n) + m = 0; + else { + s = strstr(n, ":"); + if (s) { + *s = '\0'; + s++; + channel_position = parse_channel_position(n); + if (channel_position == SND_MIXER_SCHN_UNKNOWN) { + pa_log("[%s:%u] Override map position '%s' invalid in '%s'", state->filename, state->lineno, n, state->section); + pa_xfree(n); + return -1; + } + } + if ((m = parse_mask(s ? s : n)) == 0) { + pa_log("[%s:%u] Override map '%s' invalid in '%s'", state->filename, state->lineno, s ? s : n, state->section); + pa_xfree(n); + return -1; + } + } + + if (e->masks[channel_position][channel_count-1]) { + pa_log("[%s:%u] Override map '%s' duplicate position '%s' in '%s'", state->filename, state->lineno, s ? s : n, snd_mixer_selem_channel_name(channel_position), state->section); + pa_xfree(n); + return -1; + } + e->override_map |= (1 << (channel_count - 1)); + e->masks[channel_position][channel_count-1] = m; + pa_xfree(n); + i++; + } + + return 0; +} + +static int jack_parse_state(pa_config_parser_state *state) { + pa_alsa_path *p; + pa_alsa_jack *j; + pa_available_t pa; + + pa_assert(state); + + p = state->userdata; + + if (!(j = jack_get(p, state->section))) { + pa_log("[%s:%u] state makes no sense in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "yes")) + pa = PA_AVAILABLE_YES; + else if (pa_streq(state->rvalue, "no")) + pa = PA_AVAILABLE_NO; + else if (pa_streq(state->rvalue, "unknown")) + pa = PA_AVAILABLE_UNKNOWN; + else { + pa_log("[%s:%u] state must be 'yes', 'no' or 'unknown' in '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->lvalue, "state.unplugged")) + j->state_unplugged = pa; + else { + j->state_plugged = pa; + pa_assert(pa_streq(state->lvalue, "state.plugged")); + } + + return 0; +} + +static int jack_parse_append_pcm_to_name(pa_config_parser_state *state) { + pa_alsa_path *path; + pa_alsa_jack *jack; + int b; + + pa_assert(state); + + path = state->userdata; + if (!(jack = jack_get(path, state->section))) { + pa_log("[%s:%u] Option 'append_pcm_to_name' not expected in section '%s'", + state->filename, state->lineno, state->section); + return -1; + } + + b = pa_parse_boolean(state->rvalue); + if (b < 0) { + pa_log("[%s:%u] Invalid value for 'append_pcm_to_name': %s", state->filename, state->lineno, state->rvalue); + return -1; + } + + jack->append_pcm_to_name = b; + return 0; +} + +static int element_set_option(pa_alsa_element *e, snd_mixer_t *m, int alsa_idx) { + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *me; + char buf[64]; + int r; + + pa_assert(e); + pa_assert(m); + + SELEM_INIT(sid, &e->alsa_id); + if (!(me = snd_mixer_find_selem(m, sid))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Element %s seems to have disappeared.", buf); + return -1; + } + + if (e->switch_use == PA_ALSA_SWITCH_SELECT) { + + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_set_playback_switch_all(me, alsa_idx); + else + r = snd_mixer_selem_set_capture_switch_all(me, alsa_idx); + + if (r < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno)); + } + + } else { + pa_assert(e->enumeration_use == PA_ALSA_ENUMERATION_SELECT); + + if ((r = snd_mixer_selem_set_enum_item(me, 0, alsa_idx)) < 0) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Failed to set enumeration of %s: %s", buf, pa_alsa_strerror(errno)); + } + } + + return r; +} + +static int setting_select(pa_alsa_setting *s, snd_mixer_t *m) { + pa_alsa_option *o; + uint32_t idx; + + pa_assert(s); + pa_assert(m); + + PA_IDXSET_FOREACH(o, s->options, idx) + element_set_option(o->element, m, o->alsa_idx); + + return 0; +} + +static int option_verify(pa_alsa_option *o) { + static const struct description_map well_known_descriptions[] = { + { "input", N_("Input") }, + { "input-docking", N_("Docking Station Input") }, + { "input-docking-microphone", N_("Docking Station Microphone") }, + { "input-docking-linein", N_("Docking Station Line In") }, + { "input-linein", N_("Line In") }, + { "input-microphone", N_("Microphone") }, + { "input-microphone-front", N_("Front Microphone") }, + { "input-microphone-rear", N_("Rear Microphone") }, + { "input-microphone-external", N_("External Microphone") }, + { "input-microphone-internal", N_("Internal Microphone") }, + { "input-radio", N_("Radio") }, + { "input-video", N_("Video") }, + { "input-agc-on", N_("Automatic Gain Control") }, + { "input-agc-off", N_("No Automatic Gain Control") }, + { "input-boost-on", N_("Boost") }, + { "input-boost-off", N_("No Boost") }, + { "output-amplifier-on", N_("Amplifier") }, + { "output-amplifier-off", N_("No Amplifier") }, + { "output-bass-boost-on", N_("Bass Boost") }, + { "output-bass-boost-off", N_("No Bass Boost") }, + { "output-speaker", N_("Speaker") }, + { "output-headphones", N_("Headphones") } + }; + char buf[64]; + + pa_assert(o); + + if (!o->name) { + pa_log("No name set for option %s", o->alsa_name); + return -1; + } + + if (o->element->enumeration_use != PA_ALSA_ENUMERATION_SELECT && + o->element->switch_use != PA_ALSA_SWITCH_SELECT) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id); + pa_log("Element %s of option %s not set for select.", buf, o->name); + return -1; + } + + if (o->element->switch_use == PA_ALSA_SWITCH_SELECT && + !pa_streq(o->alsa_name, "on") && + !pa_streq(o->alsa_name, "off")) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id); + pa_log("Switch %s options need be named off or on ", buf); + return -1; + } + + if (!o->description) + o->description = pa_xstrdup(lookup_description(o->name, + well_known_descriptions, + PA_ELEMENTSOF(well_known_descriptions))); + if (!o->description) + o->description = pa_xstrdup(o->name); + + return 0; +} + +static int element_verify(pa_alsa_element *e) { + pa_alsa_option *o; + char buf[64]; + + pa_assert(e); + +// pa_log_debug("Element %s, path %s: r=%d, r-any=%d, r-abs=%d", e->alsa_name, e->path->name, e->required, e->required_any, e->required_absent); + if ((e->required != PA_ALSA_REQUIRED_IGNORE && e->required == e->required_absent) || + (e->required_any != PA_ALSA_REQUIRED_IGNORE && e->required_any == e->required_absent) || + (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required_any != PA_ALSA_REQUIRED_IGNORE) || + (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required != PA_ALSA_REQUIRED_IGNORE)) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log("Element %s cannot be required and absent at the same time.", buf); + return -1; + } + + if (e->switch_use == PA_ALSA_SWITCH_SELECT && e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log("Element %s cannot set select for both switch and enumeration.", buf); + return -1; + } + + PA_LLIST_FOREACH(o, e->options) + if (option_verify(o) < 0) + return -1; + + return 0; +} + +static int path_verify(pa_alsa_path *p) { + static const struct description2_map well_known_descriptions[] = { + { "analog-input", N_("Analog Input"), PA_DEVICE_PORT_TYPE_ANALOG }, + { "analog-input-microphone", N_("Microphone"), PA_DEVICE_PORT_TYPE_MIC }, + { "analog-input-microphone-front", N_("Front Microphone"), PA_DEVICE_PORT_TYPE_MIC }, + { "analog-input-microphone-rear", N_("Rear Microphone"), PA_DEVICE_PORT_TYPE_MIC }, + { "analog-input-microphone-dock", N_("Dock Microphone"), PA_DEVICE_PORT_TYPE_MIC }, + { "analog-input-microphone-internal", N_("Internal Microphone"), PA_DEVICE_PORT_TYPE_MIC }, + { "analog-input-microphone-headset", N_("Headset Microphone"), PA_DEVICE_PORT_TYPE_HEADSET }, + { "analog-input-linein", N_("Line In"), PA_DEVICE_PORT_TYPE_LINE }, + { "analog-input-radio", N_("Radio"), PA_DEVICE_PORT_TYPE_RADIO }, + { "analog-input-video", N_("Video"), PA_DEVICE_PORT_TYPE_VIDEO }, + { "analog-output", N_("Analog Output"), PA_DEVICE_PORT_TYPE_ANALOG }, + { "analog-output-headphones", N_("Headphones"), PA_DEVICE_PORT_TYPE_HEADPHONES }, + { "analog-output-headphones-2", N_("Headphones 2"), PA_DEVICE_PORT_TYPE_HEADPHONES }, + { "analog-output-headphones-mono", N_("Headphones Mono Output"), PA_DEVICE_PORT_TYPE_HEADPHONES }, + { "analog-output-lineout", N_("Line Out"), PA_DEVICE_PORT_TYPE_LINE }, + { "analog-output-mono", N_("Analog Mono Output"), PA_DEVICE_PORT_TYPE_ANALOG }, + { "analog-output-speaker", N_("Speakers"), PA_DEVICE_PORT_TYPE_SPEAKER }, + { "hdmi-output", N_("HDMI / DisplayPort"), PA_DEVICE_PORT_TYPE_HDMI }, + { "iec958-stereo-output", N_("Digital Output (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF }, + { "iec958-stereo-input", N_("Digital Input (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF }, + { "multichannel-input", N_("Multichannel Input"), PA_DEVICE_PORT_TYPE_LINE }, + { "multichannel-output", N_("Multichannel Output"), PA_DEVICE_PORT_TYPE_LINE }, + { "steelseries-arctis-output-game-common", N_("Game Output"), PA_DEVICE_PORT_TYPE_HEADSET }, + { "steelseries-arctis-output-chat-common", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET }, + { "analog-chat-output", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET }, + { "analog-chat-input", N_("Chat Input"), PA_DEVICE_PORT_TYPE_HEADSET }, + { "virtual-surround-7.1", N_("Virtual Surround 7.1"), PA_DEVICE_PORT_TYPE_HEADPHONES }, + }; + + pa_alsa_element *e; + const char *key = p->description_key ? p->description_key : p->name; + const struct description2_map *map = lookup_description2(key, + well_known_descriptions, + PA_ELEMENTSOF(well_known_descriptions)); + + pa_assert(p); + + PA_LLIST_FOREACH(e, p->elements) + if (element_verify(e) < 0) + return -1; + + if (map) { + if (p->device_port_type == PA_DEVICE_PORT_TYPE_UNKNOWN) + p->device_port_type = map->type; + if (!p->description) + p->description = pa_xstrdup(_(map->description)); + } + + if (!p->description) { + if (p->description_key) + pa_log_warn("Path %s: Unrecognized description key: %s", p->name, p->description_key); + + p->description = pa_xstrdup(p->name); + } + + return 0; +} + +static const char *get_default_paths_dir(void) { + const char *str; +#ifdef HAVE_RUNNING_FROM_BUILD_TREE + if (pa_run_from_build_tree()) + return PA_SRCDIR "mixer/paths"; + else +#endif + if (getenv("ACP_BUILDDIR") != NULL) + return "mixer/paths"; + if ((str = getenv("ACP_PATHS_DIR")) != NULL) + return str; + return PA_ALSA_PATHS_DIR; +} + +pa_alsa_path* pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction) { + pa_alsa_path *p; + char *fn; + int r; + const char *n; + bool mute_during_activation = false; + + pa_config_item items[] = { + /* [General] */ + { "priority", pa_config_parse_unsigned, NULL, "General" }, + { "description-key", pa_config_parse_string, NULL, "General" }, + { "description", pa_config_parse_string, NULL, "General" }, + { "mute-during-activation", pa_config_parse_bool, NULL, "General" }, + { "type", parse_type, NULL, "General" }, + { "eld-device", parse_eld_device, NULL, "General" }, + + /* [Option ...] */ + { "priority", option_parse_priority, NULL, NULL }, + { "name", option_parse_name, NULL, NULL }, + + /* [Jack ...] */ + { "state.plugged", jack_parse_state, NULL, NULL }, + { "state.unplugged", jack_parse_state, NULL, NULL }, + { "append-pcm-to-name", jack_parse_append_pcm_to_name, NULL, NULL }, + + /* [Element ...] */ + { "switch", element_parse_switch, NULL, NULL }, + { "volume", element_parse_volume, NULL, NULL }, + { "enumeration", element_parse_enumeration, NULL, NULL }, + { "override-map.1", element_parse_override_map, NULL, NULL }, + { "override-map.2", element_parse_override_map, NULL, NULL }, + { "override-map.3", element_parse_override_map, NULL, NULL }, + { "override-map.4", element_parse_override_map, NULL, NULL }, + { "override-map.5", element_parse_override_map, NULL, NULL }, + { "override-map.6", element_parse_override_map, NULL, NULL }, + { "override-map.7", element_parse_override_map, NULL, NULL }, + { "override-map.8", element_parse_override_map, NULL, NULL }, +#if POSITION_MASK_CHANNELS > 8 +#error "Add override-map.9+ definitions" +#endif + /* ... later on we might add override-map.3 and so on here ... */ + { "required", element_parse_required, NULL, NULL }, + { "required-any", element_parse_required, NULL, NULL }, + { "required-absent", element_parse_required, NULL, NULL }, + { "direction", element_parse_direction, NULL, NULL }, + { "direction-try-other", element_parse_direction_try_other, NULL, NULL }, + { "volume-limit", element_parse_volume_limit, NULL, NULL }, + { NULL, NULL, NULL, NULL } + }; + + pa_assert(fname); + + p = pa_xnew0(pa_alsa_path, 1); + n = pa_path_get_filename(fname); + p->name = pa_xstrndup(n, strcspn(n, ".")); + p->proplist = pa_proplist_new(); + p->direction = direction; + p->eld_device = -1; + + items[0].data = &p->priority; + items[1].data = &p->description_key; + items[2].data = &p->description; + items[3].data = &mute_during_activation; + + if (!paths_dir) + paths_dir = get_default_paths_dir(); + + fn = pa_maybe_prefix_path(fname, paths_dir); + + r = pa_config_parse(fn, NULL, items, p->proplist, false, p); + pa_xfree(fn); + + if (r < 0) + goto fail; + + p->mute_during_activation = mute_during_activation; + + if (path_verify(p) < 0) + goto fail; + + if (p->description) { + char *tmp = p->description; + p->description = pa_xstrdup(_(tmp)); + free(tmp); + } + + return p; + +fail: + pa_alsa_path_free(p); + return NULL; +} + +pa_alsa_path *pa_alsa_path_synthesize(const char *element, pa_alsa_direction_t direction) { + pa_alsa_path *p; + pa_alsa_element *e; + char *name; + int index; + + pa_assert(element); + + name = alloca(strlen(element) + 1); + if (alsa_id_decode(element, name, &index)) + return NULL; + + p = pa_xnew0(pa_alsa_path, 1); + p->name = pa_xstrdup(element); + p->direction = direction; + p->proplist = pa_proplist_new(); + + e = pa_xnew0(pa_alsa_element, 1); + e->path = p; + e->alsa_id.name = pa_xstrdup(name); + e->alsa_id.index = index; + e->direction = direction; + e->volume_limit = -1; + + e->switch_use = PA_ALSA_SWITCH_MUTE; + e->volume_use = PA_ALSA_VOLUME_MERGE; + + PA_LLIST_PREPEND(pa_alsa_element, p->elements, e); + p->last_element = e; + return p; +} + +static bool element_drop_unsupported(pa_alsa_element *e) { + pa_alsa_option *o, *n; + + pa_assert(e); + + for (o = e->options; o; o = n) { + n = o->next; + + if (o->alsa_idx < 0) { + PA_LLIST_REMOVE(pa_alsa_option, e->options, o); + option_free(o); + } + } + + return + e->switch_use != PA_ALSA_SWITCH_IGNORE || + e->volume_use != PA_ALSA_VOLUME_IGNORE || + e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE; +} + +static void path_drop_unsupported(pa_alsa_path *p) { + pa_alsa_element *e, *n; + + pa_assert(p); + + for (e = p->elements; e; e = n) { + n = e->next; + + if (!element_drop_unsupported(e)) { + PA_LLIST_REMOVE(pa_alsa_element, p->elements, e); + element_free(e); + } + } +} + +static void path_make_options_unique(pa_alsa_path *p) { + pa_alsa_element *e; + pa_alsa_option *o, *u; + + PA_LLIST_FOREACH(e, p->elements) { + PA_LLIST_FOREACH(o, e->options) { + unsigned i; + char *m; + + for (u = o->next; u; u = u->next) + if (pa_streq(u->name, o->name)) + break; + + if (!u) + continue; + + m = pa_xstrdup(o->name); + + /* OK, this name is not unique, hence let's rename */ + for (i = 1, u = o; u; u = u->next) { + char *nn, *nd; + + if (!pa_streq(u->name, m)) + continue; + + nn = pa_sprintf_malloc("%s-%u", m, i); + pa_xfree(u->name); + u->name = nn; + + nd = pa_sprintf_malloc("%s %u", u->description, i); + pa_xfree(u->description); + u->description = nd; + + i++; + } + + pa_xfree(m); + } + } +} + +static bool element_create_settings(pa_alsa_element *e, pa_alsa_setting *template) { + pa_alsa_option *o; + + for (; e; e = e->next) + if (e->switch_use == PA_ALSA_SWITCH_SELECT || + e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) + break; + + if (!e) + return false; + + for (o = e->options; o; o = o->next) { + pa_alsa_setting *s; + + if (template) { + s = pa_xnewdup(pa_alsa_setting, template, 1); + s->options = pa_idxset_copy(template->options, NULL); + s->name = pa_sprintf_malloc("%s+%s", template->name, o->name); + s->description = + (template->description[0] && o->description[0]) + ? pa_sprintf_malloc("%s / %s", template->description, o->description) + : (template->description[0] + ? pa_xstrdup(template->description) + : pa_xstrdup(o->description)); + + s->priority = PA_MAX(template->priority, o->priority); + } else { + s = pa_xnew0(pa_alsa_setting, 1); + s->options = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + s->name = pa_xstrdup(o->name); + s->description = pa_xstrdup(o->description); + s->priority = o->priority; + } + + pa_idxset_put(s->options, o, NULL); + + if (element_create_settings(e->next, s)) + /* This is not a leaf, so let's get rid of it */ + setting_free(s); + else { + /* This is a leaf, so let's add it */ + PA_LLIST_INSERT_AFTER(pa_alsa_setting, e->path->settings, e->path->last_setting, s); + + e->path->last_setting = s; + } + } + + return true; +} + +static void path_create_settings(pa_alsa_path *p) { + pa_assert(p); + + element_create_settings(p->elements, NULL); +} + +int pa_alsa_path_probe(pa_alsa_path *p, pa_alsa_mapping *mapping, snd_mixer_t *m, bool ignore_dB) { + pa_alsa_element *e; + pa_alsa_jack *j; + double min_dB[PA_CHANNEL_POSITION_MAX], max_dB[PA_CHANNEL_POSITION_MAX]; + pa_channel_position_t t; + pa_channel_position_mask_t path_volume_channels = 0; + bool min_dB_set, max_dB_set; + char buf[64]; + + pa_assert(p); + pa_assert(m); + + if (p->probed) + return p->supported ? 0 : -1; + p->probed = true; + + pa_zero(min_dB); + pa_zero(max_dB); + + pa_log_debug("Probing path '%s'", p->name); + + PA_LLIST_FOREACH(j, p->jacks) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &j->alsa_id); + if (jack_probe(j, mapping, m) < 0) { + p->supported = false; + pa_log_debug("Probe of jack %s failed.", buf); + return -1; + } + pa_log_debug("Probe of jack %s succeeded (%s)", buf, j->has_control ? "found!" : "not found"); + } + + PA_LLIST_FOREACH(e, p->elements) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + if (element_probe(e, m) < 0) { + p->supported = false; + pa_log_debug("Probe of element %s failed.", buf); + return -1; + } + pa_log_debug("Probe of element %s succeeded (volume=%d, switch=%d, enumeration=%d, has_dB=%d).", buf, e->volume_use, e->switch_use, e->enumeration_use, e->has_dB); + + if (ignore_dB) + e->has_dB = false; + + if (e->volume_use == PA_ALSA_VOLUME_MERGE) { + + if (!p->has_volume) { + p->min_volume = e->min_volume; + p->max_volume = e->max_volume; + } + + if (e->has_dB) { + if (!p->has_volume) { + for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++) + if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) { + min_dB[t] = e->min_dB; + max_dB[t] = e->max_dB; + path_volume_channels |= PA_CHANNEL_POSITION_MASK(t); + } + + p->has_dB = true; + } else { + + if (p->has_dB) { + for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++) + if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) { + min_dB[t] += e->min_dB; + max_dB[t] += e->max_dB; + path_volume_channels |= PA_CHANNEL_POSITION_MASK(t); + } + } else { + /* Hmm, there's another element before us + * which cannot do dB volumes, so we we need + * to 'neutralize' this slider */ + e->volume_use = PA_ALSA_VOLUME_ZERO; + pa_log_info("Zeroing volume of %s on path '%s'", buf, p->name); + } + } + } else if (p->has_volume) { + /* We can't use this volume, so let's ignore it */ + e->volume_use = PA_ALSA_VOLUME_IGNORE; + pa_log_info("Ignoring volume of %s on path '%s' (missing dB info)", buf, p->name); + } + p->has_volume = true; + } + + if (e->switch_use == PA_ALSA_SWITCH_MUTE) + p->has_mute = true; + } + + if (p->has_req_any && !p->req_any_present) { + p->supported = false; + pa_log_debug("Skipping path '%s', none of required-any elements preset.", p->name); + return -1; + } + + path_drop_unsupported(p); + path_make_options_unique(p); + path_create_settings(p); + + p->supported = true; + + p->min_dB = INFINITY; + min_dB_set = false; + p->max_dB = -INFINITY; + max_dB_set = false; + + for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++) { + if (path_volume_channels & PA_CHANNEL_POSITION_MASK(t)) { + if (p->min_dB > min_dB[t]) { + p->min_dB = min_dB[t]; + min_dB_set = true; + } + + if (p->max_dB < max_dB[t]) { + p->max_dB = max_dB[t]; + max_dB_set = true; + } + } + } + + /* this is probably a wrong prediction, but it should be safe */ + if (!min_dB_set) + p->min_dB = -INFINITY; + if (!max_dB_set) + p->max_dB = 0; + + return 0; +} + +void pa_alsa_setting_dump(pa_alsa_setting *s) { + pa_assert(s); + + pa_log_debug("Setting %s (%s) priority=%u", + s->name, + pa_strnull(s->description), + s->priority); +} + +void pa_alsa_jack_dump(pa_alsa_jack *j) { + pa_assert(j); + + pa_log_debug("Jack %s, alsa_name='%s', index='%d', detection %s", j->name, j->alsa_id.name, j->alsa_id.index, j->has_control ? "possible" : "unavailable"); +} + +void pa_alsa_option_dump(pa_alsa_option *o) { + pa_assert(o); + + pa_log_debug("Option %s (%s/%s) index=%i, priority=%u", + o->alsa_name, + pa_strnull(o->name), + pa_strnull(o->description), + o->alsa_idx, + o->priority); +} + +void pa_alsa_element_dump(pa_alsa_element *e) { + char buf[64]; + + pa_alsa_option *o; + pa_assert(e); + + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_debug("Element %s, direction=%i, switch=%i, volume=%i, volume_limit=%li, enumeration=%i, required=%i, required_any=%i, required_absent=%i, mask=0x%llx, n_channels=%u, override_map=%02x", + buf, + e->direction, + e->switch_use, + e->volume_use, + e->volume_limit, + e->enumeration_use, + e->required, + e->required_any, + e->required_absent, + (long long unsigned) e->merged_mask, + e->n_channels, + e->override_map); + + PA_LLIST_FOREACH(o, e->options) + pa_alsa_option_dump(o); +} + +void pa_alsa_path_dump(pa_alsa_path *p) { + pa_alsa_element *e; + pa_alsa_jack *j; + pa_alsa_setting *s; + pa_assert(p); + + pa_log_debug("Path %s (%s), direction=%i, priority=%u, probed=%s, supported=%s, has_mute=%s, has_volume=%s, " + "has_dB=%s, min_volume=%li, max_volume=%li, min_dB=%g, max_dB=%g", + p->name, + pa_strnull(p->description), + p->direction, + p->priority, + pa_yes_no(p->probed), + pa_yes_no(p->supported), + pa_yes_no(p->has_mute), + pa_yes_no(p->has_volume), + pa_yes_no(p->has_dB), + p->min_volume, p->max_volume, + p->min_dB, p->max_dB); + + PA_LLIST_FOREACH(e, p->elements) + pa_alsa_element_dump(e); + + PA_LLIST_FOREACH(j, p->jacks) + pa_alsa_jack_dump(j); + + PA_LLIST_FOREACH(s, p->settings) + pa_alsa_setting_dump(s); +} + +static void element_set_callback(pa_alsa_element *e, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) { + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *me; + char buf[64]; + + pa_assert(e); + pa_assert(m); + pa_assert(cb); + + SELEM_INIT(sid, &e->alsa_id); + if (!(me = snd_mixer_find_selem(m, sid))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); + pa_log_warn("Element %s seems to have disappeared.", buf); + return; + } + + snd_mixer_elem_set_callback(me, cb); + snd_mixer_elem_set_callback_private(me, userdata); +} + +void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) { + pa_alsa_element *e; + + pa_assert(p); + pa_assert(m); + pa_assert(cb); + + PA_LLIST_FOREACH(e, p->elements) + element_set_callback(e, m, cb, userdata); +} + +void pa_alsa_path_set_set_callback(pa_alsa_path_set *ps, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) { + pa_alsa_path *p; + void *state; + + pa_assert(ps); + pa_assert(m); + pa_assert(cb); + + PA_HASHMAP_FOREACH(p, ps->paths, state) + pa_alsa_path_set_callback(p, m, cb, userdata); +} + +static pa_alsa_path *profile_set_get_path(pa_alsa_profile_set *ps, const char *path_name) { + pa_alsa_path *path; + + pa_assert(ps); + pa_assert(path_name); + + if ((path = pa_hashmap_get(ps->output_paths, path_name))) + return path; + + return pa_hashmap_get(ps->input_paths, path_name); +} + +static void profile_set_add_path(pa_alsa_profile_set *ps, pa_alsa_path *path) { + pa_assert(ps); + pa_assert(path); + + switch (path->direction) { + case PA_ALSA_DIRECTION_OUTPUT: + pa_assert_se(pa_hashmap_put(ps->output_paths, path->name, path) >= 0); + break; + + case PA_ALSA_DIRECTION_INPUT: + pa_assert_se(pa_hashmap_put(ps->input_paths, path->name, path) >= 0); + break; + + default: + pa_assert_not_reached(); + } +} + +pa_alsa_path_set *pa_alsa_path_set_new(pa_alsa_mapping *m, pa_alsa_direction_t direction, const char *paths_dir) { + pa_alsa_path_set *ps; + char **pn = NULL, **en = NULL, **ie; + pa_alsa_decibel_fix *db_fix; + void *state, *state2; + char name[64]; + int index; + + pa_assert(m); + pa_assert(m->profile_set); + pa_assert(m->profile_set->decibel_fixes); + pa_assert(direction == PA_ALSA_DIRECTION_OUTPUT || direction == PA_ALSA_DIRECTION_INPUT); + + if (m->direction != PA_ALSA_DIRECTION_ANY && m->direction != direction) + return NULL; + + ps = pa_xnew0(pa_alsa_path_set, 1); + ps->direction = direction; + ps->paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (direction == PA_ALSA_DIRECTION_OUTPUT) + pn = m->output_path_names; + else + pn = m->input_path_names; + + if (pn) { + char **in; + + for (in = pn; *in; in++) { + pa_alsa_path *p = NULL; + bool duplicate = false; + char **kn; + + for (kn = pn; kn < in; kn++) + if (pa_streq(*kn, *in)) { + duplicate = true; + break; + } + + if (duplicate) + continue; + + p = profile_set_get_path(m->profile_set, *in); + + if (p && p->direction != direction) { + pa_log("Configuration error: Path %s is used both as an input and as an output path.", p->name); + goto fail; + } + + if (!p) { + char *fn = pa_sprintf_malloc("%s.conf", *in); + p = pa_alsa_path_new(paths_dir, fn, direction); + pa_xfree(fn); + if (p) + profile_set_add_path(m->profile_set, p); + } + + if (p) + pa_hashmap_put(ps->paths, p, p); + + } + + goto finish; + } + + if (direction == PA_ALSA_DIRECTION_OUTPUT) + en = m->output_element; + else + en = m->input_element; + + if (!en) + goto fail; + + for (ie = en; *ie; ie++) { + char **je; + pa_alsa_path *p; + + p = pa_alsa_path_synthesize(*ie, direction); + + /* Mark all other passed elements for require-absent */ + for (je = en; *je; je++) { + pa_alsa_element *e; + + if (je == ie) + continue; + + if (strlen(*je) + 1 >= sizeof(name)) { + pa_log("Element identifier %s is too long!", *je); + continue; + } + + if (alsa_id_decode(*je, name, &index)) + continue; + + e = pa_xnew0(pa_alsa_element, 1); + e->path = p; + e->alsa_id.name = pa_xstrdup(name); + e->alsa_id.index = index; + e->direction = direction; + e->required_absent = PA_ALSA_REQUIRED_ANY; + e->volume_limit = -1; + + PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e); + p->last_element = e; + } + + pa_hashmap_put(ps->paths, *ie, p); + } + +finish: + /* Assign decibel fixes to elements. */ + PA_HASHMAP_FOREACH(db_fix, m->profile_set->decibel_fixes, state) { + pa_alsa_path *p; + + PA_HASHMAP_FOREACH(p, ps->paths, state2) { + pa_alsa_element *e; + + PA_LLIST_FOREACH(e, p->elements) { + if (e->volume_use != PA_ALSA_VOLUME_IGNORE && pa_streq(db_fix->name, e->alsa_id.name) && + db_fix->index == e->alsa_id.index) { + /* The profile set that contains the dB fix may be freed + * before the element, so we have to copy the dB fix + * object. */ + e->db_fix = pa_xnewdup(pa_alsa_decibel_fix, db_fix, 1); + e->db_fix->profile_set = NULL; + e->db_fix->key = pa_xstrdup(db_fix->key); + e->db_fix->name = pa_xstrdup(db_fix->name); + e->db_fix->db_values = pa_xmemdup(db_fix->db_values, (db_fix->max_step - db_fix->min_step + 1) * sizeof(long)); + } + } + } + } + + return ps; + +fail: + if (ps) + pa_alsa_path_set_free(ps); + + return NULL; +} + +void pa_alsa_path_set_dump(pa_alsa_path_set *ps) { + pa_alsa_path *p; + void *state; + pa_assert(ps); + + pa_log_debug("Path Set %p, direction=%i", + (void*) ps, + ps->direction); + + PA_HASHMAP_FOREACH(p, ps->paths, state) + pa_alsa_path_dump(p); +} + +static bool options_have_option(pa_alsa_option *options, const char *alsa_name) { + pa_alsa_option *o; + + pa_assert(options); + pa_assert(alsa_name); + + PA_LLIST_FOREACH(o, options) { + if (pa_streq(o->alsa_name, alsa_name)) + return true; + } + return false; +} + +static bool enumeration_is_subset(pa_alsa_option *a_options, pa_alsa_option *b_options) { + pa_alsa_option *oa, *ob; + + if (!a_options) return true; + if (!b_options) return false; + + /* If there is an option A offers that B does not, then A is not a subset of B. */ + PA_LLIST_FOREACH(oa, a_options) { + bool found = false; + PA_LLIST_FOREACH(ob, b_options) { + if (pa_streq(oa->alsa_name, ob->alsa_name)) { + found = true; + break; + } + } + if (!found) + return false; + } + return true; +} + +/** + * Compares two elements to see if a is a subset of b + */ +static bool element_is_subset(pa_alsa_element *a, pa_alsa_element *b, snd_mixer_t *m) { + char buf[64]; + + pa_assert(a); + pa_assert(b); + pa_assert(m); + + /* General rules: + * Every state is a subset of itself (with caveats for volume_limits and options) + * IGNORE is a subset of every other state */ + + /* Check the volume_use */ + if (a->volume_use != PA_ALSA_VOLUME_IGNORE) { + + /* "Constant" is subset of "Constant" only when their constant values are equal */ + if (a->volume_use == PA_ALSA_VOLUME_CONSTANT && b->volume_use == PA_ALSA_VOLUME_CONSTANT && a->constant_volume != b->constant_volume) + return false; + + /* Different volume uses when b is not "Merge" means we are definitely not a subset */ + if (a->volume_use != b->volume_use && b->volume_use != PA_ALSA_VOLUME_MERGE) + return false; + + /* "Constant" is a subset of "Merge", if there is not a "volume-limit" in "Merge" below the actual constant. + * "Zero" and "Off" are just special cases of "Constant" when comparing to "Merge" + * "Merge" with a "volume-limit" is a subset of "Merge" without a "volume-limit" or with a higher "volume-limit" */ + if (b->volume_use == PA_ALSA_VOLUME_MERGE && b->volume_limit >= 0) { + long a_limit; + + if (a->volume_use == PA_ALSA_VOLUME_CONSTANT) + a_limit = a->constant_volume; + else if (a->volume_use == PA_ALSA_VOLUME_ZERO) { + long dB = 0; + + if (a->db_fix) { + int rounding = (a->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1); + a_limit = decibel_fix_get_step(a->db_fix, &dB, rounding); + } else { + snd_mixer_selem_id_t *sid; + snd_mixer_elem_t *me; + + SELEM_INIT(sid, &a->alsa_id); + if (!(me = snd_mixer_find_selem(m, sid))) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id); + pa_log_warn("Element %s seems to have disappeared.", buf); + return false; + } + + if (a->direction == PA_ALSA_DIRECTION_OUTPUT) { + if (snd_mixer_selem_ask_playback_dB_vol(me, dB, +1, &a_limit) < 0) + return false; + } else { + if (snd_mixer_selem_ask_capture_dB_vol(me, dB, -1, &a_limit) < 0) + return false; + } + } + } else if (a->volume_use == PA_ALSA_VOLUME_OFF) + a_limit = a->min_volume; + else if (a->volume_use == PA_ALSA_VOLUME_MERGE) + a_limit = a->volume_limit; + else + pa_assert_not_reached(); + + if (a_limit > b->volume_limit) + return false; + } + + if (a->volume_use == PA_ALSA_VOLUME_MERGE) { + int s; + /* If override-maps are different, they're not subsets */ + if (a->n_channels != b->n_channels) + return false; + for (s = 0; s <= SND_MIXER_SCHN_LAST; s++) + if (a->masks[s][a->n_channels-1] != b->masks[s][b->n_channels-1]) { + pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id); + pa_log_debug("Element %s is not a subset - mask a: 0x%" PRIx64 ", mask b: 0x%" PRIx64 ", at channel %d", + buf, a->masks[s][a->n_channels-1], b->masks[s][b->n_channels-1], s); + return false; + } + } + } + + if (a->switch_use != PA_ALSA_SWITCH_IGNORE) { + /* "On" is a subset of "Mute". + * "Off" is a subset of "Mute". + * "On" is a subset of "Select", if there is an "Option:On" in B. + * "Off" is a subset of "Select", if there is an "Option:Off" in B. + * "Select" is a subset of "Select", if they have the same options (is this always true?). */ + + if (a->switch_use != b->switch_use) { + + if (a->switch_use == PA_ALSA_SWITCH_SELECT || a->switch_use == PA_ALSA_SWITCH_MUTE + || b->switch_use == PA_ALSA_SWITCH_OFF || b->switch_use == PA_ALSA_SWITCH_ON) + return false; + + if (b->switch_use == PA_ALSA_SWITCH_SELECT) { + if (a->switch_use == PA_ALSA_SWITCH_ON) { + if (!options_have_option(b->options, "on")) + return false; + } else if (a->switch_use == PA_ALSA_SWITCH_OFF) { + if (!options_have_option(b->options, "off")) + return false; + } + } + } else if (a->switch_use == PA_ALSA_SWITCH_SELECT) { + if (!enumeration_is_subset(a->options, b->options)) + return false; + } + } + + if (a->enumeration_use != PA_ALSA_ENUMERATION_IGNORE) { + if (b->enumeration_use == PA_ALSA_ENUMERATION_IGNORE) + return false; + if (!enumeration_is_subset(a->options, b->options)) + return false; + } + + return true; +} + +static void path_set_condense(pa_alsa_path_set *ps, snd_mixer_t *m) { + pa_alsa_path *p; + void *state; + + pa_assert(ps); + pa_assert(m); + + /* If we only have one path, then don't bother */ + if (pa_hashmap_size(ps->paths) < 2) + return; + + PA_HASHMAP_FOREACH(p, ps->paths, state) { + pa_alsa_path *p2; + void *state2; + + PA_HASHMAP_FOREACH(p2, ps->paths, state2) { + pa_alsa_element *ea, *eb; + pa_alsa_jack *ja, *jb; + bool is_subset = true; + + if (p == p2) + continue; + + /* If a has a jack that b does not have, a is not a subset */ + PA_LLIST_FOREACH(ja, p->jacks) { + bool exists = false; + + if (!ja->has_control) + continue; + + PA_LLIST_FOREACH(jb, p2->jacks) { + if (jb->has_control && pa_streq(ja->alsa_id.name, jb->alsa_id.name) && + (ja->alsa_id.index == jb->alsa_id.index) && + (ja->state_plugged == jb->state_plugged) && + (ja->state_unplugged == jb->state_unplugged)) { + exists = true; + break; + } + } + + if (!exists) { + is_subset = false; + break; + } + } + + /* Compare the elements of each set... */ + PA_LLIST_FOREACH(ea, p->elements) { + bool found_matching_element = false; + + if (!is_subset) + break; + + PA_LLIST_FOREACH(eb, p2->elements) { + if (pa_streq(ea->alsa_id.name, eb->alsa_id.name) && + ea->alsa_id.index == eb->alsa_id.index) { + found_matching_element = true; + is_subset = element_is_subset(ea, eb, m); + break; + } + } + + if (!found_matching_element) + is_subset = false; + } + + if (is_subset) { + pa_log_debug("Removing path '%s' as it is a subset of '%s'.", p->name, p2->name); + pa_hashmap_remove(ps->paths, p); + break; + } + } + } +} + +static pa_alsa_path* path_set_find_path_by_description(pa_alsa_path_set *ps, const char* description, pa_alsa_path *ignore) { + pa_alsa_path* p; + void *state; + + PA_HASHMAP_FOREACH(p, ps->paths, state) + if (p != ignore && pa_streq(p->description, description)) + return p; + + return NULL; +} + +static void path_set_make_path_descriptions_unique(pa_alsa_path_set *ps) { + pa_alsa_path *p, *q; + void *state, *state2; + + PA_HASHMAP_FOREACH(p, ps->paths, state) { + unsigned i; + char *old_description; + + q = path_set_find_path_by_description(ps, p->description, p); + + if (!q) + continue; + + old_description = pa_xstrdup(p->description); + + /* OK, this description is not unique, hence let's rename */ + i = 1; + PA_HASHMAP_FOREACH(q, ps->paths, state2) { + char *new_description; + + if (!pa_streq(q->description, old_description)) + continue; + + new_description = pa_sprintf_malloc("%s %u", q->description, i); + pa_xfree(q->description); + q->description = new_description; + + i++; + } + + pa_xfree(old_description); + } +} + +void pa_alsa_mapping_free(pa_alsa_mapping *m) { + pa_assert(m); + + pa_xfree(m->name); + pa_xfree(m->description); + pa_xfree(m->description_key); + + pa_proplist_free(m->proplist); + + pa_xstrfreev(m->device_strings); + pa_xstrfreev(m->input_path_names); + pa_xstrfreev(m->output_path_names); + pa_xstrfreev(m->input_element); + pa_xstrfreev(m->output_element); + if (m->input_path_set) + pa_alsa_path_set_free(m->input_path_set); + if (m->output_path_set) + pa_alsa_path_set_free(m->output_path_set); + + pa_proplist_free(m->input_proplist); + pa_proplist_free(m->output_proplist); + + pa_assert(!m->input_pcm); + pa_assert(!m->output_pcm); + + pa_alsa_ucm_mapping_context_free(&m->ucm_context); + + pa_xfree(m); +} + +void pa_alsa_profile_free(pa_alsa_profile *p) { + pa_assert(p); + + pa_xfree(p->name); + pa_xfree(p->description); + pa_xfree(p->description_key); + pa_xfree(p->input_name); + pa_xfree(p->output_name); + + pa_xstrfreev(p->input_mapping_names); + pa_xstrfreev(p->output_mapping_names); + + if (p->input_mappings) + pa_idxset_free(p->input_mappings, NULL); + + if (p->output_mappings) + pa_idxset_free(p->output_mappings, NULL); + + pa_xfree(p); +} + +void pa_alsa_profile_set_free(pa_alsa_profile_set *ps) { + pa_assert(ps); + + if (ps->input_paths) + pa_hashmap_free(ps->input_paths); + + if (ps->output_paths) + pa_hashmap_free(ps->output_paths); + + if (ps->profiles) + pa_hashmap_free(ps->profiles); + + if (ps->mappings) + pa_hashmap_free(ps->mappings); + + if (ps->decibel_fixes) + pa_hashmap_free(ps->decibel_fixes); + + pa_xfree(ps); +} + +pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name) { + pa_alsa_mapping *m; + + if (!pa_startswith(name, "Mapping ")) + return NULL; + + name += 8; + + if ((m = pa_hashmap_get(ps->mappings, name))) + return m; + + m = pa_xnew0(pa_alsa_mapping, 1); + m->profile_set = ps; + m->exact_channels = true; + m->name = pa_xstrdup(name); + pa_sample_spec_init(&m->sample_spec); + pa_channel_map_init(&m->channel_map); + m->proplist = pa_proplist_new(); + m->hw_device_index = -1; + m->input_proplist = pa_proplist_new(); + m->output_proplist = pa_proplist_new(); + + pa_hashmap_put(ps->mappings, m->name, m); + + return m; +} + +static pa_alsa_profile *profile_get(pa_alsa_profile_set *ps, const char *name) { + pa_alsa_profile *p; + + if (!pa_startswith(name, "Profile ")) + return NULL; + + name += 8; + + if ((p = pa_hashmap_get(ps->profiles, name))) + return p; + + p = pa_xnew0(pa_alsa_profile, 1); + p->profile_set = ps; + p->name = pa_xstrdup(name); + + pa_hashmap_put(ps->profiles, p->name, p); + + return p; +} + +static pa_alsa_decibel_fix *decibel_fix_get(pa_alsa_profile_set *ps, const char *alsa_id) { + pa_alsa_decibel_fix *db_fix; + char *name; + int index; + + if (!pa_startswith(alsa_id, "DecibelFix ")) + return NULL; + + alsa_id += 11; + + if ((db_fix = pa_hashmap_get(ps->decibel_fixes, alsa_id))) + return db_fix; + + name = alloca(strlen(alsa_id) + 1); + if (alsa_id_decode(alsa_id, name, &index)) + return NULL; + + db_fix = pa_xnew0(pa_alsa_decibel_fix, 1); + db_fix->profile_set = ps; + db_fix->name = pa_xstrdup(name); + db_fix->index = index; + db_fix->key = pa_xstrdup(alsa_id); + + pa_hashmap_put(ps->decibel_fixes, db_fix->key, db_fix); + + return db_fix; +} + +static int mapping_parse_device_strings(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + pa_xstrfreev(m->device_strings); + if (!(m->device_strings = pa_split_spaces_strv(state->rvalue))) { + pa_log("[%s:%u] Device string list empty of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int mapping_parse_channel_map(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if (!(pa_channel_map_parse(&m->channel_map, state->rvalue))) { + pa_log("[%s:%u] Channel map invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int mapping_parse_paths(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if (pa_streq(state->lvalue, "paths-input")) { + pa_xstrfreev(m->input_path_names); + m->input_path_names = pa_split_spaces_strv(state->rvalue); + } else { + pa_xstrfreev(m->output_path_names); + m->output_path_names = pa_split_spaces_strv(state->rvalue); + } + + return 0; +} + +static int mapping_parse_exact_channels(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + int b; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if ((b = pa_parse_boolean(state->rvalue)) < 0) { + pa_log("[%s:%u] %s has invalid value '%s'", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + m->exact_channels = b; + + return 0; +} + +static int mapping_parse_element(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if (pa_streq(state->lvalue, "element-input")) { + pa_xstrfreev(m->input_element); + m->input_element = pa_split_spaces_strv(state->rvalue); + } else { + pa_xstrfreev(m->output_element); + m->output_element = pa_split_spaces_strv(state->rvalue); + } + + return 0; +} + +static int mapping_parse_direction(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section); + return -1; + } + + if (pa_streq(state->rvalue, "input")) + m->direction = PA_ALSA_DIRECTION_INPUT; + else if (pa_streq(state->rvalue, "output")) + m->direction = PA_ALSA_DIRECTION_OUTPUT; + else if (pa_streq(state->rvalue, "any")) + m->direction = PA_ALSA_DIRECTION_ANY; + else { + pa_log("[%s:%u] Direction %s invalid.", state->filename, state->lineno, state->rvalue); + return -1; + } + + return 0; +} + +static int mapping_parse_description(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if ((m = pa_alsa_mapping_get(ps, state->section))) { + pa_xfree(m->description); + m->description = pa_xstrdup(_(state->rvalue)); + } else if ((p = profile_get(ps, state->section))) { + pa_xfree(p->description); + p->description = pa_xstrdup(_(state->rvalue)); + } else { + pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int mapping_parse_description_key(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if ((m = pa_alsa_mapping_get(ps, state->section))) { + pa_xfree(m->description_key); + m->description_key = pa_xstrdup(state->rvalue); + } else if ((p = profile_get(ps, state->section))) { + pa_xfree(p->description_key); + p->description_key = pa_xstrdup(state->rvalue); + } else { + pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + + +static int mapping_parse_priority(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + pa_alsa_mapping *m; + uint32_t prio; + + pa_assert(state); + + ps = state->userdata; + + if (pa_atou(state->rvalue, &prio) < 0) { + pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if ((m = pa_alsa_mapping_get(ps, state->section))) + m->priority = prio; + else if ((p = profile_get(ps, state->section))) + p->priority = prio; + else { + pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int mapping_parse_fallback(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + pa_alsa_mapping *m; + int k; + + pa_assert(state); + + ps = state->userdata; + + if ((k = pa_parse_boolean(state->rvalue)) < 0) { + pa_log("[%s:%u] Fallback invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + if ((m = pa_alsa_mapping_get(ps, state->section))) + m->fallback = k; + else if ((p = profile_get(ps, state->section))) + p->fallback_input = p->fallback_output = k; + else { + pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section); + return -1; + } + + return 0; +} + +static int mapping_parse_intended_roles(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_mapping *m; + + pa_assert(state); + + ps = state->userdata; + + if (!(m = pa_alsa_mapping_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + pa_proplist_sets(m->proplist, PA_PROP_DEVICE_INTENDED_ROLES, state->rvalue); + + return 0; +} + + +static int profile_parse_mappings(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + + pa_assert(state); + + ps = state->userdata; + + if (!(p = profile_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if (pa_streq(state->lvalue, "input-mappings")) { + pa_xstrfreev(p->input_mapping_names); + p->input_mapping_names = pa_split_spaces_strv(state->rvalue); + } else { + pa_xstrfreev(p->output_mapping_names); + p->output_mapping_names = pa_split_spaces_strv(state->rvalue); + } + + return 0; +} + +static int profile_parse_skip_probe(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + int b; + + pa_assert(state); + + ps = state->userdata; + + if (!(p = profile_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if ((b = pa_parse_boolean(state->rvalue)) < 0) { + pa_log("[%s:%u] Skip probe invalid of '%s'", state->filename, state->lineno, state->section); + return -1; + } + + p->supported = b; + + return 0; +} + +static int decibel_fix_parse_db_values(pa_config_parser_state *state) { + pa_alsa_profile_set *ps; + pa_alsa_decibel_fix *db_fix; + char **items; + char *item; + long *db_values; + unsigned n = 8; /* Current size of the db_values table. */ + unsigned min_step = 0; + unsigned max_step = 0; + unsigned i = 0; /* Index to the items table. */ + unsigned prev_step = 0; + double prev_db = 0; + + pa_assert(state); + + ps = state->userdata; + + if (!(db_fix = decibel_fix_get(ps, state->section))) { + pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section); + return -1; + } + + if (!(items = pa_split_spaces_strv(state->rvalue))) { + pa_log("[%s:%u] Value missing", state->filename, state->lineno); + return -1; + } + + db_values = pa_xnew(long, n); + + while ((item = items[i++])) { + char *s = item; /* Step value string. */ + char *d = item; /* dB value string. */ + uint32_t step; + double db; + + /* Move d forward until it points to a colon or to the end of the item. */ + for (; *d && *d != ':'; ++d); + + if (d == s) { + /* item started with colon. */ + pa_log("[%s:%u] No step value found in %s", state->filename, state->lineno, item); + goto fail; + } + + if (!*d || !*(d + 1)) { + /* No colon found, or it was the last character in item. */ + pa_log("[%s:%u] No dB value found in %s", state->filename, state->lineno, item); + goto fail; + } + + /* pa_atou() needs a null-terminating string. Let's replace the colon + * with a zero byte. */ + *d++ = '\0'; + + if (pa_atou(s, &step) < 0) { + pa_log("[%s:%u] Invalid step value: %s", state->filename, state->lineno, s); + goto fail; + } + + if (pa_atod(d, &db) < 0) { + pa_log("[%s:%u] Invalid dB value: %s", state->filename, state->lineno, d); + goto fail; + } + + if (step <= prev_step && i != 1) { + pa_log("[%s:%u] Step value %u not greater than the previous value %u", state->filename, state->lineno, step, prev_step); + goto fail; + } + + if (db < prev_db && i != 1) { + pa_log("[%s:%u] Decibel value %0.2f less than the previous value %0.2f", state->filename, state->lineno, db, prev_db); + goto fail; + } + + if (i == 1) { + min_step = step; + db_values[0] = (long) (db * 100.0); + prev_step = step; + prev_db = db; + } else { + /* Interpolate linearly. */ + double db_increment = (db - prev_db) / (step - prev_step); + + for (; prev_step < step; ++prev_step, prev_db += db_increment) { + + /* Reallocate the db_values table if it's about to overflow. */ + if (prev_step + 1 - min_step == n) { + n *= 2; + db_values = pa_xrenew(long, db_values, n); + } + + db_values[prev_step + 1 - min_step] = (long) ((prev_db + db_increment) * 100.0); + } + } + + max_step = step; + } + + db_fix->min_step = min_step; + db_fix->max_step = max_step; + pa_xfree(db_fix->db_values); + db_fix->db_values = db_values; + + pa_xstrfreev(items); + + return 0; + +fail: + pa_xstrfreev(items); + pa_xfree(db_values); + + return -1; +} + +/* the logic is simple: if we see the jack in multiple paths */ +/* assign all those paths to one availability_group */ +static void profile_set_set_availability_groups(pa_alsa_profile_set *ps) { + pa_dynarray *paths; + pa_alsa_path *p; + void *state; + unsigned idx1; + uint32_t num = 1; + + /* Merge ps->input_paths and ps->output_paths into one dynarray. */ + paths = pa_dynarray_new(NULL); + PA_HASHMAP_FOREACH(p, ps->input_paths, state) + pa_dynarray_append(paths, p); + PA_HASHMAP_FOREACH(p, ps->output_paths, state) + pa_dynarray_append(paths, p); + + PA_DYNARRAY_FOREACH(p, paths, idx1) { + pa_alsa_jack *j; + const char *found = NULL; + bool has_control = false; + + PA_LLIST_FOREACH(j, p->jacks) { + pa_alsa_path *p2; + unsigned idx2; + + if (!j->has_control || j->state_plugged == PA_AVAILABLE_NO) + continue; + has_control = true; + PA_DYNARRAY_FOREACH(p2, paths, idx2) { + pa_alsa_jack *j2; + + if (p2 == p) + break; + PA_LLIST_FOREACH(j2, p2->jacks) { + if (!j2->has_control || j2->state_plugged == PA_AVAILABLE_NO) + continue; + if (pa_streq(j->alsa_id.name, j2->alsa_id.name) && + j->alsa_id.index == j2->alsa_id.index) { + j->state_plugged = PA_AVAILABLE_UNKNOWN; + j2->state_plugged = PA_AVAILABLE_UNKNOWN; + found = p2->availability_group; + break; + } + } + } + if (found) + break; + } + if (!has_control) + continue; + if (!found) { + p->availability_group = pa_sprintf_malloc("Legacy %d", num); + } else { + p->availability_group = pa_xstrdup(found); + } + if (!found) + num++; + } + + pa_dynarray_free(paths); +} + +static void mapping_paths_probe(pa_alsa_mapping *m, pa_alsa_profile *profile, + pa_alsa_direction_t direction, pa_hashmap *used_paths, + pa_hashmap *mixers) { + + pa_alsa_path *p; + void *state; + snd_pcm_t *pcm_handle; + pa_alsa_path_set *ps; + snd_mixer_t *mixer_handle; + + if (direction == PA_ALSA_DIRECTION_OUTPUT) { + if (m->output_path_set) + return; /* Already probed */ + m->output_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */ + pcm_handle = m->output_pcm; + } else { + if (m->input_path_set) + return; /* Already probed */ + m->input_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */ + pcm_handle = m->input_pcm; + } + + if (!ps) + return; /* No paths */ + + pa_assert(pcm_handle); + + mixer_handle = pa_alsa_open_mixer_for_pcm(mixers, pcm_handle, true); + if (!mixer_handle) { + /* Cannot open mixer, remove all entries */ + pa_hashmap_remove_all(ps->paths); + return; + } + + PA_HASHMAP_FOREACH(p, ps->paths, state) { + if (p->autodetect_eld_device) + p->eld_device = m->hw_device_index; + + if (pa_alsa_path_probe(p, m, mixer_handle, m->profile_set->ignore_dB) < 0) + pa_hashmap_remove(ps->paths, p); + } + + path_set_condense(ps, mixer_handle); + path_set_make_path_descriptions_unique(ps); + + PA_HASHMAP_FOREACH(p, ps->paths, state) + pa_hashmap_put(used_paths, p, p); + + pa_log_debug("Available mixer paths (after tidying):"); + pa_alsa_path_set_dump(ps); +} + +static int mapping_verify(pa_alsa_mapping *m, const pa_channel_map *bonus) { + + static const struct description_map well_known_descriptions[] = { + { "analog-mono", N_("Analog Mono") }, + { "analog-mono-left", N_("Analog Mono (Left)") }, + { "analog-mono-right", N_("Analog Mono (Right)") }, + { "analog-stereo", N_("Analog Stereo") }, + { "mono-fallback", N_("Mono") }, + { "stereo-fallback", N_("Stereo") }, + /* Note: Not translated to "Analog Stereo Input", because the source + * name gets "Input" appended to it automatically, so adding "Input" + * here would lead to the source name to become "Analog Stereo Input + * Input". The same logic applies to analog-stereo-output, + * multichannel-input and multichannel-output. */ + { "analog-stereo-input", N_("Analog Stereo") }, + { "analog-stereo-output", N_("Analog Stereo") }, + { "analog-stereo-headset", N_("Headset") }, + { "analog-stereo-speakerphone", N_("Speakerphone") }, + { "multichannel-input", N_("Multichannel") }, + { "multichannel-output", N_("Multichannel") }, + { "analog-surround-21", N_("Analog Surround 2.1") }, + { "analog-surround-30", N_("Analog Surround 3.0") }, + { "analog-surround-31", N_("Analog Surround 3.1") }, + { "analog-surround-40", N_("Analog Surround 4.0") }, + { "analog-surround-41", N_("Analog Surround 4.1") }, + { "analog-surround-50", N_("Analog Surround 5.0") }, + { "analog-surround-51", N_("Analog Surround 5.1") }, + { "analog-surround-61", N_("Analog Surround 6.0") }, + { "analog-surround-61", N_("Analog Surround 6.1") }, + { "analog-surround-70", N_("Analog Surround 7.0") }, + { "analog-surround-71", N_("Analog Surround 7.1") }, + { "iec958-stereo", N_("Digital Stereo (IEC958)") }, + { "iec958-ac3-surround-40", N_("Digital Surround 4.0 (IEC958/AC3)") }, + { "iec958-ac3-surround-51", N_("Digital Surround 5.1 (IEC958/AC3)") }, + { "iec958-dts-surround-51", N_("Digital Surround 5.1 (IEC958/DTS)") }, + { "hdmi-stereo", N_("Digital Stereo (HDMI)") }, + { "hdmi-surround-51", N_("Digital Surround 5.1 (HDMI)") }, + { "gaming-headset-chat", N_("Chat") }, + { "gaming-headset-game", N_("Game") }, + }; + const char *description_key = m->description_key ? m->description_key : m->name; + + pa_assert(m); + + if (!pa_channel_map_valid(&m->channel_map)) { + pa_log("Mapping %s is missing channel map.", m->name); + return -1; + } + + if (!m->device_strings) { + pa_log("Mapping %s is missing device strings.", m->name); + return -1; + } + + if ((m->input_path_names && m->input_element) || + (m->output_path_names && m->output_element)) { + pa_log("Mapping %s must have either mixer path or mixer element, not both.", m->name); + return -1; + } + + if (!m->description) + m->description = pa_xstrdup(lookup_description(description_key, + well_known_descriptions, + PA_ELEMENTSOF(well_known_descriptions))); + + if (!m->description) + m->description = pa_xstrdup(m->name); + + if (bonus) { + if (pa_channel_map_equal(&m->channel_map, bonus)) + m->priority += 50; + else if (m->channel_map.channels == bonus->channels) + m->priority += 30; + } + + return 0; +} + +void pa_alsa_mapping_dump(pa_alsa_mapping *m) { + char cm[PA_CHANNEL_MAP_SNPRINT_MAX]; + + pa_assert(m); + + pa_log_debug("Mapping %s (%s), priority=%u, channel_map=%s, supported=%s, direction=%i", + m->name, + pa_strnull(m->description), + m->priority, + pa_channel_map_snprint(cm, sizeof(cm), &m->channel_map), + pa_yes_no(m->supported), + m->direction); +} + +static void profile_set_add_auto_pair( + pa_alsa_profile_set *ps, + pa_alsa_mapping *m, /* output */ + pa_alsa_mapping *n /* input */) { + + char *name; + pa_alsa_profile *p; + + pa_assert(ps); + pa_assert(m || n); + + if (m && m->direction == PA_ALSA_DIRECTION_INPUT) + return; + + if (n && n->direction == PA_ALSA_DIRECTION_OUTPUT) + return; + + if (m && n) + name = pa_sprintf_malloc("output:%s+input:%s", m->name, n->name); + else if (m) + name = pa_sprintf_malloc("output:%s", m->name); + else + name = pa_sprintf_malloc("input:%s", n->name); + + if (pa_hashmap_get(ps->profiles, name)) { + pa_xfree(name); + return; + } + + p = pa_xnew0(pa_alsa_profile, 1); + p->profile_set = ps; + p->name = name; + + if (m) { + p->output_name = pa_xstrdup(m->name); + p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + pa_idxset_put(p->output_mappings, m, NULL); + p->priority += m->priority * 100; + p->fallback_output = m->fallback; + } + + if (n) { + p->input_name = pa_xstrdup(n->name); + p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + pa_idxset_put(p->input_mappings, n, NULL); + p->priority += n->priority; + p->fallback_input = n->fallback; + } + + pa_hashmap_put(ps->profiles, p->name, p); +} + +static void profile_set_add_auto(pa_alsa_profile_set *ps) { + pa_alsa_mapping *m, *n; + void *m_state, *n_state; + + pa_assert(ps); + + /* The order is important here: + 1) try single inputs and outputs before trying their + combination, because if the half-duplex test failed, we don't have + to try full duplex. + 2) try the output right before the input combinations with + that output, because then the output_pcm is not closed between tests. + */ + PA_HASHMAP_FOREACH(n, ps->mappings, n_state) + profile_set_add_auto_pair(ps, NULL, n); + + PA_HASHMAP_FOREACH(m, ps->mappings, m_state) { + profile_set_add_auto_pair(ps, m, NULL); + + PA_HASHMAP_FOREACH(n, ps->mappings, n_state) + profile_set_add_auto_pair(ps, m, n); + } + +} + +static int profile_verify(pa_alsa_profile *p) { + + static const struct description_map well_known_descriptions[] = { + { "output:analog-mono+input:analog-mono", N_("Analog Mono Duplex") }, + { "output:analog-stereo+input:analog-stereo", N_("Analog Stereo Duplex") }, + { "output:analog-stereo-headset+input:analog-stereo-headset", N_("Headset") }, + { "output:analog-stereo-speakerphone+input:analog-stereo-speakerphone", N_("Speakerphone") }, + { "output:iec958-stereo+input:iec958-stereo", N_("Digital Stereo Duplex (IEC958)") }, + { "output:multichannel-output+input:multichannel-input", N_("Multichannel Duplex") }, + { "output:unknown-stereo+input:unknown-stereo", N_("Stereo Duplex") }, + { "output:analog-output-surround71+output:analog-output-chat+input:analog-input", N_("Mono Chat + 7.1 Surround") }, + { "off", N_("Off") } + }; + const char *description_key = p->description_key ? p->description_key : p->name; + + pa_assert(p); + + /* Replace the output mapping names by the actual mappings */ + if (p->output_mapping_names) { + char **name; + + pa_assert(!p->output_mappings); + p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + for (name = p->output_mapping_names; *name; name++) { + pa_alsa_mapping *m; + char **in; + bool duplicate = false; + + for (in = name + 1; *in; in++) + if (pa_streq(*name, *in)) { + duplicate = true; + break; + } + + if (duplicate) + continue; + + if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_INPUT) { + pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name); + return -1; + } + + pa_idxset_put(p->output_mappings, m, NULL); + + if (p->supported) + m->supported++; + } + + pa_xstrfreev(p->output_mapping_names); + p->output_mapping_names = NULL; + } + + /* Replace the input mapping names by the actual mappings */ + if (p->input_mapping_names) { + char **name; + + pa_assert(!p->input_mappings); + p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + for (name = p->input_mapping_names; *name; name++) { + pa_alsa_mapping *m; + char **in; + bool duplicate = false; + + for (in = name + 1; *in; in++) + if (pa_streq(*name, *in)) { + duplicate = true; + break; + } + + if (duplicate) + continue; + + if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_OUTPUT) { + pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name); + return -1; + } + + pa_idxset_put(p->input_mappings, m, NULL); + + if (p->supported) + m->supported++; + } + + pa_xstrfreev(p->input_mapping_names); + p->input_mapping_names = NULL; + } + + if (!p->input_mappings && !p->output_mappings) { + pa_log("Profile '%s' lacks mappings.", p->name); + return -1; + } + + if (!p->description) + p->description = pa_xstrdup(lookup_description(description_key, + well_known_descriptions, + PA_ELEMENTSOF(well_known_descriptions))); + + if (!p->description) { + uint32_t idx; + pa_alsa_mapping *m; + char *ptr; + size_t size; + FILE *f; + int count = 0; + + f = open_memstream(&ptr, &size); + if (f == NULL) { + pa_log("failed to open memstream: %m"); + return -1; + } + + if (p->output_mappings) + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (count++ > 0) + fprintf(f, " + "); + fprintf(f, _("%s Output"), m->description); + } + + if (p->input_mappings) + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (count++ > 0) + fprintf(f, " + "); + fprintf(f, _("%s Input"), m->description); + } + + fclose(f); + p->description = ptr; + } + + return 0; +} + +void pa_alsa_profile_dump(pa_alsa_profile *p) { + uint32_t idx; + pa_alsa_mapping *m; + pa_assert(p); + + pa_log_debug("Profile %s (%s), input=%s, output=%s priority=%u, supported=%s n_input_mappings=%u, n_output_mappings=%u", + p->name, + pa_strnull(p->description), + pa_strnull(p->input_name), + pa_strnull(p->output_name), + p->priority, + pa_yes_no(p->supported), + p->input_mappings ? pa_idxset_size(p->input_mappings) : 0, + p->output_mappings ? pa_idxset_size(p->output_mappings) : 0); + + if (p->input_mappings) + PA_IDXSET_FOREACH(m, p->input_mappings, idx) + pa_log_debug("Input %s", m->name); + + if (p->output_mappings) + PA_IDXSET_FOREACH(m, p->output_mappings, idx) + pa_log_debug("Output %s", m->name); +} + +static int decibel_fix_verify(pa_alsa_decibel_fix *db_fix) { + pa_assert(db_fix); + + /* Check that the dB mapping has been configured. Since "db-values" is + * currently the only option in the DecibelFix section, and decibel fix + * objects don't get created if a DecibelFix section is empty, this is + * actually a redundant check. Having this may prevent future bugs, + * however. */ + if (!db_fix->db_values) { + pa_log("Decibel fix for element %s lacks the dB values.", db_fix->name); + return -1; + } + + return 0; +} + +void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix) { + char *db_values = NULL; + + pa_assert(db_fix); + + if (db_fix->db_values) { + unsigned long i, nsteps; + FILE *f; + char *ptr; + size_t size; + + f = open_memstream(&ptr, &size); + if (f == NULL) + return; + + pa_assert(db_fix->min_step <= db_fix->max_step); + nsteps = db_fix->max_step - db_fix->min_step + 1; + + for (i = 0; i < nsteps; ++i) + fprintf(f, "[%li]:%0.2f ", i + db_fix->min_step, db_fix->db_values[i] / 100.0); + + fclose(f); + db_values = ptr; + } + + pa_log_debug("Decibel fix %s, min_step=%li, max_step=%li, db_values=%s", + db_fix->name, db_fix->min_step, db_fix->max_step, pa_strnull(db_values)); + + pa_xfree(db_values); +} + +static const char *get_default_profile_dir(void) { + const char *str; +#ifdef HAVE_RUNNING_FROM_BUILD_TREE + if (pa_run_from_build_tree()) + return PA_SRCDIR "mixer/profile-sets"; + else +#endif + if (getenv("ACP_BUILDDIR") != NULL) + return "mixer/profile-sets"; + if ((str = getenv("ACP_PROFILES_DIR")) != NULL) + return str; + return PA_ALSA_PROFILE_SETS_DIR; +} + +pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) { + pa_alsa_profile_set *ps; + pa_alsa_profile *p; + pa_alsa_mapping *m; + pa_alsa_decibel_fix *db_fix; + char *fn; + int r; + void *state; + + static pa_config_item items[] = { + /* [General] */ + { "auto-profiles", pa_config_parse_bool, NULL, "General" }, + + /* [Mapping ...] */ + { "device-strings", mapping_parse_device_strings, NULL, NULL }, + { "channel-map", mapping_parse_channel_map, NULL, NULL }, + { "paths-input", mapping_parse_paths, NULL, NULL }, + { "paths-output", mapping_parse_paths, NULL, NULL }, + { "element-input", mapping_parse_element, NULL, NULL }, + { "element-output", mapping_parse_element, NULL, NULL }, + { "direction", mapping_parse_direction, NULL, NULL }, + { "exact-channels", mapping_parse_exact_channels, NULL, NULL }, + { "intended-roles", mapping_parse_intended_roles, NULL, NULL }, + + /* Shared by [Mapping ...] and [Profile ...] */ + { "description", mapping_parse_description, NULL, NULL }, + { "description-key", mapping_parse_description_key,NULL, NULL }, + { "priority", mapping_parse_priority, NULL, NULL }, + { "fallback", mapping_parse_fallback, NULL, NULL }, + + /* [Profile ...] */ + { "input-mappings", profile_parse_mappings, NULL, NULL }, + { "output-mappings", profile_parse_mappings, NULL, NULL }, + { "skip-probe", profile_parse_skip_probe, NULL, NULL }, + + /* [DecibelFix ...] */ + { "db-values", decibel_fix_parse_db_values, NULL, NULL }, + { NULL, NULL, NULL, NULL } + }; + + ps = pa_xnew0(pa_alsa_profile_set, 1); + ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_mapping_free); + ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_profile_free); + ps->decibel_fixes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) decibel_fix_free); + ps->input_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free); + ps->output_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free); + + items[0].data = &ps->auto_profiles; + + fn = pa_maybe_prefix_path(fname ? fname : "default.conf", + get_default_profile_dir()); + if ((r = access(fn, R_OK)) != 0) { + if (fname != NULL) { + pa_log_warn("profile-set '%s' can't be accessed: %m", fn); + fn = pa_maybe_prefix_path("default.conf", + get_default_profile_dir()); + r = access(fn, R_OK); + } + if (r != 0) { + pa_log_warn("profile-set '%s' can't be accessed: %m", fn); + } + } + r = pa_config_parse(fn, NULL, items, NULL, false, ps); + pa_xfree(fn); + + if (r < 0) + goto fail; + + PA_HASHMAP_FOREACH(m, ps->mappings, state) + if (mapping_verify(m, bonus) < 0) + goto fail; + + if (ps->auto_profiles) + profile_set_add_auto(ps); + + PA_HASHMAP_FOREACH(p, ps->profiles, state) + if (profile_verify(p) < 0) + goto fail; + + PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state) + if (decibel_fix_verify(db_fix) < 0) + goto fail; + + return ps; + +fail: + pa_alsa_profile_set_free(ps); + return NULL; +} + +static void profile_finalize_probing(pa_alsa_profile *to_be_finalized, pa_alsa_profile *next) { + pa_alsa_mapping *m; + uint32_t idx; + + if (!to_be_finalized) + return; + + if (to_be_finalized->output_mappings) + PA_IDXSET_FOREACH(m, to_be_finalized->output_mappings, idx) { + + if (!m->output_pcm) + continue; + + if (to_be_finalized->supported) + m->supported++; + + /* If this mapping is also in the next profile, we won't close the + * pcm handle here, because it would get immediately reopened + * anyway. */ + if (next && next->output_mappings && pa_idxset_get_by_data(next->output_mappings, m, NULL)) + continue; + + pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); + pa_alsa_close(&m->output_pcm); + } + + if (to_be_finalized->input_mappings) + PA_IDXSET_FOREACH(m, to_be_finalized->input_mappings, idx) { + + if (!m->input_pcm) + continue; + + if (to_be_finalized->supported) + m->supported++; + + /* If this mapping is also in the next profile, we won't close the + * pcm handle here, because it would get immediately reopened + * anyway. */ + if (next && next->input_mappings && pa_idxset_get_by_data(next->input_mappings, m, NULL)) + continue; + + pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); + pa_alsa_close(&m->input_pcm); + } +} + +static snd_pcm_t* mapping_open_pcm(pa_alsa_mapping *m, + const pa_sample_spec *ss, + const char *dev_id, + bool exact_channels, + int mode, + unsigned default_n_fragments, + unsigned default_fragment_size_msec) { + + snd_pcm_t* handle; + pa_sample_spec try_ss = *ss; + pa_channel_map try_map = m->channel_map; + snd_pcm_uframes_t try_period_size, try_buffer_size; + + try_ss.channels = try_map.channels; + + try_period_size = + pa_usec_to_bytes(default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) / + pa_frame_size(&try_ss); + try_buffer_size = default_n_fragments * try_period_size; + + handle = pa_alsa_open_by_template( + m->device_strings, dev_id, NULL, &try_ss, + &try_map, mode, &try_period_size, + &try_buffer_size, 0, NULL, NULL, exact_channels); + if (handle && !exact_channels && m->channel_map.channels != try_map.channels) { + char buf[PA_CHANNEL_MAP_SNPRINT_MAX]; + pa_log_debug("Channel map for mapping '%s' permanently changed to '%s'", m->name, + pa_channel_map_snprint(buf, sizeof(buf), &try_map)); + m->channel_map = try_map; + } + return handle; +} + +static void paths_drop_unused(pa_hashmap* h, pa_hashmap *keep) { + + void* state = NULL; + const void* key; + pa_alsa_path* p; + + pa_assert(h); + pa_assert(keep); + + p = pa_hashmap_iterate(h, &state, &key); + while (p) { + if (pa_hashmap_get(keep, p) == NULL) + pa_hashmap_remove_and_free(h, key); + p = pa_hashmap_iterate(h, &state, &key); + } +} + +static int add_profiles_to_probe( + pa_alsa_profile **list, + pa_hashmap *profiles, + bool fallback_output, + bool fallback_input) { + + int i = 0; + void *state; + pa_alsa_profile *p; + PA_HASHMAP_FOREACH(p, profiles, state) + if (p->fallback_input == fallback_input && p->fallback_output == fallback_output) { + *list = p; + list++; + i++; + } + return i; +} + +static void mapping_query_hw_device(pa_alsa_mapping *mapping, snd_pcm_t *pcm) { + int r; + snd_pcm_info_t* pcm_info; + snd_pcm_info_alloca(&pcm_info); + + r = snd_pcm_info(pcm, pcm_info); + if (r < 0) { + pa_log("Mapping %s: snd_pcm_info() failed %s: ", mapping->name, pa_alsa_strerror(r)); + return; + } + + /* XXX: It's not clear what snd_pcm_info_get_device() does if the device is + * not backed by a hw device or if it's backed by multiple hw devices. We + * only use hw_device_index for HDMI devices, however, and for those the + * return value is expected to be always valid, so this shouldn't be a + * significant problem. */ + mapping->hw_device_index = snd_pcm_info_get_device(pcm_info); +} + +void pa_alsa_profile_set_probe( + pa_alsa_profile_set *ps, + pa_hashmap *mixers, + const char *dev_id, + const pa_sample_spec *ss, + unsigned default_n_fragments, + unsigned default_fragment_size_msec) { + + bool found_output = false, found_input = false; + + pa_alsa_profile *p, *last = NULL; + pa_alsa_profile **pp, **probe_order; + pa_alsa_mapping *m; + pa_hashmap *broken_inputs, *broken_outputs, *used_paths; + pa_alsa_mapping *selected_fallback_input = NULL, *selected_fallback_output = NULL; + + pa_assert(ps); + pa_assert(dev_id); + pa_assert(ss); + + if (ps->probed) + return; + + broken_inputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + broken_outputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + used_paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + pp = probe_order = pa_xnew0(pa_alsa_profile *, pa_hashmap_size(ps->profiles) + 1); + + pp += add_profiles_to_probe(pp, ps->profiles, false, false); + pp += add_profiles_to_probe(pp, ps->profiles, false, true); + pp += add_profiles_to_probe(pp, ps->profiles, true, false); + pp += add_profiles_to_probe(pp, ps->profiles, true, true); + + for (pp = probe_order; *pp; pp++) { + uint32_t idx; + p = *pp; + + /* Skip if fallback and already found something, but still probe already selected fallbacks. + * If UCM is used then both fallback_input and fallback_output flags are false. + * If UCM is not used then there will be only a single entry in mappings. + */ + if (found_input && p->fallback_input) + if (selected_fallback_input == NULL || pa_idxset_get_by_index(p->input_mappings, 0) != selected_fallback_input) + continue; + if (found_output && p->fallback_output) + if (selected_fallback_output == NULL || pa_idxset_get_by_index(p->output_mappings, 0) != selected_fallback_output) + continue; + + /* Skip if this is already marked that it is supported (i.e. from the config file) */ + if (!p->supported) { + + profile_finalize_probing(last, p); + p->supported = true; + + if (p->output_mappings) { + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (pa_hashmap_get(broken_outputs, m) == m) { + pa_log_debug("Skipping profile %s - will not be able to open output:%s", p->name, m->name); + p->supported = false; + break; + } + } + } + + if (p->input_mappings && p->supported) { + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (pa_hashmap_get(broken_inputs, m) == m) { + pa_log_debug("Skipping profile %s - will not be able to open input:%s", p->name, m->name); + p->supported = false; + break; + } + } + } + + if (p->supported) + pa_log_debug("Looking at profile %s", p->name); + + /* Check if we can open all new ones */ + if (p->output_mappings && p->supported) + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + + if (m->output_pcm) + continue; + + pa_log_debug("Checking for playback on %s (%s)", m->description, m->name); + if (!(m->output_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels, + SND_PCM_STREAM_PLAYBACK, + default_n_fragments, + default_fragment_size_msec))) { + p->supported = false; + if (pa_idxset_size(p->output_mappings) == 1 && + ((!p->input_mappings) || pa_idxset_size(p->input_mappings) == 0)) { + pa_log_debug("Caching failure to open output:%s", m->name); + pa_hashmap_put(broken_outputs, m, m); + } + break; + } + + if (m->hw_device_index < 0) + mapping_query_hw_device(m, m->output_pcm); + } + + if (p->input_mappings && p->supported) + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + + if (m->input_pcm) + continue; + + pa_log_debug("Checking for recording on %s (%s)", m->description, m->name); + if (!(m->input_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels, + SND_PCM_STREAM_CAPTURE, + default_n_fragments, + default_fragment_size_msec))) { + p->supported = false; + if (pa_idxset_size(p->input_mappings) == 1 && + ((!p->output_mappings) || pa_idxset_size(p->output_mappings) == 0)) { + pa_log_debug("Caching failure to open input:%s", m->name); + pa_hashmap_put(broken_inputs, m, m); + } + break; + } + + if (m->hw_device_index < 0) + mapping_query_hw_device(m, m->input_pcm); + } + + last = p; + + if (!p->supported) + continue; + } + + pa_log_debug("Profile %s supported.", p->name); + + if (p->output_mappings) + PA_IDXSET_FOREACH(m, p->output_mappings, idx) + if (m->output_pcm) { + found_output = true; + if (p->fallback_output && selected_fallback_output == NULL) { + selected_fallback_output = m; + } + mapping_paths_probe(m, p, PA_ALSA_DIRECTION_OUTPUT, used_paths, mixers); + } + + if (p->input_mappings) + PA_IDXSET_FOREACH(m, p->input_mappings, idx) + if (m->input_pcm) { + found_input = true; + if (p->fallback_input && selected_fallback_input == NULL) { + selected_fallback_input = m; + } + mapping_paths_probe(m, p, PA_ALSA_DIRECTION_INPUT, used_paths, mixers); + } + } + + /* Clean up */ + profile_finalize_probing(last, NULL); + + pa_alsa_profile_set_drop_unsupported(ps); + + paths_drop_unused(ps->input_paths, used_paths); + paths_drop_unused(ps->output_paths, used_paths); + pa_hashmap_free(broken_inputs); + pa_hashmap_free(broken_outputs); + pa_hashmap_free(used_paths); + pa_xfree(probe_order); + + profile_set_set_availability_groups(ps); + + ps->probed = true; +} + +void pa_alsa_profile_set_dump(pa_alsa_profile_set *ps) { + pa_alsa_profile *p; + pa_alsa_mapping *m; + pa_alsa_decibel_fix *db_fix; + void *state; + + pa_assert(ps); + + pa_log_debug("Profile set %p, auto_profiles=%s, probed=%s, n_mappings=%u, n_profiles=%u, n_decibel_fixes=%u", + (void*) + ps, + pa_yes_no(ps->auto_profiles), + pa_yes_no(ps->probed), + pa_hashmap_size(ps->mappings), + pa_hashmap_size(ps->profiles), + pa_hashmap_size(ps->decibel_fixes)); + + PA_HASHMAP_FOREACH(m, ps->mappings, state) + pa_alsa_mapping_dump(m); + + PA_HASHMAP_FOREACH(p, ps->profiles, state) + pa_alsa_profile_dump(p); + + PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state) + pa_alsa_decibel_fix_dump(db_fix); +} + +void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *ps) { + pa_alsa_profile *p; + pa_alsa_mapping *m; + void *state; + + PA_HASHMAP_FOREACH(p, ps->profiles, state) { + if (!p->supported) + pa_hashmap_remove_and_free(ps->profiles, p->name); + } + + PA_HASHMAP_FOREACH(m, ps->mappings, state) { + if (m->supported <= 0) + pa_hashmap_remove_and_free(ps->mappings, m->name); + } +} + +static pa_device_port* device_port_alsa_init(pa_hashmap *ports, /* card ports */ + const char* name, + const char* description, + pa_alsa_path *path, + pa_alsa_setting *setting, + pa_card_profile *cp, + pa_hashmap *extra, /* sink/source ports */ + pa_core *core) { + + pa_device_port *p; + + pa_assert(path); + + p = pa_hashmap_get(ports, name); + + if (!p) { + pa_alsa_port_data *data; + pa_device_port_new_data port_data; + + pa_device_port_new_data_init(&port_data); + pa_device_port_new_data_set_name(&port_data, name); + pa_device_port_new_data_set_description(&port_data, description); + pa_device_port_new_data_set_direction(&port_data, path->direction == PA_ALSA_DIRECTION_OUTPUT ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT); + pa_device_port_new_data_set_type(&port_data, path->device_port_type); + pa_device_port_new_data_set_availability_group(&port_data, path->availability_group); + + p = pa_device_port_new(core, &port_data, sizeof(pa_alsa_port_data)); + pa_device_port_new_data_done(&port_data); + pa_assert(p); + pa_hashmap_put(ports, p->name, p); + pa_proplist_update(p->proplist, PA_UPDATE_REPLACE, path->proplist); + + data = PA_DEVICE_PORT_DATA(p); + /* Ownership of the path and setting is not transferred to the port data, so we don't deal with freeing them */ + data->path = path; + data->setting = setting; + path->port = p; + } + + if (cp) + pa_hashmap_put(p->profiles, cp->name, cp); + + if (extra) { + pa_hashmap_put(extra, p->name, p); + } + + return p; +} + +void pa_alsa_path_set_add_ports( + pa_alsa_path_set *ps, + pa_card_profile *cp, + pa_hashmap *ports, /* card ports */ + pa_hashmap *extra, /* sink/source ports */ + pa_core *core) { + + pa_alsa_path *path; + void *state; + + pa_assert(ports); + + if (!ps) + return; + + PA_HASHMAP_FOREACH(path, ps->paths, state) { + if (!path->settings || !path->settings->next) { + /* If there is no or just one setting we only need a + * single entry */ + pa_device_port *port = device_port_alsa_init(ports, path->name, + path->description, path, path->settings, cp, extra, core); + port->priority = path->priority * 100; + + } else { + pa_alsa_setting *s; + PA_LLIST_FOREACH(s, path->settings) { + pa_device_port *port; + char *n, *d; + + n = pa_sprintf_malloc("%s;%s", path->name, s->name); + + if (s->description[0]) + d = pa_sprintf_malloc("%s / %s", path->description, s->description); + else + d = pa_xstrdup(path->description); + + port = device_port_alsa_init(ports, n, d, path, s, cp, extra, core); + port->priority = path->priority * 100 + s->priority; + + pa_xfree(n); + pa_xfree(d); + } + } + } +} + +void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card) { + pa_assert(ps); + + if (ps->paths && pa_hashmap_size(ps->paths) > 0) { + pa_assert(card); + pa_alsa_path_set_add_ports(ps, NULL, card->ports, ports, card->core); + } + + pa_log_debug("Added %u ports", pa_hashmap_size(ports)); +} diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h new file mode 100644 index 0000000..643f03d --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-mixer.h @@ -0,0 +1,459 @@ +#ifndef fooalsamixerhfoo +#define fooalsamixerhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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; + + char **device_strings; + + char **input_path_names; + char **output_path_names; + char **input_element; /* list of fallbacks */ + char **output_element; + pa_alsa_path_set *input_path_set; + pa_alsa_path_set *output_path_set; + + unsigned supported; + bool exact_channels:1; + bool fallback:1; + + /* The "y" in "hw:x,y". This is set to -1 before the device index has been + * queried, or if the query failed. */ + int hw_device_index; + + /* Temporarily used during probing */ + snd_pcm_t *input_pcm; + snd_pcm_t *output_pcm; + + pa_proplist *input_proplist; + pa_proplist *output_proplist; + + pa_alsa_device output; + pa_alsa_device input; + + /* ucm device context*/ + pa_alsa_ucm_mapping_context ucm_context; +}; + +struct pa_alsa_profile { + struct acp_card_profile profile; + + pa_alsa_profile_set *profile_set; + + char *name; + char *description; + char *description_key; + unsigned priority; + + char *input_name; + char *output_name; + + bool supported:1; + bool fallback_input:1; + bool fallback_output:1; + + char **input_mapping_names; + char **output_mapping_names; + + pa_idxset *input_mappings; + pa_idxset *output_mappings; + + struct { + pa_dynarray devices; + } out; +}; + +struct pa_alsa_decibel_fix { + char *key; + + pa_alsa_profile_set *profile_set; + + char *name; /* Alsa volume element name. */ + int index; /* Alsa volume element index. */ + long min_step; + long max_step; + + /* An array that maps alsa volume element steps to decibels. The steps can + * be used as indices to this array, after subtracting min_step from the + * real value. + * + * The values are actually stored as integers representing millibels, + * because that's the format the alsa API uses. */ + long *db_values; +}; + +struct pa_alsa_profile_set { + pa_hashmap *mappings; + pa_hashmap *profiles; + pa_hashmap *decibel_fixes; + pa_hashmap *input_paths; + pa_hashmap *output_paths; + + bool auto_profiles; + bool ignore_dB:1; + bool probed:1; +}; + +void pa_alsa_mapping_dump(pa_alsa_mapping *m); +void pa_alsa_profile_dump(pa_alsa_profile *p); +void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix); +pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name); +void pa_alsa_mapping_free (pa_alsa_mapping *m); +void pa_alsa_profile_free (pa_alsa_profile *p); + +pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus); +void pa_alsa_profile_set_probe(pa_alsa_profile_set *ps, pa_hashmap *mixers, const char *dev_id, const pa_sample_spec *ss, unsigned default_n_fragments, unsigned default_fragment_size_msec); +void pa_alsa_profile_set_free(pa_alsa_profile_set *s); +void pa_alsa_profile_set_dump(pa_alsa_profile_set *s); +void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *s); + +void pa_alsa_mixer_use_for_poll(pa_hashmap *mixers, snd_mixer_t *mixer_handle); + +#if 0 +pa_alsa_fdlist *pa_alsa_fdlist_new(void); +void pa_alsa_fdlist_free(pa_alsa_fdlist *fdl); +int pa_alsa_fdlist_set_handle(pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, snd_hctl_t *hctl_handle, pa_mainloop_api* m); + +/* Alternative for handling alsa mixer events in io-thread. */ + +pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void); +void pa_alsa_mixer_pdata_free(pa_alsa_mixer_pdata *pd); +int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp); +#endif + +/* Data structure for inclusion in pa_device_port for alsa + * sinks/sources. This contains nothing that needs to be freed + * individually */ +struct pa_alsa_port_data { + pa_alsa_path *path; + pa_alsa_setting *setting; + bool suspend_when_unavailable; +}; + +void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card); +void pa_alsa_path_set_add_ports(pa_alsa_path_set *ps, pa_alsa_profile *cp, pa_hashmap *ports, pa_hashmap *extra, pa_core *core); + +#endif diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c new file mode 100644 index 0000000..f66b771 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -0,0 +1,2420 @@ +/*** + This file is part of PulseAudio. + + Copyright 2011 Wolfson Microelectronics PLC + Author Margarita Olaya + 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 **devices, unsigned n_devices); +static void ucm_port_data_free(pa_device_port *port); +static void ucm_port_update_available(pa_alsa_ucm_port_data *port); + +static struct ucm_type types[] = { + {"None", PA_DEVICE_PORT_TYPE_UNKNOWN}, + {"Speaker", PA_DEVICE_PORT_TYPE_SPEAKER}, + {"Line", PA_DEVICE_PORT_TYPE_LINE}, + {"Mic", PA_DEVICE_PORT_TYPE_MIC}, + {"Headphones", PA_DEVICE_PORT_TYPE_HEADPHONES}, + {"Headset", PA_DEVICE_PORT_TYPE_HEADSET}, + {"Handset", PA_DEVICE_PORT_TYPE_HANDSET}, + {"Bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH}, + {"Earpiece", PA_DEVICE_PORT_TYPE_EARPIECE}, + {"SPDIF", PA_DEVICE_PORT_TYPE_SPDIF}, + {"HDMI", PA_DEVICE_PORT_TYPE_HDMI}, + {NULL, 0} +}; + +static struct ucm_items item[] = { + {"PlaybackPCM", PA_ALSA_PROP_UCM_SINK}, + {"CapturePCM", PA_ALSA_PROP_UCM_SOURCE}, + {"PlaybackCTL", PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE}, + {"PlaybackVolume", PA_ALSA_PROP_UCM_PLAYBACK_VOLUME}, + {"PlaybackSwitch", PA_ALSA_PROP_UCM_PLAYBACK_SWITCH}, + {"PlaybackMixer", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE}, + {"PlaybackMixerElem", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM}, + {"PlaybackMasterElem", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM}, + {"PlaybackMasterType", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE}, + {"PlaybackPriority", PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY}, + {"PlaybackRate", PA_ALSA_PROP_UCM_PLAYBACK_RATE}, + {"PlaybackChannels", PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS}, + {"CaptureCTL", PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE}, + {"CaptureVolume", PA_ALSA_PROP_UCM_CAPTURE_VOLUME}, + {"CaptureSwitch", PA_ALSA_PROP_UCM_CAPTURE_SWITCH}, + {"CaptureMixer", PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE}, + {"CaptureMixerElem", PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM}, + {"CaptureMasterElem", PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM}, + {"CaptureMasterType", PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE}, + {"CapturePriority", PA_ALSA_PROP_UCM_CAPTURE_PRIORITY}, + {"CaptureRate", PA_ALSA_PROP_UCM_CAPTURE_RATE}, + {"CaptureChannels", PA_ALSA_PROP_UCM_CAPTURE_CHANNELS}, + {"TQ", PA_ALSA_PROP_UCM_QOS}, + {"JackCTL", PA_ALSA_PROP_UCM_JACK_DEVICE}, + {"JackControl", PA_ALSA_PROP_UCM_JACK_CONTROL}, + {"JackHWMute", PA_ALSA_PROP_UCM_JACK_HW_MUTE}, + {NULL, NULL}, +}; + +/* UCM verb info - this should eventually be part of policy management */ +static struct ucm_info verb_info[] = { + {SND_USE_CASE_VERB_INACTIVE, 0}, + {SND_USE_CASE_VERB_HIFI, 8000}, + {SND_USE_CASE_VERB_HIFI_LOW_POWER, 7000}, + {SND_USE_CASE_VERB_VOICE, 6000}, + {SND_USE_CASE_VERB_VOICE_LOW_POWER, 5000}, + {SND_USE_CASE_VERB_VOICECALL, 4000}, + {SND_USE_CASE_VERB_IP_VOICECALL, 4000}, + {SND_USE_CASE_VERB_ANALOG_RADIO, 3000}, + {SND_USE_CASE_VERB_DIGITAL_RADIO, 3000}, + {NULL, 0} +}; + +/* UCM device info - should be overwritten by ucm property */ +static struct ucm_info dev_info[] = { + {SND_USE_CASE_DEV_SPEAKER, 100}, + {SND_USE_CASE_DEV_LINE, 100}, + {SND_USE_CASE_DEV_HEADPHONES, 100}, + {SND_USE_CASE_DEV_HEADSET, 300}, + {SND_USE_CASE_DEV_HANDSET, 200}, + {SND_USE_CASE_DEV_BLUETOOTH, 400}, + {SND_USE_CASE_DEV_EARPIECE, 100}, + {SND_USE_CASE_DEV_SPDIF, 100}, + {SND_USE_CASE_DEV_HDMI, 100}, + {SND_USE_CASE_DEV_NONE, 100}, + {NULL, 0} +}; + + +static char *ucm_verb_value( + snd_use_case_mgr_t *uc_mgr, + const char *verb_name, + const char *id) { + + const char *value; + char *_id = pa_sprintf_malloc("=%s//%s", id, verb_name); + int err = snd_use_case_get(uc_mgr, _id, &value); + pa_xfree(_id); + if (err < 0) + return NULL; + pa_log_debug("Got %s for verb %s: %s", id, verb_name, value); + /* Use the cast here to allow free() call without casting for callers. + * The snd_use_case_get() returns mallocated string. + * See the Note: in use-case.h for snd_use_case_get(). + */ + return (char *)value; +} + +static int ucm_device_exists(pa_idxset *idxset, pa_alsa_ucm_device *dev) { + pa_alsa_ucm_device *d; + uint32_t idx; + + PA_IDXSET_FOREACH(d, idxset, idx) + if (d == dev) + return 1; + + return 0; +} + +static void ucm_add_devices_to_idxset( + pa_idxset *idxset, + pa_alsa_ucm_device *me, + pa_alsa_ucm_device *devices, + const char **dev_names, + int n) { + + pa_alsa_ucm_device *d; + + PA_LLIST_FOREACH(d, devices) { + const char *name; + int i; + + if (d == me) + continue; + + name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + for (i = 0; i < n; i++) + if (pa_streq(dev_names[i], name)) + pa_idxset_put(idxset, d, NULL); + } +} + +/* Split a string into words. Like pa_split_spaces() but handle '' and "". */ +static char *ucm_split_devnames(const char *c, const char **state) { + const char *current = *state ? *state : c; + char h; + size_t l; + + if (!*current || *c == 0) + return NULL; + + current += strspn(current, "\n\r \t"); + h = *current; + if (h == '\'' || h =='"') { + c = ++current; + for (l = 0; *c && *c != h; l++) c++; + if (*c != h) + return NULL; + *state = c + 1; + } else { + l = strcspn(current, "\n\r \t"); + *state = current+l; + } + + return pa_xstrndup(current, l); +} + + +static void ucm_volume_free(pa_alsa_ucm_volume *vol) { + pa_assert(vol); + pa_xfree(vol->mixer_elem); + pa_xfree(vol->master_elem); + pa_xfree(vol->master_type); + pa_xfree(vol); +} + +/* Get the volume identifier */ +static char *ucm_get_mixer_id( + pa_alsa_ucm_device *device, + const char *mprop, + const char *cprop, + const char *cid) +{ +#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */ + snd_ctl_elem_id_t *ctl; + int err; +#endif + const char *value; + char *value2; + int index; + + /* mixer element as first, if it's found, return it without modifications */ + value = pa_proplist_gets(device->proplist, mprop); + if (value) + return pa_xstrdup(value); + /* fallback, get the control element identifier */ + /* and try to do some heuristic to determine the mixer element name */ + value = pa_proplist_gets(device->proplist, cprop); + if (value == NULL) + return NULL; +#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */ + /* The new parser may return also element index. */ + snd_ctl_elem_id_alloca(&ctl); + err = snd_use_case_parse_ctl_elem_id(ctl, cid, value); + if (err < 0) + return NULL; + value = snd_ctl_elem_id_get_name(ctl); + index = snd_ctl_elem_id_get_index(ctl); +#else +#warning "Upgrade to alsa-lib 1.2.1!" + index = 0; +#endif + if (!(value2 = pa_str_strip_suffix(value, " Playback Volume"))) + if (!(value2 = pa_str_strip_suffix(value, " Capture Volume"))) + if (!(value2 = pa_str_strip_suffix(value, " Volume"))) + value2 = pa_xstrdup(value); + if (index > 0) { + char *mix = pa_sprintf_malloc("'%s',%d", value2, index); + pa_xfree(value2); + return mix; + } + return value2; +} + +/* Get the volume identifier */ +static pa_alsa_ucm_volume *ucm_get_mixer_volume( + pa_alsa_ucm_device *device, + const char *mprop, + const char *cprop, + const char *cid, + const char *masterid, + const char *mastertype) +{ + pa_alsa_ucm_volume *vol; + char *mixer_elem; + + mixer_elem = ucm_get_mixer_id(device, mprop, cprop, cid); + if (mixer_elem == NULL) + return NULL; + vol = pa_xnew0(pa_alsa_ucm_volume, 1); + if (vol == NULL) { + pa_xfree(mixer_elem); + return NULL; + } + vol->mixer_elem = mixer_elem; + vol->master_elem = pa_xstrdup(pa_proplist_gets(device->proplist, masterid)); + vol->master_type = pa_xstrdup(pa_proplist_gets(device->proplist, mastertype)); + return vol; +} + +/* Get the ALSA mixer device for the UCM device */ +static const char *get_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) +{ + const char *dev_name; + + if (is_sink) { + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE); + if (!dev_name) + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE); + } else { + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE); + if (!dev_name) + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE); + } + return dev_name; +} + +/* Get the ALSA mixer device for the UCM jack */ +static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_DEVICE); + if (!dev_name) + return get_mixer_device(dev, is_sink); + return dev_name; +} + +/* Create a property list for this ucm device */ +static int ucm_get_device_property( + pa_alsa_ucm_device *device, + snd_use_case_mgr_t *uc_mgr, + pa_alsa_ucm_verb *verb, + const char *device_name) { + + const char *value; + const char **devices; + char *id, *s; + int i; + int err; + uint32_t ui; + int n_confdev, n_suppdev; + pa_alsa_ucm_volume *vol; + + /* determine the device type */ + device->type = PA_DEVICE_PORT_TYPE_UNKNOWN; + id = s = pa_xstrdup(device_name); + while (s && *s && isalpha(*s)) s++; + if (s) + *s = '\0'; + for (i = 0; types[i].prefix; i++) + if (pa_streq(id, types[i].prefix)) { + device->type = types[i].type; + break; + } + pa_xfree(id); + + /* set properties */ + for (i = 0; item[i].id; i++) { + id = pa_sprintf_malloc("%s/%s", item[i].id, device_name); + err = snd_use_case_get(uc_mgr, id, &value); + pa_xfree(id); + if (err < 0) + continue; + + pa_log_debug("Got %s for device %s: %s", item[i].id, device_name, value); + pa_proplist_sets(device->proplist, item[i].property, value); + free((void*)value); + } + + /* get direction and channels */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS); + if (value) { /* output */ + /* get channels */ + if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui)) + device->playback_channels = ui; + else + pa_log("UCM playback channels %s for device %s out of range", value, device_name); + + /* get pcm */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK); + if (!value) /* take pcm from verb playback default */ + pa_log("UCM playback device %s fetch pcm failed", device_name); + } + + if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK) && + device->playback_channels == 0) { + pa_log_info("UCM file does not specify 'PlaybackChannels' " + "for device %s, assuming stereo.", device_name); + device->playback_channels = 2; + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS); + if (value) { /* input */ + /* get channels */ + if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui)) + device->capture_channels = ui; + else + pa_log("UCM capture channels %s for device %s out of range", value, device_name); + + /* get pcm */ + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (!value) /* take pcm from verb capture default */ + pa_log("UCM capture device %s fetch pcm failed", device_name); + } + + if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE) && + device->capture_channels == 0) { + pa_log_info("UCM file does not specify 'CaptureChannels' " + "for device %s, assuming stereo.", device_name); + device->capture_channels = 2; + } + + /* get rate and priority of device */ + if (device->playback_channels) { /* sink device */ + /* get rate */ + if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_RATE))) { + if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) { + pa_log_debug("UCM playback device %s rate %d", device_name, ui); + device->playback_rate = ui; + } else + pa_log_debug("UCM playback device %s has bad rate %s", device_name, value); + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY); + if (value) { + /* get priority from ucm config */ + if (pa_atou(value, &ui) == 0) + device->playback_priority = ui; + else + pa_log_debug("UCM playback priority %s for device %s error", value, device_name); + } + + vol = ucm_get_mixer_volume(device, + PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM, + PA_ALSA_PROP_UCM_PLAYBACK_VOLUME, + "PlaybackVolume", + PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM, + PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE); + if (vol) + pa_hashmap_put(device->playback_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol); + } + + if (device->capture_channels) { /* source device */ + /* get rate */ + if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_RATE))) { + if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) { + pa_log_debug("UCM capture device %s rate %d", device_name, ui); + device->capture_rate = ui; + } else + pa_log_debug("UCM capture device %s has bad rate %s", device_name, value); + } + + value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_PRIORITY); + if (value) { + /* get priority from ucm config */ + if (pa_atou(value, &ui) == 0) + device->capture_priority = ui; + else + pa_log_debug("UCM capture priority %s for device %s error", value, device_name); + } + + vol = ucm_get_mixer_volume(device, + PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM, + PA_ALSA_PROP_UCM_CAPTURE_VOLUME, + "CaptureVolume", + PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM, + PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE); + if (vol) + pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol); + } + + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) { + /* get priority from static table */ + for (i = 0; dev_info[i].id; i++) { + if (strcasecmp(dev_info[i].id, device_name) == 0) { + PA_UCM_DEVICE_PRIORITY_SET(device, dev_info[i].priority); + break; + } + } + } + + if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) { + /* fall through to default priority */ + device->playback_priority = 100; + } + + if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) { + /* fall through to default priority */ + device->capture_priority = 100; + } + + id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", device_name); + n_confdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + if (n_confdev <= 0) + pa_log_debug("No %s for device %s", "_conflictingdevs", device_name); + else { + device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + ucm_add_devices_to_idxset(device->conflicting_devices, device, verb->devices, devices, n_confdev); + snd_use_case_free_list(devices, n_confdev); + } + + id = pa_sprintf_malloc("%s/%s", "_supporteddevs", device_name); + n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices); + pa_xfree(id); + + if (n_suppdev <= 0) + pa_log_debug("No %s for device %s", "_supporteddevs", device_name); + else { + device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + ucm_add_devices_to_idxset(device->supported_devices, device, verb->devices, devices, n_suppdev); + snd_use_case_free_list(devices, n_suppdev); + } + + return 0; +}; + +/* Create a property list for this ucm modifier */ +static int ucm_get_modifier_property(pa_alsa_ucm_modifier *modifier, snd_use_case_mgr_t *uc_mgr, const char *modifier_name) { + const char *value; + char *id; + int i; + + for (i = 0; item[i].id; i++) { + int err; + + id = pa_sprintf_malloc("=%s/%s", item[i].id, modifier_name); + err = snd_use_case_get(uc_mgr, id, &value); + pa_xfree(id); + if (err < 0) + continue; + + pa_log_debug("Got %s for modifier %s: %s", item[i].id, modifier_name, value); + pa_proplist_sets(modifier->proplist, item[i].property, value); + free((void*)value); + } + + id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", modifier_name); + modifier->n_confdev = snd_use_case_get_list(uc_mgr, id, &modifier->conflicting_devices); + pa_xfree(id); + if (modifier->n_confdev < 0) + pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name); + + id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name); + modifier->n_suppdev = snd_use_case_get_list(uc_mgr, id, &modifier->supported_devices); + pa_xfree(id); + if (modifier->n_suppdev < 0) + pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name); + + return 0; +}; + +/* Create a list of devices for this verb */ +static int ucm_get_devices(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) { + const char **dev_list; + int num_dev, i; + + num_dev = snd_use_case_get_list(uc_mgr, "_devices", &dev_list); + if (num_dev < 0) + return num_dev; + + for (i = 0; i < num_dev; i += 2) { + pa_alsa_ucm_device *d = pa_xnew0(pa_alsa_ucm_device, 1); + + d->proplist = pa_proplist_new(); + pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(dev_list[i])); + pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(dev_list[i + 1])); + d->ucm_ports = pa_dynarray_new(NULL); + d->hw_mute_jacks = pa_dynarray_new(NULL); + d->available = PA_AVAILABLE_UNKNOWN; + + d->playback_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, + (pa_free_cb_t) ucm_volume_free); + d->capture_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, + (pa_free_cb_t) ucm_volume_free); + + PA_LLIST_PREPEND(pa_alsa_ucm_device, verb->devices, d); + } + + snd_use_case_free_list(dev_list, num_dev); + + return 0; +}; + +static int ucm_get_modifiers(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) { + const char **mod_list; + int num_mod, i; + + num_mod = snd_use_case_get_list(uc_mgr, "_modifiers", &mod_list); + if (num_mod < 0) + return num_mod; + + for (i = 0; i < num_mod; i += 2) { + pa_alsa_ucm_modifier *m; + + if (!mod_list[i]) { + pa_log_warn("Got a modifier with a null name. Skipping."); + continue; + } + + m = pa_xnew0(pa_alsa_ucm_modifier, 1); + m->proplist = pa_proplist_new(); + + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_NAME, mod_list[i]); + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(mod_list[i + 1])); + + PA_LLIST_PREPEND(pa_alsa_ucm_modifier, verb->modifiers, m); + } + + snd_use_case_free_list(mod_list, num_mod); + + return 0; +}; + +static void add_role_to_device(pa_alsa_ucm_device *dev, const char *dev_name, const char *role_name, const char *role) { + const char *cur = pa_proplist_gets(dev->proplist, role_name); + + if (!cur) + pa_proplist_sets(dev->proplist, role_name, role); + else if (!pa_str_in_list_spaces(cur, role)) { /* does not exist */ + char *value = pa_sprintf_malloc("%s %s", cur, role); + + pa_proplist_sets(dev->proplist, role_name, value); + pa_xfree(value); + } + + pa_log_info("Add role %s to device %s(%s), result %s", role, dev_name, role_name, pa_proplist_gets(dev->proplist, + role_name)); +} + +static void add_media_role(const char *name, pa_alsa_ucm_device *list, const char *role_name, const char *role, bool is_sink) { + pa_alsa_ucm_device *d; + + PA_LLIST_FOREACH(d, list) { + const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + if (pa_streq(dev_name, name)) { + const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK); + const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (is_sink && sink) + add_role_to_device(d, dev_name, role_name, role); + else if (!is_sink && source) + add_role_to_device(d, dev_name, role_name, role); + break; + } + } +} + +static char *modifier_name_to_role(const char *mod_name, bool *is_sink) { + char *sub = NULL, *tmp; + + *is_sink = false; + + if (pa_startswith(mod_name, "Play")) { + *is_sink = true; + sub = pa_xstrdup(mod_name + 4); + } else if (pa_startswith(mod_name, "Capture")) + sub = pa_xstrdup(mod_name + 7); + + if (!sub || !*sub) { + pa_xfree(sub); + pa_log_warn("Can't match media roles for modifier %s", mod_name); + return NULL; + } + + tmp = sub; + + do { + *tmp = tolower(*tmp); + } while (*(++tmp)); + + return sub; +} + +static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, pa_alsa_ucm_device *list, const char *mod_name) { + int i; + bool is_sink = false; + char *sub = NULL; + const char *role_name; + + sub = modifier_name_to_role(mod_name, &is_sink); + if (!sub) + return; + + modifier->action_direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + modifier->media_role = sub; + + role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES; + for (i = 0; i < modifier->n_suppdev; i++) { + /* if modifier has no specific pcm, we add role intent to its supported devices */ + if (!pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SINK) && + !pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SOURCE)) + add_media_role(modifier->supported_devices[i], list, role_name, sub, is_sink); + } +} + +static void append_lost_relationship(pa_alsa_ucm_device *dev) { + uint32_t idx; + pa_alsa_ucm_device *d; + + if (dev->conflicting_devices) { + PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) { + if (!d->conflicting_devices) + d->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (pa_idxset_put(d->conflicting_devices, dev, NULL) == 0) + pa_log_warn("Add lost conflicting device %s to %s", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), + pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME)); + } + } + + if (dev->supported_devices) { + PA_IDXSET_FOREACH(d, dev->supported_devices, idx) { + if (!d->supported_devices) + d->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + if (pa_idxset_put(d->supported_devices, dev, NULL) == 0) + pa_log_warn("Add lost supported device %s to %s", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), + pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME)); + } + } +} + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) { + char *card_name; + const char **verb_list, *value; + int num_verbs, i, err = 0; + + /* support multiple card instances, address card directly by index */ + card_name = pa_sprintf_malloc("hw:%i", card_index); + if (card_name == NULL) + return -PA_ALSA_ERR_UNSPECIFIED; + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + /* fallback longname: is UCM available for this card ? */ + pa_xfree(card_name); + err = snd_card_get_name(card_index, &card_name); + if (err < 0) { + pa_log("Card can't get card_name from card_index %d", card_index); + err = -PA_ALSA_ERR_UNSPECIFIED; + goto name_fail; + } + + err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name); + if (err < 0) { + pa_log_info("UCM not available for card %s", card_name); + err = -PA_ALSA_ERR_UCM_OPEN; + goto ucm_mgr_fail; + } + } + + err = snd_use_case_get(ucm->ucm_mgr, "=Linked", &value); + if (err >= 0) { + if (strcasecmp(value, "true") == 0 || strcasecmp(value, "1") == 0) { + free((void *)value); + pa_log_info("Empty (linked) UCM for card %s", card_name); + err = -PA_ALSA_ERR_UCM_LINKED; + goto ucm_verb_fail; + } + free((void *)value); + } + + pa_log_info("UCM available for card %s", card_name); + + if (snd_use_case_get(ucm->ucm_mgr, "_alibpref", &value) == 0) { + if (value[0]) { + ucm->alib_prefix = pa_xstrdup(value); + pa_log_debug("UCM _alibpref=%s", ucm->alib_prefix); + } + free((void *)value); + } + + /* get a list of all UCM verbs (profiles) for this card */ + num_verbs = snd_use_case_verb_list(ucm->ucm_mgr, &verb_list); + if (num_verbs < 0) { + pa_log("UCM verb list not found for %s", card_name); + err = -PA_ALSA_ERR_UNSPECIFIED; + goto ucm_verb_fail; + } + + /* get the properties of each UCM verb */ + for (i = 0; i < num_verbs; i += 2) { + pa_alsa_ucm_verb *verb; + + /* Get devices and modifiers for each verb */ + err = pa_alsa_ucm_get_verb(ucm->ucm_mgr, verb_list[i], verb_list[i+1], &verb); + if (err < 0) { + pa_log("Failed to get the verb %s", verb_list[i]); + continue; + } + + PA_LLIST_PREPEND(pa_alsa_ucm_verb, ucm->verbs, verb); + } + + if (!ucm->verbs) { + pa_log("No UCM verb is valid for %s", card_name); + err = -PA_ALSA_ERR_UCM_NO_VERB; + } + + snd_use_case_free_list(verb_list, num_verbs); + +ucm_verb_fail: + if (err < 0) { + snd_use_case_mgr_close(ucm->ucm_mgr); + ucm->ucm_mgr = NULL; + } + +ucm_mgr_fail: + pa_xfree(card_name); + +name_fail: + return err; +} + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) { + pa_alsa_ucm_device *d; + pa_alsa_ucm_modifier *mod; + pa_alsa_ucm_verb *verb; + char *value; + unsigned ui; + int err = 0; + + *p_verb = NULL; + pa_log_info("Set UCM verb to %s", verb_name); + err = snd_use_case_set(uc_mgr, "_verb", verb_name); + if (err < 0) + return err; + + verb = pa_xnew0(pa_alsa_ucm_verb, 1); + verb->proplist = pa_proplist_new(); + + pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(verb_name)); + pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(verb_desc)); + + value = ucm_verb_value(uc_mgr, verb_name, "Priority"); + if (value && !pa_atou(value, &ui)) + verb->priority = ui > 10000 ? 10000 : ui; + free(value); + + err = ucm_get_devices(verb, uc_mgr); + if (err < 0) + pa_log("No UCM devices for verb %s", verb_name); + + err = ucm_get_modifiers(verb, uc_mgr); + if (err < 0) + pa_log("No UCM modifiers for verb %s", verb_name); + + PA_LLIST_FOREACH(d, verb->devices) { + const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME); + + /* Devices properties */ + ucm_get_device_property(d, uc_mgr, verb, dev_name); + } + /* make conflicting or supported device mutual */ + PA_LLIST_FOREACH(d, verb->devices) + append_lost_relationship(d); + + PA_LLIST_FOREACH(mod, verb->modifiers) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + /* Modifier properties */ + ucm_get_modifier_property(mod, uc_mgr, mod_name); + + /* Set PA_PROP_DEVICE_INTENDED_ROLES property to devices */ + pa_log_debug("Set media roles for verb %s, modifier %s", verb_name, mod_name); + ucm_set_media_roles(mod, verb->devices, mod_name); + } + + *p_verb = verb; + return 0; +} + +static int pa_alsa_ucm_device_cmp(const void *a, const void *b) { + const pa_alsa_ucm_device *d1 = *(pa_alsa_ucm_device **)a; + const pa_alsa_ucm_device *d2 = *(pa_alsa_ucm_device **)b; + + return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME)); +} + +static void set_eld_devices(pa_hashmap *hash) +{ + pa_device_port *port; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_device *dev; + const char *eld_mixer_device_name; + void *state; + int idx, eld_device; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + eld_mixer_device_name = NULL; + eld_device = -1; + PA_DYNARRAY_FOREACH(dev, data->devices, idx) { + if (dev->eld_device >= 0 && dev->eld_mixer_device_name) { + if (eld_device >= 0 && eld_device != dev->eld_device) { + pa_log_error("The ELD device is already set!"); + } else if (eld_mixer_device_name && pa_streq(dev->eld_mixer_device_name, eld_mixer_device_name)) { + pa_log_error("The ELD mixer device is already set (%s, %s)!", dev->eld_mixer_device_name, dev->eld_mixer_device_name); + } else { + eld_mixer_device_name = dev->eld_mixer_device_name; + eld_device = dev->eld_device; + } + } + } + data->eld_device = eld_device; + if (data->eld_mixer_device_name) + pa_xfree(data->eld_mixer_device_name); + data->eld_mixer_device_name = pa_xstrdup(eld_mixer_device_name); + } +} + +static void update_mixer_paths(pa_hashmap *ports, const char *profile) { + pa_device_port *port; + pa_alsa_ucm_port_data *data; + void *state; + + /* select volume controls on ports */ + PA_HASHMAP_FOREACH(port, ports, state) { + pa_log_info("Updating mixer path for %s: %s", profile, port->name); + data = PA_DEVICE_PORT_DATA(port); + data->path = pa_hashmap_get(data->paths, profile); + } +} + +static void probe_volumes(pa_hashmap *hash, bool is_sink, snd_pcm_t *pcm_handle, pa_hashmap *mixers, bool ignore_dB) { + pa_device_port *port; + pa_alsa_path *path; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_device *dev; + snd_mixer_t *mixer_handle; + const char *profile, *mdev, *mdev2; + void *state, *state2; + int idx; + + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + + mdev = NULL; + PA_DYNARRAY_FOREACH(dev, data->devices, idx) { + mdev2 = get_mixer_device(dev, is_sink); + if (mdev && mdev2 && !pa_streq(mdev, mdev2)) { + pa_log_error("Two mixer device names found ('%s', '%s'), using s/w volume", mdev, mdev2); + goto fail; + } + if (mdev2) + mdev = mdev2; + } + + if (mdev == NULL || !(mixer_handle = pa_alsa_open_mixer_by_name(mixers, mdev, true))) { + pa_log_error("Failed to find a working mixer device (%s).", mdev); + goto fail; + } + + PA_HASHMAP_FOREACH_KV(profile, path, data->paths, state2) { + if (pa_alsa_path_probe(path, NULL, mixer_handle, ignore_dB) < 0) { + pa_log_warn("Could not probe path: %s, using s/w volume", path->name); + pa_hashmap_remove(data->paths, profile); + } else if (!path->has_volume && !path->has_mute) { + pa_log_warn("Path %s is not a volume or mute control", path->name); + pa_hashmap_remove(data->paths, profile); + } else + pa_log_debug("Set up h/w %s using '%s' for %s:%s", path->has_volume ? "volume" : "mute", + path->name, profile, port->name); + } + } + + return; + +fail: + /* We could not probe the paths we created. Free them and revert to software volumes. */ + PA_HASHMAP_FOREACH(port, hash, state) { + data = PA_DEVICE_PORT_DATA(port); + pa_hashmap_remove_all(data->paths); + } +} + +static void ucm_add_port_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_alsa_ucm_device **pdevices, + int num, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_device_port *port; + int i; + unsigned priority; + double prio2; + char *name, *desc; + const char *dev_name; + const char *direction; + const char *profile; + pa_alsa_ucm_device *sorted[num], *dev; + pa_alsa_ucm_port_data *data; + pa_alsa_ucm_volume *vol; + pa_alsa_jack *jack, *jack2; + pa_device_port_type_t type, type2; + void *state; + + for (i = 0; i < num; i++) + sorted[i] = pdevices[i]; + + /* Sort by alphabetical order so as to have a deterministic naming scheme + * for combination ports */ + qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp); + + dev = sorted[0]; + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + name = pa_sprintf_malloc("%s%s", is_sink ? PA_UCM_PRE_TAG_OUTPUT : PA_UCM_PRE_TAG_INPUT, dev_name); + desc = num == 1 ? pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION)) + : pa_sprintf_malloc("Combination port for %s", dev_name); + + priority = is_sink ? dev->playback_priority : dev->capture_priority; + prio2 = (priority == 0 ? 0 : 1.0/priority); + jack = ucm_get_jack(context->ucm, dev); + type = dev->type; + + for (i = 1; i < num; i++) { + char *tmp; + + dev = sorted[i]; + dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + tmp = pa_sprintf_malloc("%s+%s", name, dev_name); + pa_xfree(name); + name = tmp; + + tmp = pa_sprintf_malloc("%s,%s", desc, dev_name); + pa_xfree(desc); + desc = tmp; + + priority = is_sink ? dev->playback_priority : dev->capture_priority; + if (priority != 0 && prio2 > 0) + prio2 += 1.0/priority; + + jack2 = ucm_get_jack(context->ucm, dev); + if (jack2) { + if (jack && jack != jack2) + pa_log_warn("Multiple jacks per combined device '%s': '%s' '%s'", name, jack->name, jack2->name); + jack = jack2; + } + + type2 = dev->type; + if (type2 != PA_DEVICE_PORT_TYPE_UNKNOWN) { + if (type != PA_DEVICE_PORT_TYPE_UNKNOWN && type != type2) + pa_log_warn("Multiple device types per combined device '%s': %d %d", name, type, type2); + type = type2; + } + } + + /* Make combination ports always have lower priority, and use the formula + 1/p = 1/p1 + 1/p2 + ... 1/pn. + This way, the result will always be less than the individual components, + yet higher components will lead to higher result. */ + + if (num > 1) + priority = prio2 > 0 ? 1.0/prio2 : 0; + + port = pa_hashmap_get(ports, name); + if (!port) { + pa_device_port_new_data port_data; + + pa_device_port_new_data_init(&port_data); + pa_device_port_new_data_set_name(&port_data, name); + pa_device_port_new_data_set_description(&port_data, desc); + pa_device_port_new_data_set_type(&port_data, type); + pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT); + if (jack) + pa_device_port_new_data_set_availability_group(&port_data, jack->name); + + port = pa_device_port_new(core, &port_data, sizeof(pa_alsa_ucm_port_data)); + pa_device_port_new_data_done(&port_data); + + data = PA_DEVICE_PORT_DATA(port); + ucm_port_data_init(data, context->ucm, port, pdevices, num); + port->impl_free = ucm_port_data_free; + + pa_hashmap_put(ports, port->name, port); + pa_log_debug("Add port %s: %s", port->name, port->description); + + if (num == 1) { + /* To keep things simple and not worry about stacking controls, we only support hardware volumes on non-combination + * ports. */ + PA_HASHMAP_FOREACH_KV(profile, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) { + pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem, + is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT); + + if (!path) + pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem); + else { + if (vol->master_elem) { + pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false); + e->switch_use = PA_ALSA_SWITCH_MUTE; + e->volume_use = PA_ALSA_VOLUME_MERGE; + } + + pa_hashmap_put(data->paths, pa_xstrdup(profile), path); + + /* Add path also to already created empty path set */ + dev = sorted[0]; + if (is_sink) + pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + else + pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path); + } + } + } + } + + port->priority = priority; + + pa_xfree(name); + pa_xfree(desc); + + direction = is_sink ? "output" : "input"; + pa_log_debug("Port %s direction %s, priority %d", port->name, direction, priority); + + if (cp) { + pa_log_debug("Adding profile %s to port %s.", cp->name, port->name); + pa_hashmap_put(port->profiles, cp->name, cp); + } + + if (hash) { + pa_hashmap_put(hash, port->name, port); + } +} + +static int ucm_port_contains(const char *port_name, const char *dev_name, bool is_sink) { + int ret = 0; + const char *r; + const char *state = NULL; + size_t len; + + if (!port_name || !dev_name) + return false; + + port_name += is_sink ? strlen(PA_UCM_PRE_TAG_OUTPUT) : strlen(PA_UCM_PRE_TAG_INPUT); + + while ((r = pa_split_in_place(port_name, "+", &len, &state))) { + if (strlen(dev_name) == len && !strncmp(r, dev_name, len)) { + ret = 1; + break; + } + } + + return ret; +} + +static int ucm_check_conformance( + pa_alsa_ucm_mapping_context *context, + pa_alsa_ucm_device **pdevices, + int dev_num, + pa_alsa_ucm_device *dev) { + + uint32_t idx; + pa_alsa_ucm_device *d; + int i; + + pa_assert(dev); + + pa_log_debug("Check device %s conformance with %d other devices", + pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), dev_num); + if (dev_num == 0) { + pa_log_debug("First device in combination, number 1"); + return 1; + } + + if (dev->conflicting_devices) { /* the device defines conflicting devices */ + PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) { + for (i = 0; i < dev_num; i++) { + if (pdevices[i] == d) { + pa_log_debug("Conflicting device found"); + return 0; + } + } + } + } else if (dev->supported_devices) { /* the device defines supported devices */ + for (i = 0; i < dev_num; i++) { + if (!ucm_device_exists(dev->supported_devices, pdevices[i])) { + pa_log_debug("Supported device not found"); + return 0; + } + } + } else { /* not support any other devices */ + pa_log_debug("Not support any other devices"); + return 0; + } + + pa_log_debug("Device added to combination, number %d", dev_num + 1); + return 1; +} + +static inline pa_alsa_ucm_device *get_next_device(pa_idxset *idxset, uint32_t *idx) { + pa_alsa_ucm_device *dev; + + if (*idx == PA_IDXSET_INVALID) + dev = pa_idxset_first(idxset, idx); + else + dev = pa_idxset_next(idxset, idx); + + return dev; +} + +static void ucm_add_ports_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_alsa_ucm_device **pdevices, + int dev_num, + uint32_t map_index, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_alsa_ucm_device *dev; + uint32_t idx = map_index; + + if ((dev = get_next_device(context->ucm_devices, &idx)) == NULL) + return; + + /* check if device at map_index can combine with existing devices combination */ + if (ucm_check_conformance(context, pdevices, dev_num, dev)) { + /* add device at map_index to devices combination */ + pdevices[dev_num] = dev; + /* add current devices combination as a new port */ + ucm_add_port_combination(hash, context, is_sink, pdevices, dev_num + 1, ports, cp, core); + /* try more elements combination */ + ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num + 1, idx, ports, cp, core); + } + + /* try other device with current elements number */ + ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num, idx, ports, cp, core); +} + +static char* merge_roles(const char *cur, const char *add) { + char *r, *ret; + const char *state = NULL; + + if (add == NULL) + return pa_xstrdup(cur); + else if (cur == NULL) + return pa_xstrdup(add); + + ret = pa_xstrdup(cur); + + while ((r = pa_split_spaces(add, &state))) { + char *value; + + if (!pa_str_in_list_spaces(ret, r)) + value = pa_sprintf_malloc("%s %s", ret, r); + else { + pa_xfree(r); + continue; + } + + pa_xfree(ret); + ret = value; + pa_xfree(r); + } + + return ret; +} + +void pa_alsa_ucm_add_ports_combination( + pa_hashmap *p, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { + + pa_alsa_ucm_device **pdevices; + + pa_assert(context->ucm_devices); + + if (pa_idxset_size(context->ucm_devices) > 0) { + pdevices = pa_xnew(pa_alsa_ucm_device *, pa_idxset_size(context->ucm_devices)); + ucm_add_ports_combination(p, context, is_sink, pdevices, 0, PA_IDXSET_INVALID, ports, cp, core); + pa_xfree(pdevices); + } + + /* ELD devices */ + set_eld_devices(ports); +} + +void pa_alsa_ucm_add_ports( + pa_hashmap **p, + pa_proplist *proplist, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_card *card, + snd_pcm_t *pcm_handle, + bool ignore_dB) { + + uint32_t idx; + char *merged_roles; + const char *role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + char *tmp; + + pa_assert(p); + pa_assert(*p); + + /* add ports first */ + pa_alsa_ucm_add_ports_combination(*p, context, is_sink, card->ports, NULL, card->core); + + /* now set up volume paths if any */ + probe_volumes(*p, is_sink, pcm_handle, context->ucm->mixers, ignore_dB); + + /* probe_volumes() removes per-profile paths from ports if probing them + * fails. The path for the current profile is cached in + * pa_alsa_ucm_port_data.path, which is not cleared by probe_volumes() if + * the path gets removed, so we have to call update_mixer_paths() here to + * unset the cached path if needed. */ + if (card->card.active_profile_index < card->card.n_profiles) + update_mixer_paths(*p, card->card.profiles[card->card.active_profile_index]->name); + + /* then set property PA_PROP_DEVICE_INTENDED_ROLES */ + merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES)); + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + const char *roles = pa_proplist_gets(dev->proplist, role_name); + tmp = merge_roles(merged_roles, roles); + pa_xfree(merged_roles); + merged_roles = tmp; + } + + if (context->ucm_modifiers) + PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) { + tmp = merge_roles(merged_roles, mod->media_role); + pa_xfree(merged_roles); + merged_roles = tmp; + } + + if (merged_roles) + pa_proplist_sets(proplist, PA_PROP_DEVICE_INTENDED_ROLES, merged_roles); + + pa_log_info("ALSA device %s roles: %s", pa_proplist_gets(proplist, PA_PROP_DEVICE_STRING), pa_strnull(merged_roles)); + pa_xfree(merged_roles); +} + +/* Change UCM verb and device to match selected card profile */ +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) { + int ret = 0; + const char *profile; + pa_alsa_ucm_verb *verb; + + if (new_profile == old_profile) + return ret; + else if (new_profile == NULL || old_profile == NULL) + profile = new_profile ? new_profile : SND_USE_CASE_VERB_INACTIVE; + else if (!pa_streq(new_profile, old_profile)) + profile = new_profile; + else + return ret; + + /* change verb */ + pa_log_info("Set UCM verb to %s", profile); + if ((ret = snd_use_case_set(ucm->ucm_mgr, "_verb", profile)) < 0) { + pa_log("Failed to set verb %s: %s", profile, snd_strerror(ret)); + } + + /* find active verb */ + ucm->active_verb = NULL; + PA_LLIST_FOREACH(verb, ucm->verbs) { + const char *verb_name; + verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + if (pa_streq(verb_name, profile)) { + ucm->active_verb = verb; + break; + } + } + + update_mixer_paths(card->ports, profile); + return ret; +} + +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) { + int i; + int ret = 0; + pa_alsa_ucm_config *ucm; + const char **enable_devs; + int enable_num = 0; + uint32_t idx; + pa_alsa_ucm_device *dev; + + pa_assert(context && context->ucm); + + ucm = context->ucm; + pa_assert(ucm->ucm_mgr); + + enable_devs = pa_xnew(const char *, pa_idxset_size(context->ucm_devices)); + + /* first disable then enable */ + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + if (ucm_port_contains(port->name, dev_name, is_sink)) + enable_devs[enable_num++] = dev_name; + else { + pa_log_debug("Disable ucm device %s", dev_name); + if (snd_use_case_set(ucm->ucm_mgr, "_disdev", dev_name) > 0) { + pa_log("Failed to disable ucm device %s", dev_name); + ret = -1; + break; + } + } + } + + for (i = 0; i < enable_num; i++) { + pa_log_debug("Enable ucm device %s", enable_devs[i]); + if (snd_use_case_set(ucm->ucm_mgr, "_enadev", enable_devs[i]) < 0) { + pa_log("Failed to enable ucm device %s", enable_devs[i]); + ret = -1; + break; + } + } + + pa_xfree(enable_devs); + + return ret; +} + +static void ucm_add_mapping(pa_alsa_profile *p, pa_alsa_mapping *m) { + + pa_alsa_path_set *ps; + + /* create empty path set for the future path additions */ + ps = pa_xnew0(pa_alsa_path_set, 1); + ps->direction = m->direction; + ps->paths = pa_hashmap_new_full(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func, pa_xfree, + (pa_free_cb_t) pa_alsa_path_free); + + switch (m->direction) { + case PA_ALSA_DIRECTION_ANY: + pa_idxset_put(p->output_mappings, m, NULL); + pa_idxset_put(p->input_mappings, m, NULL); + m->output_path_set = ps; + m->input_path_set = ps; + break; + case PA_ALSA_DIRECTION_OUTPUT: + pa_idxset_put(p->output_mappings, m, NULL); + m->output_path_set = ps; + break; + case PA_ALSA_DIRECTION_INPUT: + pa_idxset_put(p->input_mappings, m, NULL); + m->input_path_set = ps; + break; + } +} + +static void alsa_mapping_add_ucm_device(pa_alsa_mapping *m, pa_alsa_ucm_device *device) { + char *cur_desc; + const char *new_desc, *mdev; + bool is_sink = m->direction == PA_ALSA_DIRECTION_OUTPUT; + + pa_idxset_put(m->ucm_context.ucm_devices, device, NULL); + + new_desc = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + cur_desc = m->description; + if (cur_desc) + m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc); + else + m->description = pa_xstrdup(new_desc); + pa_xfree(cur_desc); + + /* walk around null case */ + m->description = m->description ? m->description : pa_xstrdup(""); + + /* save mapping to ucm device */ + if (is_sink) + device->playback_mapping = m; + else + device->capture_mapping = m; + + mdev = get_mixer_device(device, is_sink); + if (mdev) + pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev); +} + +static void alsa_mapping_add_ucm_modifier(pa_alsa_mapping *m, pa_alsa_ucm_modifier *modifier) { + char *cur_desc; + const char *new_desc, *mod_name, *channel_str; + uint32_t channels = 0; + + pa_idxset_put(m->ucm_context.ucm_modifiers, modifier, NULL); + + new_desc = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + cur_desc = m->description; + if (cur_desc) + m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc); + else + m->description = pa_xstrdup(new_desc); + pa_xfree(cur_desc); + + m->description = m->description ? m->description : pa_xstrdup(""); + + /* Modifier sinks should not be routed to by default */ + m->priority = 0; + + mod_name = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_NAME); + pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_MODIFIER, mod_name); + + /* save mapping to ucm modifier */ + if (m->direction == PA_ALSA_DIRECTION_OUTPUT) { + modifier->playback_mapping = m; + channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS); + } else { + modifier->capture_mapping = m; + channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS); + } + + if (channel_str) { + /* FIXME: channel_str is unsanitized input from the UCM configuration, + * we should do proper error handling instead of asserting. + * https://bugs.freedesktop.org/show_bug.cgi?id=71823 */ + pa_assert_se(pa_atou(channel_str, &channels) == 0 && pa_channels_valid(channels)); + pa_log_debug("Got channel count %" PRIu32 " for modifier", channels); + } + + if (channels) + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + else + pa_channel_map_init(&m->channel_map); +} + +static pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps, const char *verb_name, const char *device_str, bool is_sink) { + pa_alsa_mapping *m; + char *mapping_name; + size_t ucm_alibpref_len = 0; + + /* find private alsa-lib's configuration device prefix */ + + if (ucm->alib_prefix && pa_startswith(device_str, ucm->alib_prefix)) + ucm_alibpref_len = strlen(ucm->alib_prefix); + + mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, device_str + ucm_alibpref_len, is_sink ? "sink" : "source"); + + m = pa_alsa_mapping_get(ps, mapping_name); + + if (!m) + pa_log("No mapping for %s", mapping_name); + + pa_xfree(mapping_name); + + return m; +} + +static int ucm_create_mapping_direction( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_device *device, + const char *verb_name, + const char *device_name, + const char *device_str, + bool is_sink) { + + pa_alsa_mapping *m; + unsigned priority, rate, channels; + + m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink); + + if (!m) + return -1; + + pa_log_debug("UCM mapping: %s dev %s", m->name, device_name); + + priority = is_sink ? device->playback_priority : device->capture_priority; + rate = is_sink ? device->playback_rate : device->capture_rate; + channels = is_sink ? device->playback_channels : device->capture_channels; + + if (!m->ucm_context.ucm_devices) { /* new mapping */ + m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm = ucm; + m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + + m->device_strings = pa_xnew0(char*, 2); + m->device_strings[0] = pa_xstrdup(device_str); + m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT; + + ucm_add_mapping(p, m); + if (rate) + m->sample_spec.rate = rate; + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + } + + /* mapping priority is the highest one of ucm devices */ + if (priority > m->priority) + m->priority = priority; + + /* mapping channels is the lowest one of ucm devices */ + if (channels < m->channel_map.channels) + pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA); + + alsa_mapping_add_ucm_device(m, device); + + return 0; +} + +static int ucm_create_mapping_for_modifier( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_modifier *modifier, + const char *verb_name, + const char *mod_name, + const char *device_str, + bool is_sink) { + + pa_alsa_mapping *m; + + m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink); + + if (!m) + return -1; + + pa_log_info("UCM mapping: %s modifier %s", m->name, mod_name); + + if (!m->ucm_context.ucm_devices && !m->ucm_context.ucm_modifiers) { /* new mapping */ + m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + m->ucm_context.ucm = ucm; + m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT; + + m->device_strings = pa_xnew0(char*, 2); + m->device_strings[0] = pa_xstrdup(device_str); + m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT; + /* Modifier sinks should not be routed to by default */ + m->priority = 0; + + ucm_add_mapping(p, m); + } else if (!m->ucm_context.ucm_modifiers) /* share pcm with device */ + m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + alsa_mapping_add_ucm_modifier(m, modifier); + + return 0; +} + +static int ucm_create_mapping( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_profile *p, + pa_alsa_ucm_device *device, + const char *verb_name, + const char *device_name, + const char *sink, + const char *source) { + + int ret = 0; + + if (!sink && !source) { + pa_log("No sink and source at %s: %s", verb_name, device_name); + return -1; + } + + if (sink) + ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, sink, true); + if (ret == 0 && source) + ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, source, false); + + return ret; +} + +static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device) { + pa_alsa_jack *j; + const char *device_name; + const char *jack_control; + const char *mixer_device_name; + char *name; + + pa_assert(ucm); + pa_assert(device); + + device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); + + jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL); + if (jack_control) { +#if SND_LIB_VERSION >= 0x10201 + snd_ctl_elem_id_t *ctl; + int err, index; + snd_ctl_elem_id_alloca(&ctl); + err = snd_use_case_parse_ctl_elem_id(ctl, "JackControl", jack_control); + if (err < 0) + return NULL; + jack_control = snd_ctl_elem_id_get_name(ctl); + index = snd_ctl_elem_id_get_index(ctl); + if (index > 0) { + pa_log("[%s] Invalid JackControl index value: \"%s\",%d", device_name, jack_control, index); + return NULL; + } +#else +#warning "Upgrade to alsa-lib 1.2.1!" +#endif + if (!pa_endswith(jack_control, " Jack")) { + pa_log("[%s] Invalid JackControl value: \"%s\"", device_name, jack_control); + return NULL; + } + + /* pa_alsa_jack_new() expects a jack name without " Jack" at the + * end, so drop the trailing " Jack". */ + name = pa_xstrndup(jack_control, strlen(jack_control) - 5); + } else { + /* The jack control hasn't been explicitly configured, fail. */ + return NULL; + } + + PA_LLIST_FOREACH(j, ucm->jacks) + if (pa_streq(j->name, name)) + goto finish; + + mixer_device_name = get_jack_mixer_device(device, true); + if (!mixer_device_name) + mixer_device_name = get_jack_mixer_device(device, false); + if (!mixer_device_name) { + pa_log("[%s] No mixer device name for JackControl \"%s\"", device_name, jack_control); + j = NULL; + goto finish; + } + j = pa_alsa_jack_new(NULL, mixer_device_name, name, 0); + PA_LLIST_PREPEND(pa_alsa_jack, ucm->jacks, j); + +finish: + pa_xfree(name); + + return j; +} + +static int ucm_create_profile( + pa_alsa_ucm_config *ucm, + pa_alsa_profile_set *ps, + pa_alsa_ucm_verb *verb, + const char *verb_name, + const char *verb_desc) { + + pa_alsa_profile *p; + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + int i = 0; + const char *name, *sink, *source; + unsigned int priority; + + pa_assert(ps); + + if (pa_hashmap_get(ps->profiles, verb_name)) { + pa_log("Verb %s already exists", verb_name); + return -1; + } + + p = pa_xnew0(pa_alsa_profile, 1); + p->profile_set = ps; + p->name = pa_xstrdup(verb_name); + p->description = pa_xstrdup(verb_desc); + + p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func); + + p->supported = true; + pa_hashmap_put(ps->profiles, p->name, p); + + /* TODO: get profile priority from policy management */ + priority = verb->priority; + + if (priority == 0) { + char *verb_cmp, *c; + c = verb_cmp = pa_xstrdup(verb_name); + while (*c) { + if (*c == '_') *c = ' '; + c++; + } + for (i = 0; verb_info[i].id; i++) { + if (strcasecmp(verb_info[i].id, verb_cmp) == 0) { + priority = verb_info[i].priority; + break; + } + } + pa_xfree(verb_cmp); + } + + p->priority = priority; + + PA_LLIST_FOREACH(dev, verb->devices) { + pa_alsa_jack *jack; + const char *jack_hw_mute; + + name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME); + + sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE); + + ucm_create_mapping(ucm, ps, p, dev, verb_name, name, sink, source); + + jack = ucm_get_jack(ucm, dev); + if (jack) + device_set_jack(dev, jack); + + /* JackHWMute contains a list of device names. Each listed device must + * be associated with the jack object that we just created. */ + jack_hw_mute = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_HW_MUTE); + if (jack_hw_mute && !jack) { + pa_log("[%s] JackHWMute set, but JackControl is missing", name); + jack_hw_mute = NULL; + } + if (jack_hw_mute) { + char *hw_mute_device_name; + const char *state = NULL; + + while ((hw_mute_device_name = ucm_split_devnames(jack_hw_mute, &state))) { + pa_alsa_ucm_verb *verb2; + bool device_found = false; + + /* Search the referenced device from all verbs. If there are + * multiple verbs that have a device with this name, we add the + * hw mute association to each of those devices. */ + PA_LLIST_FOREACH(verb2, ucm->verbs) { + pa_alsa_ucm_device *hw_mute_device; + + hw_mute_device = verb_find_device(verb2, hw_mute_device_name); + if (hw_mute_device) { + device_found = true; + device_add_hw_mute_jack(hw_mute_device, jack); + } + } + + if (!device_found) + pa_log("[%s] JackHWMute references an unknown device: %s", name, hw_mute_device_name); + + pa_xfree(hw_mute_device_name); + } + } + } + + /* Now find modifiers that have their own PlaybackPCM and create + * separate sinks for them. */ + PA_LLIST_FOREACH(mod, verb->modifiers) { + name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + sink = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SINK); + source = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SOURCE); + + if (sink) + ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, sink, true); + else if (source) + ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, source, false); + } + + pa_alsa_profile_dump(p); + + return 0; +} + +static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm) +{ + pa_alsa_ucm_mapping_context *context = &m->ucm_context; + pa_alsa_ucm_device *dev; + uint32_t idx; + char *mdev, *alib_prefix; + snd_pcm_info_t *info; + int pcm_card, pcm_device; + + snd_pcm_info_alloca(&info); + if (snd_pcm_info(pcm, info) < 0) + return; + + if ((pcm_card = snd_pcm_info_get_card(info)) < 0) + return; + if ((pcm_device = snd_pcm_info_get_device(info)) < 0) + return; + + alib_prefix = context->ucm->alib_prefix; + + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + mdev = pa_sprintf_malloc("%shw:%i", alib_prefix ? alib_prefix : "", pcm_card); + if (mdev == NULL) + continue; + dev->eld_mixer_device_name = mdev; + dev->eld_device = pcm_device; + } +} + +static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) { + snd_pcm_t* pcm; + pa_sample_spec try_ss = ucm->default_sample_spec; + pa_channel_map try_map; + snd_pcm_uframes_t try_period_size, try_buffer_size; + bool exact_channels = m->channel_map.channels > 0; + + if (exact_channels) { + try_map = m->channel_map; + try_ss.channels = try_map.channels; + } else + pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA); + + try_period_size = + pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) / + pa_frame_size(&try_ss); + try_buffer_size = ucm->default_n_fragments * try_period_size; + + pcm = pa_alsa_open_by_device_string(m->device_strings[0], NULL, &try_ss, + &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, exact_channels); + + if (pcm) { + if (!exact_channels) + m->channel_map = try_map; + mapping_init_eld(m, pcm); + } + + return pcm; +} + +static void profile_finalize_probing(pa_alsa_profile *p) { + pa_alsa_mapping *m; + uint32_t idx; + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (p->supported) + m->supported++; + + if (!m->output_pcm) + continue; + + pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); + pa_alsa_close(&m->output_pcm); + } + + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (p->supported) + m->supported++; + + if (!m->input_pcm) + continue; + + pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); + pa_alsa_close(&m->input_pcm); + } +} + +static void ucm_mapping_jack_probe(pa_alsa_mapping *m, pa_hashmap *mixers) { + snd_mixer_t *mixer_handle; + pa_alsa_ucm_mapping_context *context = &m->ucm_context; + pa_alsa_ucm_device *dev; + uint32_t idx; + + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + bool has_control; + + if (!dev->jack || !dev->jack->mixer_device_name) + continue; + + mixer_handle = pa_alsa_open_mixer_by_name(mixers, dev->jack->mixer_device_name, true); + if (!mixer_handle) { + pa_log_error("Unable to determine open mixer device '%s' for jack %s", dev->jack->mixer_device_name, dev->jack->name); + continue; + } + + has_control = pa_alsa_mixer_find_card(mixer_handle, &dev->jack->alsa_id, 0) != NULL; + pa_alsa_jack_set_has_control(dev->jack, has_control); + pa_log_info("UCM jack %s has_control=%d", dev->jack->name, dev->jack->has_control); + } +} + +static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps) { + void *state; + pa_alsa_profile *p; + pa_alsa_mapping *m; + uint32_t idx; + + PA_HASHMAP_FOREACH(p, ps->profiles, state) { + /* change verb */ + pa_log_info("Set ucm verb to %s", p->name); + + if ((snd_use_case_set(ucm->ucm_mgr, "_verb", p->name)) < 0) { + pa_log("Failed to set verb %s", p->name); + p->supported = false; + continue; + } + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) { + if (PA_UCM_IS_MODIFIER_MAPPING(m)) { + /* Skip jack probing on modifier PCMs since we expect this to + * only be controlled on the main device/verb PCM. */ + continue; + } + + m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK); + if (!m->output_pcm) { + p->supported = false; + break; + } + } + + if (p->supported) { + PA_IDXSET_FOREACH(m, p->input_mappings, idx) { + if (PA_UCM_IS_MODIFIER_MAPPING(m)) { + /* Skip jack probing on modifier PCMs since we expect this to + * only be controlled on the main device/verb PCM. */ + continue; + } + + m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE); + if (!m->input_pcm) { + p->supported = false; + break; + } + } + } + + if (!p->supported) { + profile_finalize_probing(p); + continue; + } + + pa_log_debug("Profile %s supported.", p->name); + + PA_IDXSET_FOREACH(m, p->output_mappings, idx) + if (!PA_UCM_IS_MODIFIER_MAPPING(m)) + ucm_mapping_jack_probe(m, ucm->mixers); + + PA_IDXSET_FOREACH(m, p->input_mappings, idx) + if (!PA_UCM_IS_MODIFIER_MAPPING(m)) + ucm_mapping_jack_probe(m, ucm->mixers); + + profile_finalize_probing(p); + } + + /* restore ucm state */ + snd_use_case_set(ucm->ucm_mgr, "_verb", SND_USE_CASE_VERB_INACTIVE); + + pa_alsa_profile_set_drop_unsupported(ps); +} + +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) { + pa_alsa_ucm_verb *verb; + pa_alsa_profile_set *ps; + + ps = pa_xnew0(pa_alsa_profile_set, 1); + ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) pa_alsa_mapping_free); + ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, + (pa_free_cb_t) pa_alsa_profile_free); + ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + /* create a profile for each verb */ + PA_LLIST_FOREACH(verb, ucm->verbs) { + const char *verb_name; + const char *verb_desc; + + verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME); + verb_desc = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION); + if (verb_name == NULL) { + pa_log("Verb with no name"); + continue; + } + + ucm_create_profile(ucm, ps, verb, verb_name, verb_desc); + } + + ucm_probe_profile_set(ucm, ps); + ps->probed = true; + + return ps; +} + +static void free_verb(pa_alsa_ucm_verb *verb) { + pa_alsa_ucm_device *di, *dn; + pa_alsa_ucm_modifier *mi, *mn; + + PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) { + PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di); + + if (di->hw_mute_jacks) + pa_dynarray_free(di->hw_mute_jacks); + + if (di->ucm_ports) + pa_dynarray_free(di->ucm_ports); + + if (di->playback_volumes) + pa_hashmap_free(di->playback_volumes); + if (di->capture_volumes) + pa_hashmap_free(di->capture_volumes); + + pa_proplist_free(di->proplist); + + if (di->conflicting_devices) + pa_idxset_free(di->conflicting_devices, NULL); + if (di->supported_devices) + pa_idxset_free(di->supported_devices, NULL); + + pa_xfree(di->eld_mixer_device_name); + + pa_xfree(di); + } + + PA_LLIST_FOREACH_SAFE(mi, mn, verb->modifiers) { + PA_LLIST_REMOVE(pa_alsa_ucm_modifier, verb->modifiers, mi); + pa_proplist_free(mi->proplist); + if (mi->n_suppdev > 0) + snd_use_case_free_list(mi->supported_devices, mi->n_suppdev); + if (mi->n_confdev > 0) + snd_use_case_free_list(mi->conflicting_devices, mi->n_confdev); + pa_xfree(mi->media_role); + pa_xfree(mi); + } + pa_proplist_free(verb->proplist); + pa_xfree(verb); +} + +static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name) { + pa_alsa_ucm_device *device; + + pa_assert(verb); + pa_assert(device_name); + + PA_LLIST_FOREACH(device, verb->devices) { + const char *name; + + name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); + if (pa_streq(name, device_name)) + return device; + } + + return NULL; +} + +void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) { + pa_alsa_ucm_verb *vi, *vn; + pa_alsa_jack *ji, *jn; + + PA_LLIST_FOREACH_SAFE(vi, vn, ucm->verbs) { + PA_LLIST_REMOVE(pa_alsa_ucm_verb, ucm->verbs, vi); + free_verb(vi); + } + PA_LLIST_FOREACH_SAFE(ji, jn, ucm->jacks) { + PA_LLIST_REMOVE(pa_alsa_jack, ucm->jacks, ji); + pa_alsa_jack_free(ji); + } + if (ucm->ucm_mgr) { + snd_use_case_mgr_close(ucm->ucm_mgr); + ucm->ucm_mgr = NULL; + } + pa_xfree(ucm->alib_prefix); + ucm->alib_prefix = NULL; +} + +void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) { + pa_alsa_ucm_device *dev; + pa_alsa_ucm_modifier *mod; + uint32_t idx; + + if (context->ucm_devices) { + /* clear ucm device pointer to mapping */ + PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) { + if (context->direction == PA_DIRECTION_OUTPUT) + dev->playback_mapping = NULL; + else + dev->capture_mapping = NULL; + } + + pa_idxset_free(context->ucm_devices, NULL); + } + + if (context->ucm_modifiers) { + PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) { + if (context->direction == PA_DIRECTION_OUTPUT) + mod->playback_mapping = NULL; + else + mod->capture_mapping = NULL; + } + + pa_idxset_free(context->ucm_modifiers, NULL); + } +} + +/* Enable the modifier when the first stream with matched role starts */ +void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { + pa_alsa_ucm_modifier *mod; + + if (!ucm->active_verb) + return; + + PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) { + if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) { + if (mod->enabled_counter == 0) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + pa_log_info("Enable ucm modifier %s", mod_name); + if (snd_use_case_set(ucm->ucm_mgr, "_enamod", mod_name) < 0) { + pa_log("Failed to enable ucm modifier %s", mod_name); + } + } + + mod->enabled_counter++; + break; + } + } +} + +/* Disable the modifier when the last stream with matched role ends */ +void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { + pa_alsa_ucm_modifier *mod; + + if (!ucm->active_verb) + return; + + PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) { + if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) { + + mod->enabled_counter--; + if (mod->enabled_counter == 0) { + const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME); + + pa_log_info("Disable ucm modifier %s", mod_name); + if (snd_use_case_set(ucm->ucm_mgr, "_dismod", mod_name) < 0) { + pa_log("Failed to disable ucm modifier %s", mod_name); + } + } + + break; + } + } +} + +static void device_add_ucm_port(pa_alsa_ucm_device *device, pa_alsa_ucm_port_data *port) { + pa_assert(device); + pa_assert(port); + + pa_dynarray_append(device->ucm_ports, port); +} + +static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(jack); + + device->jack = jack; + pa_alsa_jack_add_ucm_device(jack, device); + + pa_alsa_ucm_device_update_available(device); +} + +static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) { + pa_assert(device); + pa_assert(jack); + + pa_dynarray_append(device->hw_mute_jacks, jack); + pa_alsa_jack_add_ucm_hw_mute_device(jack, device); + + pa_alsa_ucm_device_update_available(device); +} + +static void device_set_available(pa_alsa_ucm_device *device, pa_available_t available) { + pa_alsa_ucm_port_data *port; + unsigned idx; + + pa_assert(device); + + if (available == device->available) + return; + + device->available = available; + + PA_DYNARRAY_FOREACH(port, device->ucm_ports, idx) + ucm_port_update_available(port); +} + +void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device) { + pa_available_t available = PA_AVAILABLE_UNKNOWN; + pa_alsa_jack *jack; + unsigned idx; + + pa_assert(device); + + if (device->jack && device->jack->has_control) + available = device->jack->plugged_in ? PA_AVAILABLE_YES : PA_AVAILABLE_NO; + + PA_DYNARRAY_FOREACH(jack, device->hw_mute_jacks, idx) { + if (jack->plugged_in) { + available = PA_AVAILABLE_NO; + break; + } + } + + device_set_available(device, available); +} + +static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port, + pa_alsa_ucm_device **devices, unsigned n_devices) { + unsigned i; + + pa_assert(ucm); + pa_assert(core_port); + pa_assert(devices); + + port->ucm = ucm; + port->core_port = core_port; + port->devices = pa_dynarray_new(NULL); + port->eld_device = -1; + + for (i = 0; i < n_devices; i++) { + pa_dynarray_append(port->devices, devices[i]); + device_add_ucm_port(devices[i], port); + } + + port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, NULL); + + ucm_port_update_available(port); +} + +static void ucm_port_data_free(pa_device_port *port) { + pa_alsa_ucm_port_data *ucm_port; + + pa_assert(port); + + ucm_port = PA_DEVICE_PORT_DATA(port); + + if (ucm_port->devices) + pa_dynarray_free(ucm_port->devices); + + if (ucm_port->paths) + pa_hashmap_free(ucm_port->paths); + + pa_xfree(ucm_port->eld_mixer_device_name); +} + +static void ucm_port_update_available(pa_alsa_ucm_port_data *port) { + pa_alsa_ucm_device *device; + unsigned idx; + pa_available_t available = PA_AVAILABLE_YES; + + pa_assert(port); + + PA_DYNARRAY_FOREACH(device, port->devices, idx) { + if (device->available == PA_AVAILABLE_UNKNOWN) + available = PA_AVAILABLE_UNKNOWN; + else if (device->available == PA_AVAILABLE_NO) { + available = PA_AVAILABLE_NO; + break; + } + } + + pa_device_port_set_available(port->core_port, available); +} + +#else /* HAVE_ALSA_UCM */ + +/* Dummy functions for systems without UCM support */ + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) { + pa_log_info("UCM not available."); + return -1; +} + +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) { + return NULL; +} + +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) { + return -1; +} + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) { + return -1; +} + +void pa_alsa_ucm_add_ports( + pa_hashmap **hash, + pa_proplist *proplist, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_card *card, + snd_pcm_t *pcm_handle, + bool ignore_dB) { +} + +void pa_alsa_ucm_add_ports_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core) { +} + +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) { + return -1; +} + +void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) { +} + +void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) { +} + +void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { +} + +void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) { +} + +#endif diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h new file mode 100644 index 0000000..696209e --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-ucm.h @@ -0,0 +1,301 @@ +#ifndef fooalsaucmhfoo +#define fooalsaucmhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2011 Wolfson Microelectronics PLC + Author Margarita Olaya + 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_port_data pa_alsa_ucm_port_data; +typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume; + +int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index); +pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map); +int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile); + +int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb); + +void pa_alsa_ucm_add_ports( + pa_hashmap **hash, + pa_proplist *proplist, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_card *card, + snd_pcm_t *pcm_handle, + bool ignore_dB); +void pa_alsa_ucm_add_ports_combination( + pa_hashmap *hash, + pa_alsa_ucm_mapping_context *context, + bool is_sink, + pa_hashmap *ports, + pa_card_profile *cp, + pa_core *core); +int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink); + +void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm); +void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context); + +void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir); +void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir); + +/* UCM - Use Case Manager is available on some audio cards */ + +struct pa_alsa_ucm_device { + PA_LLIST_FIELDS(pa_alsa_ucm_device); + + pa_proplist *proplist; + + pa_device_port_type_t type; + + unsigned playback_priority; + unsigned capture_priority; + + unsigned playback_rate; + unsigned capture_rate; + + unsigned playback_channels; + unsigned capture_channels; + + /* These may be different per verb, so we store this as a hashmap of verb -> volume_control. We might eventually want to + * make this a hashmap of verb -> per-verb-device-properties-struct. */ + pa_hashmap *playback_volumes; + pa_hashmap *capture_volumes; + + pa_alsa_mapping *playback_mapping; + pa_alsa_mapping *capture_mapping; + + pa_idxset *conflicting_devices; + pa_idxset *supported_devices; + + /* One device may be part of multiple ports, since each device has + * a dedicated port, and in addition to that we sometimes generate ports + * that represent combinations of devices. */ + pa_dynarray *ucm_ports; /* struct ucm_port */ + + pa_alsa_jack *jack; + pa_dynarray *hw_mute_jacks; /* pa_alsa_jack */ + pa_available_t available; + + char *eld_mixer_device_name; + int eld_device; +}; + +void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device); + +struct pa_alsa_ucm_modifier { + PA_LLIST_FIELDS(pa_alsa_ucm_modifier); + + pa_proplist *proplist; + + int n_confdev; + int n_suppdev; + + const char **conflicting_devices; + const char **supported_devices; + + pa_direction_t action_direction; + + char *media_role; + + /* Non-NULL if the modifier has its own PlaybackPCM/CapturePCM */ + pa_alsa_mapping *playback_mapping; + pa_alsa_mapping *capture_mapping; + + /* Count how many role matched streams are running */ + int enabled_counter; +}; + +struct pa_alsa_ucm_verb { + PA_LLIST_FIELDS(pa_alsa_ucm_verb); + + pa_proplist *proplist; + unsigned priority; + + PA_LLIST_HEAD(pa_alsa_ucm_device, devices); + PA_LLIST_HEAD(pa_alsa_ucm_modifier, modifiers); +}; + +struct pa_alsa_ucm_config { + pa_sample_spec default_sample_spec; + pa_channel_map default_channel_map; + unsigned default_fragment_size_msec; + unsigned default_n_fragments; + + snd_use_case_mgr_t *ucm_mgr; + pa_alsa_ucm_verb *active_verb; + char *alib_prefix; + + pa_hashmap *mixers; + PA_LLIST_HEAD(pa_alsa_ucm_verb, verbs); + PA_LLIST_HEAD(pa_alsa_jack, jacks); +}; + +struct pa_alsa_ucm_mapping_context { + pa_alsa_ucm_config *ucm; + pa_direction_t direction; + + pa_idxset *ucm_devices; + pa_idxset *ucm_modifiers; +}; + +struct pa_alsa_ucm_port_data { + pa_alsa_ucm_config *ucm; + pa_device_port *core_port; + + /* A single port will be associated with multiple devices if it represents + * a combination of devices. */ + pa_dynarray *devices; /* pa_alsa_ucm_device */ + + /* profile name -> pa_alsa_path for volume control */ + pa_hashmap *paths; + /* Current path, set when activating profile */ + pa_alsa_path *path; + + /* ELD info */ + char *eld_mixer_device_name; + int eld_device; /* PCM device number */ +}; + +struct pa_alsa_ucm_volume { + char *mixer_elem; /* mixer element identifier */ + char *master_elem; /* master mixer element identifier */ + char *master_type; +}; + +#endif diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c new file mode 100644 index 0000000..c76cef3 --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-util.c @@ -0,0 +1,1904 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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" + +#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_alsa_profile_set *ps, + pa_alsa_mapping **mapping) { + + char *d; + snd_pcm_t *pcm_handle; + void *state; + pa_alsa_mapping *m; + + pa_assert(dev_id); + pa_assert(dev); + pa_assert(ss); + pa_assert(map); + pa_assert(ps); + + /* First we try to find a device string with a superset of the + * requested channel map. We iterate through our device table from + * top to bottom and take the first that matches. If we didn't + * find a working device that way, we iterate backwards, and check + * all devices that do not provide a superset of the requested + * channel map.*/ + + PA_HASHMAP_FOREACH(m, ps->mappings, state) { + if (!pa_channel_map_superset(&m->channel_map, map)) + continue; + + pa_log_debug("Checking for superset %s (%s)", m->name, m->device_strings[0]); + + pcm_handle = pa_alsa_open_by_device_id_mapping( + dev_id, + dev, + ss, + map, + mode, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + m); + + if (pcm_handle) { + if (mapping) + *mapping = m; + + return pcm_handle; + } + } + + PA_HASHMAP_FOREACH_BACKWARDS(m, ps->mappings, state) { + if (pa_channel_map_superset(&m->channel_map, map)) + continue; + + pa_log_debug("Checking for subset %s (%s)", m->name, m->device_strings[0]); + + pcm_handle = pa_alsa_open_by_device_id_mapping( + dev_id, + dev, + ss, + map, + mode, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + m); + + if (pcm_handle) { + if (mapping) + *mapping = m; + + return pcm_handle; + } + } + + /* OK, we didn't find any good device, so let's try the raw hw: stuff */ + d = pa_sprintf_malloc("hw:%s", dev_id); + pa_log_debug("Trying %s as last resort...", d); + pcm_handle = pa_alsa_open_by_device_string( + d, + dev, + ss, + map, + mode, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + false); + pa_xfree(d); + + if (pcm_handle && mapping) + *mapping = NULL; + + return pcm_handle; +} +#endif + +snd_pcm_t *pa_alsa_open_by_device_id_mapping( + const char *dev_id, + char **dev, + pa_sample_spec *ss, + pa_channel_map* map, + int mode, + snd_pcm_uframes_t *period_size, + snd_pcm_uframes_t *buffer_size, + snd_pcm_uframes_t tsched_size, + bool *use_mmap, + bool *use_tsched, + pa_alsa_mapping *m) { + + snd_pcm_t *pcm_handle; + pa_sample_spec try_ss; + pa_channel_map try_map; + + pa_assert(dev_id); + pa_assert(dev); + pa_assert(ss); + pa_assert(map); + pa_assert(m); + + try_ss.channels = m->channel_map.channels; + try_ss.rate = ss->rate; + try_ss.format = ss->format; + try_map = m->channel_map; + + pcm_handle = pa_alsa_open_by_template( + m->device_strings, + dev_id, + dev, + &try_ss, + &try_map, + mode, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + pa_channel_map_valid(&m->channel_map) /* Query the channel count if we don't know what we want */); + + if (!pcm_handle) + return NULL; + + *ss = try_ss; + *map = try_map; + pa_assert(map->channels == ss->channels); + + return pcm_handle; +} + +int pa_alsa_close(snd_pcm_t **pcm) +{ + int err; + pa_assert(pcm); + pa_log_info("ALSA device close %p", *pcm); + if (*pcm == NULL) + return 0; + if ((err = snd_pcm_close(*pcm)) < 0) { + pa_log_warn("ALSA close failed: %s", snd_strerror(err)); + } + *pcm = NULL; + return err; +} + +snd_pcm_t *pa_alsa_open_by_device_string( + const char *device, + char **dev, + pa_sample_spec *ss, + pa_channel_map* map, + int mode, + snd_pcm_uframes_t *period_size, + snd_pcm_uframes_t *buffer_size, + snd_pcm_uframes_t tsched_size, + bool *use_mmap, + bool *use_tsched, + bool require_exact_channel_number) { + + int err; + char *d; + snd_pcm_t *pcm_handle; + bool reformat = false; + + pa_assert(device); + pa_assert(ss); + pa_assert(map); + + d = pa_xstrdup(device); + + for (;;) { + pa_log_debug("Trying %s %s SND_PCM_NO_AUTO_FORMAT ...", d, reformat ? "without" : "with"); + + if ((err = snd_pcm_open(&pcm_handle, d, mode, + SND_PCM_NONBLOCK| + SND_PCM_NO_AUTO_RESAMPLE| + SND_PCM_NO_AUTO_CHANNELS| + (reformat ? 0 : SND_PCM_NO_AUTO_FORMAT))) < 0) { + pa_log_info("Error opening PCM device %s: %s", d, pa_alsa_strerror(err)); + goto fail; + } + pa_log_info("ALSA device open '%s' %s: %p", d, + mode == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", pcm_handle); + + if ((err = pa_alsa_set_hw_params( + pcm_handle, + ss, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + require_exact_channel_number)) < 0) { + + if (!reformat) { + reformat = true; + + pa_alsa_close(&pcm_handle); + continue; + } + + /* Hmm, some hw is very exotic, so we retry with plug, if without it didn't work */ + if (!pa_startswith(d, "plug:") && !pa_startswith(d, "plughw:")) { + char *t; + + t = pa_sprintf_malloc("plug:SLAVE='%s'", d); + pa_xfree(d); + d = t; + + reformat = false; + + pa_alsa_close(&pcm_handle); + continue; + } + + pa_log_info("Failed to set hardware parameters on %s: %s", d, pa_alsa_strerror(err)); + pa_alsa_close(&pcm_handle); + + goto fail; + } + + if (ss->channels > PA_CHANNELS_MAX) { + pa_log("Device %s has %u channels, but PulseAudio supports only %u channels. Unable to use the device.", + d, ss->channels, PA_CHANNELS_MAX); + pa_alsa_close(&pcm_handle); + goto fail; + } + + if (dev) + *dev = d; + else + pa_xfree(d); + + if (ss->channels != map->channels) + pa_channel_map_init_extend(map, ss->channels, PA_CHANNEL_MAP_ALSA); + + return pcm_handle; + } + +fail: + pa_xfree(d); + + return NULL; +} + +snd_pcm_t *pa_alsa_open_by_template( + char **template, + const char *dev_id, + char **dev, + pa_sample_spec *ss, + pa_channel_map* map, + int mode, + snd_pcm_uframes_t *period_size, + snd_pcm_uframes_t *buffer_size, + snd_pcm_uframes_t tsched_size, + bool *use_mmap, + bool *use_tsched, + bool require_exact_channel_number) { + + snd_pcm_t *pcm_handle; + char **i; + + for (i = template; *i; i++) { + char *d; + + d = pa_replace(*i, "%f", dev_id); + + pcm_handle = pa_alsa_open_by_device_string( + d, + dev, + ss, + map, + mode, + period_size, + buffer_size, + tsched_size, + use_mmap, + use_tsched, + require_exact_channel_number); + + pa_xfree(d); + + if (pcm_handle) + return pcm_handle; + } + + return NULL; +} + +void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm) { + int err; + snd_output_t *out; + + pa_assert(pcm); + + pa_assert_se(snd_output_buffer_open(&out) == 0); + + if ((err = snd_pcm_dump(pcm, out)) < 0) + pa_logl(level, "snd_pcm_dump(): %s", pa_alsa_strerror(err)); + else { + char *s = NULL; + snd_output_buffer_string(out, &s); + pa_logl(level, "snd_pcm_dump():\n%s", pa_strnull(s)); + } + + pa_assert_se(snd_output_close(out) == 0); +} + +#if 0 +void pa_alsa_dump_status(snd_pcm_t *pcm) { + int err; + snd_output_t *out; + snd_pcm_status_t *status; + char *s = NULL; + + pa_assert(pcm); + + snd_pcm_status_alloca(&status); + + if ((err = snd_output_buffer_open(&out)) < 0) { + pa_log_debug("snd_output_buffer_open() failed: %s", pa_cstrerror(err)); + return; + } + + if ((err = snd_pcm_status(pcm, status)) < 0) { + pa_log_debug("snd_pcm_status() failed: %s", pa_cstrerror(err)); + goto finish; + } + + if ((err = snd_pcm_status_dump(status, out)) < 0) { + pa_log_debug("snd_pcm_status_dump(): %s", pa_alsa_strerror(err)); + goto finish; + } + + snd_output_buffer_string(out, &s); + pa_log_debug("snd_pcm_status_dump():\n%s", pa_strnull(s)); + +finish: + + snd_output_close(out); +} +#endif + +static PA_PRINTF_FUNC(5,6) void alsa_error_handler(const char *file, int line, const char *function, int err, const char *fmt,...) { + va_list ap; +// char *alsa_file; + +// alsa_file = pa_sprintf_malloc("(alsa-lib)%s", file); + + va_start(ap, fmt); + + pa_log_levelv_meta(PA_LOG_INFO, file, line, function, fmt, ap); + + va_end(ap); + +// pa_xfree(alsa_file); +} + +static int n_error_handler_installed = 0; + +typedef void (*snd_lib2_error_handler_t)(const char *file, int line, const char *function, int err, const char *fmt, ...) PA_PRINTF_FUNC(5,6) /* __attribute__ ((format (printf, 5, 6))) */; + +extern int snd_lib_error_set_handler(snd_lib2_error_handler_t handler); + +void pa_alsa_refcnt_inc(void) { + /* This is not really thread safe, but we do our best */ + if (n_error_handler_installed++ == 0) + snd_lib_error_set_handler(alsa_error_handler); +} + +void pa_alsa_refcnt_dec(void) { + int r; + + pa_assert_se((r = n_error_handler_installed--) >= 1); + + if (r == 1) { + snd_lib_error_set_handler(NULL); + snd_config_update_free_global(); + } +} + +bool pa_alsa_init_description(pa_proplist *p, pa_card *card) { + const char *d, *k; + pa_assert(p); + + if (pa_alsa_device_init_description(p, card)) + return true; + + if (!(d = pa_proplist_gets(p, "alsa.card_name"))) + d = pa_proplist_gets(p, "alsa.name"); + + if (!d) + return false; + + k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION); + + if (d && k) + pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k); + else if (d) + pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d); + + return false; +} + +void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card) { + char *cn, *lcn, *dn; + + pa_assert(p); + pa_assert(card >= 0); + + pa_proplist_setf(p, "alsa.card", "%i", card); + + if (snd_card_get_name(card, &cn) >= 0) { + pa_proplist_sets(p, "alsa.card_name", pa_strip(cn)); + free(cn); + } + + if (snd_card_get_longname(card, &lcn) >= 0) { + pa_proplist_sets(p, "alsa.long_card_name", pa_strip(lcn)); + free(lcn); + } + + if ((dn = pa_alsa_get_driver_name(card))) { + pa_proplist_sets(p, "alsa.driver_name", dn); + pa_xfree(dn); + } + +#ifdef HAVE_UDEV + pa_udev_get_info(card, p); +#endif +} + +void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info) { + + static const char * const alsa_class_table[SND_PCM_CLASS_LAST+1] = { + [SND_PCM_CLASS_GENERIC] = "generic", + [SND_PCM_CLASS_MULTI] = "multi", + [SND_PCM_CLASS_MODEM] = "modem", + [SND_PCM_CLASS_DIGITIZER] = "digitizer" + }; + static const char * const class_table[SND_PCM_CLASS_LAST+1] = { + [SND_PCM_CLASS_GENERIC] = "sound", + [SND_PCM_CLASS_MULTI] = NULL, + [SND_PCM_CLASS_MODEM] = "modem", + [SND_PCM_CLASS_DIGITIZER] = NULL + }; + static const char * const alsa_subclass_table[SND_PCM_SUBCLASS_LAST+1] = { + [SND_PCM_SUBCLASS_GENERIC_MIX] = "generic-mix", + [SND_PCM_SUBCLASS_MULTI_MIX] = "multi-mix" + }; + + snd_pcm_class_t class; + snd_pcm_subclass_t subclass; + const char *n, *id, *sdn; + int card; + + pa_assert(p); + pa_assert(pcm_info); + + pa_proplist_sets(p, PA_PROP_DEVICE_API, "alsa"); + + if ((class = snd_pcm_info_get_class(pcm_info)) <= SND_PCM_CLASS_LAST) { + if (class_table[class]) + pa_proplist_sets(p, PA_PROP_DEVICE_CLASS, class_table[class]); + if (alsa_class_table[class]) + pa_proplist_sets(p, "alsa.class", alsa_class_table[class]); + } + + if ((subclass = snd_pcm_info_get_subclass(pcm_info)) <= SND_PCM_SUBCLASS_LAST) + if (alsa_subclass_table[subclass]) + pa_proplist_sets(p, "alsa.subclass", alsa_subclass_table[subclass]); + + if ((n = snd_pcm_info_get_name(pcm_info))) { + char *t = pa_xstrdup(n); + pa_proplist_sets(p, "alsa.name", pa_strip(t)); + pa_xfree(t); + } + + if ((id = snd_pcm_info_get_id(pcm_info))) + pa_proplist_sets(p, "alsa.id", id); + + pa_proplist_setf(p, "alsa.subdevice", "%u", snd_pcm_info_get_subdevice(pcm_info)); + if ((sdn = snd_pcm_info_get_subdevice_name(pcm_info))) + pa_proplist_sets(p, "alsa.subdevice_name", sdn); + + pa_proplist_setf(p, "alsa.device", "%u", snd_pcm_info_get_device(pcm_info)); + + if ((card = snd_pcm_info_get_card(pcm_info)) >= 0) + pa_alsa_init_proplist_card(c, p, card); +} + +void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm) { + snd_pcm_hw_params_t *hwparams; + snd_pcm_info_t *info; + int bits, err; + + snd_pcm_hw_params_alloca(&hwparams); + snd_pcm_info_alloca(&info); + + if ((err = snd_pcm_hw_params_current(pcm, hwparams)) < 0) + pa_log_warn("Error fetching hardware parameter info: %s", pa_alsa_strerror(err)); + else { + + if ((bits = snd_pcm_hw_params_get_sbits(hwparams)) >= 0) + pa_proplist_setf(p, "alsa.resolution_bits", "%i", bits); + } + + if ((err = snd_pcm_info(pcm, info)) < 0) + pa_log_warn("Error fetching PCM info: %s", pa_alsa_strerror(err)); + else + pa_alsa_init_proplist_pcm_info(c, p, info); +} + +#if 0 +void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name) { + int err; + snd_ctl_t *ctl; + snd_ctl_card_info_t *info; + const char *t; + + pa_assert(p); + + snd_ctl_card_info_alloca(&info); + + if ((err = snd_ctl_open(&ctl, name, 0)) < 0) { + pa_log_warn("Error opening low-level control device '%s': %s", name, snd_strerror(err)); + return; + } + + if ((err = snd_ctl_card_info(ctl, info)) < 0) { + pa_log_warn("Control device %s card info: %s", name, snd_strerror(err)); + snd_ctl_close(ctl); + return; + } + + if ((t = snd_ctl_card_info_get_mixername(info)) && *t) + pa_proplist_sets(p, "alsa.mixer_name", t); + + if ((t = snd_ctl_card_info_get_components(info)) && *t) + pa_proplist_sets(p, "alsa.components", t); + + snd_ctl_close(ctl); +} + +int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents) { + snd_pcm_state_t state; + snd_pcm_hw_params_t *hwparams; + int err; + + pa_assert(pcm); + + if (revents & POLLERR) + pa_log_debug("Got POLLERR from ALSA"); + if (revents & POLLNVAL) + pa_log_warn("Got POLLNVAL from ALSA"); + if (revents & POLLHUP) + pa_log_warn("Got POLLHUP from ALSA"); + if (revents & POLLPRI) + pa_log_warn("Got POLLPRI from ALSA"); + if (revents & POLLIN) + pa_log_debug("Got POLLIN from ALSA"); + if (revents & POLLOUT) + pa_log_debug("Got POLLOUT from ALSA"); + + state = snd_pcm_state(pcm); + pa_log_debug("PCM state is %s", snd_pcm_state_name(state)); + + /* Try to recover from this error */ + + switch (state) { + + case SND_PCM_STATE_DISCONNECTED: + /* Do not try to recover */ + pa_log_info("Device disconnected."); + return -1; + + case SND_PCM_STATE_XRUN: + if ((err = snd_pcm_recover(pcm, -EPIPE, 1)) != 0) { + pa_log_warn("Could not recover from POLLERR|POLLNVAL|POLLHUP and XRUN: %s", pa_alsa_strerror(err)); + return -1; + } + break; + + case SND_PCM_STATE_SUSPENDED: + snd_pcm_hw_params_alloca(&hwparams); + + if ((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0) { + pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(err)); + return -1; + } + + if (snd_pcm_hw_params_can_resume(hwparams)) { + /* Retry resume 3 times before giving up, then fallback to restarting the stream. */ + for (int i = 0; i < 3; i++) { + if ((err = snd_pcm_resume(pcm)) == 0) + return 0; + if (err != -EAGAIN) + break; + pa_msleep(25); + } + pa_log_warn("Could not recover alsa device from SUSPENDED state, trying to restart PCM"); + } + /* Fall through */ + + default: + + snd_pcm_drop(pcm); + return 1; + } + + return 0; +} + +pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll) { + int n, err; + struct pollfd *pollfd; + pa_rtpoll_item *item; + + pa_assert(pcm); + + if ((n = snd_pcm_poll_descriptors_count(pcm)) < 0) { + pa_log("snd_pcm_poll_descriptors_count() failed: %s", pa_alsa_strerror(n)); + return NULL; + } + + item = pa_rtpoll_item_new(rtpoll, PA_RTPOLL_NEVER, (unsigned) n); + pollfd = pa_rtpoll_item_get_pollfd(item, NULL); + + if ((err = snd_pcm_poll_descriptors(pcm, pollfd, (unsigned) n)) < 0) { + pa_log("snd_pcm_poll_descriptors() failed: %s", pa_alsa_strerror(err)); + pa_rtpoll_item_free(item); + return NULL; + } + + return item; +} + +snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss) { + snd_pcm_sframes_t n; + size_t k; + + pa_assert(pcm); + pa_assert(hwbuf_size > 0); + pa_assert(ss); + + /* Some ALSA driver expose weird bugs, let's inform the user about + * what is going on */ + + n = snd_pcm_avail(pcm); + + if (n <= 0) + return n; + + k = (size_t) n * pa_frame_size(ss); + + if (PA_UNLIKELY(k >= hwbuf_size * 5 || + k >= pa_bytes_per_second(ss)*10)) { + + PA_ONCE_BEGIN { + char *dn = pa_alsa_get_driver_name_by_pcm(pcm); + pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + (unsigned long) k), + (unsigned long) k, + (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC), + pa_strnull(dn)); + pa_xfree(dn); + pa_alsa_dump(PA_LOG_DEBUG, pcm); + } PA_ONCE_END; + + /* Mhmm, let's try not to fail completely */ + n = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss)); + } + + return n; +} + +int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss, + bool capture) { + ssize_t k; + size_t abs_k; + int err; + snd_pcm_sframes_t avail = 0; +#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */ + snd_pcm_audio_tstamp_config_t tstamp_config; +#endif + + pa_assert(pcm); + pa_assert(delay); + pa_assert(hwbuf_size > 0); + pa_assert(ss); + + /* Some ALSA driver expose weird bugs, let's inform the user about + * what is going on. We're going to get both the avail and delay values so + * that we can compare and check them for capture. + * This is done with snd_pcm_status() which provides + * avail, delay and timestamp values in a single kernel call to improve + * timer-based scheduling */ + +#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */ + + /* The time stamp configuration needs to be set so that the + * ALSA code will use the internal delay reported by the driver. + * The time stamp configuration was introduced in alsa version 1.1.0. */ + tstamp_config.type_requested = 1; /* ALSA default time stamp type */ + tstamp_config.report_delay = 1; + snd_pcm_status_set_audio_htstamp_config(status, &tstamp_config); +#endif + + if ((err = snd_pcm_status(pcm, status)) < 0) + return err; + + avail = snd_pcm_status_get_avail(status); + *delay = snd_pcm_status_get_delay(status); + + k = (ssize_t) *delay * (ssize_t) pa_frame_size(ss); + + abs_k = k >= 0 ? (size_t) k : (size_t) -k; + + if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 || + abs_k >= pa_bytes_per_second(ss)*10)) { + + PA_ONCE_BEGIN { + char *dn = pa_alsa_get_driver_name_by_pcm(pcm); + pa_log_debug(ngettext("snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + "snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + (signed long) k), + (signed long) k, + k < 0 ? "-" : "", + (unsigned long) (pa_bytes_to_usec(abs_k, ss) / PA_USEC_PER_MSEC), + pa_strnull(dn)); + pa_xfree(dn); + pa_alsa_dump(PA_LOG_DEBUG, pcm); + } PA_ONCE_END; + + /* Mhmm, let's try not to fail completely */ + if (k < 0) + *delay = -(snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss)); + else + *delay = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss)); + } + + if (capture) { + abs_k = (size_t) avail * pa_frame_size(ss); + + if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 || + abs_k >= pa_bytes_per_second(ss)*10)) { + + PA_ONCE_BEGIN { + char *dn = pa_alsa_get_driver_name_by_pcm(pcm); + pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + (unsigned long) k), + (unsigned long) k, + (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC), + pa_strnull(dn)); + pa_xfree(dn); + pa_alsa_dump(PA_LOG_DEBUG, pcm); + } PA_ONCE_END; + + /* Mhmm, let's try not to fail completely */ + avail = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss)); + } + + if (PA_UNLIKELY(*delay < avail)) { + PA_ONCE_BEGIN { + char *dn = pa_alsa_get_driver_name_by_pcm(pcm); + pa_log(_("snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."), + (unsigned long) *delay, + (unsigned long) avail, + pa_strnull(dn)); + pa_xfree(dn); + pa_alsa_dump(PA_LOG_ERROR, pcm); + } PA_ONCE_END; + + /* try to fixup */ + *delay = avail; + } + } + + return 0; +} + +int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss) { + int r; + snd_pcm_uframes_t before; + size_t k; + + pa_assert(pcm); + pa_assert(areas); + pa_assert(offset); + pa_assert(frames); + pa_assert(hwbuf_size > 0); + pa_assert(ss); + + before = *frames; + + r = snd_pcm_mmap_begin(pcm, areas, offset, frames); + + if (r < 0) + return r; + + k = (size_t) *frames * pa_frame_size(ss); + + if (PA_UNLIKELY(*frames > before || + k >= hwbuf_size * 3 || + k >= pa_bytes_per_second(ss)*10)) + PA_ONCE_BEGIN { + char *dn = pa_alsa_get_driver_name_by_pcm(pcm); + pa_log_debug(ngettext("snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" + "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.", + (unsigned long) k), + (unsigned long) k, + (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC), + pa_strnull(dn)); + pa_xfree(dn); + pa_alsa_dump(PA_LOG_DEBUG, pcm); + } PA_ONCE_END; + + return r; +} +#endif + +char *pa_alsa_get_driver_name(int card) { + char *t, *m, *n; + + pa_assert(card >= 0); + + t = pa_sprintf_malloc("/sys/class/sound/card%i/device/driver/module", card); + m = pa_readlink(t); + pa_xfree(t); + + if (!m) + return NULL; + + n = pa_xstrdup(pa_path_get_filename(m)); + pa_xfree(m); + + return n; +} + +char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm) { + int card; + snd_pcm_info_t* info; + snd_pcm_info_alloca(&info); + + pa_assert(pcm); + + if (snd_pcm_info(pcm, info) < 0) + return NULL; + + if ((card = snd_pcm_info_get_card(info)) < 0) + return NULL; + + return pa_alsa_get_driver_name(card); +} + +#if 0 +char *pa_alsa_get_reserve_name(const char *device) { + const char *t; + int i; + + pa_assert(device); + + if ((t = strchr(device, ':'))) + device = t+1; + + if ((i = snd_card_get_index(device)) < 0) { + int32_t k; + + if (pa_atoi(device, &k) < 0) + return NULL; + + i = (int) k; + } + + return pa_sprintf_malloc("Audio%i", i); +} + +unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate) { + static unsigned int all_rates[] = { 8000, 11025, 12000, + 16000, 22050, 24000, + 32000, 44100, 48000, + 64000, 88200, 96000, + 128000, 176400, 192000, + 384000 }; + bool supported[PA_ELEMENTSOF(all_rates)] = { false, }; + snd_pcm_hw_params_t *hwparams; + unsigned int i, j, n, *rates = NULL; + int ret; + + snd_pcm_hw_params_alloca(&hwparams); + + if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) { + pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret)); + return NULL; + } + + for (i = 0, n = 0; i < PA_ELEMENTSOF(all_rates); i++) { + if (snd_pcm_hw_params_test_rate(pcm, hwparams, all_rates[i], 0) == 0) { + supported[i] = true; + n++; + } + } + + if (n > 0) { + rates = pa_xnew(unsigned int, n + 1); + + for (i = 0, j = 0; i < PA_ELEMENTSOF(all_rates); i++) { + if (supported[i]) + rates[j++] = all_rates[i]; + } + + rates[j] = 0; + } else { + rates = pa_xnew(unsigned int, 2); + + rates[0] = fallback_rate; + if ((ret = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rates[0], NULL)) < 0) { + pa_log_debug("snd_pcm_hw_params_set_rate_near() failed: %s", pa_alsa_strerror(ret)); + pa_xfree(rates); + return NULL; + } + + rates[1] = 0; + } + + return rates; +} + +pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format) { + static const snd_pcm_format_t format_trans_to_pa[] = { + [SND_PCM_FORMAT_U8] = PA_SAMPLE_U8, + [SND_PCM_FORMAT_A_LAW] = PA_SAMPLE_ALAW, + [SND_PCM_FORMAT_MU_LAW] = PA_SAMPLE_ULAW, + [SND_PCM_FORMAT_S16_LE] = PA_SAMPLE_S16LE, + [SND_PCM_FORMAT_S16_BE] = PA_SAMPLE_S16BE, + [SND_PCM_FORMAT_FLOAT_LE] = PA_SAMPLE_FLOAT32LE, + [SND_PCM_FORMAT_FLOAT_BE] = PA_SAMPLE_FLOAT32BE, + [SND_PCM_FORMAT_S32_LE] = PA_SAMPLE_S32LE, + [SND_PCM_FORMAT_S32_BE] = PA_SAMPLE_S32BE, + [SND_PCM_FORMAT_S24_3LE] = PA_SAMPLE_S24LE, + [SND_PCM_FORMAT_S24_3BE] = PA_SAMPLE_S24BE, + [SND_PCM_FORMAT_S24_LE] = PA_SAMPLE_S24_32LE, + [SND_PCM_FORMAT_S24_BE] = PA_SAMPLE_S24_32BE, + }; + static const snd_pcm_format_t all_formats[] = { + SND_PCM_FORMAT_U8, + SND_PCM_FORMAT_A_LAW, + SND_PCM_FORMAT_MU_LAW, + SND_PCM_FORMAT_S16_LE, + SND_PCM_FORMAT_S16_BE, + SND_PCM_FORMAT_FLOAT_LE, + SND_PCM_FORMAT_FLOAT_BE, + SND_PCM_FORMAT_S32_LE, + SND_PCM_FORMAT_S32_BE, + SND_PCM_FORMAT_S24_3LE, + SND_PCM_FORMAT_S24_3BE, + SND_PCM_FORMAT_S24_LE, + SND_PCM_FORMAT_S24_BE, + }; + bool supported[PA_ELEMENTSOF(all_formats)] = { + false, + }; + snd_pcm_hw_params_t *hwparams; + unsigned int i, j, n; + pa_sample_format_t *formats = NULL; + int ret; + + snd_pcm_hw_params_alloca(&hwparams); + + if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) { + pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret)); + return NULL; + } + + for (i = 0, n = 0; i < PA_ELEMENTSOF(all_formats); i++) { + if (snd_pcm_hw_params_test_format(pcm, hwparams, all_formats[i]) == 0) { + supported[i] = true; + n++; + } + } + + if (n > 0) { + formats = pa_xnew(pa_sample_format_t, n + 1); + + for (i = 0, j = 0; i < PA_ELEMENTSOF(all_formats); i++) { + if (supported[i]) + formats[j++] = format_trans_to_pa[all_formats[i]]; + } + + formats[j] = PA_SAMPLE_MAX; + } else { + formats = pa_xnew(pa_sample_format_t, 2); + + formats[0] = fallback_format; + if ((ret = snd_pcm_hw_params_set_format(pcm, hwparams, format_trans_to_pa[formats[0]])) < 0) { + pa_log_debug("snd_pcm_hw_params_set_format() failed: %s", pa_alsa_strerror(ret)); + pa_xfree(formats); + return NULL; + } + + formats[1] = PA_SAMPLE_MAX; + } + + return formats; +} +#endif + +bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm) { + snd_pcm_info_t* info; + snd_pcm_info_alloca(&info); + + pa_assert(pcm); + + if (snd_pcm_info(pcm, info) < 0) + return false; + + return snd_pcm_info_get_card(info) >= 0; +} + +bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm) { + snd_pcm_info_t* info; + snd_pcm_info_alloca(&info); + + pa_assert(pcm); + + if (snd_pcm_info(pcm, info) < 0) + return false; + + return snd_pcm_info_get_class(info) == SND_PCM_CLASS_MODEM; +} + +const char* pa_alsa_strerror(int errnum) { + return snd_strerror(errnum); +} + +#if 0 +bool pa_alsa_may_tsched(bool want) { + + if (!want) + return false; + + if (!pa_rtclock_hrtimer()) { + /* We cannot depend on being woken up in time when the timers + are inaccurate, so let's fallback to classic IO based playback + then. */ + pa_log_notice("Disabling timer-based scheduling because high-resolution timers are not available from the kernel."); + return false; } + + if (pa_running_in_vm()) { + /* We cannot depend on being woken up when we ask for in a VM, + * so let's fallback to classic IO based playback then. */ + pa_log_notice("Disabling timer-based scheduling because running inside a VM."); + return false; + } + + return true; +} +#endif + +#define SND_MIXER_ELEM_PULSEAUDIO (SND_MIXER_ELEM_LAST + 10) + +static snd_mixer_elem_t *pa_alsa_mixer_find(snd_mixer_t *mixer, + snd_ctl_elem_iface_t iface, + const char *name, + unsigned int index, + unsigned int device) { + snd_mixer_elem_t *elem; + + for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) { + snd_hctl_elem_t *helem; + if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_PULSEAUDIO) + continue; + helem = snd_mixer_elem_get_private(elem); + if (snd_hctl_elem_get_interface(helem) != iface) + continue; + if (!pa_streq(snd_hctl_elem_get_name(helem), name)) + continue; + if (snd_hctl_elem_get_index(helem) != index) + continue; + if (snd_hctl_elem_get_device(helem) != device) + continue; + return elem; + } + return NULL; +} + +snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device) { + return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_CARD, alsa_id->name, alsa_id->index, device); +} + +snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device) { + return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_PCM, name, 0, device); +} + +static int mixer_class_compare(const snd_mixer_elem_t *c1, const snd_mixer_elem_t *c2) +{ + /* Dummy compare function */ + return c1 == c2 ? 0 : (c1 > c2 ? 1 : -1); +} + +static int mixer_class_event(snd_mixer_class_t *class, unsigned int mask, + snd_hctl_elem_t *helem, snd_mixer_elem_t *melem) +{ + int err; + const char *name = snd_hctl_elem_get_name(helem); + // NOTE: The remove event defined as '~0U`. + if (mask == SND_CTL_EVENT_MASK_REMOVE) { + // NOTE: unless remove pointer to melem from link-list at private_data of helem, hits + // assersion in alsa-lib since the list is not empty. + snd_mixer_elem_detach(melem, helem); + } else if (mask & SND_CTL_EVENT_MASK_ADD) { + snd_ctl_elem_iface_t iface = snd_hctl_elem_get_interface(helem); + if (iface == SND_CTL_ELEM_IFACE_CARD || iface == SND_CTL_ELEM_IFACE_PCM) { + snd_mixer_elem_t *new_melem; + + /* Put the hctl pointer as our private data - it will be useful for callbacks */ + if ((err = snd_mixer_elem_new(&new_melem, SND_MIXER_ELEM_PULSEAUDIO, 0, helem, NULL)) < 0) { + pa_log_warn("snd_mixer_elem_new failed: %s", pa_alsa_strerror(err)); + return 0; + } + + if ((err = snd_mixer_elem_attach(new_melem, helem)) < 0) { + pa_log_warn("snd_mixer_elem_attach failed: %s", pa_alsa_strerror(err)); + snd_mixer_elem_free(melem); + return 0; + } + + if ((err = snd_mixer_elem_add(new_melem, class)) < 0) { + pa_log_warn("snd_mixer_elem_add failed: %s", pa_alsa_strerror(err)); + return 0; + } + } + } + else if (mask & SND_CTL_EVENT_MASK_VALUE) { + snd_mixer_elem_value(melem); /* Calls the element callback */ + return 0; + } + else + pa_log_info("Got an unknown mixer class event for %s: mask 0x%x", name, mask); + + return 0; +} + +static int prepare_mixer(snd_mixer_t *mixer, const char *dev, snd_hctl_t *hctl) { + int err; + snd_mixer_class_t *class; + + pa_assert(mixer); + pa_assert(dev); + + if ((err = snd_mixer_attach_hctl(mixer, hctl)) < 0) { + pa_log_info("Unable to attach to mixer %s: %s", dev, pa_alsa_strerror(err)); + return -1; + } + + if (snd_mixer_class_malloc(&class)) { + pa_log_info("Failed to allocate mixer class for %s", dev); + return -1; + } + snd_mixer_class_set_event(class, mixer_class_event); + snd_mixer_class_set_compare(class, mixer_class_compare); + if ((err = snd_mixer_class_register(class, mixer)) < 0) { + pa_log_info("Unable register mixer class for %s: %s", dev, pa_alsa_strerror(err)); + snd_mixer_class_free(class); + return -1; + } + /* From here on, the mixer class is deallocated by alsa on snd_mixer_close/free. */ + + if ((err = snd_mixer_selem_register(mixer, NULL, NULL)) < 0) { + pa_log_warn("Unable to register mixer: %s", pa_alsa_strerror(err)); + return -1; + } + + if ((err = snd_mixer_load(mixer)) < 0) { + pa_log_warn("Unable to load mixer: %s", pa_alsa_strerror(err)); + return -1; + } + + pa_log_info("Successfully attached to mixer '%s'", dev); + return 0; +} + +snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe) { + char *md = pa_sprintf_malloc("hw:%i", alsa_card_index); + snd_mixer_t *m = pa_alsa_open_mixer_by_name(mixers, md, probe); + pa_xfree(md); + return m; +} + +pa_alsa_mixer *pa_alsa_create_mixer(pa_hashmap *mixers, const char *dev, snd_mixer_t *m, bool probe) { + pa_alsa_mixer *pm; + + pm = pa_xnew0(pa_alsa_mixer, 1); + if (pm == NULL) + return NULL; + + pm->used_for_probe_only = probe; + pm->mixer_handle = m; + pa_hashmap_put(mixers, pa_xstrdup(dev), pm); + return pm; +} + +snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe) { + int err; + snd_mixer_t *m; + snd_hctl_t *hctl; + pa_alsa_mixer *pm; + + pa_assert(mixers); + pa_assert(dev); + + pm = pa_hashmap_get(mixers, dev); + if (pm) { + if (!probe) + pm->used_for_probe_only = false; + return pm->mixer_handle; + } + + if ((err = snd_mixer_open(&m, 0)) < 0) { + pa_log("Error opening mixer: %s", pa_alsa_strerror(err)); + return NULL; + } + + err = snd_hctl_open(&hctl, dev, 0); + if (err < 0) { + pa_log("Error opening hctl device: %s", pa_alsa_strerror(err)); + goto __close; + } + + if (prepare_mixer(m, dev, hctl) >= 0) { + /* get the ALSA card number (index) and ID (alias) and create two identical mixers */ + char *p, *dev2, *dev_idx, *dev_id; + snd_ctl_card_info_t *info; + snd_ctl_card_info_alloca(&info); + err = snd_ctl_card_info(snd_hctl_ctl(hctl), info); + if (err < 0) + goto __std; + dev2 = pa_xstrdup(dev); + if (dev2 == NULL) + goto __close; + p = strchr(dev2, ':'); + /* sanity check - only hw: devices */ + if (p == NULL || (p - dev2) < 2 || !pa_strneq(p - 2, "hw:", 3)) { + pa_xfree(dev2); + goto __std; + } + *p = '\0'; + dev_idx = pa_sprintf_malloc("%s:%u", dev2, snd_ctl_card_info_get_card(info)); + dev_id = pa_sprintf_malloc("%s:%s", dev2, snd_ctl_card_info_get_id(info)); + pa_log_debug("ALSA alias mixers: %s = %s", dev_idx, dev_id); + if (dev_idx && dev_id && (strcmp(dev, dev_idx) == 0 || strcmp(dev, dev_id) == 0)) { + pm = pa_alsa_create_mixer(mixers, dev_idx, m, probe); + if (pm) { + pa_alsa_mixer *pm2; + pm2 = pa_alsa_create_mixer(mixers, dev_id, m, probe); + if (pm2) { + pm->alias = pm2; + pm2->alias = pm; + } + } + } + pa_xfree(dev_id); + pa_xfree(dev_idx); + pa_xfree(dev2); + __std: + if (pm == NULL) + pm = pa_alsa_create_mixer(mixers, dev, m, probe); + if (pm) + return m; + } + +__close: + snd_mixer_close(m); + return NULL; +} + +snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe) { + snd_pcm_info_t* info; + snd_pcm_info_alloca(&info); + + pa_assert(pcm); + + if (snd_pcm_info(pcm, info) >= 0) { + int card_idx; + + if ((card_idx = snd_pcm_info_get_card(info)) >= 0) + return pa_alsa_open_mixer(mixers, card_idx, probe); + } + + return NULL; +} + +#if 0 +void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer_handle, pa_mainloop_api *ml) +{ + pa_alsa_mixer *pm; + void *state; + + PA_HASHMAP_FOREACH(pm, mixers, state) + if (pm->mixer_handle == mixer_handle) { + pm->used_for_probe_only = false; + if (!pm->fdl) { + pm->fdl = pa_alsa_fdlist_new(); + if (pm->fdl) + pa_alsa_fdlist_set_handle(pm->fdl, pm->mixer_handle, NULL, ml); + } + } +} +#endif + +void pa_alsa_mixer_free(pa_alsa_mixer *mixer) +{ + if (mixer->mixer_handle && mixer->alias == NULL) + snd_mixer_close(mixer->mixer_handle); + if (mixer->alias) + mixer->alias->alias = NULL; + pa_xfree(mixer); +} + +int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) { + + /* The ELD format is specific to HDA Intel sound cards and defined in the + HDA specification: http://www.intel.com/content/www/us/en/standards/high-definition-audio-specification.html */ + int err; + snd_ctl_elem_info_t *info; + snd_ctl_elem_value_t *value; + uint8_t *elddata; + unsigned int eldsize, mnl; + unsigned int device; + + pa_assert(eld != NULL); + pa_assert(elem != NULL); + + /* Does it have any contents? */ + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_value_alloca(&value); + if ((err = snd_hctl_elem_info(elem, info)) < 0 || + (err = snd_hctl_elem_read(elem, value)) < 0) { + pa_log_warn("Accessing ELD control failed with error %s", snd_strerror(err)); + return -1; + } + + device = snd_hctl_elem_get_device(elem); + eldsize = snd_ctl_elem_info_get_count(info); + elddata = (unsigned char *) snd_ctl_elem_value_get_bytes(value); + if (elddata == NULL || eldsize == 0) { + pa_log_debug("ELD info empty (for device=%d)", device); + return -1; + } + if (eldsize < 20 || eldsize > 256) { + pa_log_debug("ELD info has wrong size (for device=%d)", device); + return -1; + } + + /* Try to fetch monitor name */ + mnl = elddata[4] & 0x1f; + if (mnl == 0 || mnl > 16 || 20 + mnl > eldsize) { + pa_log_debug("No monitor name in ELD info (for device=%d)", device); + mnl = 0; + } + memcpy(eld->monitor_name, &elddata[20], mnl); + eld->monitor_name[mnl] = '\0'; + if (mnl) + pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device); + + return 0; +} diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h new file mode 100644 index 0000000..b18b98d --- /dev/null +++ b/spa/plugins/alsa/acp/alsa-util.h @@ -0,0 +1,176 @@ +#ifndef fooalsautilhfoo +#define fooalsautilhfoo + +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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_alsa_profile_set *ps, + pa_alsa_mapping **mapping); /* modified at return */ +#endif + +/* Uses the specified mapping */ +snd_pcm_t *pa_alsa_open_by_device_id_mapping( + const char *dev_id, + char **dev, /* modified at return */ + pa_sample_spec *ss, /* modified at return */ + pa_channel_map* map, /* modified at return */ + int mode, + snd_pcm_uframes_t *period_size, /* modified at return */ + snd_pcm_uframes_t *buffer_size, /* modified at return */ + snd_pcm_uframes_t tsched_size, + bool *use_mmap, /* modified at return */ + bool *use_tsched, /* modified at return */ + pa_alsa_mapping *mapping); + +/* Opens the explicit ALSA device */ +snd_pcm_t *pa_alsa_open_by_device_string( + const char *dir, + char **dev, /* modified at return */ + pa_sample_spec *ss, /* modified at return */ + pa_channel_map* map, /* modified at return */ + int mode, + snd_pcm_uframes_t *period_size, /* modified at return */ + snd_pcm_uframes_t *buffer_size, /* modified at return */ + snd_pcm_uframes_t tsched_size, + bool *use_mmap, /* modified at return */ + bool *use_tsched, /* modified at return */ + bool require_exact_channel_number); + +/* Opens the explicit ALSA device with a fallback list */ +snd_pcm_t *pa_alsa_open_by_template( + char **template, + const char *dev_id, + char **dev, /* modified at return */ + pa_sample_spec *ss, /* modified at return */ + pa_channel_map* map, /* modified at return */ + int mode, + snd_pcm_uframes_t *period_size, /* modified at return */ + snd_pcm_uframes_t *buffer_size, /* modified at return */ + snd_pcm_uframes_t tsched_size, + bool *use_mmap, /* modified at return */ + bool *use_tsched, /* modified at return */ + bool require_exact_channel_number); + +#if 0 +void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm); +void pa_alsa_dump_status(snd_pcm_t *pcm); +#endif +int pa_alsa_close(snd_pcm_t **pcm); + +void pa_alsa_refcnt_inc(void); +void pa_alsa_refcnt_dec(void); + +void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info); +void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card); +void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm); +#if 0 +void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name); +#endif +bool pa_alsa_init_description(pa_proplist *p, pa_card *card); + +#if 0 +int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents); + +pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll); + +snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss); +int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss, bool capture); +int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss); +#endif + +char *pa_alsa_get_driver_name(int card); +char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm); + +char *pa_alsa_get_reserve_name(const char *device); + +unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate); +pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format); + +bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm); +bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm); + +const char* pa_alsa_strerror(int errnum); + +#if 0 +bool pa_alsa_may_tsched(bool want); +#endif + +snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device); +snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device); + +snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe); +snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe); +snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe); +#if 0 +void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer, pa_mainloop_api *ml); +#endif +void pa_alsa_mixer_free(pa_alsa_mixer *mixer); + +typedef struct pa_hdmi_eld pa_hdmi_eld; +struct pa_hdmi_eld { + char monitor_name[17]; +}; + +int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld); + +#endif diff --git a/spa/plugins/alsa/acp/array.h b/spa/plugins/alsa/acp/array.h new file mode 100644 index 0000000..8a976ca --- /dev/null +++ b/spa/plugins/alsa/acp/array.h @@ -0,0 +1,150 @@ +/* ALSA Card Profile + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef PA_ARRAY_H +#define PA_ARRAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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..5a94064 --- /dev/null +++ b/spa/plugins/alsa/acp/card.h @@ -0,0 +1,75 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 auto_profile; + bool auto_port; + bool ignore_dB; + uint32_t rate; + + pa_alsa_ucm_config ucm; + pa_alsa_profile_set *profile_set; + + pa_hashmap *ports; + pa_hashmap *profiles; + pa_hashmap *jacks; + + struct { + pa_dynarray ports; + pa_dynarray profiles; + pa_dynarray devices; + } out; + + const struct acp_card_events *events; + void *user_data; +}; + +bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card); + +#ifdef __cplusplus +} +#endif + +#endif /* PULSE_CARD_H */ diff --git a/spa/plugins/alsa/acp/channelmap.h b/spa/plugins/alsa/acp/channelmap.h new file mode 100644 index 0000000..adb4868 --- /dev/null +++ b/spa/plugins/alsa/acp/channelmap.h @@ -0,0 +1,476 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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..7703444 --- /dev/null +++ b/spa/plugins/alsa/acp/compat.c @@ -0,0 +1,210 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2009 Lennart Poettering + Copyright 2006 Pierre Ossman 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 "compat.h" +#include "device-port.h" +#include "alsa-mixer.h" + +static const char *port_types[] = { + [PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown", + [PA_DEVICE_PORT_TYPE_AUX] = "aux", + [PA_DEVICE_PORT_TYPE_SPEAKER] = "speaker", + [PA_DEVICE_PORT_TYPE_HEADPHONES] = "headphones", + [PA_DEVICE_PORT_TYPE_LINE] = "line", + [PA_DEVICE_PORT_TYPE_MIC] = "mic", + [PA_DEVICE_PORT_TYPE_HEADSET] = "headset", + [PA_DEVICE_PORT_TYPE_HANDSET] = "handset", + [PA_DEVICE_PORT_TYPE_EARPIECE] = "earpiece", + [PA_DEVICE_PORT_TYPE_SPDIF] = "spdif", + [PA_DEVICE_PORT_TYPE_HDMI] = "hdmi", + [PA_DEVICE_PORT_TYPE_TV] = "tv", + [PA_DEVICE_PORT_TYPE_RADIO] = "radio", + [PA_DEVICE_PORT_TYPE_VIDEO] = "video", + [PA_DEVICE_PORT_TYPE_USB] = "usb", + [PA_DEVICE_PORT_TYPE_BLUETOOTH] = "bluetooth", + [PA_DEVICE_PORT_TYPE_PORTABLE] = "portable", + [PA_DEVICE_PORT_TYPE_HANDSFREE] = "handsfree", + [PA_DEVICE_PORT_TYPE_CAR] = "car", + [PA_DEVICE_PORT_TYPE_HIFI] = "hifi", + [PA_DEVICE_PORT_TYPE_PHONE] = "phone", + [PA_DEVICE_PORT_TYPE_NETWORK] = "network", + [PA_DEVICE_PORT_TYPE_ANALOG] = "analog", +}; + +static const char *str_port_type(pa_device_port_type_t type) +{ + int idx = (type < PA_ELEMENTSOF(port_types)) ? type : 0; + return port_types[idx]; +} + +pa_device_port_new_data *pa_device_port_new_data_init(pa_device_port_new_data *data) +{ + pa_assert(data); + pa_zero(*data); + data->type = PA_DEVICE_PORT_TYPE_UNKNOWN; + data->available = PA_AVAILABLE_UNKNOWN; + return data; +} + +void pa_device_port_new_data_set_name(pa_device_port_new_data *data, const char *name) +{ + pa_assert(data); + pa_xfree(data->name); + data->name = pa_xstrdup(name); +} + +void pa_device_port_new_data_set_description(pa_device_port_new_data *data, const char *description) +{ + pa_assert(data); + pa_xfree(data->description); + data->description = pa_xstrdup(description); +} + +void pa_device_port_new_data_set_available(pa_device_port_new_data *data, pa_available_t available) +{ + pa_assert(data); + data->available = available; +} + +void pa_device_port_new_data_set_availability_group(pa_device_port_new_data *data, const char *group) +{ + pa_assert(data); + pa_xfree(data->availability_group); + data->availability_group = pa_xstrdup(group); +} + +void pa_device_port_new_data_set_direction(pa_device_port_new_data *data, pa_direction_t direction) +{ + pa_assert(data); + data->direction = direction; +} + +void pa_device_port_new_data_set_type(pa_device_port_new_data *data, pa_device_port_type_t type) +{ + pa_assert(data); + data->type = type; +} + +void pa_device_port_new_data_done(pa_device_port_new_data *data) +{ + pa_assert(data); + pa_xfree(data->name); + pa_xfree(data->description); + pa_xfree(data->availability_group); +} + +pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, size_t extra) +{ + pa_device_port *p; + + pa_assert(data); + pa_assert(data->name); + pa_assert(data->description); + pa_assert(data->direction == PA_DIRECTION_OUTPUT || data->direction == PA_DIRECTION_INPUT); + + p = calloc(1, sizeof(pa_device_port) + extra); + + p->port.name = p->name = data->name; + data->name = NULL; + p->port.description = p->description = data->description; + data->description = NULL; + p->priority = p->port.priority = 0; + p->available = data->available; + p->port.available = (enum acp_available) data->available; + p->availability_group = data->availability_group; + data->availability_group = NULL; + p->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + p->direction = data->direction; + p->port.direction = data->direction == PA_DIRECTION_OUTPUT ? + ACP_DIRECTION_PLAYBACK : ACP_DIRECTION_CAPTURE; + p->type = data->type; + + p->proplist = pa_proplist_new(); + pa_proplist_sets(p->proplist, ACP_KEY_PORT_TYPE, str_port_type(data->type)); + if (p->availability_group) + pa_proplist_sets(p->proplist, ACP_KEY_PORT_AVAILABILITY_GROUP, p->availability_group); + + p->user_data = (void*)((uint8_t*)p + sizeof(pa_device_port)); + + return p; +} + +void pa_device_port_free(pa_device_port *port) +{ + pa_xfree(port->name); + pa_xfree(port->description); + pa_xfree(port->availability_group); + pa_hashmap_free(port->profiles); + pa_proplist_free(port->proplist); + if (port->impl_free) + port->impl_free (port); + free(port); +} + +void pa_device_port_set_available(pa_device_port *p, pa_available_t status) +{ + pa_available_t old = p->available; + + if (old == status) + return; + p->available = status; + p->port.available = (enum acp_available) status; + + if (p->card && p->card->events && p->card->events->port_available) + p->card->events->port_available(p->card->user_data, p->port.index, + (enum acp_available)old, p->port.available); +} + +bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card) { + const char *s, *d = NULL, *k; + pa_assert(p); + + if (pa_proplist_contains(p, PA_PROP_DEVICE_DESCRIPTION)) + return true; + + if (card) + if ((s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_DESCRIPTION))) + d = s; + + if (!d) + if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_FORM_FACTOR))) + if (pa_streq(s, "internal")) + d = _("Built-in Audio"); + + if (!d) + if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_CLASS))) + if (pa_streq(s, "modem")) + d = _("Modem"); + + if (!d) + d = pa_proplist_gets(p, PA_PROP_DEVICE_PRODUCT_NAME); + + if (!d) + return false; + + k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION); + + if (d && k) + pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k); + else if (d) + pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d); + + return true; +} diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h new file mode 100644 index 0000000..46ef1fd --- /dev/null +++ b/spa/plugins/alsa/acp/compat.h @@ -0,0 +1,656 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 + +typedef struct pa_core pa_core; + +typedef void *(*pa_copy_func_t)(const void *p); +typedef void (*pa_free_cb_t)(void *p); + +#ifdef __GNUC__ +#define PA_LIKELY(x) (__builtin_expect(!!(x),1)) +#define PA_UNLIKELY(x) (__builtin_expect(!!(x),0)) +#define PA_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1))) +#else +#define PA_LIKELY(x) (x) +#define PA_UNLIKELY(x) (x) +#define PA_PRINTF_FUNC(fmt, arg1) +#endif + +#define PA_MIN(a,b) \ +({ \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + PA_LIKELY(_a < _b) ? _a : _b; \ +}) +#define PA_MAX(a,b) \ +({ \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + PA_LIKELY(_a > _b) ? _a : _b; \ +}) +#define PA_CLAMP_UNLIKELY(v,low,high) \ +({ \ + __typeof__(v) _v = (v); \ + __typeof__(low) _low = (low); \ + __typeof__(high) _high = (high); \ + PA_MIN(PA_MAX(_v, _low), _high); \ +}) + +#define PA_PTR_TO_UINT(p) ((unsigned int) ((uintptr_t) (p))) +#define PA_UINT_TO_PTR(u) ((void*) ((uintptr_t) (u))) + +#include "array.h" +#include "llist.h" +#include "hashmap.h" +#include "dynarray.h" +#include "idxset.h" +#include "proplist.h" + +typedef enum pa_direction { + PA_DIRECTION_OUTPUT = 0x0001U, /**< Output direction */ + PA_DIRECTION_INPUT = 0x0002U /**< Input direction */ +} pa_direction_t; + +/* This enum replaces pa_port_available_t (defined in pulse/def.h) for + * internal use, so make sure both enum types stay in sync. */ +typedef enum pa_available { + PA_AVAILABLE_UNKNOWN = 0, + PA_AVAILABLE_NO = 1, + PA_AVAILABLE_YES = 2, +} pa_available_t; + +#define PA_RATE_MAX (48000U*8U) + +typedef enum pa_sample_format { + PA_SAMPLE_U8, /**< Unsigned 8 Bit PCM */ + PA_SAMPLE_ALAW, /**< 8 Bit a-Law */ + PA_SAMPLE_ULAW, /**< 8 Bit mu-Law */ + PA_SAMPLE_S16LE, /**< Signed 16 Bit PCM, little endian (PC) */ + PA_SAMPLE_S16BE, /**< Signed 16 Bit PCM, big endian */ + PA_SAMPLE_FLOAT32LE, /**< 32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0 */ + PA_SAMPLE_FLOAT32BE, /**< 32 Bit IEEE floating point, big endian, range -1.0 to 1.0 */ + PA_SAMPLE_S32LE, /**< Signed 32 Bit PCM, little endian (PC) */ + PA_SAMPLE_S32BE, /**< Signed 32 Bit PCM, big endian */ + PA_SAMPLE_S24LE, /**< Signed 24 Bit PCM packed, little endian (PC). \since 0.9.15 */ + PA_SAMPLE_S24BE, /**< Signed 24 Bit PCM packed, big endian. \since 0.9.15 */ + PA_SAMPLE_S24_32LE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC). \since 0.9.15 */ + PA_SAMPLE_S24_32BE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, big endian. \since 0.9.15 */ + PA_SAMPLE_MAX, /**< Upper limit of valid sample types */ + PA_SAMPLE_INVALID = -1 /**< An invalid value */ +} pa_sample_format_t; + +static inline int pa_sample_format_valid(unsigned format) +{ + return format < PA_SAMPLE_MAX; +} + +#ifdef WORDS_BIGENDIAN +#define PA_SAMPLE_S16NE PA_SAMPLE_S16BE +#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32BE +#define PA_SAMPLE_S32NE PA_SAMPLE_S32BE +#define PA_SAMPLE_S24NE PA_SAMPLE_S24BE +#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32BE +#define PA_SAMPLE_S16RE PA_SAMPLE_S16LE +#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32LE +#define PA_SAMPLE_S32RE PA_SAMPLE_S32LE +#define PA_SAMPLE_S24RE PA_SAMPLE_S24LE +#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32LE +#else +#define PA_SAMPLE_S16NE PA_SAMPLE_S16LE +#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32LE +#define PA_SAMPLE_S32NE PA_SAMPLE_S32LE +#define PA_SAMPLE_S24NE PA_SAMPLE_S24LE +#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32LE +#define PA_SAMPLE_S16RE PA_SAMPLE_S16BE +#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32BE +#define PA_SAMPLE_S32RE PA_SAMPLE_S32BE +#define PA_SAMPLE_S24RE PA_SAMPLE_S24BE +#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32BE +#endif + +static const size_t pa_sample_size_table[] = { + [PA_SAMPLE_U8] = 1, + [PA_SAMPLE_ULAW] = 1, + [PA_SAMPLE_ALAW] = 1, + [PA_SAMPLE_S16LE] = 2, + [PA_SAMPLE_S16BE] = 2, + [PA_SAMPLE_FLOAT32LE] = 4, + [PA_SAMPLE_FLOAT32BE] = 4, + [PA_SAMPLE_S32LE] = 4, + [PA_SAMPLE_S32BE] = 4, + [PA_SAMPLE_S24LE] = 3, + [PA_SAMPLE_S24BE] = 3, + [PA_SAMPLE_S24_32LE] = 4, + [PA_SAMPLE_S24_32BE] = 4 +}; + +static inline const char *pa_sample_format_to_string(pa_sample_format_t f) +{ + static const char* const table[]= { + [PA_SAMPLE_U8] = "u8", + [PA_SAMPLE_ALAW] = "aLaw", + [PA_SAMPLE_ULAW] = "uLaw", + [PA_SAMPLE_S16LE] = "s16le", + [PA_SAMPLE_S16BE] = "s16be", + [PA_SAMPLE_FLOAT32LE] = "float32le", + [PA_SAMPLE_FLOAT32BE] = "float32be", + [PA_SAMPLE_S32LE] = "s32le", + [PA_SAMPLE_S32BE] = "s32be", + [PA_SAMPLE_S24LE] = "s24le", + [PA_SAMPLE_S24BE] = "s24be", + [PA_SAMPLE_S24_32LE] = "s24-32le", + [PA_SAMPLE_S24_32BE] = "s24-32be", + }; + + if (!pa_sample_format_valid(f)) + return NULL; + return table[f]; +} + +typedef struct pa_sample_spec { + pa_sample_format_t format; + uint32_t rate; + uint8_t channels; +} pa_sample_spec; + +typedef uint64_t pa_usec_t; +#define PA_MSEC_PER_SEC ((pa_usec_t) 1000ULL) +#define PA_USEC_PER_SEC ((pa_usec_t) 1000000ULL) +#define PA_USEC_PER_MSEC ((pa_usec_t) 1000ULL) + +static inline size_t pa_usec_to_bytes(pa_usec_t t, const pa_sample_spec *spec) { + return (size_t) (((t * spec->rate) / PA_USEC_PER_SEC)) * + (pa_sample_size_table[spec->format] * spec->channels); +} + +static inline int pa_sample_rate_valid(uint32_t rate) { + return rate > 0 && rate <= PA_RATE_MAX * 101 / 100; +} + +static inline size_t pa_frame_size(const pa_sample_spec *spec) { + return pa_sample_size_table[spec->format] * spec->channels; +} + +typedef enum pa_log_level { + PA_LOG_ERROR = 0, /* Error messages */ + PA_LOG_WARN = 1, /* Warning messages */ + PA_LOG_NOTICE = 2, /* Notice messages */ + PA_LOG_INFO = 3, /* Info messages */ + PA_LOG_DEBUG = 4, /* Debug messages */ + PA_LOG_LEVEL_MAX +} pa_log_level_t; + +extern int _acp_log_level; +extern acp_log_func _acp_log_func; +extern void * _acp_log_data; + +#define pa_log_level_enabled(lev) (_acp_log_level >= (int)(lev)) + +#define pa_log_levelv_meta(lev,f,l,func,fmt,ap) \ +({ \ + if (pa_log_level_enabled (lev) && _acp_log_func) \ + _acp_log_func(_acp_log_data,lev,f,l,func,fmt,ap); \ +}) + +static inline PA_PRINTF_FUNC(5, 6) void pa_log_level_meta(enum pa_log_level level, + const char *file, int line, const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args,fmt); + pa_log_levelv_meta(level,file,line,func,fmt,args); + va_end(args); +} + +#define pa_logl(lev,fmt,...) pa_log_level_meta(lev,__FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__) +#define pa_log_error(fmt,...) pa_logl(PA_LOG_ERROR, fmt, ##__VA_ARGS__) +#define pa_log_warn(fmt,...) pa_logl(PA_LOG_WARN, fmt, ##__VA_ARGS__) +#define pa_log_notice(fmt,...) pa_logl(PA_LOG_NOTICE, fmt, ##__VA_ARGS__) +#define pa_log_info(fmt,...) pa_logl(PA_LOG_INFO, fmt, ##__VA_ARGS__) +#define pa_log_debug(fmt,...) pa_logl(PA_LOG_DEBUG, fmt, ##__VA_ARGS__) +#define pa_log pa_log_error + +#define pa_assert_se(expr) \ + do { \ + if (PA_UNLIKELY(!(expr))) { \ + fprintf(stderr, "'%s' failed at %s:%u %s()\n", \ + #expr , __FILE__, __LINE__, __func__); \ + abort(); \ + } \ + } while (false) + +#define pa_assert(expr) \ + do { \ + if (PA_UNLIKELY(!(expr))) { \ + fprintf(stderr, "'%s' failed at %s:%u %s()\n", \ + #expr , __FILE__, __LINE__, __func__); \ + abort(); \ + } \ + } while (false) + +#define pa_assert_not_reached() \ + do { \ + fprintf(stderr, "Code should not be reached at %s:%u %s()\n", \ + __FILE__, __LINE__, __func__); \ + abort(); \ + } while (false) + + +#define pa_memzero(x,l) (memset((x), 0, (l))) +#define pa_zero(x) (pa_memzero(&(x), sizeof(x))) + +#define PA_ELEMENTSOF(x) (sizeof(x)/sizeof((x)[0])) + +#define pa_streq(a,b) (!strcmp((a),(b))) +#define pa_strneq(a,b,n) (!strncmp((a),(b),(n))) +#define pa_strnull(s) ((s) ? (s) : "null") +#define pa_startswith(s,pfx) (strstr(s, pfx) == s) + +PA_PRINTF_FUNC(3, 0) +static inline size_t pa_vsnprintf(char *str, size_t size, const char *format, va_list ap) +{ + int ret; + + pa_assert(str); + pa_assert(size > 0); + pa_assert(format); + + ret = vsnprintf(str, size, format, ap); + + str[size-1] = 0; + + if (ret < 0) + return strlen(str); + + if ((size_t) ret > size-1) + return size-1; + + return (size_t) ret; +} + +PA_PRINTF_FUNC(3, 4) +static inline size_t pa_snprintf(char *str, size_t size, const char *format, ...) +{ + size_t ret; + va_list ap; + + pa_assert(str); + pa_assert(size > 0); + pa_assert(format); + + va_start(ap, format); + ret = pa_vsnprintf(str, size, format, ap); + va_end(ap); + + return ret; +} + +#define pa_xstrdup(s) ((s) != NULL ? strdup(s) : NULL) +#define pa_xstrndup(s,n) ((s) != NULL ? strndup(s,n) : NULL) +#define pa_xfree free +#define pa_xmalloc malloc +#define pa_xnew0(t,n) calloc(n, sizeof(t)) +#define pa_xnew(t,n) pa_xnew0(t,n) +#define pa_xrealloc realloc +#define pa_xrenew(t,p,n) ((t*) realloc(p, (n)*sizeof(t))) + +static inline void* pa_xmemdup(const void *p, size_t l) { + return memcpy(malloc(l), p, l); + +} +#define pa_xnewdup(t,p,n) ((t*) pa_xmemdup((p), (n)*sizeof(t))) + +static inline void pa_xfreev(void**a) +{ + int i; + for (i = 0; a && a[i]; i++) + free(a[i]); + free(a); +} +static inline void pa_xstrfreev(char **a) { + pa_xfreev((void**)a); +} + + +#define pa_cstrerror strerror + +#define PA_PATH_SEP "/" +#define PA_PATH_SEP_CHAR '/' + +#define PA_WHITESPACE "\n\r \t" + +static PA_PRINTF_FUNC(1,2) inline char *pa_sprintf_malloc(const char *fmt, ...) +{ + char *res; + va_list args; + va_start(args, fmt); + if (vasprintf(&res, fmt, args) < 0) + res = NULL; + va_end(args); + return res; +} + +#define pa_fopen_cloexec(f,m) fopen(f,m"e") + +static inline char *pa_path_get_filename(const char *p) +{ + char *fn; + if (!p) + return NULL; + if ((fn = strrchr(p, PA_PATH_SEP_CHAR))) + return fn+1; + return (char*) p; +} + +static inline bool pa_is_path_absolute(const char *fn) +{ + return *fn == PA_PATH_SEP_CHAR; +} + +static inline char* pa_maybe_prefix_path(const char *path, const char *prefix) +{ + if (pa_is_path_absolute(path)) + return pa_xstrdup(path); + return pa_sprintf_malloc("%s" PA_PATH_SEP "%s", prefix, path); +} + +static inline bool pa_endswith(const char *s, const char *sfx) +{ + size_t l1, l2; + l1 = strlen(s); + l2 = strlen(sfx); + return l1 >= l2 && pa_streq(s + l1 - l2, sfx); +} + +static inline char *pa_replace(const char*s, const char*a, const char *b) +{ + struct pa_array res; + size_t an, bn; + + an = strlen(a); + bn = strlen(b); + pa_array_init(&res, an); + + for (;;) { + const char *p; + + if (!(p = strstr(s, a))) + break; + + pa_array_add_data(&res, s, p-s); + pa_array_add_data(&res, b, bn); + s = p + an; + } + pa_array_add_data(&res, s, strlen(s) + 1); + return res.data; +} + +static inline char *pa_split(const char *c, const char *delimiter, const char**state) +{ + const char *current = *state ? *state : c; + size_t l; + if (!*current) + return NULL; + l = strcspn(current, delimiter); + *state = current+l; + if (**state) + (*state)++; + return pa_xstrndup(current, l); +} + +static inline char *pa_split_spaces(const char *c, const char **state) +{ + const char *current = *state ? *state : c; + size_t l; + if (!*current || *c == 0) + return NULL; + current += strspn(current, PA_WHITESPACE); + l = strcspn(current, PA_WHITESPACE); + *state = current+l; + return pa_xstrndup(current, l); +} + +static inline char **pa_split_spaces_strv(const char *s) +{ + char **t, *e; + unsigned i = 0, n = 8; + const char *state = NULL; + + t = pa_xnew(char*, n); + while ((e = pa_split_spaces(s, &state))) { + t[i++] = e; + if (i >= n) { + n *= 2; + t = pa_xrenew(char*, t, n); + } + } + if (i <= 0) { + pa_xfree(t); + return NULL; + } + t[i] = NULL; + return t; +} + +static inline char* pa_str_strip_suffix(const char *str, const char *suffix) +{ + size_t str_l, suf_l, prefix; + char *ret; + + str_l = strlen(str); + suf_l = strlen(suffix); + + if (str_l < suf_l) + return NULL; + prefix = str_l - suf_l; + if (!pa_streq(&str[prefix], suffix)) + return NULL; + ret = pa_xmalloc(prefix + 1); + memcpy(ret, str, prefix); + ret[prefix] = '\0'; + return ret; +} + +static inline const char *pa_split_in_place(const char *c, const char *delimiter, size_t *n, const char**state) +{ + const char *current = *state ? *state : c; + size_t l; + if (!*current) + return NULL; + l = strcspn(current, delimiter); + *state = current+l; + if (**state) + (*state)++; + *n = l; + return current; +} + +static inline const char *pa_split_spaces_in_place(const char *c, size_t *n, const char **state) +{ + const char *current = *state ? *state : c; + size_t l; + if (!*current || *c == 0) + return NULL; + current += strspn(current, PA_WHITESPACE); + l = strcspn(current, PA_WHITESPACE); + *state = current+l; + *n = l; + return current; +} + +static inline bool pa_str_in_list_spaces(const char *haystack, const char *needle) +{ + const char *s; + size_t n; + const char *state = NULL; + + if (!haystack || !needle) + return false; + + while ((s = pa_split_spaces_in_place(haystack, &n, &state))) { + if (pa_strneq(needle, s, n)) + return true; + } + + return false; +} + +static inline char *pa_strip(char *s) +{ + char *e, *l = NULL; + s += strspn(s, PA_WHITESPACE); + for (e = s; *e; e++) + if (!strchr(PA_WHITESPACE, *e)) + l = e; + if (l) + *(l+1) = 0; + else + *s = 0; + return s; +} + +static inline int pa_atod(const char *s, double *ret_d) +{ + if (spa_atod(s, ret_d) && !isnan(*ret_d)) + return 0; + errno = EINVAL; + return -1; +} +static inline int pa_atoi(const char *s, int32_t *ret_i) +{ + if (spa_atoi32(s, ret_i, 0)) + return 0; + errno = EINVAL; + return -1; +} +static inline int pa_atou(const char *s, uint32_t *ret_u) +{ + if (spa_atou32(s, ret_u, 0)) + return 0; + errno = EINVAL; + return -1; +} +static inline int pa_atol(const char *s, long *ret_l) +{ + int64_t res; + if (spa_atoi64(s, &res, 0)) { + *ret_l = res; + if (*ret_l == res) + return 0; + } + errno = EINVAL; + return -1; +} + +static inline int pa_parse_boolean(const char *v) +{ + if (pa_streq(v, "1") || !strcasecmp(v, "y") || !strcasecmp(v, "t") + || !strcasecmp(v, "yes") || !strcasecmp(v, "true") || !strcasecmp(v, "on")) + return 1; + else if (pa_streq(v, "0") || !strcasecmp(v, "n") || !strcasecmp(v, "f") + || !strcasecmp(v, "no") || !strcasecmp(v, "false") || !strcasecmp(v, "off")) + return 0; + errno = EINVAL; + return -1; +} + +static inline const char *pa_yes_no(bool b) { + return b ? "yes" : "no"; +} + +static inline const char *pa_strna(const char *x) { + return x ? x : "n/a"; +} + +static inline pa_sample_spec* pa_sample_spec_init(pa_sample_spec *spec) +{ + spec->format = PA_SAMPLE_INVALID; + spec->rate = 0; + spec->channels = 0; + return spec; +} + +static inline char *pa_readlink(const char *p) { +#ifdef HAVE_READLINK + size_t l = 100; + + for (;;) { + char *c; + ssize_t n; + + c = pa_xmalloc(l); + + if ((n = readlink(p, c, l-1)) < 0) { + pa_xfree(c); + return NULL; + } + + if ((size_t) n < l-1) { + c[n] = 0; + return c; + } + + pa_xfree(c); + l *= 2; + } +#else + return NULL; +#endif +} + +#include + +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..762c84c --- /dev/null +++ b/spa/plugins/alsa/acp/dynarray.h @@ -0,0 +1,159 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef PA_DYNARRAY_H +#define PA_DYNARRAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "array.h" + +typedef struct pa_dynarray_item { + void *ptr; +} pa_dynarray_item; + +typedef struct pa_dynarray { + pa_array array; + pa_free_cb_t free_cb; +} pa_dynarray; + +static inline void pa_dynarray_init(pa_dynarray *array, pa_free_cb_t free_cb) +{ + pa_array_init(&array->array, 16); + array->free_cb = free_cb; +} + +static inline void pa_dynarray_item_free(pa_dynarray *array, pa_dynarray_item *item) +{ + if (array->free_cb) + array->free_cb(item->ptr); +} + +static inline void pa_dynarray_clear(pa_dynarray *array) +{ + pa_dynarray_item *item; + pa_array_for_each(item, &array->array) + pa_dynarray_item_free(array, item); + pa_array_clear(&array->array); +} + +static inline pa_dynarray* pa_dynarray_new(pa_free_cb_t free_cb) +{ + pa_dynarray *d = calloc(1, sizeof(*d)); + pa_dynarray_init(d, free_cb); + return d; +} + +static inline void pa_dynarray_free(pa_dynarray *array) +{ + pa_dynarray_clear(array); + free(array); +} + +static inline void pa_dynarray_append(pa_dynarray *array, void *p) +{ + pa_dynarray_item *item = pa_array_add(&array->array, sizeof(*item)); + item->ptr = p; +} + +static inline pa_dynarray_item *pa_dynarray_find_item(pa_dynarray *array, void *p) +{ + pa_dynarray_item *item; + pa_array_for_each(item, &array->array) { + if (item->ptr == p) + return item; + } + return NULL; +} + +static inline pa_dynarray_item *pa_dynarray_get_item(pa_dynarray *array, unsigned i) +{ + if (!pa_array_check_index(&array->array, i, pa_dynarray_item)) + return NULL; + return pa_array_get_unchecked(&array->array, i, pa_dynarray_item); +} + +static inline void *pa_dynarray_get(pa_dynarray *array, unsigned i) +{ + pa_dynarray_item *item = pa_dynarray_get_item(array, i); + if (item == NULL) + return NULL; + return item->ptr; +} + +static inline int pa_dynarray_insert_by_index(pa_dynarray *array, void *p, unsigned i) +{ + unsigned j, len; + pa_dynarray_item *item; + + len = pa_array_get_len(&array->array, pa_dynarray_item); + + if (i > len) + return -EINVAL; + + item = pa_array_add(&array->array, sizeof(*item)); + for (j = len; j > i; j--) { + item--; + item[1].ptr = item[0].ptr; + } + item->ptr = p; + return 0; +} + +static inline int pa_dynarray_remove_by_index(pa_dynarray *array, unsigned i) +{ + pa_dynarray_item *item = pa_dynarray_get_item(array, i); + if (item == NULL) + return -ENOENT; + pa_dynarray_item_free(array, item); + pa_array_remove(&array->array, item); + return 0; +} + +static inline int pa_dynarray_remove_by_data(pa_dynarray *array, void *p) +{ + pa_dynarray_item *item = pa_dynarray_find_item(array, p); + if (item == NULL) + return -ENOENT; + pa_dynarray_item_free(array, item); + pa_array_remove(&array->array, item); + return 0; +} + +static inline unsigned pa_dynarray_size(pa_dynarray *array) +{ + return pa_array_get_len(&array->array, pa_dynarray_item); +} + +#define PA_DYNARRAY_FOREACH(elem, array, idx) \ + for ((idx) = 0; ((elem) = pa_dynarray_get(array, idx)); (idx)++) + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PA_DYNARRAY_H */ diff --git a/spa/plugins/alsa/acp/hashmap.h b/spa/plugins/alsa/acp/hashmap.h new file mode 100644 index 0000000..d8dbadb --- /dev/null +++ b/spa/plugins/alsa/acp/hashmap.h @@ -0,0 +1,219 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef PA_HASHMAP_H +#define PA_HASHMAP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "array.h" + +typedef unsigned (*pa_hash_func_t)(const void *p); +typedef int (*pa_compare_func_t)(const void *a, const void *b); + +typedef struct pa_hashmap_item { + void *key; + void *value; +} pa_hashmap_item; + +typedef struct pa_hashmap { + pa_array array; + pa_hash_func_t hash_func; + pa_compare_func_t compare_func; + pa_free_cb_t key_free_func; + pa_free_cb_t value_free_func; +} pa_hashmap; + +static inline pa_hashmap *pa_hashmap_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func) +{ + pa_hashmap *m = calloc(1, sizeof(pa_hashmap)); + pa_array_init(&m->array, 16); + m->hash_func = hash_func; + m->compare_func = compare_func; + return m; +} + +static inline pa_hashmap *pa_hashmap_new_full(pa_hash_func_t hash_func, pa_compare_func_t compare_func, + pa_free_cb_t key_free_func, pa_free_cb_t value_free_func) +{ + pa_hashmap *m = pa_hashmap_new(hash_func, compare_func); + m->key_free_func = key_free_func; + m->value_free_func = value_free_func; + return m; +} + +static inline void pa_hashmap_item_free(pa_hashmap *h, pa_hashmap_item *item) +{ + if (h->key_free_func && item->key) + h->key_free_func(item->key); + if (h->value_free_func && item->value) + h->value_free_func(item->value); +} + +static inline void pa_hashmap_remove_all(pa_hashmap *h) +{ + pa_hashmap_item *item; + pa_array_for_each(item, &h->array) + pa_hashmap_item_free(h, item); + pa_array_reset(&h->array); +} + +static inline void pa_hashmap_free(pa_hashmap *h) +{ + pa_hashmap_remove_all(h); + pa_array_clear(&h->array); + free(h); +} + +static inline pa_hashmap_item* pa_hashmap_find_free(pa_hashmap *h) +{ + pa_hashmap_item *item; + pa_array_for_each(item, &h->array) { + if (item->key == NULL) + return item; + } + return pa_array_add(&h->array, sizeof(*item)); +} + +static inline pa_hashmap_item* pa_hashmap_find(const pa_hashmap *h, const void *key) +{ + pa_hashmap_item *item = NULL; + pa_array_for_each(item, &h->array) { + if (item->key != NULL && h->compare_func(item->key, key) == 0) + return item; + } + return NULL; +} + +static inline void* pa_hashmap_get(const pa_hashmap *h, const void *key) +{ + const pa_hashmap_item *item = pa_hashmap_find(h, key); + if (item == NULL) + return NULL; + return item->value; +} + +static inline int pa_hashmap_put(pa_hashmap *h, void *key, void *value) +{ + pa_hashmap_item *item = pa_hashmap_find(h, key); + if (item != NULL) + return -1; + item = pa_hashmap_find_free(h); + item->key = key; + item->value = value; + return 0; +} + +static inline void* pa_hashmap_remove(pa_hashmap *h, const void *key) +{ + pa_hashmap_item *item = pa_hashmap_find(h, key); + void *value; + if (item == NULL) + return NULL; + value = item->value; + if (h->key_free_func) + h->key_free_func(item->key); + item->key = NULL; + item->value = NULL; + return value; +} + +static inline int pa_hashmap_remove_and_free(pa_hashmap *h, const void *key) +{ + void *val = pa_hashmap_remove(h, key); + if (val && h->value_free_func) + h->value_free_func(val); + return val ? 0 : -1; +} + +static inline void *pa_hashmap_first(const pa_hashmap *h) +{ + pa_hashmap_item *item; + pa_array_for_each(item, &h->array) { + if (item->key != NULL) + return item->value; + } + return NULL; +} + +static inline void *pa_hashmap_iterate(const pa_hashmap *h, void **state, const void **key) +{ + pa_hashmap_item *it = *state; + if (it == NULL) + *state = pa_array_first(&h->array); + do { + it = *state; + if (!pa_array_check(&h->array, it)) + return NULL; + *state = it + 1; + } while (it->key == NULL); + if (key) + *key = it->key; + return it->value; +} + +static inline bool pa_hashmap_isempty(const pa_hashmap *h) +{ + pa_hashmap_item *item; + pa_array_for_each(item, &h->array) + if (item->key != NULL) + return false; + return true; +} + +static inline unsigned pa_hashmap_size(const pa_hashmap *h) +{ + unsigned count = 0; + pa_hashmap_item *item; + pa_array_for_each(item, &h->array) + if (item->key != NULL) + count++; + return count; +} + +static inline void pa_hashmap_sort(pa_hashmap *h, + int (*compar)(const void *, const void *)) +{ + qsort((void*)h->array.data, + pa_array_get_len(&h->array, pa_hashmap_item), + sizeof(pa_hashmap_item), compar); +} + +#define PA_HASHMAP_FOREACH(e, h, state) \ + for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), NULL); \ + (e); (e) = pa_hashmap_iterate((h), &(state), NULL)) + +/* A macro to ease iteration through all key, value pairs */ +#define PA_HASHMAP_FOREACH_KV(k, e, h, state) \ + for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k)); \ + (e); (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k))) + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PA_HASHMAP_H */ diff --git a/spa/plugins/alsa/acp/idxset.h b/spa/plugins/alsa/acp/idxset.h new file mode 100644 index 0000000..6e88a84 --- /dev/null +++ b/spa/plugins/alsa/acp/idxset.h @@ -0,0 +1,200 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef PA_IDXSET_H +#define PA_IDXSET_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "array.h" + +#define PA_IDXSET_INVALID ((uint32_t) -1) + +typedef unsigned (*pa_hash_func_t)(const void *p); +typedef int (*pa_compare_func_t)(const void *a, const void *b); + +typedef struct pa_idxset_item { + void *ptr; +} pa_idxset_item; + +typedef struct pa_idxset { + pa_array array; + pa_hash_func_t hash_func; + pa_compare_func_t compare_func; +} pa_idxset; + +static inline unsigned pa_idxset_trivial_hash_func(const void *p) +{ + return PA_PTR_TO_UINT(p); +} + +static inline int pa_idxset_trivial_compare_func(const void *a, const void *b) +{ + return a < b ? -1 : (a > b ? 1 : 0); +} + +static inline unsigned pa_idxset_string_hash_func(const void *p) +{ + unsigned hash = 0; + const char *c; + for (c = p; *c; c++) + hash = 31 * hash + (unsigned) *c; + return hash; +} + +static inline int pa_idxset_string_compare_func(const void *a, const void *b) +{ + return strcmp(a, b); +} + +static inline pa_idxset *pa_idxset_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func) +{ + pa_idxset *s = calloc(1, sizeof(pa_idxset)); + pa_array_init(&s->array, 16); + s->hash_func = hash_func; + s->compare_func = compare_func; + return s; +} + +static inline void pa_idxset_free(pa_idxset *s, pa_free_cb_t free_cb) +{ + if (free_cb) { + pa_idxset_item *item; + pa_array_for_each(item, &s->array) + free_cb(item->ptr); + } + pa_array_clear(&s->array); + free(s); +} + +static inline pa_idxset_item* pa_idxset_find(const pa_idxset *s, const void *ptr) +{ + pa_idxset_item *item; + pa_array_for_each(item, &s->array) { + if (item->ptr == ptr) + return item; + } + return NULL; +} + +static inline int pa_idxset_put(pa_idxset*s, void *p, uint32_t *idx) +{ + pa_idxset_item *item = pa_idxset_find(s, p); + int res = item ? -1 : 0; + if (item == NULL) { + item = pa_idxset_find(s, NULL); + if (item == NULL) + item = pa_array_add(&s->array, sizeof(*item)); + item->ptr = p; + } + if (idx) + *idx = item - (pa_idxset_item*)s->array.data; + return res; +} + +static inline pa_idxset *pa_idxset_copy(pa_idxset *s, pa_copy_func_t copy_func) +{ + pa_idxset_item *item; + pa_idxset *copy = pa_idxset_new(s->hash_func, s->compare_func); + pa_array_for_each(item, &s->array) { + if (item->ptr) + pa_idxset_put(copy, copy_func ? copy_func(item->ptr) : item->ptr, NULL); + } + return copy; +} + +static inline bool pa_idxset_isempty(const pa_idxset *s) +{ + pa_idxset_item *item; + pa_array_for_each(item, &s->array) + if (item->ptr != NULL) + return false; + return true; +} +static inline unsigned pa_idxset_size(pa_idxset*s) +{ + unsigned count = 0; + pa_idxset_item *item; + pa_array_for_each(item, &s->array) + if (item->ptr != NULL) + count++; + return count; +} + +static inline void *pa_idxset_search(pa_idxset *s, uint32_t *idx) +{ + pa_idxset_item *item; + for (item = pa_array_get_unchecked(&s->array, *idx, pa_idxset_item); + pa_array_check(&s->array, item); item++, (*idx)++) { + if (item->ptr != NULL) + return item->ptr; + } + *idx = PA_IDXSET_INVALID; + return NULL; +} + +static inline void *pa_idxset_next(pa_idxset *s, uint32_t *idx) +{ + (*idx)++;; + return pa_idxset_search(s, idx); +} + +static inline void* pa_idxset_first(pa_idxset *s, uint32_t *idx) +{ + uint32_t i = 0; + void *ptr = pa_idxset_search(s, &i); + if (idx) + *idx = i; + return ptr; +} + +static inline void* pa_idxset_get_by_data(pa_idxset*s, const void *p, uint32_t *idx) +{ + pa_idxset_item *item = pa_idxset_find(s, p); + if (item == NULL) + return NULL; + if (idx) + *idx = item - (pa_idxset_item*)s->array.data; + return item->ptr; +} + +static inline void* pa_idxset_get_by_index(pa_idxset*s, uint32_t idx) +{ + pa_idxset_item *item; + if (!pa_array_check_index(&s->array, idx, pa_idxset_item)) + return NULL; + item = pa_array_get_unchecked(&s->array, idx, pa_idxset_item); + return item->ptr; +} + +#define PA_IDXSET_FOREACH(e, s, idx) \ + for ((e) = pa_idxset_first((s), &(idx)); (e); (e) = pa_idxset_next((s), &(idx))) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PA_IDXSET_H */ diff --git a/spa/plugins/alsa/acp/llist.h b/spa/plugins/alsa/acp/llist.h new file mode 100644 index 0000000..950375f --- /dev/null +++ b/spa/plugins/alsa/acp/llist.h @@ -0,0 +1,109 @@ +#ifndef foollistfoo +#define foollistfoo + +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with PulseAudio; if not, see . +***/ + +/* Some macros for maintaining doubly linked lists */ + +/* The head of the linked list. Use this in the structure that shall + * contain the head of the linked list */ +#define PA_LLIST_HEAD(t,name) \ + t *name + +/* The pointers in the linked list's items. Use this in the item structure */ +#define PA_LLIST_FIELDS(t) \ + t *next, *prev + +/* Initialize the list's head */ +#define PA_LLIST_HEAD_INIT(t,item) \ + do { \ + (item) = (t*) NULL; } \ + while(0) + +/* Initialize a list item */ +#define PA_LLIST_INIT(t,item) \ + do { \ + t *_item = (item); \ + pa_assert(_item); \ + _item->prev = _item->next = NULL; \ + } while(0) + +/* Prepend an item to the list */ +#define PA_LLIST_PREPEND(t,head,item) \ + do { \ + t **_head = &(head), *_item = (item); \ + pa_assert(_item); \ + if ((_item->next = *_head)) \ + _item->next->prev = _item; \ + _item->prev = NULL; \ + *_head = _item; \ + } while (0) + +/* Remove an item from the list */ +#define PA_LLIST_REMOVE(t,head,item) \ + do { \ + t **_head = &(head), *_item = (item); \ + pa_assert(_item); \ + if (_item->next) \ + _item->next->prev = _item->prev; \ + if (_item->prev) \ + _item->prev->next = _item->next; \ + else { \ + pa_assert(*_head == _item); \ + *_head = _item->next; \ + } \ + _item->next = _item->prev = NULL; \ + } while(0) + +/* Find the head of the list */ +#define PA_LLIST_FIND_HEAD(t,item,head) \ + do { \ + t **_head = (head), *_item = (item); \ + *_head = _item; \ + pa_assert(_head); \ + while ((*_head)->prev) \ + *_head = (*_head)->prev; \ + } while (0) + +/* Insert an item after another one (a = where, b = what) */ +#define PA_LLIST_INSERT_AFTER(t,head,a,b) \ + do { \ + t **_head = &(head), *_a = (a), *_b = (b); \ + pa_assert(_b); \ + if (!_a) { \ + if ((_b->next = *_head)) \ + _b->next->prev = _b; \ + _b->prev = NULL; \ + *_head = _b; \ + } else { \ + if ((_b->next = _a->next)) \ + _b->next->prev = _b; \ + _b->prev = _a; \ + _a->next = _b; \ + } \ + } while (0) + +#define PA_LLIST_FOREACH(i,head) \ + for (i = (head); i; i = i->next) + +#define PA_LLIST_FOREACH_SAFE(i,n,head) \ + for (i = (head); i && ((n = i->next), 1); i = n) + +#endif diff --git a/spa/plugins/alsa/acp/meson.build b/spa/plugins/alsa/acp/meson.build new file mode 100644 index 0000000..0ec97e2 --- /dev/null +++ b/spa/plugins/alsa/acp/meson.build @@ -0,0 +1,22 @@ +acp_sources = [ + 'acp.c', + 'compat.c', + 'alsa-mixer.c', + 'alsa-ucm.c', + 'alsa-util.c', + 'conf-parser.c', +] + +acp_c_args = [ + '-DHAVE_ALSA_UCM', + '-DHAVE_READLINK', +] + +acp_lib = static_library( + 'acp', + acp_sources, + c_args : acp_c_args, + include_directories : [configinc, includes_inc ], + dependencies : [ spa_dep, alsa_dep, mathlib, ] + ) +acp_dep = declare_dependency(link_with: acp_lib) diff --git a/spa/plugins/alsa/acp/proplist.h b/spa/plugins/alsa/acp/proplist.h new file mode 100644 index 0000000..ed4cc5d --- /dev/null +++ b/spa/plugins/alsa/acp/proplist.h @@ -0,0 +1,211 @@ +/* ALSA Card Profile + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef PA_PROPLIST_H +#define PA_PROPLIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "array.h" +#include "acp.h" + +#define PA_PROP_DEVICE_DESCRIPTION "device.description" + +#define PA_PROP_DEVICE_CLASS "device.class" + +#define PA_PROP_DEVICE_FORM_FACTOR "device.form_factor" + +#define PA_PROP_DEVICE_INTENDED_ROLES "device.intended_roles" + +#define PA_PROP_DEVICE_PROFILE_NAME "device.profile.name" + +#define PA_PROP_DEVICE_STRING "device.string" + +#define PA_PROP_DEVICE_API "device.api" + +#define PA_PROP_DEVICE_PRODUCT_NAME "device.product.name" + +#define PA_PROP_DEVICE_PROFILE_DESCRIPTION "device.profile.description" + +typedef struct pa_proplist_item { + char *key; + char *value; +} pa_proplist_item; + +typedef struct pa_proplist { + struct pa_array array; +} pa_proplist; + +static inline pa_proplist* pa_proplist_new(void) +{ + pa_proplist *p = calloc(1, sizeof(*p)); + pa_array_init(&p->array, 16); + return p; +} +static inline pa_proplist_item* pa_proplist_item_find(const pa_proplist *p, const void *key) +{ + pa_proplist_item *item; + pa_array_for_each(item, &p->array) { + if (strcmp(key, item->key) == 0) + return item; + } + return NULL; +} + +static inline void pa_proplist_item_free(pa_proplist_item* it) +{ + free(it->key); + free(it->value); +} + +static inline void pa_proplist_clear(pa_proplist* p) +{ + pa_proplist_item *item; + pa_array_for_each(item, &p->array) + pa_proplist_item_free(item); + pa_array_reset(&p->array); +} + +static inline void pa_proplist_free(pa_proplist* p) +{ + pa_proplist_clear(p); + pa_array_clear(&p->array); + free(p); +} + +static inline unsigned pa_proplist_size(const pa_proplist *p) +{ + return pa_array_get_len(&p->array, pa_proplist_item); +} + +static inline int pa_proplist_contains(const pa_proplist *p, const char *key) +{ + return pa_proplist_item_find(p, key) ? 1 : 0; +} + +static inline int pa_proplist_sets(pa_proplist *p, const char *key, const char *value) +{ + pa_proplist_item *item = pa_proplist_item_find(p, key); + if (item != NULL) + pa_proplist_item_free(item); + else + item = pa_array_add(&p->array, sizeof(*item)); + item->key = strdup(key); + item->value = strdup(value); + return 0; +} + +static inline int pa_proplist_unset(pa_proplist *p, const char *key) +{ + pa_proplist_item *item = pa_proplist_item_find(p, key); + if (item == NULL) + return -ENOENT; + pa_proplist_item_free(item); + pa_array_remove(&p->array, item); + return 0; +} + +static PA_PRINTF_FUNC(3,4) inline int pa_proplist_setf(pa_proplist *p, const char *key, const char *format, ...) +{ + pa_proplist_item *item = pa_proplist_item_find(p, key); + va_list args; + int res; + + va_start(args, format); + if (item != NULL) + pa_proplist_item_free(item); + else + item = pa_array_add(&p->array, sizeof(*item)); + item->key = strdup(key); + if ((res = vasprintf(&item->value, format, args)) < 0) + res = -errno; + va_end(args); + return res; +} + +static inline const char *pa_proplist_gets(const pa_proplist *p, const char *key) +{ + pa_proplist_item *item = pa_proplist_item_find(p, key); + return item ? item->value : NULL; +} + +typedef enum pa_update_mode { + PA_UPDATE_SET + /**< Replace the entire property list with the new one. Don't keep + * any of the old data around. */, + PA_UPDATE_MERGE + /**< Merge new property list into the existing one, not replacing + * any old entries if they share a common key with the new + * property list. */, + PA_UPDATE_REPLACE + /**< Merge new property list into the existing one, replacing all + * old entries that share a common key with the new property + * list. */ +} pa_update_mode_t; + + +static inline void pa_proplist_update(pa_proplist *p, pa_update_mode_t mode, const pa_proplist *other) +{ + pa_proplist_item *item; + + if (mode == PA_UPDATE_SET) + pa_proplist_clear(p); + + pa_array_for_each(item, &other->array) { + if (mode == PA_UPDATE_MERGE && pa_proplist_contains(p, item->key)) + continue; + pa_proplist_sets(p, item->key, item->value); + } +} + +static inline pa_proplist* pa_proplist_new_dict(const struct acp_dict *dict) +{ + pa_proplist *p = pa_proplist_new(); + if (dict) { + const struct acp_dict_item *item; + struct acp_dict_item *it; + acp_dict_for_each(item, dict) { + it = pa_array_add(&p->array, sizeof(*it)); + it->key = strdup(item->key); + it->value = strdup(item->value); + } + } + return p; +} + +static inline void pa_proplist_as_dict(const pa_proplist *p, struct acp_dict *dict) +{ + dict->n_items = pa_proplist_size(p); + dict->items = p->array.data; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PA_PROPLIST_H */ diff --git a/spa/plugins/alsa/acp/volume.h b/spa/plugins/alsa/acp/volume.h new file mode 100644 index 0000000..b916bd2 --- /dev/null +++ b/spa/plugins/alsa/acp/volume.h @@ -0,0 +1,207 @@ +/*** + This file is part of PulseAudio. + + Copyright 2004-2006 Lennart Poettering + Copyright 2006 Pierre Ossman 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 (isinf(dB) < 0 || dB <= PA_DECIBEL_MININFTY) + return PA_VOLUME_MUTED; + return pa_sw_volume_from_linear(pa_volume_dB_to_linear(dB)); +} + +static inline pa_cvolume* pa_cvolume_set(pa_cvolume *a, unsigned channels, pa_volume_t v) +{ + uint32_t i; + a->channels = (uint8_t) channels; + for (i = 0; i < a->channels; i++) + a->values[i] = PA_CLAMP_VOLUME(v); + return a; +} + +static inline int pa_cvolume_equal(const pa_cvolume *a, const pa_cvolume *b) +{ + uint32_t i; + if (PA_UNLIKELY(a == b)) + return 1; + if (a->channels != b->channels) + return 0; + for (i = 0; i < a->channels; i++) + if (a->values[i] != b->values[i]) + return 0; + return 1; +} + +static inline pa_volume_t pa_sw_volume_multiply(pa_volume_t a, pa_volume_t b) +{ + uint64_t result; + result = ((uint64_t) a * (uint64_t) b + (uint64_t) PA_VOLUME_NORM / 2ULL) / + (uint64_t) PA_VOLUME_NORM; + if (result > (uint64_t)PA_VOLUME_MAX) + pa_log_warn("pa_sw_volume_multiply: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings."); + return (pa_volume_t) PA_CLAMP_VOLUME(result); +} + +static inline pa_cvolume *pa_sw_cvolume_multiply(pa_cvolume *dest, + const pa_cvolume *a, const pa_cvolume *b) +{ + unsigned i; + dest->channels = PA_MIN(a->channels, b->channels); + for (i = 0; i < dest->channels; i++) + dest->values[i] = pa_sw_volume_multiply(a->values[i], b->values[i]); + return dest; +} + +static inline pa_cvolume *pa_sw_cvolume_multiply_scalar(pa_cvolume *dest, + const pa_cvolume *a, pa_volume_t b) +{ + unsigned i; + for (i = 0; i < a->channels; i++) + dest->values[i] = pa_sw_volume_multiply(a->values[i], b); + dest->channels = (uint8_t) i; + return dest; +} + +static inline pa_volume_t pa_sw_volume_divide(pa_volume_t a, pa_volume_t b) +{ + uint64_t result; + if (b <= PA_VOLUME_MUTED) + return 0; + result = ((uint64_t) a * (uint64_t) PA_VOLUME_NORM + (uint64_t) b / 2ULL) / (uint64_t) b; + if (result > (uint64_t)PA_VOLUME_MAX) + pa_log_warn("pa_sw_volume_divide: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings."); + return (pa_volume_t) PA_CLAMP_VOLUME(result); +} + +static inline pa_cvolume *pa_sw_cvolume_divide_scalar(pa_cvolume *dest, + const pa_cvolume *a, pa_volume_t b) { + unsigned i; + for (i = 0; i < a->channels; i++) + dest->values[i] = pa_sw_volume_divide(a->values[i], b); + dest->channels = (uint8_t) i; + return dest; +} + +static inline pa_cvolume *pa_sw_cvolume_divide(pa_cvolume *dest, + const pa_cvolume *a, const pa_cvolume *b) +{ + unsigned i; + dest->channels = PA_MIN(a->channels, b->channels); + for (i = 0; i < dest->channels; i++) + dest->values[i] = pa_sw_volume_divide(a->values[i], b->values[i]); + return dest; +} + +#define pa_cvolume_reset(a, n) pa_cvolume_set((a), (n), PA_VOLUME_NORM) +#define pa_cvolume_mute(a, n) pa_cvolume_set((a), (n), PA_VOLUME_MUTED) + +static inline int pa_cvolume_compatible_with_channel_map(const pa_cvolume *v, + const pa_channel_map *cm) +{ + return v->channels == cm->channels; +} + +static inline pa_volume_t pa_cvolume_max(const pa_cvolume *a) +{ + pa_volume_t m = PA_VOLUME_MUTED; + unsigned c; + for (c = 0; c < a->channels; c++) + if (a->values[c] > m) + m = a->values[c]; + return m; +} + +static inline pa_volume_t pa_cvolume_min(const pa_cvolume *a) +{ + pa_volume_t m = PA_VOLUME_MAX; + unsigned c; + for (c = 0; c < a->channels; c++) + if (a->values[c] < m) + m = a->values[c]; + return m; +} + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PA_VOLUME_H */ diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c new file mode 100644 index 0000000..bcd7491 --- /dev/null +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -0,0 +1,1133 @@ +/* Spa ALSA Device + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 emit_node(struct impl *this, struct acp_device *dev) +{ + struct spa_dict_item *items; + const struct acp_dict_item *it; + uint32_t n_items, i; + char device_name[128], path[180], channels[16], ch[12], routes[16]; + char card_index[16], *p; + char positions[SPA_AUDIO_MAX_CHANNELS * 12]; + struct spa_device_object_info info; + struct acp_card *card = this->card; + const char *stream, *devstr; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + + if (dev->direction == ACP_DIRECTION_PLAYBACK) { + info.factory_name = SPA_NAME_API_ALSA_PCM_SINK; + stream = "playback"; + } else { + info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE; + stream = "capture"; + } + + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + + items = alloca((dev->props.n_items + 8) * sizeof(*items)); + n_items = 0; + + snprintf(card_index, sizeof(card_index), "%d", card->index); + + devstr = dev->device_strings[0]; + p = strstr(devstr, "%f"); + if (p) { + snprintf(device_name, sizeof(device_name), "%.*s%d%s", + (int)SPA_PTRDIFF(p, devstr), devstr, + card->index, p+2); + } else { + snprintf(device_name, sizeof(device_name), "%s", devstr); + } + snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", card_index, device_name, stream); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name); + if (dev->flags & ACP_DEVICE_UCM_DEVICE) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_OPEN_UCM, "true"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card_index); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, stream); + + snprintf(channels, sizeof(channels), "%d", dev->format.channels); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels); + + p = positions; + for (i = 0; i < dev->format.channels; i++) { + p += snprintf(p, 12, "%s%s", i == 0 ? "" : ",", + acp_channel_str(ch, sizeof(ch), dev->format.map[i])); + } + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions); + + snprintf(routes, sizeof(routes), "%d", dev->n_ports); + items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes); + + acp_dict_for_each(it, &dev->props) + items[n_items++] = SPA_DICT_ITEM_INIT(it->key, it->value); + + info.props = &SPA_DICT_INIT(items, n_items); + + spa_device_emit_object_info(&this->hooks, dev->index, &info); + + return 0; +} + +static int emit_info(struct impl *this, bool full) +{ + int err = 0; + struct spa_dict_item *items; + uint32_t n_items; + const struct acp_dict_item *it; + struct acp_card *card = this->card; + char path[128]; + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + n_items = card->props.n_items + 4; + items = alloca(n_items * sizeof(*items)); + + n_items = 0; +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + snprintf(path, sizeof(path), "alsa:pcm:%d", card->index); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device); + acp_dict_for_each(it, &card->props) + ADD_ITEM(it->key, it->value); + this->info.props = &SPA_DICT_INIT(items, n_items); +#undef ADD_ITEM + + if (this->info.change_mask & SPA_DEVICE_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(this->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } + return err; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + struct acp_card *card; + struct acp_card_profile *profile; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + card = this->card; + if (card->active_profile_index < card->n_profiles) + profile = card->profiles[card->active_profile_index]; + else + profile = NULL; + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info || events->object_info) + emit_info(this, true); + + if (profile) { + for (i = 0; i < profile->n_devices; i++) + emit_node(this, profile->devices[i]); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static struct spa_pod *build_profile(struct spa_pod_builder *b, uint32_t id, + struct acp_card_profile *pr, bool current) +{ + struct spa_pod_frame f[2]; + uint32_t i, n_classes, n_capture = 0, n_playback = 0; + uint32_t *capture, *playback; + + capture = alloca(sizeof(uint32_t) * pr->n_devices); + playback = alloca(sizeof(uint32_t) * pr->n_devices); + + for (i = 0; i < pr->n_devices; i++) { + struct acp_device *dev = pr->devices[i]; + switch (dev->direction) { + case ACP_DIRECTION_PLAYBACK: + playback[n_playback++] = dev->index; + break; + case ACP_DIRECTION_CAPTURE: + capture[n_capture++] = dev->index; + break; + } + } + n_classes = n_capture > 0 ? 1 : 0; + n_classes += n_playback > 0 ? 1 : 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id); + spa_pod_builder_add(b, + SPA_PARAM_PROFILE_index, SPA_POD_Int(pr->index), + SPA_PARAM_PROFILE_name, SPA_POD_String(pr->name), + SPA_PARAM_PROFILE_description, SPA_POD_String(pr->description), + SPA_PARAM_PROFILE_priority, SPA_POD_Int(pr->priority), + SPA_PARAM_PROFILE_available, SPA_POD_Id(pr->available), + 0); + spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0); + spa_pod_builder_push_struct(b, &f[1]); + spa_pod_builder_int(b, n_classes); + if (n_capture > 0) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Source"), + SPA_POD_Int(n_capture), + SPA_POD_String("card.profile.devices"), + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, + n_capture, capture)); + } + if (n_playback > 0) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Sink"), + SPA_POD_Int(n_playback), + SPA_POD_String("card.profile.devices"), + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, + n_playback, playback)); + } + spa_pod_builder_pop(b, &f[1]); + if (current) { + spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0); + spa_pod_builder_bool(b, SPA_FLAG_IS_SET(pr->flags, ACP_PROFILE_SAVE)); + } + + return spa_pod_builder_pop(b, &f[0]); +} + +static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id, + struct acp_port *p, struct acp_device *dev, uint32_t profile) +{ + struct spa_pod_frame f[2]; + const struct acp_dict_item *item; + uint32_t i; + enum spa_direction direction; + + switch (p->direction) { + case ACP_DIRECTION_PLAYBACK: + direction = SPA_DIRECTION_OUTPUT; + break; + case ACP_DIRECTION_CAPTURE: + direction = SPA_DIRECTION_INPUT; + break; + default: + errno = EINVAL; + return NULL; + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id); + spa_pod_builder_add(b, + SPA_PARAM_ROUTE_index, SPA_POD_Int(p->index), + SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction), + SPA_PARAM_ROUTE_name, SPA_POD_String(p->name), + SPA_PARAM_ROUTE_description, SPA_POD_String(p->description), + SPA_PARAM_ROUTE_priority, SPA_POD_Int(p->priority), + SPA_PARAM_ROUTE_available, SPA_POD_Id(p->available), + 0); + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, SPA_POD_PROP_FLAG_HINT_DICT); + spa_pod_builder_push_struct(b, &f[1]); + spa_pod_builder_int(b, p->props.n_items + (dev ? 2 : 0)); + acp_dict_for_each(item, &p->props) { + spa_pod_builder_add(b, + SPA_POD_String(item->key), + SPA_POD_String(item->value), + NULL); + } + if (dev != NULL) { + const char *str; + str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ? "true" : "false"; + spa_pod_builder_add(b, + SPA_POD_String("route.hw-mute"), + SPA_POD_String(str), NULL); + + str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ? "true" : "false"; + spa_pod_builder_add(b, + SPA_POD_String("route.hw-volume"), + SPA_POD_String(str), NULL); + } + spa_pod_builder_pop(b, &f[1]); + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0); + spa_pod_builder_push_array(b, &f[1]); + for (i = 0; i < p->n_profiles; i++) + spa_pod_builder_int(b, p->profiles[i]->index); + spa_pod_builder_pop(b, &f[1]); + if (dev != NULL) { + uint32_t channels = dev->format.channels; + float volumes[channels]; + float soft_volumes[channels]; + bool mute; + + acp_device_get_mute(dev, &mute); + spa_zero(volumes); + spa_zero(soft_volumes); + acp_device_get_volume(dev, volumes, channels); + acp_device_get_soft_volume(dev, soft_volumes, channels); + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0); + spa_pod_builder_int(b, dev->index); + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0); + spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id); + + spa_pod_builder_prop(b, SPA_PROP_mute, + SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ? + SPA_POD_PROP_FLAG_HARDWARE : 0); + spa_pod_builder_bool(b, mute); + + spa_pod_builder_prop(b, SPA_PROP_channelVolumes, + SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ? + SPA_POD_PROP_FLAG_HARDWARE : 0); + spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float, + channels, volumes); + + spa_pod_builder_prop(b, SPA_PROP_volumeBase, SPA_POD_PROP_FLAG_READONLY); + spa_pod_builder_float(b, dev->base_volume); + spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY); + spa_pod_builder_float(b, dev->volume_step); + + spa_pod_builder_prop(b, SPA_PROP_channelMap, 0); + spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id, + channels, dev->format.map); + + spa_pod_builder_prop(b, SPA_PROP_softVolumes, 0); + spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float, + channels, soft_volumes); + + spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0); + spa_pod_builder_long(b, dev->latency_ns); + + if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_IEC958)) { + spa_pod_builder_prop(b, SPA_PROP_iec958Codecs, 0); + spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id, + dev->n_codecs, dev->codecs); + } + + spa_pod_builder_pop(b, &f[1]); + } + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0); + spa_pod_builder_push_array(b, &f[1]); + for (i = 0; i < p->n_devices; i++) + spa_pod_builder_int(b, p->devices[i]->index); + spa_pod_builder_pop(b, &f[1]); + + if (profile != SPA_ID_INVALID) { + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0); + spa_pod_builder_int(b, profile); + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0); + spa_pod_builder_bool(b, SPA_FLAG_IS_SET(p->flags, ACP_PORT_SAVE)); + } + return spa_pod_builder_pop(b, &f[0]); +} + +static struct acp_port *find_port_for_device(struct acp_card *card, struct acp_device *dev) +{ + uint32_t i; + for (i = 0; i < dev->n_ports; i++) { + struct acp_port *p = dev->ports[i]; + if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_ACTIVE)) + return p; + } + return NULL; +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_device_params result; + uint32_t count = 0; + struct acp_card *card; + struct acp_card_profile *pr; + struct acp_port *p; + struct acp_device *dev; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + card = this->card; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumProfile: + if (result.index >= card->n_profiles) + return 0; + + pr = card->profiles[result.index]; + param = build_profile(&b, id, pr, false); + break; + + case SPA_PARAM_Profile: + if (result.index > 0 || card->active_profile_index >= card->n_profiles) + return 0; + + pr = card->profiles[card->active_profile_index]; + param = build_profile(&b, id, pr, true); + break; + + case SPA_PARAM_EnumRoute: + if (result.index >= card->n_ports) + return 0; + + p = card->ports[result.index]; + param = build_route(&b, id, p, NULL, SPA_ID_INVALID); + break; + + case SPA_PARAM_Route: + while (true) { + if (result.index >= card->n_devices) + return 0; + + dev = card->devices[result.index]; + if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_ACTIVE) && + (p = find_port_for_device(card, dev)) != NULL) + break; + + result.index++; + } + result.next = result.index + 1; + param = build_route(&b, id, p, dev, card->active_profile_index); + if (param == NULL) + return -errno; + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_device_emit_result(&this->hooks, seq, 0, + SPA_RESULT_TYPE_DEVICE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static void on_latency_changed(void *data, struct acp_device *dev) +{ + struct impl *this = data; + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + + spa_log_info(this->log, "device %s latency changed", dev->name); + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].user++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, dev->index); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(dev->latency_ns)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); +} + +static void on_codecs_changed(void *data, struct acp_device *dev) +{ + struct impl *this = data; + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + + spa_log_info(this->log, "device %s codecs changed", dev->name); + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].user++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, dev->index); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, dev->n_codecs, dev->codecs)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); +} + +static int apply_device_props(struct impl *this, struct acp_device *dev, struct spa_pod *props) +{ + float volume = 0; + bool mute = 0; + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) props; + int changed = 0; + float volumes[ACP_MAX_CHANNELS]; + uint32_t channels[ACP_MAX_CHANNELS]; + uint32_t n_volumes = 0; + + if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props)) + return -EINVAL; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + if (spa_pod_get_float(&prop->value, &volume) == 0) { + acp_device_set_volume(dev, &volume, 1); + changed++; + } + break; + case SPA_PROP_mute: + if (spa_pod_get_bool(&prop->value, &mute) == 0) { + acp_device_set_mute(dev, mute); + changed++; + } + break; + case SPA_PROP_channelVolumes: + if ((n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + volumes, ACP_MAX_CHANNELS)) > 0) { + changed++; + } + break; + case SPA_PROP_channelMap: + if (spa_pod_copy_array(&prop->value, SPA_TYPE_Id, + channels, ACP_MAX_CHANNELS) > 0) { + changed++; + } + break; + case SPA_PROP_latencyOffsetNsec: + { + int64_t latency_ns; + if (spa_pod_get_long(&prop->value, &latency_ns) == 0) { + if (dev->latency_ns != latency_ns) { + dev->latency_ns = latency_ns; + on_latency_changed(this, dev); + changed++; + } + } + break; + } + case SPA_PROP_iec958Codecs: + { + uint32_t codecs[32], n_codecs; + + n_codecs = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, + codecs, SPA_N_ELEMENTS(codecs)); + if (n_codecs != dev->n_codecs || + memcmp(dev->codecs, codecs, n_codecs * sizeof(uint32_t)) != 0) { + memcpy(dev->codecs, codecs, n_codecs * sizeof(uint32_t)); + dev->n_codecs = n_codecs; + on_codecs_changed(this, dev); + changed++; + } + break; + } + default: + break; + } + } + if (n_volumes > 0) + acp_device_set_volume(dev, volumes, n_volumes); + + return changed; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Profile: + { + uint32_t idx; + bool save = false; + + if (param == NULL) { + idx = acp_card_find_best_profile_index(this->card, NULL); + save = true; + } else if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx), + SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) { + spa_log_warn(this->log, "can't parse profile"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + + acp_card_set_profile(this->card, idx, save ? ACP_PROFILE_SAVE : 0); + emit_info(this, false); + break; + } + case SPA_PARAM_Route: + { + uint32_t idx, device; + struct spa_pod *props = NULL; + struct acp_device *dev; + bool save = false; + + if (param == NULL) + return -EINVAL; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamRoute, NULL, + SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx), + SPA_PARAM_ROUTE_device, SPA_POD_Int(&device), + SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props), + SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) { + spa_log_warn(this->log, "can't parse route"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + if (device >= this->card->n_devices) + return -EINVAL; + + dev = this->card->devices[device]; + acp_device_set_port(dev, idx, save ? ACP_PORT_SAVE : 0); + if (props) + apply_device_props(this, dev, props); + emit_info(this, false); + break; + } + default: + return -ENOENT; + } + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static void card_props_changed(void *data) +{ + struct impl *this = data; + spa_log_info(this->log, "card properties changed"); +} + +static bool has_device(struct acp_card_profile *pr, uint32_t index) +{ + uint32_t i; + + for (i = 0; i < pr->n_devices; i++) + if (pr->devices[i]->index == index) + return true; + return false; +} + +static void card_profile_changed(void *data, uint32_t old_index, uint32_t new_index) +{ + struct impl *this = data; + struct acp_card *card = this->card; + struct acp_card_profile *op = card->profiles[old_index]; + struct acp_card_profile *np = card->profiles[new_index]; + uint32_t i; + + spa_log_info(this->log, "card profile changed from %s to %s", + op->name, np->name); + + for (i = 0; i < op->n_devices; i++) { + uint32_t index = op->devices[i]->index; + if (has_device(np, index)) + continue; + spa_device_emit_object_info(&this->hooks, index, NULL); + } + for (i = 0; i < np->n_devices; i++) { + emit_node(this, np->devices[i]); + } + setup_sources(this); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Profile].user++; + this->params[IDX_Route].user++; + this->params[IDX_EnumRoute].user++; +} + +static void card_profile_available(void *data, uint32_t index, + enum acp_available old, enum acp_available available) +{ + struct impl *this = data; + struct acp_card *card = this->card; + struct acp_card_profile *p = card->profiles[index]; + + spa_log_info(this->log, "card profile %s available %s -> %s", p->name, + acp_available_str(old), acp_available_str(available)); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_EnumProfile].user++; + this->params[IDX_Profile].user++; + + if (this->props.auto_profile) { + uint32_t best = acp_card_find_best_profile_index(card, NULL); + acp_card_set_profile(card, best, 0); + } +} + +static void card_port_changed(void *data, uint32_t old_index, uint32_t new_index) +{ + struct impl *this = data; + struct acp_card *card = this->card; + struct acp_port *op = card->ports[old_index]; + struct acp_port *np = card->ports[new_index]; + + spa_log_info(this->log, "card port changed from %s to %s", + op->name, np->name); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].user++; +} + +static void card_port_available(void *data, uint32_t index, + enum acp_available old, enum acp_available available) +{ + struct impl *this = data; + struct acp_card *card = this->card; + struct acp_port *p = card->ports[index]; + + spa_log_info(this->log, "card port %s available %s -> %s", p->name, + acp_available_str(old), acp_available_str(available)); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_EnumRoute].user++; + this->params[IDX_Route].user++; + + if (this->props.auto_port) { + uint32_t i; + + for (i = 0; i < p->n_devices; i++) { + struct acp_device *d = p->devices[i]; + uint32_t best; + + if (!(d->flags & ACP_DEVICE_ACTIVE)) + continue; + + best = acp_device_find_best_port_index(d, NULL); + acp_device_set_port(d, best, 0); + } + } +} + +static void on_volume_changed(void *data, struct acp_device *dev) +{ + struct impl *this = data; + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + uint32_t n_volume = dev->format.channels; + float volume[n_volume]; + float soft_volume[n_volume]; + + spa_log_info(this->log, "device %s volume changed", dev->name); + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].user++; + + spa_zero(volume); + spa_zero(soft_volume); + acp_device_get_volume(dev, volume, n_volume); + acp_device_get_soft_volume(dev, soft_volume, n_volume); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, dev->index); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, n_volume, volume), + SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, dev->format.channels, + dev->format.map), + SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, n_volume, soft_volume)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); +} + +static void on_mute_changed(void *data, struct acp_device *dev) +{ + struct impl *this = data; + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + bool mute; + + spa_log_info(this->log, "device %s mute changed", dev->name); + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].user++; + + acp_device_get_mute(dev, &mute); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, dev->index); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_mute, SPA_POD_Bool(mute), + SPA_PROP_softMute, SPA_POD_Bool(mute)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); +} + +static const struct acp_card_events card_events = { + ACP_VERSION_CARD_EVENTS, + .props_changed = card_props_changed, + .profile_changed = card_profile_changed, + .profile_available = card_profile_available, + .port_changed = card_port_changed, + .port_available = card_port_available, + .volume_changed = on_volume_changed, + .mute_changed = on_mute_changed, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static SPA_PRINTF_FUNC(6,0) void impl_acp_log_func(void *data, + int level, const char *file, int line, const char *func, + const char *fmt, va_list arg) +{ + struct spa_log *log = data; + spa_log_logv(log, (enum spa_log_level)level, file, line, func, fmt, arg); +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + remove_sources(this); + if (this->card) { + acp_card_destroy(this->card); + this->card = NULL; + } + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + struct acp_dict_item *items = NULL; + const struct spa_dict_item *it; + uint32_t n_items = 0; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + acp_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N); + if (this->loop == NULL) { + spa_log_error(this->log, "a Loop interface is needed"); + return -EINVAL; + } + + acp_set_log_func(impl_acp_log_func, this->log); + acp_set_log_level(6); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + if (info) { + if ((str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH)) != NULL) + snprintf(this->props.device, sizeof(this->props.device), "%s", str); + if ((str = spa_dict_lookup(info, "api.acp.auto-port")) != NULL) + this->props.auto_port = spa_atob(str); + if ((str = spa_dict_lookup(info, "api.acp.auto-profile")) != NULL) + this->props.auto_profile = spa_atob(str); + + items = alloca((info->n_items) * sizeof(*items)); + spa_dict_for_each(it, info) + items[n_items++] = ACP_DICT_ITEM_INIT(it->key, it->value); + } + + spa_log_debug(this->log, "probe card %s", this->props.device); + if ((str = strchr(this->props.device, ':')) == NULL) + return -EINVAL; + + this->card = acp_card_new(atoi(str+1), &ACP_DICT_INIT(items, n_items)); + if (this->card == NULL) + return -errno; + + setup_sources(this); + + acp_card_add_listener(this->card, &card_events, this); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS | + SPA_DEVICE_CHANGE_MASK_PARAMS; + + this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE); + this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ); + this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 4; + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_alsa_acp_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_ALSA_ACP_DEVICE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/alsa/alsa-compress-offload-sink.c b/spa/plugins/alsa/alsa-compress-offload-sink.c new file mode 100644 index 0000000..966e680 --- /dev/null +++ b/spa/plugins/alsa/alsa-compress-offload-sink.c @@ -0,0 +1,1143 @@ +/* Spa ALSA Compress-Offload sink + * + * Copyright © 2022 Wim Taymans + * © 2022 Asymptotic Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 + +/* + * This creates a PipeWire sink node which uses the tinycompress user space + * library to use the ALSA Compress-Offload API for writing compressed data + * like MP3, FLAC etc. to an DSP that can handle such data directly. + * + * These show up under /dev/snd like comprCxDx, as opposed to regular + * ALSA PCM devices. + * + * root@dragonboard-845c:~# ls /dev/snd + * by-path comprC0D3 controlC0 pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p pcmC0D2c pcmC0D2p timer + * + * ## Example configuration + *\code{.unparsed} + * context.objects = [ + * { factory = spa-node-factory + * args = { + * factory.name = api.alsa.compress.offload.sink + * node.name = Compress-Offload-Sink + * media.class = "Audio/Sink" + * api.alsa.path = "hw:0,3" + * } + * } + *] + *\endcode + * + * TODO: + * - Clocking + * - Implement pause and resume + * - Having a better wait mechanism + * - Automatic loading using alsa-udev + * + */ + +#define NAME "compress-offload-audio-sink" +#define DEFAULT_CHANNELS 2 +#define DEFAULT_RATE 44100 +#define MAX_BUFFERS 4 +#define MAX_PORTS 1 +#define MAX_CODECS 32 /* See include/sound/compress_params.h */ + +#define MIN_FRAGMENT_SIZE (4 * 1024) +#define MAX_FRAGMENT_SIZE (64 * 1024) +#define MIN_NUM_FRAGMENTS (4) +#define MAX_NUM_FRAGMENTS (8 * 4) + +struct props { + uint32_t channels; + uint32_t rate; + uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + char device[64]; +}; + +static void reset_props(struct props *props) +{ + props->channels = 0; + props->rate = 0; +} + +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_buffer *outbuf; +}; + +struct impl; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + struct spa_audio_info current_format; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + uint32_t written; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + struct spa_log *log; + struct props props; + + struct spa_node_info info; + struct spa_param_info params[1]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + struct port port; + + unsigned int started_node:1; + unsigned int started_compress:1; + uint64_t info_all; + uint32_t quantum_limit; + + struct compr_config compr_conf; + struct snd_codec codec; + struct compress *compress; + + int32_t codecs_supported[MAX_CODECS]; + uint32_t num_codecs; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS) + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "alsa" }, + { SPA_KEY_MEDIA_CLASS, "Audio/Sink" }, + { SPA_KEY_NODE_DRIVER, "false" }, + { SPA_KEY_NODE_PAUSE_ON_IDLE, "false" }, +}; + +static const struct codec_id { + uint32_t codec_id; +} codec_info[] = { + { SND_AUDIOCODEC_MP3, }, + { SND_AUDIOCODEC_AAC, }, + { SND_AUDIOCODEC_WMA, }, + { SND_AUDIOCODEC_VORBIS, }, + { SND_AUDIOCODEC_FLAC, }, + { SND_AUDIOCODEC_ALAC, }, + { SND_AUDIOCODEC_APE, }, + { SND_AUDIOCODEC_REAL, }, + { SND_AUDIOCODEC_AMR, }, + { SND_AUDIOCODEC_AMRWB, }, +}; + +static int +open_compress(struct impl *this) +{ + struct compress *compress; + + compress = compress_open_by_name(this->props.device, COMPRESS_IN, &this->compr_conf); + if (!compress || !is_compress_ready(compress)) { + spa_log_error(this->log, NAME " %p: Unable to open compress device", this); + return -EINVAL; + } + + this->compress = compress; + + compress_nonblock(this->compress, 1); + + return 0; +} + +static int +write_compress(struct impl *this, void *buf, int32_t size) +{ + int32_t wrote; + int32_t to_write = size; + struct port *port = &this->port; + +retry: + wrote = compress_write(this->compress, buf, to_write); + if (wrote < 0) { + spa_log_error(this->log, NAME " %p: Error playing sample: %s", + this, compress_get_error(this->compress)); + return wrote; + } + port->written += wrote; + + spa_log_debug(this->log, NAME " %p: We wrote %d, DSP accepted %d\n", this, size, wrote); + + if (wrote < to_write) { + /* + * The choice of 20ms as the time to wait is + * completely arbitrary. + */ + compress_wait(this->compress, 20); + buf = (uint8_t *)buf + wrote; + to_write = to_write - wrote; + goto retry; + } + + /* + * One write has to happen before starting the compressed node. Calling + * compress_start before writing MIN_NUM_FRAGMENTS * MIN_FRAGMENT_SIZE + * will result in a distorted audio playback. + */ + if (!this->started_compress && + (port->written >= (MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS))) { + compress_start(this->compress); + this->started_compress = true; + } + + return size; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + case SPA_PARAM_PortConfig: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough)); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int +impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int do_start(struct impl *this) +{ + if (this->started_node) + return 0; + + spa_log_debug(this->log, "Open compressed device: %s", this->props.device); + if (open_compress(this) < 0) + return -EINVAL; + + this->started_node = true; + this->started_compress = false; + + return 0; +} + +static int do_drain(struct impl *this) +{ + if (!this->started_node) + return 0; + + if (this->started_compress) { + spa_log_debug(this->log, NAME " %p: Issuing drain command", this); + compress_drain(this->compress); + spa_log_debug(this->log, NAME " %p: Finished drain", this); + } + + return 0; +} + +static int do_stop(struct impl *this) +{ + if (!this->started_node) + return 0; + + compress_stop(this->compress); + compress_close(this->compress); + spa_log_info(this->log, NAME " %p: Closed compress device", this); + + this->compress = NULL; + this->started_node = false; + this->started_compress = false; + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + do_start(this); + break; + } + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + do_drain(this); + do_stop(this); + break; + + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +port_enum_formats(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_audio_info info; + uint32_t codec; + + if (index >= this->num_codecs) + return 0; + + codec = this->codecs_supported[index]; + + spa_zero(info); + info.media_type = SPA_MEDIA_TYPE_audio; + + switch (codec) { + case SND_AUDIOCODEC_MP3: + info.media_subtype = SPA_MEDIA_SUBTYPE_mp3; + info.info.mp3.rate = this->props.rate; + info.info.mp3.channels = this->props.channels; + break; + case SND_AUDIOCODEC_AAC: + info.media_subtype = SPA_MEDIA_SUBTYPE_aac; + info.info.aac.rate = this->props.rate; + info.info.aac.channels = this->props.channels; + break; + case SND_AUDIOCODEC_WMA: + info.media_subtype = SPA_MEDIA_SUBTYPE_wma; + info.info.wma.rate = this->props.rate; + info.info.wma.channels = this->props.channels; + break; + case SND_AUDIOCODEC_VORBIS: + info.media_subtype = SPA_MEDIA_SUBTYPE_vorbis; + info.info.vorbis.rate = this->props.rate; + info.info.vorbis.channels = this->props.channels; + break; + case SND_AUDIOCODEC_FLAC: + info.media_subtype = SPA_MEDIA_SUBTYPE_flac; + info.info.flac.rate = this->props.rate; + info.info.flac.channels = this->props.channels; + break; + case SND_AUDIOCODEC_ALAC: + info.media_subtype = SPA_MEDIA_SUBTYPE_alac; + info.info.alac.rate = this->props.rate; + info.info.alac.channels = this->props.channels; + break; + case SND_AUDIOCODEC_APE: + info.media_subtype = SPA_MEDIA_SUBTYPE_ape; + info.info.ape.rate = this->props.rate; + info.info.ape.channels = this->props.channels; + break; + case SND_AUDIOCODEC_REAL: + info.media_subtype = SPA_MEDIA_SUBTYPE_ra; + info.info.ra.rate = this->props.rate; + info.info.ra.channels = this->props.channels; + break; + case SND_AUDIOCODEC_AMR: + info.media_subtype = SPA_MEDIA_SUBTYPE_amr; + info.info.amr.rate = this->props.rate; + info.info.amr.channels = this->props.channels; + info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB; + break; + case SND_AUDIOCODEC_AMRWB: + info.media_subtype = SPA_MEDIA_SUBTYPE_amr; + info.info.amr.rate = this->props.rate; + info.info.amr.channels = this->props.channels; + info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB; + break; + default: + return -ENOTSUP; + } + if ((*param = spa_format_audio_build(builder, SPA_PARAM_EnumFormat, &info)) == NULL) + return -errno; + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, ¶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_build(&b, id, &port->current_format); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(0), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, + MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, + MAX_FRAGMENT_SIZE), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(0)); + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_info(this->log, NAME " %p: clear buffers", this); + port->n_buffers = 0; + this->started_node = false; + } + return 0; +} + +static int +compress_setup(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate) +{ + struct compr_config *config; + struct snd_codec *codec; + uint32_t channels, rate; + + memset(&this->codec, 0, sizeof(this->codec)); + memset(&this->compr_conf, 0, sizeof(this->compr_conf)); + + config = &this->compr_conf; + codec = &this->codec; + + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_vorbis: + codec->id = SND_AUDIOCODEC_VORBIS; + rate = info->info.vorbis.rate; + channels = info->info.vorbis.channels; + break; + case SPA_MEDIA_SUBTYPE_mp3: + codec->id = SND_AUDIOCODEC_MP3; + rate = info->info.mp3.rate; + channels = info->info.mp3.channels; + break; + case SPA_MEDIA_SUBTYPE_aac: + codec->id = SND_AUDIOCODEC_AAC; + rate = info->info.aac.rate; + channels = info->info.aac.channels; + break; + case SPA_MEDIA_SUBTYPE_flac: + codec->id = SND_AUDIOCODEC_FLAC; + /* + * Taken from the fcplay utility in tinycompress. Required for + * FLAC to work. + */ + codec->options.flac_d.sample_size = 16; + codec->options.flac_d.min_blk_size = 16; + codec->options.flac_d.max_blk_size = 65535; + codec->options.flac_d.min_frame_size = 11; + codec->options.flac_d.max_frame_size = 8192 * 4; + rate = info->info.flac.rate; + channels = info->info.flac.channels; + break; + case SPA_MEDIA_SUBTYPE_wma: + codec->id = SND_AUDIOCODEC_WMA; + /* + * WMA does not work with Compress-Offload if codec profile + * is not set. + */ + switch (info->info.wma.profile) { + case SPA_AUDIO_WMA_PROFILE_WMA9: + codec->profile = SND_AUDIOPROFILE_WMA9; + break; + case SPA_AUDIO_WMA_PROFILE_WMA9_PRO: + codec->profile = SND_AUDIOPROFILE_WMA9_PRO; + break; + case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS; + break; + case SPA_AUDIO_WMA_PROFILE_WMA10: + codec->profile = SND_AUDIOPROFILE_WMA10; + break; + case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS; + break; + default: + spa_log_error(this->log, NAME " %p: Invalid WMA codec profile", this); + return -EINVAL; + } + codec->bit_rate = info->info.wma.bitrate; + codec->align = info->info.wma.block_align; + rate = info->info.wma.rate; + channels = info->info.wma.channels; + break; + case SPA_MEDIA_SUBTYPE_alac: + codec->id = SND_AUDIOCODEC_ALAC; + rate = info->info.alac.rate; + channels = info->info.alac.channels; + break; + case SPA_MEDIA_SUBTYPE_ape: + codec->id = SND_AUDIOCODEC_APE; + rate = info->info.ape.rate; + channels = info->info.ape.channels; + break; + case SPA_MEDIA_SUBTYPE_ra: + codec->id = SND_AUDIOCODEC_REAL; + rate = info->info.ra.rate; + channels = info->info.ra.channels; + break; + case SPA_MEDIA_SUBTYPE_amr: + if (info->info.amr.band_mode == SPA_AUDIO_AMR_BAND_MODE_WB) + codec->id = SND_AUDIOCODEC_AMRWB; + else + codec->id = SND_AUDIOCODEC_AMR; + rate = info->info.amr.rate; + channels = info->info.amr.channels; + break; + break; + default: + return -ENOTSUP; + } + + codec->ch_in = channels; + codec->ch_out = channels; + codec->sample_rate = rate; + *out_rate = rate; + + codec->rate_control = 0; + codec->level = 0; + codec->ch_mode = 0; + codec->format = 0; + + spa_log_info(this->log, NAME " %p: Codec info, profile: %d align: %d rate: %d bitrate: %d", + this, codec->profile, codec->align, codec->sample_rate, codec->bit_rate); + + if (!is_codec_supported_by_name(this->props.device, 0, codec)) { + spa_log_error(this->log, NAME " %p: Requested codec is not supported by DSP", this); + return -EINVAL; + } + + config->codec = codec; + config->fragment_size = MIN_FRAGMENT_SIZE; + config->fragments = MIN_NUM_FRAGMENTS; + + return 0; +} + +static int +port_set_format(struct impl *this, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + struct port *port = &this->port; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + uint32_t rate; + + if ((res = spa_format_audio_parse(format, &info)) < 0) { + spa_log_error(this->log, NAME " %p: format parse error: %s", this, + spa_strerror(res)); + return res; + } + + if ((res = compress_setup(this, &info, &rate)) < 0) { + spa_log_error(this->log, NAME " %p: can't setup compress: %s", + this, spa_strerror(res)); + return res; + } + + port->current_format = info; + port->have_format = true; + port->info.rate = SPA_FRACTION(1, rate); + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + emit_node_info(this, false); + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + + if (port->have_format) { + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } + return 0; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + if (!port->have_format) + return -EIO; + + clear_buffers(this, port); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->outbuf = buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + struct buffer *b; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + + io = port->io; + spa_return_val_if_fail(io != NULL, -EIO); + + if (io->status != SPA_STATUS_HAVE_DATA) + return io->status; + + if (io->buffer_id >= port->n_buffers) { + io->status = -EINVAL; + return io->status; + } + + b = &port->buffers[io->buffer_id]; + + for (i = 0; i < b->outbuf->n_datas; i++) { + int32_t offs, size; + int32_t wrote; + void *buf; + + struct spa_data *d = b->outbuf->datas; + d = b->outbuf->datas; + + offs = SPA_MIN(d->chunk->offset, d->maxsize); + size = SPA_MIN(d->maxsize - offs, d->chunk->size); + buf = SPA_PTROFF(d[0].data, offs, void); + + wrote = write_compress(this, buf, size); + if (wrote < 0) { + spa_log_error(this->log, NAME " %p: Error playing sample: %s", + this, compress_get_error(this->compress)); + io->status = wrote; + return SPA_STATUS_STOPPED; + } + } + + io->status = SPA_STATUS_OK; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = 1; + reset_props(&this->props); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 4; + port->written = 0; + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &this->quantum_limit, 0); + } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { + this->props.channels = atoi(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { + this->props.rate = atoi(s); + } + } + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) { + if ((str[0] == 'h') || (str[1] == 'w') || (str[2] == ':')) { + snprintf(this->props.device, sizeof(this->props.device), "%s", str); + } else { + spa_log_error(this->log, NAME " %p: Invalid Compress-Offload hw %s", this, str); + return -EINVAL; + } + } else { + spa_log_error(this->log, NAME " %p: Invalid compress hw", this); + return -EINVAL; + } + + /* + * TODO: + * + * Move this to use new compress_get_supported_codecs_by_name API once + * merged upstream. + * + * Right now, we pretend all codecs are supported and then error out + * at runtime in port_set_format during compress_setup if not + * supported. + */ + this->num_codecs = SPA_N_ELEMENTS (codec_info); + for (i = 0; i < this->num_codecs; i++) { + this->codecs_supported[i] = codec_info[i].codec_id; + } + + spa_log_info(this->log, NAME " %p: Initialized Compress-Offload sink %s", + this, this->props.device); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity " }, + { 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..984f1d0 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-device.c @@ -0,0 +1,581 @@ +/* Spa ALSA Device + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; + + struct spa_hook_list hooks; + + struct props props; + uint32_t n_nodes; + uint32_t n_capture; + uint32_t n_playback; + + uint32_t profile; +}; + +static const char *get_stream(snd_pcm_info_t *pcminfo) +{ + switch (snd_pcm_info_get_stream(pcminfo)) { + case SND_PCM_STREAM_PLAYBACK: + return "playback"; + case SND_PCM_STREAM_CAPTURE: + return "capture"; + default: + return "unknown"; + } +} + +static const char *get_class(snd_pcm_info_t *pcminfo) +{ + switch (snd_pcm_info_get_class(pcminfo)) { + case SND_PCM_CLASS_GENERIC: + return "generic"; + case SND_PCM_CLASS_MULTI: + return "multichannel"; + case SND_PCM_CLASS_MODEM: + return "modem"; + case SND_PCM_CLASS_DIGITIZER: + return "digitizer"; + default: + return "unknown"; + } +} + +static const char *get_subclass(snd_pcm_info_t *pcminfo) +{ + switch (snd_pcm_info_get_subclass(pcminfo)) { + case SND_PCM_SUBCLASS_GENERIC_MIX: + return "generic-mix"; + case SND_PCM_SUBCLASS_MULTI_MIX: + return "multichannel-mix"; + default: + return "unknown"; + } +} + +static int emit_node(struct impl *this, snd_ctl_card_info_t *cardinfo, snd_pcm_info_t *pcminfo, uint32_t id) +{ + struct spa_dict_item items[12]; + char device_name[128], path[180]; + char sync_name[128], dev[16], subdev[16], card[16]; + struct spa_device_object_info info; + snd_pcm_sync_id_t sync_id; + const char *stream; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + + if (snd_pcm_info_get_stream(pcminfo) == SND_PCM_STREAM_PLAYBACK) { + info.factory_name = SPA_NAME_API_ALSA_PCM_SINK; + stream = "playback"; + } else { + info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE; + stream = "capture"; + } + + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + + snprintf(card, sizeof(card), "%d", snd_pcm_info_get_card(pcminfo)); + snprintf(dev, sizeof(dev), "%d", snd_pcm_info_get_device(pcminfo)); + snprintf(subdev, sizeof(subdev), "%d", snd_pcm_info_get_subdevice(pcminfo)); + snprintf(device_name, sizeof(device_name), "%s,%s", this->props.device, dev); + snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", snd_ctl_card_info_get_id(cardinfo), dev, stream); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); + items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name); + items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card); + items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_DEVICE, dev); + items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBDEVICE, subdev); + items[5] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, get_stream(pcminfo)); + items[6] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_ID, snd_pcm_info_get_id(pcminfo)); + items[7] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_NAME, snd_pcm_info_get_name(pcminfo)); + items[8] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBNAME, snd_pcm_info_get_subdevice_name(pcminfo)); + items[9] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CLASS, get_class(pcminfo)); + items[10] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBCLASS, get_subclass(pcminfo)); + sync_id = snd_pcm_info_get_sync(pcminfo); + snprintf(sync_name, sizeof(sync_name), "%08x:%08x:%08x:%08x", + sync_id.id32[0], sync_id.id32[1], sync_id.id32[2], sync_id.id32[3]); + items[11] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SYNC_ID, sync_name); + info.props = &SPA_DICT_INIT_ARRAY(items); + + spa_device_emit_object_info(&this->hooks, id, &info); + + return 0; +} + +static int activate_profile(struct impl *this, snd_ctl_t *ctl_hndl, uint32_t id) +{ + int err = 0, dev; + uint32_t i, n_cap, n_play; + snd_pcm_info_t *pcminfo; + snd_ctl_card_info_t *cardinfo; + + spa_log_debug(this->log, "profile %d", id); + this->profile = id; + + snd_ctl_card_info_alloca(&cardinfo); + if ((err = snd_ctl_card_info(ctl_hndl, cardinfo)) < 0) { + spa_log_error(this->log, "error card info: %s", snd_strerror(err)); + return err; + } + + for (i = 0; i < this->n_nodes; i++) + spa_device_emit_object_info(&this->hooks, i, NULL); + + this->n_nodes = this->n_capture = this->n_playback = 0; + + if (id == 0) + return 0; + + snd_pcm_info_alloca(&pcminfo); + dev = -1; + i = n_cap = n_play = 0; + while (1) { + if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) { + spa_log_error(this->log, "error iterating devices: %s", snd_strerror(err)); + break; + } + if (dev < 0) + break; + + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK); + if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) { + if (err != -ENOENT) + spa_log_error(this->log, "error pcm info: %s", snd_strerror(err)); + } + if (err >= 0) { + n_play++; + emit_node(this, cardinfo, pcminfo, i++); + } + + snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); + if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) { + if (err != -ENOENT) + spa_log_error(this->log, "error pcm info: %s", snd_strerror(err)); + } + if (err >= 0) { + n_cap++; + emit_node(this, cardinfo, pcminfo, i++); + } + } + this->n_capture = n_cap; + this->n_playback = n_play; + this->n_nodes = i; + return err; +} + +static int set_profile(struct impl *this, uint32_t id) +{ + snd_ctl_t *ctl_hndl; + int err; + + spa_log_debug(this->log, "open card %s", this->props.device); + if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) { + spa_log_error(this->log, "can't open control for card %s: %s", + this->props.device, snd_strerror(err)); + return err; + } + + err = activate_profile(this, ctl_hndl, id); + + spa_log_debug(this->log, "close card %s", this->props.device); + snd_ctl_close(ctl_hndl); + + return err; +} + +static int emit_info(struct impl *this, bool full) +{ + int err = 0; + struct spa_dict_item items[20]; + uint32_t n_items = 0; + snd_ctl_t *ctl_hndl; + snd_ctl_card_info_t *info; + struct spa_device_info dinfo; + struct spa_param_info params[2]; + char path[128]; + + spa_log_debug(this->log, "open card %s", this->props.device); + if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) { + spa_log_error(this->log, "can't open control for card %s: %s", + this->props.device, snd_strerror(err)); + return err; + } + + snd_ctl_card_info_alloca(&info); + if ((err = snd_ctl_card_info(ctl_hndl, info)) < 0) { + spa_log_error(this->log, "error hardware info: %s", snd_strerror(err)); + goto exit; + } + + dinfo = SPA_DEVICE_INFO_INIT(); + + dinfo.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + snprintf(path, sizeof(path), "alsa:pcm:%s", snd_ctl_card_info_get_id(info)); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_ID, snd_ctl_card_info_get_id(info)); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_COMPONENTS, snd_ctl_card_info_get_components(info)); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_DRIVER, snd_ctl_card_info_get_driver(info)); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_NAME, snd_ctl_card_info_get_name(info)); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_LONGNAME, snd_ctl_card_info_get_longname(info)); + ADD_ITEM(SPA_KEY_API_ALSA_CARD_MIXERNAME, snd_ctl_card_info_get_mixername(info)); + dinfo.props = &SPA_DICT_INIT(items, n_items); +#undef ADD_ITEM + + dinfo.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE); + dinfo.n_params = SPA_N_ELEMENTS(params); + dinfo.params = params; + + spa_device_emit_info(&this->hooks, &dinfo); + + exit: + spa_log_debug(this->log, "close card %s", this->props.device); + snd_ctl_close(ctl_hndl); + return err; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info || events->object_info) + emit_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b, + uint32_t id, uint32_t index) +{ + struct spa_pod_frame f[2]; + const char *name, *desc; + + switch (index) { + case 0: + name = "off"; + desc = "Off"; + break; + case 1: + name = "on"; + desc = "On"; + break; + default: + errno = EINVAL; + return NULL; + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id); + spa_pod_builder_add(b, + SPA_PARAM_PROFILE_index, SPA_POD_Int(index), + SPA_PARAM_PROFILE_name, SPA_POD_String(name), + SPA_PARAM_PROFILE_description, SPA_POD_String(desc), + 0); + if (index == 1) { + spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0); + spa_pod_builder_push_struct(b, &f[1]); + if (this->n_capture) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Source"), + SPA_POD_Int(this->n_capture)); + } + if (this->n_playback) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Sink"), + SPA_POD_Int(this->n_playback)); + } + spa_pod_builder_pop(b, &f[1]); + } + return spa_pod_builder_pop(b, &f[0]); + +} +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_device_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumProfile: + { + switch (result.index) { + case 0: + case 1: + param = build_profile(this, &b, id, result.index); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Profile: + { + switch (result.index) { + case 0: + param = build_profile(this, &b, id, this->profile); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_device_emit_result(&this->hooks, seq, 0, + SPA_RESULT_TYPE_DEVICE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Profile: + { + uint32_t idx; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx))) < 0) { + spa_log_warn(this->log, "can't parse profile"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + + set_profile(this, idx); + break; + } + default: + return -ENOENT; + } + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + snd_config_update_free_global(); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) + snprintf(this->props.device, 64, "%s", str); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_alsa_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_ALSA_PCM_DEVICE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c new file mode 100644 index 0000000..a8c40b1 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-sink.c @@ -0,0 +1,1014 @@ +/* Spa ALSA Sink + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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; +} + +static void emit_node_info(struct state *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + struct spa_dict_item items[7]; + uint32_t i, n_items = 0; + char latency[64], period[64], nperiods[64], headroom[64]; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + if (this->have_format) { + snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency); + snprintf(period, sizeof(period), "%lu", this->period_frames); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period); + snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods); + snprintf(headroom, sizeof(headroom), "%u", this->headroom); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom); + } + this->info.props = &SPA_DICT_INIT(items, n_items); + + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + this->params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[i].user = 0; + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + + this->info.change_mask = old; + } +} + +static void emit_port_info(struct state *this, bool full) +{ + uint64_t old = full ? this->port_info.change_mask : 0; + + if (full) + this->port_info.change_mask = this->port_info_all; + if (this->port_info.change_mask) { + uint32_t i; + + if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->port_info.n_params; i++) { + if (this->port_params[i].user > 0) { + this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->port_params[i].user = 0; + } + } + } + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &this->port_info); + this->port_info.change_mask = old; + } +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct state *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name))); + break; + case 3: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec), + SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC)); + break; + case 4: + if (!this->is_iec958 && !this->is_hdmi) + goto next; + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_iec958Codecs), + SPA_PROP_INFO_name, SPA_POD_String("iec958.codecs"), + SPA_PROP_INFO_description, SPA_POD_String("Enabled IEC958 (S/PDIF) codecs"), + SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_IEC958_CODEC_UNKNOWN), + SPA_PROP_INFO_params, SPA_POD_Bool(true), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + default: + param = spa_alsa_enum_propinfo(this, result.index - 5, &b); + if (param == NULL) + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod_frame f; + uint32_t codecs[16], n_codecs; + + switch (result.index) { + case 0: + spa_pod_builder_push_object(&b, &f, + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b, + SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)), + SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)), + SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)), + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns), + 0); + + if (this->is_iec958 || this->is_hdmi) { + n_codecs = spa_alsa_get_iec958_codecs(this, codecs, SPA_N_ELEMENTS(codecs)); + spa_pod_builder_prop(&b, SPA_PROP_iec958Codecs, 0); + spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id, + n_codecs, codecs); + } + spa_alsa_add_prop_params(this, &b); + param = spa_pod_builder_pop(&b, &f); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_ProcessLatency: + switch (result.index) { + case 0: + param = spa_process_latency_build(&b, id, &this->process_latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + spa_alsa_reassign_follower(this); + + return 0; +} + +static void handle_process_latency(struct state *this, + const struct spa_process_latency_info *info) +{ + bool ns_changed = this->process_latency.ns != info->ns; + + if (this->process_latency.quantum == info->quantum && + this->process_latency.rate == info->rate && + !ns_changed) + return; + + this->process_latency = *info; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + if (ns_changed) + this->params[NODE_Props].user++; + this->params[NODE_ProcessLatency].user++; + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod *iec958_codecs = NULL, *params = NULL; + int64_t lat_ns = -1; + + if (param == NULL) { + reset_props(p); + return 0; + } + + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)), + SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns), + SPA_PROP_iec958Codecs, SPA_POD_OPT_Pod(&iec958_codecs), + SPA_PROP_params, SPA_POD_OPT_Pod(¶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); + } + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + case SPA_PARAM_ProcessLatency: + { + struct spa_process_latency_info info; + if (param == NULL) + spa_zero(info); + else if ((res = spa_process_latency_parse(param, &info)) < 0) + return res; + + handle_process_latency(this, &info); + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_alsa_open(this, NULL)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (this->have_format) + return 0; + if ((res = spa_alsa_close(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if ((res = spa_alsa_pause(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct state *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +impl_node_sync(void *object, int seq) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct state *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + return spa_alsa_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + switch (this->current_format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + param = spa_format_audio_raw_build(&b, id, + &this->current_format.info.raw); + break; + case SPA_MEDIA_SUBTYPE_iec958: + param = spa_format_audio_iec958_build(&b, id, + &this->current_format.info.iec958); + break; + case SPA_MEDIA_SUBTYPE_dsd: + param = spa_format_audio_dsd_build(&b, id, + &this->current_format.info.dsd); + break; + default: + return -EIO; + } + break; + + case SPA_PARAM_Buffers: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->frame_size * this->frame_scale, + 16 * this->frame_size * this->frame_scale, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + struct spa_latency_info latency = this->latency[result.index]; + if (latency.direction == SPA_DIRECTION_INPUT) + spa_process_latency_info_add(&this->process_latency, &latency); + param = spa_latency_build(&b, id, &latency); + break; + } + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct state *this) +{ + if (this->n_buffers > 0) { + spa_list_init(&this->ready); + this->n_buffers = 0; + } + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct state *this = object; + int err = 0; + + if (format == NULL) { + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "clear format"); + spa_alsa_close(this); + clear_buffers(this); + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio) + return -EINVAL; + + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_iec958: + if (spa_format_audio_iec958_parse(format, &info.info.iec958) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_dsd: + if (spa_format_audio_dsd_parse(format, &info.info.dsd) < 0) + return -EINVAL; + break; + default: + return -EINVAL; + } + + if ((err = spa_alsa_set_format(this, &info, flags)) < 0) + return err; + + this->current_format = info; + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (this->have_format) { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->port_params[PORT_Latency].user++; + } else { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, false); + + return err; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, direction, port_id, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + emit_port_info(this, false); + res = 0; + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + uint32_t i; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (this->n_buffers > 0) { + spa_alsa_pause(this); + if ((res = clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !this->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data); + } + this->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + this->io = data; + break; + case SPA_IO_RateMatch: + this->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if ((io = this->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status, + io->buffer_id, this->n_buffers); + + if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + if (io->status == SPA_STATUS_HAVE_DATA && + io->buffer_id < this->n_buffers) { + struct buffer *b = &this->buffers[io->buffer_id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_warn(this->log, "%p: buffer %u in use", + this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id); + spa_list_append(&this->ready, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + io->buffer_id = SPA_ID_INVALID; + + spa_alsa_write(this); + + io->status = SPA_STATUS_OK; + } + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_alsa_close(this); + spa_alsa_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) +{ + struct state *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + spa_hook_list_init(&this->hooks); + + this->stream = SND_PCM_STREAM_PLAYBACK; + this->port_direction = SPA_DIRECTION_INPUT; + this->latency[this->port_direction] = SPA_LATENCY_INFO( + this->port_direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + reset_props(&this->props); + + this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + this->port_info = SPA_PORT_INFO_INIT(); + this->port_info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->port_info.params = this->port_params; + this->port_info.n_params = N_PORT_PARAMS; + + spa_list_init(&this->ready); + + return spa_alsa_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { 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..f079bf6 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm-source.c @@ -0,0 +1,964 @@ +/* Spa ALSA Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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; +} + +static void emit_node_info(struct state *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + struct spa_dict_item items[7]; + uint32_t i, n_items = 0; + char latency[64], period[64], nperiods[64], headroom[64]; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + if (this->have_format) { + snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency); + snprintf(period, sizeof(period), "%lu", this->period_frames); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period); + snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods); + snprintf(headroom, sizeof(headroom), "%u", this->headroom); + items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom); + } + this->info.props = &SPA_DICT_INIT(items, n_items); + + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + this->params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[i].user = 0; + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct state *this, bool full) +{ + uint64_t old = full ? this->port_info.change_mask : 0; + if (full) + this->port_info.change_mask = this->port_info_all; + if (this->port_info.change_mask) { + uint32_t i; + + if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->port_info.n_params; i++) { + if (this->port_params[i].user > 0) { + this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->port_params[i].user = 0; + } + } + } + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &this->port_info); + this->port_info.change_mask = old; + } +} + + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct state *this = object; + struct spa_pod *param; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct props *p; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + p = &this->props; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name))); + break; + case 3: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec), + SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC)); + break; + default: + param = spa_alsa_enum_propinfo(this, result.index - 4, &b); + if (param == NULL) + return 0; + } + break; + + case SPA_PARAM_Props: + { + struct spa_pod_frame f; + + switch (result.index) { + case 0: + spa_pod_builder_push_object(&b, &f, + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b, + SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)), + SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)), + SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)), + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns), + 0); + spa_alsa_add_prop_params(this, &b); + param = spa_pod_builder_pop(&b, &f); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_ProcessLatency: + switch (result.index) { + case 0: + param = spa_process_latency_build(&b, id, &this->process_latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + if (size > 0 && size < sizeof(struct spa_io_clock)) + return -EINVAL; + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + spa_alsa_reassign_follower(this); + return 0; +} + +static void handle_process_latency(struct state *this, + const struct spa_process_latency_info *info) +{ + bool ns_changed = this->process_latency.ns != info->ns; + + if (this->process_latency.quantum == info->quantum && + this->process_latency.rate == info->rate && + !ns_changed) + return; + + this->process_latency = *info; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + if (ns_changed) + this->params[NODE_Props].user++; + this->params[NODE_ProcessLatency].user++; + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod *params = NULL; + int64_t lat_ns = -1; + + if (param == NULL) { + reset_props(p); + return 0; + } + + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)), + SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns), + SPA_PROP_params, SPA_POD_OPT_Pod(¶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); + } + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + case SPA_PARAM_ProcessLatency: + { + struct spa_process_latency_info info; + if (param == NULL) + spa_zero(info); + else if ((res = spa_process_latency_parse(param, &info)) < 0) + return res; + + handle_process_latency(this, &info); + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_alsa_open(this, NULL)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (this->have_format) + return 0; + if ((res = spa_alsa_close(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_alsa_pause(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct state *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct state *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + return spa_alsa_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_format_audio_raw_build(&b, id, &this->current_format.info.raw); + break; + + case SPA_PARAM_Buffers: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->frame_size, + 16 * this->frame_size, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + struct spa_latency_info latency = this->latency[result.index]; + if (latency.direction == SPA_DIRECTION_OUTPUT) + spa_process_latency_info_add(&this->process_latency, &latency); + param = spa_latency_build(&b, id, &latency); + break; + } + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct state *this) +{ + if (this->n_buffers > 0) { + spa_list_init(&this->free); + spa_list_init(&this->ready); + this->n_buffers = 0; + } + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, const struct spa_pod *format) +{ + struct state *this = object; + int err = 0; + + if (format == NULL) { + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "clear format"); + spa_alsa_close(this); + clear_buffers(this); + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if ((err = spa_alsa_set_format(this, &info, flags)) < 0) + return err; + + this->current_format = info; + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (this->have_format) { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->port_params[PORT_Latency].user++; + } else { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, false); + + return err; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, direction, port_id, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + emit_port_info(this, false); + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + int res; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (this->n_buffers > 0) { + spa_alsa_pause(this); + if ((res = clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !this->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = 0; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_list_append(&this->free, &b->link); + } + this->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + this->io = data; + break; + case SPA_IO_RateMatch: + this->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(port_id == 0, -EINVAL); + + if (this->n_buffers == 0) + return -EIO; + + if (buffer_id >= this->n_buffers) + return -EINVAL; + + spa_alsa_recycle_buffer(this, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct spa_io_buffers *io; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if ((io = this->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p; status %d", this, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < this->n_buffers) { + spa_alsa_recycle_buffer(this, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&this->ready) && this->following) { + if (this->freewheel) + spa_alsa_skip(this); + else + spa_alsa_read(this); + } + if (spa_list_is_empty(&this->ready) || !this->following) + return SPA_STATUS_OK; + + b = spa_list_first(&this->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_alsa_close(this); + spa_alsa_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct state *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "%p: a data loop is needed", this); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "%p: a data system is needed", this); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this); + + spa_hook_list_init(&this->hooks); + this->stream = SND_PCM_STREAM_CAPTURE; + this->port_direction = SPA_DIRECTION_OUTPUT; + this->latency[this->port_direction] = SPA_LATENCY_INFO( + this->port_direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + reset_props(&this->props); + + this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + this->port_info = SPA_PORT_INFO_INIT(); + this->port_info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->port_info.params = this->port_params; + this->port_info.n_params = N_PORT_PARAMS; + + spa_list_init(&this->free); + spa_list_init(&this->ready); + + return spa_alsa_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { 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..012b460 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm.c @@ -0,0 +1,2696 @@ +#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 card *find_card(uint32_t index) +{ + struct card *c; + spa_list_for_each(c, &cards, link) { + if (c->index == index) { + c->ref++; + return c; + } + } + return NULL; +} + +static struct card *ensure_card(uint32_t index, bool ucm) +{ + struct card *c; + char card_name[64]; + const char *alibpref = NULL; + int err; + + if ((c = find_card(index)) != NULL) + return c; + + c = calloc(1, sizeof(*c)); + c->ref = 1; + c->index = index; + + if (ucm) { + snprintf(card_name, sizeof(card_name), "hw:%i", index); + err = snd_use_case_mgr_open(&c->ucm, card_name); + if (err < 0) { + char *name; + err = snd_card_get_name(index, &name); + if (err < 0) + goto error; + + snprintf(card_name, sizeof(card_name), "%s", name); + free(name); + + err = snd_use_case_mgr_open(&c->ucm, card_name); + if (err < 0) + goto error; + } + if ((snd_use_case_get(c->ucm, "_alibpref", &alibpref) != 0)) + alibpref = NULL; + c->ucm_prefix = (char*)alibpref; + } + spa_list_append(&cards, &c->link); + + return c; +error: + free(c); + errno = -err; + return NULL; +} + +static void release_card(struct card *c) +{ + spa_assert(c->ref > 0); + + if (--c->ref > 0) + return; + + spa_list_remove(&c->link); + if (c->ucm) { + free(c->ucm_prefix); + snd_use_case_mgr_close(c->ucm); + } + free(c); +} + +static int alsa_set_param(struct state *state, const char *k, const char *s) +{ + int fmt_change = 0; + if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { + state->default_channels = atoi(s); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { + state->default_rate = atoi(s); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) { + state->default_format = spa_alsa_format_from_name(s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + spa_alsa_parse_position(&state->default_pos, s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) { + state->n_allowed_rates = spa_alsa_parse_rates(state->allowed_rates, + MAX_RATES, s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, "iec958.codecs")) { + spa_alsa_parse_iec958_codecs(&state->iec958_codecs, s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, "api.alsa.period-size")) { + state->default_period_size = atoi(s); + } else if (spa_streq(k, "api.alsa.period-num")) { + state->default_period_num = atoi(s); + } else if (spa_streq(k, "api.alsa.headroom")) { + state->default_headroom = atoi(s); + } else if (spa_streq(k, "api.alsa.start-delay")) { + state->default_start_delay = atoi(s); + } else if (spa_streq(k, "api.alsa.disable-mmap")) { + state->disable_mmap = spa_atob(s); + } else if (spa_streq(k, "api.alsa.disable-batch")) { + state->disable_batch = spa_atob(s); + } else if (spa_streq(k, "api.alsa.use-chmap")) { + state->props.use_chmap = spa_atob(s); + } else if (spa_streq(k, "api.alsa.multi-rate")) { + state->multi_rate = spa_atob(s); + } else if (spa_streq(k, "latency.internal.rate")) { + state->process_latency.rate = atoi(s); + } else if (spa_streq(k, "latency.internal.ns")) { + state->process_latency.ns = atoi(s); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(state->clock_name, + sizeof(state->clock_name), "%s", s); + } else + return 0; + + if (fmt_change > 0) { + state->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + state->port_params[PORT_EnumFormat].user++; + } + return 1; +} + +static int position_to_string(struct channel_map *map, char *val, size_t len) +{ + uint32_t i, o = 0; + int r; + o += snprintf(val, len, "[ "); + for (i = 0; i < map->channels; i++) { + r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ", + spa_debug_type_find_short_name(spa_type_audio_channel, + map->pos[i])); + if (r < 0 || o + r >= len) + return -ENOSPC; + o += r; + } + if (len > o) + o += snprintf(val+o, len-o, " ]"); + return 0; +} + +static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len) +{ + uint32_t i, o = 0; + int r; + o += snprintf(val, len, "[ "); + for (i = 0; i < n_vals; i++) { + r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]); + if (r < 0 || o + r >= len) + return -ENOSPC; + o += r; + } + if (len > o) + o += snprintf(val+o, len-o, " ]"); + return 0; +} + +struct spa_pod *spa_alsa_enum_propinfo(struct state *state, + uint32_t idx, struct spa_pod_builder *b) +{ + struct spa_pod *param; + + switch (idx) { + case 0: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS), + SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"), + SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 1: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE), + SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"), + SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 2: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT), + SPA_PROP_INFO_description, SPA_POD_String("Audio Format"), + SPA_PROP_INFO_type, SPA_POD_String( + spa_debug_type_find_short_name(spa_type_audio_format, + state->default_format)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 3: + { + char buf[1024]; + position_to_string(&state->default_pos, buf, sizeof(buf)); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION), + SPA_PROP_INFO_description, SPA_POD_String("Audio Position"), + SPA_PROP_INFO_type, SPA_POD_String(buf), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + } + case 4: + { + char buf[1024]; + uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf)); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES), + SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"), + SPA_PROP_INFO_type, SPA_POD_String(buf), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + } + case 5: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-size"), + SPA_PROP_INFO_description, SPA_POD_String("Period Size"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_size, 0, 8192), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 6: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-num"), + SPA_PROP_INFO_description, SPA_POD_String("Number of Periods"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_num, 0, 1024), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 7: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.headroom"), + SPA_PROP_INFO_description, SPA_POD_String("Headroom"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_headroom, 0, 8192), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 8: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.start-delay"), + SPA_PROP_INFO_description, SPA_POD_String("Start Delay"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_start_delay, 0, 8192), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 9: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-mmap"), + SPA_PROP_INFO_description, SPA_POD_String("Disable MMAP"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_mmap), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 10: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-batch"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Batch"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_batch), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 11: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.use-chmap"), + SPA_PROP_INFO_description, SPA_POD_String("Use the driver channelmap"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->props.use_chmap), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 12: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("api.alsa.multi-rate"), + SPA_PROP_INFO_description, SPA_POD_String("Support multiple rates"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->multi_rate), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 13: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"), + SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate, + 0, 65536), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 14: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"), + SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns, + 0LL, 2 * SPA_NSEC_PER_SEC), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 15: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("clock.name"), + SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"), + SPA_PROP_INFO_type, SPA_POD_String(state->clock_name), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + default: + return NULL; + } + return param; +} + +int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; + char buf[1024]; + + spa_pod_builder_prop(b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(b, &f[0]); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS); + spa_pod_builder_int(b, state->default_channels); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE); + spa_pod_builder_int(b, state->default_rate); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT); + spa_pod_builder_string(b, + spa_debug_type_find_short_name(spa_type_audio_format, + state->default_format)); + + position_to_string(&state->default_pos, buf, sizeof(buf)); + spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION); + spa_pod_builder_string(b, buf); + + uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, + buf, sizeof(buf)); + spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES); + spa_pod_builder_string(b, buf); + + spa_pod_builder_string(b, "api.alsa.period-size"); + spa_pod_builder_int(b, state->default_period_size); + + spa_pod_builder_string(b, "api.alsa.period-num"); + spa_pod_builder_int(b, state->default_period_num); + + spa_pod_builder_string(b, "api.alsa.headroom"); + spa_pod_builder_int(b, state->default_headroom); + + spa_pod_builder_string(b, "api.alsa.start-delay"); + spa_pod_builder_int(b, state->default_start_delay); + + spa_pod_builder_string(b, "api.alsa.disable-mmap"); + spa_pod_builder_bool(b, state->disable_mmap); + + spa_pod_builder_string(b, "api.alsa.disable-batch"); + spa_pod_builder_bool(b, state->disable_batch); + + spa_pod_builder_string(b, "api.alsa.use-chmap"); + spa_pod_builder_bool(b, state->props.use_chmap); + + spa_pod_builder_string(b, "api.alsa.multi-rate"); + spa_pod_builder_bool(b, state->multi_rate); + + spa_pod_builder_string(b, "latency.internal.rate"); + spa_pod_builder_int(b, state->process_latency.rate); + + spa_pod_builder_string(b, "latency.internal.ns"); + spa_pod_builder_long(b, state->process_latency.ns); + + spa_pod_builder_string(b, "clock.name"); + spa_pod_builder_string(b, state->clock_name); + + spa_pod_builder_pop(b, &f[0]); + return 0; +} + +int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + int changed = 0; + + if (params == NULL) + return 0; + + spa_pod_parser_pod(&prs, params); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + struct spa_pod *pod; + char value[512]; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + + if (spa_pod_parser_get_pod(&prs, &pod) < 0) + break; + if (spa_pod_is_string(pod)) { + spa_pod_copy_string(pod, sizeof(value), value); + } else if (spa_pod_is_int(pod)) { + snprintf(value, sizeof(value), "%d", + SPA_POD_VALUE(struct spa_pod_int, pod)); + } else if (spa_pod_is_long(pod)) { + snprintf(value, sizeof(value), "%"PRIi64, + SPA_POD_VALUE(struct spa_pod_long, pod)); + } else if (spa_pod_is_bool(pod)) { + snprintf(value, sizeof(value), "%s", + SPA_POD_VALUE(struct spa_pod_bool, pod) ? + "true" : "false"); + } else + continue; + + spa_log_info(state->log, "key:'%s' val:'%s'", name, value); + alsa_set_param(state, name, value); + changed++; + } + if (changed > 0) { + state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + state->params[NODE_Props].user++; + } + return changed; +} + +#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; } + +static ssize_t log_write(void *cookie, const char *buf, size_t size) +{ + struct state *state = cookie; + int len; + + while (size > 0) { + len = strcspn(buf, "\n"); + if (len > 0) + spa_log_debug(state->log, "%.*s", (int)len, buf); + buf += len + 1; + size -= len + 1; + } + return size; +} + +static cookie_io_functions_t io_funcs = { + .write = log_write, +}; + +int spa_alsa_init(struct state *state, const struct spa_dict *info) +{ + uint32_t i; + int err; + + snd_config_update_free_global(); + + state->multi_rate = true; + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) { + snprintf(state->props.device, 63, "%s", s); + } else if (spa_streq(k, SPA_KEY_API_ALSA_PCM_CARD)) { + state->card_index = atoi(s); + } else if (spa_streq(k, SPA_KEY_API_ALSA_OPEN_UCM)) { + state->open_ucm = spa_atob(s); + } else if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &state->quantum_limit, 0); + } else { + alsa_set_param(state, k, s); + } + } + if (state->clock_name[0] == '\0') + snprintf(state->clock_name, sizeof(state->clock_name), + "api.alsa.%s-%u", + state->stream == SND_PCM_STREAM_PLAYBACK ? "p" : "c", + state->card_index); + + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + state->is_iec958 = spa_strstartswith(state->props.device, "iec958"); + state->is_hdmi = spa_strstartswith(state->props.device, "hdmi"); + state->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM; + } + + state->card = ensure_card(state->card_index, state->open_ucm); + if (state->card == NULL) { + spa_log_error(state->log, "can't create card %u", state->card_index); + return -errno; + } + state->log_file = fopencookie(state, "w", io_funcs); + if (state->log_file == NULL) { + spa_log_error(state->log, "can't create log file"); + return -errno; + } + CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed"); + + state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + state->rate_limit.burst = 1; + + return 0; +} + +int spa_alsa_clear(struct state *state) +{ + int err; + + release_card(state->card); + + state->card = NULL; + state->card_index = SPA_ID_INVALID; + + if ((err = snd_output_close(state->output)) < 0) + spa_log_warn(state->log, "output close failed: %s", snd_strerror(err)); + fclose(state->log_file); + + return err; +} + +int spa_alsa_open(struct state *state, const char *params) +{ + int err; + struct props *props = &state->props; + char device_name[256]; + + if (state->opened) + return 0; + + spa_scnprintf(device_name, sizeof(device_name), "%s%s%s", + state->card->ucm_prefix ? state->card->ucm_prefix : "", + props->device, params ? params : ""); + + spa_log_info(state->log, "%p: ALSA device open '%s' %s", state, device_name, + state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback"); + CHECK(snd_pcm_open(&state->hndl, + device_name, + state->stream, + SND_PCM_NONBLOCK | + SND_PCM_NO_AUTO_RESAMPLE | + SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_FORMAT), "'%s': %s open failed", + device_name, + state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback"); + + if ((err = spa_system_timerfd_create(state->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_exit_close; + + state->timerfd = err; + + if (state->clock) + spa_scnprintf(state->clock->name, sizeof(state->clock->name), + "%s", state->clock_name); + state->opened = true; + state->sample_count = 0; + state->sample_time = 0; + + return 0; + +error_exit_close: + spa_log_info(state->log, "%p: Device '%s' closing: %s", state, state->props.device, + spa_strerror(err)); + snd_pcm_close(state->hndl); + return err; +} + +int spa_alsa_close(struct state *state) +{ + int err = 0; + + if (!state->opened) + return 0; + + spa_alsa_pause(state); + + spa_log_info(state->log, "%p: Device '%s' closing", state, state->props.device); + if ((err = snd_pcm_close(state->hndl)) < 0) + spa_log_warn(state->log, "%s: close failed: %s", state->props.device, + snd_strerror(err)); + + spa_system_close(state->data_system, state->timerfd); + + if (state->have_format) + state->card->format_ref--; + + state->have_format = false; + state->opened = false; + + return err; +} + +struct format_info { + uint32_t spa_format; + uint32_t spa_pformat; + snd_pcm_format_t format; +}; + +static const struct format_info format_info[] = { + { SPA_AUDIO_FORMAT_UNKNOWN, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_UNKNOWN}, + { SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_LE}, + { SPA_AUDIO_FORMAT_F32_BE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_BE}, + { SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_LE}, + { SPA_AUDIO_FORMAT_S32_BE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_BE}, + { SPA_AUDIO_FORMAT_S24_32_LE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_LE}, + { SPA_AUDIO_FORMAT_S24_32_BE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_BE}, + { SPA_AUDIO_FORMAT_S24_LE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3LE}, + { SPA_AUDIO_FORMAT_S24_BE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3BE}, + { SPA_AUDIO_FORMAT_S16_LE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_LE}, + { SPA_AUDIO_FORMAT_S16_BE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_BE}, + { SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_S8}, + { SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_U8P, SND_PCM_FORMAT_U8}, + { SPA_AUDIO_FORMAT_U16_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_LE}, + { SPA_AUDIO_FORMAT_U16_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_BE}, + { SPA_AUDIO_FORMAT_U24_32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_LE}, + { SPA_AUDIO_FORMAT_U24_32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_BE}, + { SPA_AUDIO_FORMAT_U24_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3LE}, + { SPA_AUDIO_FORMAT_U24_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3BE}, + { SPA_AUDIO_FORMAT_U32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_LE}, + { SPA_AUDIO_FORMAT_U32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_BE}, + { SPA_AUDIO_FORMAT_F64_LE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_LE}, + { SPA_AUDIO_FORMAT_F64_BE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_BE}, +}; + +static snd_pcm_format_t spa_format_to_alsa(uint32_t format, bool *planar) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + *planar = i->spa_pformat == format; + if (i->spa_format == format || *planar) + return i->format; + } + return SND_PCM_FORMAT_UNKNOWN; +} + +struct chmap_info { + enum snd_pcm_chmap_position pos; + enum spa_audio_channel channel; +}; + +static const struct chmap_info chmap_info[] = { + [SND_CHMAP_UNKNOWN] = { SND_CHMAP_UNKNOWN, SPA_AUDIO_CHANNEL_UNKNOWN }, + [SND_CHMAP_NA] = { SND_CHMAP_NA, SPA_AUDIO_CHANNEL_NA }, + [SND_CHMAP_MONO] = { SND_CHMAP_MONO, SPA_AUDIO_CHANNEL_MONO }, + [SND_CHMAP_FL] = { SND_CHMAP_FL, SPA_AUDIO_CHANNEL_FL }, + [SND_CHMAP_FR] = { SND_CHMAP_FR, SPA_AUDIO_CHANNEL_FR }, + [SND_CHMAP_RL] = { SND_CHMAP_RL, SPA_AUDIO_CHANNEL_RL }, + [SND_CHMAP_RR] = { SND_CHMAP_RR, SPA_AUDIO_CHANNEL_RR }, + [SND_CHMAP_FC] = { SND_CHMAP_FC, SPA_AUDIO_CHANNEL_FC }, + [SND_CHMAP_LFE] = { SND_CHMAP_LFE, SPA_AUDIO_CHANNEL_LFE }, + [SND_CHMAP_SL] = { SND_CHMAP_SL, SPA_AUDIO_CHANNEL_SL }, + [SND_CHMAP_SR] = { SND_CHMAP_SR, SPA_AUDIO_CHANNEL_SR }, + [SND_CHMAP_RC] = { SND_CHMAP_RC, SPA_AUDIO_CHANNEL_RC }, + [SND_CHMAP_FLC] = { SND_CHMAP_FLC, SPA_AUDIO_CHANNEL_FLC }, + [SND_CHMAP_FRC] = { SND_CHMAP_FRC, SPA_AUDIO_CHANNEL_FRC }, + [SND_CHMAP_RLC] = { SND_CHMAP_RLC, SPA_AUDIO_CHANNEL_RLC }, + [SND_CHMAP_RRC] = { SND_CHMAP_RRC, SPA_AUDIO_CHANNEL_RRC }, + [SND_CHMAP_FLW] = { SND_CHMAP_FLW, SPA_AUDIO_CHANNEL_FLW }, + [SND_CHMAP_FRW] = { SND_CHMAP_FRW, SPA_AUDIO_CHANNEL_FRW }, + [SND_CHMAP_FLH] = { SND_CHMAP_FLH, SPA_AUDIO_CHANNEL_FLH }, + [SND_CHMAP_FCH] = { SND_CHMAP_FCH, SPA_AUDIO_CHANNEL_FCH }, + [SND_CHMAP_FRH] = { SND_CHMAP_FRH, SPA_AUDIO_CHANNEL_FRH }, + [SND_CHMAP_TC] = { SND_CHMAP_TC, SPA_AUDIO_CHANNEL_TC }, + [SND_CHMAP_TFL] = { SND_CHMAP_TFL, SPA_AUDIO_CHANNEL_TFL }, + [SND_CHMAP_TFR] = { SND_CHMAP_TFR, SPA_AUDIO_CHANNEL_TFR }, + [SND_CHMAP_TFC] = { SND_CHMAP_TFC, SPA_AUDIO_CHANNEL_TFC }, + [SND_CHMAP_TRL] = { SND_CHMAP_TRL, SPA_AUDIO_CHANNEL_TRL }, + [SND_CHMAP_TRR] = { SND_CHMAP_TRR, SPA_AUDIO_CHANNEL_TRR }, + [SND_CHMAP_TRC] = { SND_CHMAP_TRC, SPA_AUDIO_CHANNEL_TRC }, + [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, SPA_AUDIO_CHANNEL_TFLC }, + [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, SPA_AUDIO_CHANNEL_TFRC }, + [SND_CHMAP_TSL] = { SND_CHMAP_TSL, SPA_AUDIO_CHANNEL_TSL }, + [SND_CHMAP_TSR] = { SND_CHMAP_TSR, SPA_AUDIO_CHANNEL_TSR }, + [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, SPA_AUDIO_CHANNEL_LLFE }, + [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, SPA_AUDIO_CHANNEL_RLFE }, + [SND_CHMAP_BC] = { SND_CHMAP_BC, SPA_AUDIO_CHANNEL_BC }, + [SND_CHMAP_BLC] = { SND_CHMAP_BLC, SPA_AUDIO_CHANNEL_BLC }, + [SND_CHMAP_BRC] = { SND_CHMAP_BRC, SPA_AUDIO_CHANNEL_BRC }, +}; + +#define _M(ch) (1LL << SND_CHMAP_ ##ch) + +struct def_mask { + int channels; + uint64_t mask; +}; + +static const struct def_mask default_layouts[] = { + { 0, 0 }, + { 1, _M(MONO) }, + { 2, _M(FL) | _M(FR) }, + { 3, _M(FL) | _M(FR) | _M(LFE) }, + { 4, _M(FL) | _M(FR) | _M(RL) |_M(RR) }, + { 5, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) }, + { 6, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) | _M(LFE) }, + { 7, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) }, + { 8, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) | _M(LFE) }, +}; + +#define _C(ch) (SPA_AUDIO_CHANNEL_ ##ch) + +static const struct channel_map default_map[] = { + { 0, { 0, } } , + { 1, { _C(MONO), } }, + { 2, { _C(FL), _C(FR), } }, + { 3, { _C(FL), _C(FR), _C(LFE) } }, + { 4, { _C(FL), _C(FR), _C(RL), _C(RR), } }, + { 5, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC) } }, + { 6, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), } }, + { 7, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(SL), _C(SR), } }, + { 8, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), _C(SL), _C(SR), } }, +}; + +static enum spa_audio_channel chmap_position_to_channel(enum snd_pcm_chmap_position pos) +{ + return chmap_info[pos].channel; +} + +static void sanitize_map(snd_pcm_chmap_t* map) +{ + uint64_t mask = 0, p, dup = 0; + const struct def_mask *def; + uint32_t i, j, pos; + + for (i = 0; i < map->channels; i++) { + if (map->pos[i] > SND_CHMAP_LAST) + map->pos[i] = SND_CHMAP_UNKNOWN; + + p = 1LL << map->pos[i]; + if (mask & p) { + /* duplicate channel */ + for (j = 0; j <= i; j++) + if (map->pos[j] == map->pos[i]) + map->pos[j] = SND_CHMAP_UNKNOWN; + dup |= p; + p = 1LL << SND_CHMAP_UNKNOWN; + } + mask |= p; + } + if ((mask & (1LL << SND_CHMAP_UNKNOWN)) == 0) + return; + + def = &default_layouts[map->channels]; + + /* remove duplicates */ + mask &= ~dup; + /* keep unassigned channels */ + mask = def->mask & ~mask; + + pos = 0; + for (i = 0; i < map->channels; i++) { + if (map->pos[i] == SND_CHMAP_UNKNOWN) { + do { + mask >>= 1; + pos++; + } + while (mask != 0 && (mask & 1) == 0); + map->pos[i] = mask ? pos : 0; + } + + } +} + +static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val) +{ + uint32_t i; + for (i = 0; i < n_vals; i++) + if (vals[i] == val) + return true; + return false; +} + +static int add_rate(struct state *state, uint32_t scale, uint32_t interleave, bool all, uint32_t index, uint32_t *next, + uint32_t min_allowed_rate, snd_pcm_hw_params_t *params, struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; + int err, dir; + unsigned int min, max; + struct spa_pod_choice *choice; + uint32_t rate; + + CHECK(snd_pcm_hw_params_get_rate_min(params, &min, &dir), "get_rate_min"); + CHECK(snd_pcm_hw_params_get_rate_max(params, &max, &dir), "get_rate_max"); + + spa_log_debug(state->log, "min:%u max:%u min-allowed:%u scale:%u interleave:%u all:%d", + min, max, min_allowed_rate, scale, interleave, all); + + min = SPA_MAX(min_allowed_rate * scale / interleave, min) * interleave / scale; + max = max * interleave / scale; + if (max < min) + return 0; + + if (!state->multi_rate && state->card->format_ref > 0) + rate = state->card->rate; + else + rate = state->default_rate; + + if (rate < min || rate > max) + rate = 0; + + if (rate != 0 && !all) + min = max = rate; + + if (rate == 0) + rate = state->position ? state->position->clock.rate.denom : DEFAULT_RATE; + + rate = SPA_CLAMP(rate, min, max); + + spa_log_debug(state->log, "rate:%u multi:%d card:%d def:%d", + rate, state->multi_rate, state->card->rate, state->default_rate); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]); + + if (state->n_allowed_rates > 0) { + uint32_t i, v, last = 0, count = 0; + + if (uint32_array_contains(state->allowed_rates, state->n_allowed_rates, rate)) { + spa_pod_builder_int(b, rate * scale); + count++; + } + for (i = 0; i < state->n_allowed_rates; i++) { + v = SPA_CLAMP(state->allowed_rates[i], min, max); + if (v != last && + uint32_array_contains(state->allowed_rates, state->n_allowed_rates, v)) { + spa_pod_builder_int(b, v * scale); + if (count == 0) + spa_pod_builder_int(b, v * scale); + count++; + } + last = v; + } + if (count > 1) + choice->body.type = SPA_CHOICE_Enum; + } else { + spa_pod_builder_int(b, rate * scale); + + if (min != max) { + spa_pod_builder_int(b, min * scale); + spa_pod_builder_int(b, max * scale); + choice->body.type = SPA_CHOICE_Range; + } + } + spa_pod_builder_pop(b, &f[0]); + + return 1; +} + +static int add_channels(struct state *state, bool all, uint32_t index, uint32_t *next, + snd_pcm_hw_params_t *params, struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; + size_t i; + int err; + snd_pcm_t *hndl = state->hndl; + snd_pcm_chmap_query_t **maps; + unsigned int min, max; + + CHECK(snd_pcm_hw_params_get_channels_min(params, &min), "get_channels_min"); + CHECK(snd_pcm_hw_params_get_channels_max(params, &max), "get_channels_max"); + spa_log_debug(state->log, "channels (%d %d) default:%d all:%d", + min, max, state->default_channels, all); + + if (state->default_channels != 0 && !all) { + if (min < state->default_channels) + min = state->default_channels; + if (max > state->default_channels) + max = state->default_channels; + } + min = SPA_MIN(min, SPA_AUDIO_MAX_CHANNELS); + max = SPA_MIN(max, SPA_AUDIO_MAX_CHANNELS); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_channels, 0); + + if (state->props.use_chmap && (maps = snd_pcm_query_chmaps(hndl)) != NULL) { + uint32_t channel; + snd_pcm_chmap_t* map; + +skip_channels: + if (maps[index] == NULL) { + snd_pcm_free_chmaps(maps); + return 0; + } + map = &maps[index]->map; + + spa_log_debug(state->log, "map %d channels (%d %d)", map->channels, min, max); + + if (map->channels < min || map->channels > max) { + index = (*next)++; + goto skip_channels; + } + + sanitize_map(map); + spa_pod_builder_int(b, map->channels); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0); + spa_pod_builder_push_array(b, &f[0]); + for (i = 0; i < map->channels; i++) { + spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]); + channel = chmap_position_to_channel(map->pos[i]); + spa_pod_builder_id(b, channel); + } + spa_pod_builder_pop(b, &f[0]); + + snd_pcm_free_chmaps(maps); + } + else { + const struct channel_map *map = NULL; + struct spa_pod_choice *choice; + + if (index > 0) + return 0; + + spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]); + spa_pod_builder_int(b, max); + if (min != max) { + spa_pod_builder_int(b, min); + spa_pod_builder_int(b, max); + choice->body.type = SPA_CHOICE_Range; + } + spa_pod_builder_pop(b, &f[0]); + + if (min == max) { + if (state->default_pos.channels == min) + map = &state->default_pos; + else if (min == max && min <= 8) + map = &default_map[min]; + } + if (map) { + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0); + spa_pod_builder_push_array(b, &f[0]); + for (i = 0; i < map->channels; i++) { + spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]); + spa_pod_builder_id(b, map->pos[i]); + } + spa_pod_builder_pop(b, &f[0]); + } + } + return 1; +} + +static void debug_hw_params(struct state *state, const char *prefix, snd_pcm_hw_params_t *params) +{ + if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) { + spa_log_debug(state->log, "%s:", prefix); + snd_pcm_hw_params_dump(params, state->output); + fflush(state->log_file); + } +} +static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next, + struct spa_pod **result, struct spa_pod_builder *b) +{ + int res, err; + size_t j; + snd_pcm_t *hndl; + snd_pcm_hw_params_t *params; + struct spa_pod_frame f[2]; + snd_pcm_format_mask_t *fmask; + snd_pcm_access_mask_t *amask; + unsigned int rrate, rchannels; + struct spa_pod_choice *choice; + + hndl = state->hndl; + snd_pcm_hw_params_alloca(¶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->props.device, state->default_channels, rchannels); + } + } + if (state->default_rate != 0) { + rrate = state->default_rate; + CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &rrate, 0), "set_rate_near"); + if (state->default_rate != rrate) { + spa_log_warn(state->log, "%s: Rate doesn't match (requested %u, got %u)", + state->props.device, state->default_rate, rrate); + } + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + 0); + + snd_pcm_format_mask_alloca(&fmask); + snd_pcm_hw_params_get_format_mask(params, fmask); + + snd_pcm_access_mask_alloca(&amask); + snd_pcm_hw_params_get_access_mask(params, amask); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_format, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + + j = 0; + SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) { + if (fi->format == SND_PCM_FORMAT_UNKNOWN) + continue; + + if (snd_pcm_format_mask_test(fmask, fi->format)) { + if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED) || + snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_NONINTERLEAVED)) && + fi->spa_pformat != SPA_AUDIO_FORMAT_UNKNOWN && + (state->default_format == 0 || state->default_format == fi->spa_pformat)) { + if (j++ == 0) + spa_pod_builder_id(b, fi->spa_pformat); + spa_pod_builder_id(b, fi->spa_pformat); + } + if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_INTERLEAVED) || + snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_INTERLEAVED)) && + (state->default_format == 0 || state->default_format == fi->spa_format)) { + if (j++ == 0) + spa_pod_builder_id(b, fi->spa_format); + spa_pod_builder_id(b, fi->spa_format); + } + } + } + if (j == 0) { + char buf[1024]; + int i, r, offs; + + for (i = 0, offs = 0; i <= SND_PCM_FORMAT_LAST; i++) { + if (snd_pcm_format_mask_test(fmask, (snd_pcm_format_t)i)) { + r = snprintf(&buf[offs], sizeof(buf) - offs, + "%s ", snd_pcm_format_name((snd_pcm_format_t)i)); + if (r < 0 || r + offs >= (int)sizeof(buf)) + return -ENOSPC; + offs += r; + } + } + spa_log_warn(state->log, "%s: no format found (def:%d) formats:%s", + state->props.device, state->default_format, buf); + + for (i = 0, offs = 0; i <= SND_PCM_ACCESS_LAST; i++) { + if (snd_pcm_access_mask_test(amask, (snd_pcm_access_t)i)) { + r = snprintf(&buf[offs], sizeof(buf) - offs, + "%s ", snd_pcm_access_name((snd_pcm_access_t)i)); + if (r < 0 || r + offs >= (int)sizeof(buf)) + return -ENOSPC; + offs += r; + } + } + spa_log_warn(state->log, "%s: access:%s", state->props.device, buf); + return -ENOTSUP; + } + if (j > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if ((res = add_rate(state, 1, 1, false, index & 0xffff, next, 0, params, b)) != 1) + return res; + + if ((res = add_channels(state, false, index & 0xffff, next, params, b)) != 1) + return res; + + *result = spa_pod_builder_pop(b, &f[0]); + return 1; +} + +static bool codec_supported(uint32_t codec, unsigned int chmax, unsigned int rmax) +{ + switch (codec) { + case SPA_AUDIO_IEC958_CODEC_PCM: + case SPA_AUDIO_IEC958_CODEC_DTS: + case SPA_AUDIO_IEC958_CODEC_AC3: + case SPA_AUDIO_IEC958_CODEC_MPEG: + case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC: + if (chmax >= 2) + return true; + break; + case SPA_AUDIO_IEC958_CODEC_EAC3: + if (rmax >= 48000 * 4 && chmax >= 2) + return true; + break; + case SPA_AUDIO_IEC958_CODEC_TRUEHD: + case SPA_AUDIO_IEC958_CODEC_DTSHD: + if (chmax >= 8) + return true; + break; + } + return false; +} + +static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *next, + struct spa_pod **result, struct spa_pod_builder *b) +{ + int res, err, dir; + snd_pcm_t *hndl; + snd_pcm_hw_params_t *params; + struct spa_pod_frame f[2]; + unsigned int rmin, rmax; + unsigned int chmin, chmax; + uint32_t i, c, codecs[16], n_codecs; + + if ((index & 0xffff) > 0) + return 0; + + if (!(state->is_iec958 || state->is_hdmi)) + return 0; + if (state->iec958_codecs == 0) + return 0; + + hndl = state->hndl; + snd_pcm_hw_params_alloca(¶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; +} + +int +spa_alsa_enum_format(struct state *state, int seq, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod *fmt; + int err, res; + bool opened; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened, + state->have_format, state->started); + + opened = state->opened; + if (!state->started && state->have_format) + spa_alsa_close(state); + if ((err = spa_alsa_open(state, NULL)) < 0) + return err; + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + if (result.index < 0x10000) { + if ((res = enum_pcm_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x10000; + goto next; + } + } + else if (result.index < 0x20000) { + if ((res = enum_iec958_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x20000; + goto next; + } + } + else if (result.index < 0x30000) { + if ((res = enum_dsd_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x30000; + goto next; + } + } + else + goto enum_end; + + if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) + goto next; + + spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + if (!opened) + spa_alsa_close(state); + return res; +} + +int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags) +{ + unsigned int rrate, rchannels, val, rscale = 1; + snd_pcm_uframes_t period_size; + int err, dir; + snd_pcm_hw_params_t *params; + snd_pcm_format_t rformat; + snd_pcm_access_mask_t *amask; + snd_pcm_t *hndl; + unsigned int periods; + bool match = true, planar = false, is_batch; + char spdif_params[128] = ""; + + spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened, + state->have_format, state->started); + + state->use_mmap = !state->disable_mmap; + + switch (fmt->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + { + struct spa_audio_info_raw *f = &fmt->info.raw; + rrate = f->rate; + rchannels = f->channels; + rformat = spa_format_to_alsa(f->format, &planar); + break; + } + case SPA_MEDIA_SUBTYPE_iec958: + { + struct spa_audio_info_iec958 *f = &fmt->info.iec958; + unsigned aes3; + + spa_log_info(state->log, "using IEC958 Codec:%s rate:%d", + spa_debug_type_find_short_name(spa_type_audio_iec958_codec, f->codec), + f->rate); + + rformat = SND_PCM_FORMAT_S16_LE; + rchannels = 2; + rrate = f->rate; + + switch (f->codec) { + case SPA_AUDIO_IEC958_CODEC_PCM: + case SPA_AUDIO_IEC958_CODEC_DTS: + case SPA_AUDIO_IEC958_CODEC_AC3: + case SPA_AUDIO_IEC958_CODEC_MPEG: + case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC: + break; + case SPA_AUDIO_IEC958_CODEC_EAC3: + /* EAC3 has 3 rates, 32, 44.1 and 48KHz. We need to + * open the device in 4x that rate. Some clients + * already multiply (mpv,..) others don't (vlc). */ + if (rrate <= 48000) + rrate *= 4; + break; + case SPA_AUDIO_IEC958_CODEC_TRUEHD: + case SPA_AUDIO_IEC958_CODEC_DTSHD: + rchannels = 8; + break; + default: + return -ENOTSUP; + } + switch (rrate) { + case 22050: aes3 = IEC958_AES3_CON_FS_22050; break; + case 24000: aes3 = IEC958_AES3_CON_FS_24000; break; + case 32000: aes3 = IEC958_AES3_CON_FS_32000; break; + case 44100: aes3 = IEC958_AES3_CON_FS_44100; break; + case 48000: aes3 = IEC958_AES3_CON_FS_48000; break; + case 88200: aes3 = IEC958_AES3_CON_FS_88200; break; + case 96000: aes3 = IEC958_AES3_CON_FS_96000; break; + case 176400: aes3 = IEC958_AES3_CON_FS_176400; break; + case 192000: aes3 = IEC958_AES3_CON_FS_192000; break; + case 768000: aes3 = IEC958_AES3_CON_FS_768000; break; + default: aes3 = IEC958_AES3_CON_FS_NOTID; break; + } + spa_scnprintf(spdif_params, sizeof(spdif_params), + ",AES0=0x%x,AES1=0x%x,AES2=0x%x,AES3=0x%x", + IEC958_AES0_CON_EMPHASIS_NONE | IEC958_AES0_NONAUDIO, + IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER, + 0, aes3); + break; + } + case SPA_MEDIA_SUBTYPE_dsd: + { + struct spa_audio_info_dsd *f = &fmt->info.dsd; + + rrate = f->rate; + rchannels = f->channels; + + switch (f->interleave) { + case 4: + rformat = SND_PCM_FORMAT_DSD_U32_BE; + rrate /= 4; + rscale = 4; + break; + case -4: + rformat = SND_PCM_FORMAT_DSD_U32_LE; + rrate /= 4; + rscale = 4; + break; + case 2: + rformat = SND_PCM_FORMAT_DSD_U16_BE; + rrate /= 2; + rscale = 2; + break; + case -2: + rformat = SND_PCM_FORMAT_DSD_U16_LE; + rrate /= 2; + rscale = 2; + break; + case 1: + rformat = SND_PCM_FORMAT_DSD_U8; + rscale = 1; + break; + default: + return -ENOTSUP; + } + break; + } + default: + return -ENOTSUP; + } + + if (rformat == SND_PCM_FORMAT_UNKNOWN) { + spa_log_warn(state->log, "%s: unknown format", + state->props.device); + return -EINVAL; + } + + if (!state->started && state->have_format) + spa_alsa_close(state); + if ((err = spa_alsa_open(state, spdif_params)) < 0) + return err; + + hndl = state->hndl; + + snd_pcm_hw_params_alloca(¶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->props.device, snd_strerror(err)); + return err; + } + } + + /* set the sample format */ + spa_log_debug(state->log, "%p: Stream parameters are %iHz fmt:%s access:%s-%s channels:%i", + state, rrate, snd_pcm_format_name(rformat), + state->use_mmap ? "mmap" : "rw", + planar ? "planar" : "interleaved", rchannels); + CHECK(snd_pcm_hw_params_set_format(hndl, params, rformat), "set_format"); + + /* set the count of channels */ + val = rchannels; + CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &val), "set_channels"); + if (rchannels != val) { + spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)", + state->props.device, rchannels, val); + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) + return -EINVAL; + if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + rchannels = val; + fmt->info.raw.channels = rchannels; + match = false; + } + + if (!state->multi_rate && + state->card->format_ref > 0 && + state->card->rate != rrate) { + spa_log_error(state->log, "%p: card already opened at rate:%i", + state, state->card->rate); + return -EINVAL; + } + + /* set the stream rate */ + val = rrate; + CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &val, 0), "set_rate_near"); + if (rrate != val) { + spa_log_warn(state->log, "%s: Rate doesn't match (requested %iHz, got %iHz)", + state->props.device, rrate, val); + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) + return -EINVAL; + if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + rrate = val; + fmt->info.raw.rate = rrate; + match = false; + } + if (rchannels == 0 || rrate == 0) { + spa_log_error(state->log, "%s: invalid channels:%d or rate:%d", + state->props.device, rchannels, rrate); + return -EIO; + } + + state->format = rformat; + state->channels = rchannels; + state->rate = rrate; + state->frame_size = snd_pcm_format_physical_width(rformat) / 8; + state->frame_scale = rscale; + state->planar = planar; + state->blocks = 1; + if (planar) + state->blocks *= rchannels; + else + state->frame_size *= rchannels; + + state->have_format = true; + if (state->card->format_ref++ == 0) + state->card->rate = rrate; + + dir = 0; + period_size = state->default_period_size; + is_batch = snd_pcm_hw_params_is_batch(params) && + !state->disable_batch; + + if (is_batch) { + if (period_size == 0) + period_size = state->position ? state->position->clock.duration : DEFAULT_PERIOD; + if (period_size == 0) + period_size = DEFAULT_PERIOD; + /* batch devices get their hw pointers updated every period. Make + * the period smaller and add one period of headroom. Limit the + * period size to our default so that we don't create too much + * headroom. */ + period_size = SPA_MIN(period_size, DEFAULT_PERIOD) / 2; + spa_log_info(state->log, "%s: batch mode, period_size:%ld", + state->props.device, period_size); + } else { + if (period_size == 0) + period_size = DEFAULT_PERIOD; + /* disable ALSA wakeups, we use a timer */ + if (snd_pcm_hw_params_can_disable_period_wakeup(params)) + CHECK(snd_pcm_hw_params_set_period_wakeup(hndl, params, 0), "set_period_wakeup"); + } + + CHECK(snd_pcm_hw_params_set_period_size_near(hndl, params, &period_size, &dir), "set_period_size_near"); + + if (period_size == 0) { + spa_log_error(state->log, "%s: invalid period_size 0 (driver error?)", state->props.device); + return -EIO; + } + + state->period_frames = period_size; + + if (state->default_period_num != 0) { + periods = state->default_period_num; + CHECK(snd_pcm_hw_params_set_periods_near(hndl, params, &periods, &dir), "set_periods"); + state->buffer_frames = period_size * periods; + } else { + CHECK(snd_pcm_hw_params_get_buffer_size_max(params, &state->buffer_frames), "get_buffer_size_max"); + + state->buffer_frames = SPA_MIN(state->buffer_frames, state->quantum_limit * 4)* state->frame_scale; + + CHECK(snd_pcm_hw_params_set_buffer_size_min(hndl, params, &state->buffer_frames), "set_buffer_size_min"); + CHECK(snd_pcm_hw_params_set_buffer_size_near(hndl, params, &state->buffer_frames), "set_buffer_size_near"); + periods = state->buffer_frames / period_size; + } + if (state->buffer_frames == 0) { + spa_log_error(state->log, "%s: invalid buffer_frames 0 (driver error?)", state->props.device); + return -EIO; + } + + state->headroom = state->default_headroom; + if (is_batch) + state->headroom += period_size; + + if (spa_strstartswith(state->props.device, "a52") || + spa_strstartswith(state->props.device, "dca")) + state->min_delay = SPA_MIN(2048u, state->buffer_frames); + else + state->min_delay = 0; + + state->headroom = SPA_MIN(state->headroom, state->buffer_frames); + state->start_delay = state->default_start_delay; + + state->latency[state->port_direction].min_rate = + state->latency[state->port_direction].max_rate = + SPA_MAX(state->min_delay, state->headroom); + + spa_log_info(state->log, "%s (%s): format:%s access:%s-%s rate:%d channels:%d " + "buffer frames %lu, period frames %lu, periods %u, frame_size %zd " + "headroom %u start-delay:%u", + state->props.device, + state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", + snd_pcm_format_name(state->format), + state->use_mmap ? "mmap" : "rw", + planar ? "planar" : "interleaved", + state->rate, state->channels, state->buffer_frames, state->period_frames, + periods, state->frame_size, state->headroom, state->start_delay); + + /* write the parameters to device */ + CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params"); + + return match ? 0 : 1; +} + +static int set_swparams(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + int err = 0; + snd_pcm_sw_params_t *params; + + snd_pcm_sw_params_alloca(¶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"); + + CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event"); + + /* write the parameters to the playback device */ + CHECK(snd_pcm_sw_params(hndl, params), "sw_params"); + + if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) { + spa_log_debug(state->log, "state after sw_params:"); + snd_pcm_dump(hndl, state->output); + fflush(state->log_file); + } + + return 0; +} + +static int set_timeout(struct state *state, uint64_t time) +{ + struct itimerspec ts; + + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, + state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); + return 0; +} + +int spa_alsa_silence(struct state *state, snd_pcm_uframes_t silence) +{ + snd_pcm_t *hndl = state->hndl; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t frames, offset; + int i, res; + + if (state->use_mmap) { + frames = state->buffer_frames; + + if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + silence = SPA_MIN(silence, frames); + + spa_log_trace_fp(state->log, "%p: frames:%ld offset:%ld silence %ld", + state, frames, offset, silence); + snd_pcm_areas_silence(my_areas, offset, state->channels, silence, state->format); + + if (SPA_UNLIKELY((res = snd_pcm_mmap_commit(hndl, offset, silence)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->props.device, snd_strerror(res)); + return res; + } + } else { + uint8_t buffer[silence * state->frame_size]; + memset(buffer, 0, silence * state->frame_size); + + if (state->planar) { + void *bufs[state->channels]; + for (i = 0; i < state->channels; i++) + bufs[i] = buffer; + snd_pcm_writen(hndl, bufs, silence); + } else { + snd_pcm_writei(hndl, buffer, silence); + } + } + return 0; +} + +static inline int do_start(struct state *state) +{ + int res; + if (SPA_UNLIKELY(!state->alsa_started)) { + spa_log_trace(state->log, "%p: snd_pcm_start", state); + if ((res = snd_pcm_start(state->hndl)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_start: %s", + state->props.device, snd_strerror(res)); + return res; + } + state->alsa_started = true; + } + return 0; +} + +static int alsa_recover(struct state *state, int err) +{ + int res, st; + snd_pcm_status_t *status; + + snd_pcm_status_alloca(&status); + if (SPA_UNLIKELY((res = snd_pcm_status(state->hndl, status)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_status error: %s", + state->props.device, snd_strerror(res)); + goto recover; + } + + st = snd_pcm_status_get_state(status); + switch (st) { + case SND_PCM_STATE_XRUN: + { + struct timeval now, trigger, diff; + uint64_t delay, missing; + + snd_pcm_status_get_tstamp (status, &now); + snd_pcm_status_get_trigger_tstamp (status, &trigger); + timersub(&now, &trigger, &diff); + + delay = SPA_TIMEVAL_TO_USEC(&diff); + missing = delay * state->rate / SPA_USEC_PER_SEC; + + spa_log_trace(state->log, "%p: xrun of %"PRIu64" usec %"PRIu64, + state, delay, missing); + + spa_node_call_xrun(&state->callbacks, + SPA_TIMEVAL_TO_USEC(&trigger), delay, NULL); + + state->sample_count += missing ? missing : state->threshold; + break; + } + case SND_PCM_STATE_SUSPENDED: + spa_log_info(state->log, "%s: recover from state %s", + state->props.device, snd_pcm_state_name(st)); + res = snd_pcm_resume(state->hndl); + if (res >= 0) + return res; + err = -ESTRPIPE; + break; + default: + spa_log_error(state->log, "%s: recover from error state %s", + state->props.device, snd_pcm_state_name(st)); + break; + } + +recover: + if (SPA_UNLIKELY((res = snd_pcm_recover(state->hndl, err, true)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_recover error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_dll_init(&state->dll); + state->alsa_recovering = true; + state->alsa_started = false; + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom); + + return do_start(state); +} + +static int get_avail(struct state *state, uint64_t current_time) +{ + int res, missed; + snd_pcm_sframes_t avail; + + if (SPA_UNLIKELY((avail = snd_pcm_avail(state->hndl)) < 0)) { + if ((res = alsa_recover(state, avail)) < 0) + return res; + if ((avail = snd_pcm_avail(state->hndl)) < 0) { + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d missed) snd_pcm_avail after recover: %s", + state->props.device, missed, snd_strerror(avail)); + } + avail = state->threshold * 2; + } + } else { + state->alsa_recovering = false; + } + return avail; +} + +#if 0 +static int get_avail_htimestamp(struct state *state, uint64_t current_time) +{ + int res, missed; + snd_pcm_uframes_t avail; + snd_htimestamp_t tstamp; + uint64_t then; + + if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) { + if ((res = alsa_recover(state, avail)) < 0) + return res; + if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) { + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d missed) snd_pcm_htimestamp error: %s", + state->props.device, missed, snd_strerror(res)); + } + avail = state->threshold * 2; + } + } else { + state->alsa_recovering = false; + } + + if ((then = SPA_TIMESPEC_TO_NSEC(&tstamp)) != 0) { + if (then < current_time) + avail += (current_time - then) * state->rate / SPA_NSEC_PER_SEC; + else + avail -= (then - current_time) * state->rate / SPA_NSEC_PER_SEC; + } + return SPA_MIN(avail, state->buffer_frames); +} +#endif + +static int get_status(struct state *state, uint64_t current_time, + snd_pcm_uframes_t *delay, snd_pcm_uframes_t *target) +{ + int avail; + + if ((avail = get_avail(state, current_time)) < 0) + return avail; + + avail = SPA_MIN(avail, (int)state->buffer_frames); + + *target = state->threshold + state->headroom; + + if (state->resample && state->rate_match) { + state->delay = state->rate_match->delay; + state->read_size = state->rate_match->size; + } else { + state->delay = 0; + state->read_size = state->threshold; + } + + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + *delay = state->buffer_frames - avail; + } else { + *delay = avail; + *target = SPA_MAX(*target, state->read_size); + } + *target = SPA_CLAMP(*target, state->min_delay, state->buffer_frames); + return 0; +} + +static int update_time(struct state *state, uint64_t current_time, snd_pcm_sframes_t delay, + snd_pcm_sframes_t target, bool follower) +{ + double err, corr; + int32_t diff; + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + err = delay - target; + else + err = target - delay; + + if (SPA_UNLIKELY(state->dll.bw == 0.0)) { + spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, state->rate); + state->next_time = current_time; + state->base_time = current_time; + } + diff = (int32_t) (state->last_threshold - state->threshold); + + if (SPA_UNLIKELY(diff != 0)) { + err -= diff; + spa_log_trace(state->log, "%p: follower:%d quantum change %d -> %d (%d) %f", + state, follower, state->last_threshold, state->threshold, diff, err); + state->last_threshold = state->threshold; + state->alsa_sync = true; + state->alsa_sync_warning = false; + } + if (err > state->max_error) { + err = state->max_error; + state->alsa_sync = true; + } else if (err < -state->max_error) { + err = -state->max_error; + state->alsa_sync = true; + } + + if (!follower || state->matching) + corr = spa_dll_update(&state->dll, err); + else + corr = 1.0; + + if (diff < 0) + state->next_time += diff / corr * 1e9 / state->rate; + + if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) { + state->base_time = state->next_time; + + spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f " + "bw:%f thr:%u del:%ld target:%ld err:%f max:%f", + state->props.device, follower, state->matching, + corr, state->dll.bw, state->threshold, delay, target, + err, state->max_error); + } + + if (state->rate_match) { + if (state->stream == SND_PCM_STREAM_PLAYBACK) + state->rate_match->rate = corr; + else + state->rate_match->rate = 1.0/corr; + + SPA_FLAG_UPDATE(state->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, state->matching); + } + + state->next_time += state->threshold / corr * 1e9 / state->rate; + + if (SPA_LIKELY(!follower && state->clock)) { + state->clock->nsec = current_time; + state->clock->position += state->duration; + state->clock->duration = state->duration; + state->clock->delay = delay + state->delay; + state->clock->rate_diff = corr; + state->clock->next_nsec = state->next_time; + } + + spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %f %ld %f %f %u", + state, follower, current_time, corr, delay, err, state->threshold * corr, + state->threshold); + + return 0; +} + +static inline bool is_following(struct state *state) +{ + return state->position && state->clock && state->position->clock.id != state->clock->id; +} + +static int setup_matching(struct state *state) +{ + state->matching = state->following; + + if (state->position == NULL) + return -ENOTSUP; + + spa_log_debug(state->log, "driver clock:'%s' our clock:'%s'", + state->position->clock.name, state->clock_name); + + if (spa_streq(state->position->clock.name, state->clock_name)) + state->matching = false; + + state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching; + + spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d", + state->position->clock.name, state->rate_denom, + state->clock_name, state->rate, + state->matching, state->resample); + return 0; +} + +static inline void check_position_config(struct state *state) +{ + if (SPA_UNLIKELY(state->position == NULL)) + return; + + if (SPA_UNLIKELY((state->duration != state->position->clock.duration) || + (state->rate_denom != state->position->clock.rate.denom))) { + state->duration = state->position->clock.duration; + state->rate_denom = state->position->clock.rate.denom; + state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom); + state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f); + state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching; + state->alsa_sync = true; + } +} + +int spa_alsa_write(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write; + snd_pcm_sframes_t commitres; + int res = 0, missed; + size_t frame_size = state->frame_size; + + check_position_config(state); + + max_write = state->buffer_frames; + + if (state->following && state->alsa_started) { + uint64_t current_time; + snd_pcm_uframes_t delay, target; + + current_time = state->position->clock.nsec; + + if (SPA_UNLIKELY((res = get_status(state, current_time, &delay, &target)) < 0)) + return res; + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0)) + return res; + + if (SPA_UNLIKELY(state->alsa_sync)) { + enum spa_log_level lev; + + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; + + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, " + "resync (%d missed)", state->props.device, delay, + target, state->threshold, missed); + } + + if (delay > target) + snd_pcm_rewind(state->hndl, delay - target); + else if (delay < target) + spa_alsa_silence(state, target - delay); + delay = target; + state->alsa_sync = false; + } else + state->alsa_sync_warning = true; + } + + total_written = 0; +again: + + frames = max_write; + if (state->use_mmap && frames > 0) { + if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_log_trace_fp(state->log, "%p: begin %ld %ld %d", + state, offset, frames, state->threshold); + off = offset; + } else { + off = 0; + } + + to_write = frames; + written = 0; + + while (!spa_list_is_empty(&state->ready) && to_write > 0) { + size_t n_bytes, n_frames; + struct buffer *b; + struct spa_data *d; + uint32_t i, offs, size, last_offset; + + b = spa_list_first(&state->ready, struct buffer, link); + d = b->buf->datas; + + offs = d[0].chunk->offset + state->ready_offset; + last_offset = d[0].chunk->size; + size = last_offset - state->ready_offset; + + offs = SPA_MIN(offs, d[0].maxsize); + size = SPA_MIN(d[0].maxsize - offs, size); + + n_frames = SPA_MIN(size / frame_size, to_write); + n_bytes = n_frames * frame_size; + + if (SPA_LIKELY(state->use_mmap)) { + for (i = 0; i < b->buf->n_datas; i++) { + spa_memcpy(SPA_PTROFF(my_areas[i].addr, off * frame_size, void), + SPA_PTROFF(d[i].data, offs, void), n_bytes); + } + } else { + void *bufs[b->buf->n_datas]; + for (i = 0; i < b->buf->n_datas; i++) + bufs[i] = SPA_PTROFF(d[i].data, offs, void); + + if (state->planar) + snd_pcm_writen(hndl, bufs, n_frames); + else + snd_pcm_writei(hndl, bufs[0], n_frames); + } + + state->ready_offset += n_bytes; + + if (state->ready_offset >= last_offset) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + state->io->buffer_id = b->id; + spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id); + + spa_node_call_reuse_buffer(&state->callbacks, 0, b->id); + + state->ready_offset = 0; + } + written += n_frames; + off += n_frames; + to_write -= n_frames; + } + + spa_log_trace_fp(state->log, "%p: commit %ld %ld %"PRIi64, + state, offset, written, state->sample_count); + total_written += written; + + if (state->use_mmap && written > 0) { + if (SPA_UNLIKELY((commitres = snd_pcm_mmap_commit(hndl, offset, written)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->props.device, snd_strerror(commitres)); + if (commitres != -EPIPE && commitres != -ESTRPIPE) + return res; + } + if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) { + spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld", + state->props.device, commitres, written); + } + } + + if (!spa_list_is_empty(&state->ready) && written > 0) + goto again; + + state->sample_count += total_written; + + if (SPA_UNLIKELY(!state->alsa_started && (total_written > 0 || frames == 0))) + do_start(state); + + return 0; +} + +void spa_alsa_recycle_buffer(struct state *this, uint32_t buffer_id) +{ + struct buffer *b = &this->buffers[buffer_id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&this->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } +} + +static snd_pcm_uframes_t +push_frames(struct state *state, + const snd_pcm_channel_area_t *my_areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t frames) +{ + snd_pcm_uframes_t total_frames = 0; + + if (spa_list_is_empty(&state->free)) { + spa_log_warn(state->log, "%s: no more buffers", state->props.device); + total_frames = frames; + } else { + size_t n_bytes, left, frame_size = state->frame_size; + struct buffer *b; + struct spa_data *d; + uint32_t i, avail, l0, l1; + + b = spa_list_first(&state->free, struct buffer, link); + spa_list_remove(&b->link); + + if (b->h) { + b->h->seq = state->sample_count; + b->h->pts = state->next_time; + b->h->dts_offset = 0; + } + + d = b->buf->datas; + + avail = d[0].maxsize / frame_size; + total_frames = SPA_MIN(avail, frames); + n_bytes = total_frames * frame_size; + + if (my_areas) { + left = state->buffer_frames - offset; + l0 = SPA_MIN(n_bytes, left * frame_size); + l1 = n_bytes - l0; + + for (i = 0; i < b->buf->n_datas; i++) { + spa_memcpy(d[i].data, + SPA_PTROFF(my_areas[i].addr, offset * frame_size, void), + l0); + if (SPA_UNLIKELY(l1 > 0)) + spa_memcpy(SPA_PTROFF(d[i].data, l0, void), + my_areas[i].addr, + l1); + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = frame_size; + } + } else { + void *bufs[b->buf->n_datas]; + for (i = 0; i < b->buf->n_datas; i++) { + bufs[i] = d[i].data; + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = frame_size; + } + if (state->planar) { + snd_pcm_readn(state->hndl, bufs, total_frames); + } else { + snd_pcm_readi(state->hndl, bufs[0], total_frames); + } + } + spa_log_trace_fp(state->log, "%p: wrote %ld frames into buffer %d", + state, total_frames, b->id); + + spa_list_append(&state->ready, &b->link); + } + return total_frames; +} + + +int spa_alsa_read(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + snd_pcm_uframes_t total_read = 0, to_read, max_read; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t read, frames, offset; + snd_pcm_sframes_t commitres; + int res = 0, missed; + + check_position_config(state); + + max_read = state->buffer_frames; + + if (state->following && state->alsa_started) { + uint64_t current_time; + snd_pcm_uframes_t avail, delay, target; + + current_time = state->position->clock.nsec; + + if ((res = get_status(state, current_time, &delay, &target)) < 0) + return res; + + avail = delay; + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0)) + return res; + + if (state->alsa_sync) { + enum spa_log_level lev; + + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; + + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, " + "resync (%d missed)", state->props.device, delay, + target, state->threshold, missed); + } + + if (delay < target) + max_read = target - delay; + else if (delay > target) + snd_pcm_forward(state->hndl, delay - target); + delay = target; + state->alsa_sync = false; + } else + state->alsa_sync_warning = true; + + if (avail < state->read_size) + max_read = 0; + } + + frames = SPA_MIN(max_read, state->read_size); + + if (state->use_mmap) { + to_read = state->buffer_frames; + if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &to_read)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld to_read:%ld thres:%d", state, + offset, frames, to_read, state->threshold); + } else { + my_areas = NULL; + offset = 0; + } + + if (frames > 0) { + read = push_frames(state, my_areas, offset, frames); + total_read += read; + } else { + spa_alsa_skip(state); + total_read += state->read_size; + read = 0; + } + + if (state->use_mmap && read > 0) { + spa_log_trace_fp(state->log, "%p: commit offs:%ld read:%ld count:%"PRIi64, state, + offset, read, state->sample_count); + if ((commitres = snd_pcm_mmap_commit(hndl, offset, read)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error %lu %lu: %s", + state->props.device, frames, read, snd_strerror(commitres)); + if (commitres != -EPIPE && commitres != -ESTRPIPE) + return res; + } + if (commitres > 0 && read != (snd_pcm_uframes_t) commitres) { + spa_log_warn(state->log, "%s: mmap_commit read %ld instead of %ld", + state->props.device, commitres, read); + } + } + + state->sample_count += total_read; + + return 0; +} + +int spa_alsa_skip(struct state *state) +{ + struct buffer *b; + struct spa_data *d; + uint32_t i, avail, total_frames, n_bytes, frames; + + if (spa_list_is_empty(&state->free)) { + spa_log_warn(state->log, "%s: no more buffers", state->props.device); + return -EPIPE; + } + + frames = state->read_size; + + b = spa_list_first(&state->free, struct buffer, link); + spa_list_remove(&b->link); + + d = b->buf->datas; + + avail = d[0].maxsize / state->frame_size; + total_frames = SPA_MIN(avail, frames); + n_bytes = total_frames * state->frame_size; + + for (i = 0; i < b->buf->n_datas; i++) { + memset(d[i].data, 0, n_bytes); + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = state->frame_size; + } + spa_list_append(&state->ready, &b->link); + + return 0; +} + + +static int handle_play(struct state *state, uint64_t current_time, + snd_pcm_uframes_t delay, snd_pcm_uframes_t target) +{ + int res; + + if (state->alsa_started && SPA_UNLIKELY(delay > target + state->max_error)) { + spa_log_trace(state->log, "%p: early wakeup %lu %lu", state, delay, target); + if (delay > target * 3) + delay = target * 3; + state->next_time = current_time + (delay - target) * SPA_NSEC_PER_SEC / state->rate; + return -EAGAIN; + } + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, false)) < 0)) + return res; + + if (spa_list_is_empty(&state->ready)) { + struct spa_io_buffers *io = state->io; + + spa_log_trace_fp(state->log, "%p: %d", state, io->status); + + io->status = SPA_STATUS_NEED_DATA; + + res = spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA); + } + else { + res = spa_alsa_write(state); + } + return res; +} + +static int handle_capture(struct state *state, uint64_t current_time, + snd_pcm_uframes_t delay, snd_pcm_uframes_t target) +{ + int res; + struct spa_io_buffers *io; + + if (SPA_UNLIKELY(delay < target)) { + spa_log_trace(state->log, "%p: early wakeup %ld %ld", state, delay, target); + state->next_time = current_time + (target - delay) * SPA_NSEC_PER_SEC / + state->rate; + return -EAGAIN; + } + + if (SPA_UNLIKELY(res = update_time(state, current_time, delay, target, false)) < 0) + return res; + + if ((res = spa_alsa_read(state)) < 0) + return res; + + if (spa_list_is_empty(&state->ready)) + return 0; + + io = state->io; + if (io != NULL && + (io->status != SPA_STATUS_HAVE_DATA || state->rate_match != NULL)) { + struct buffer *b; + + if (io->buffer_id < state->n_buffers) + spa_alsa_recycle_buffer(state, io->buffer_id); + + b = spa_list_first(&state->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id); + } + spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA); + return 0; +} + +static void alsa_on_timeout_event(struct spa_source *source) +{ + struct state *state = source->data; + snd_pcm_uframes_t delay, target; + uint64_t expire, current_time; + int res; + + if (SPA_LIKELY(state->started)) { + if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system, + state->timerfd, &expire)) < 0)) { + /* we can get here when the timer is changed since the last + * timerfd wakeup, for example by do_reassign_follower() executed + * in the same epoll wakeup cycle */ + if (res != -EAGAIN) + spa_log_warn(state->log, "%p: error reading timerfd: %s", + state, spa_strerror(res)); + return; + } + } + + check_position_config(state); + + current_time = state->next_time; + + if (SPA_UNLIKELY(get_status(state, current_time, &delay, &target) < 0)) { + spa_log_error(state->log, "get_status error"); + state->next_time += state->threshold * 1e9 / state->rate; + goto done; + } + +#ifndef FASTPATH + if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { + struct timespec now; + uint64_t nsec; + if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0) + return; + nsec = SPA_TIMESPEC_TO_NSEC(&now); + spa_log_trace_fp(state->log, "%p: timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64 + " %d %"PRIi64, state, delay, target, nsec, nsec, + nsec - current_time, state->threshold, state->sample_count); + } +#endif + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + handle_play(state, current_time, delay, target); + else + handle_capture(state, current_time, delay, target); + +done: + if (state->next_time > current_time + SPA_NSEC_PER_SEC || + current_time > state->next_time + SPA_NSEC_PER_SEC) { + spa_log_error(state->log, "%s: impossible timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64 + " %d %"PRIi64, state->props.device, delay, target, current_time, state->next_time, + state->next_time - current_time, state->threshold, state->sample_count); + state->next_time = current_time + state->threshold * 1e9 / state->rate; + } + set_timeout(state, state->next_time); +} + +static void reset_buffers(struct state *this) +{ + uint32_t i; + + spa_list_init(&this->free); + spa_list_init(&this->ready); + + for (i = 0; i < this->n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + if (this->stream == SND_PCM_STREAM_PLAYBACK) { + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } else { + spa_list_append(&this->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } + } +} + +static int set_timers(struct state *state) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0) + return res; + state->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (state->following) { + set_timeout(state, 0); + } else { + set_timeout(state, state->next_time); + } + return 0; +} + +int spa_alsa_start(struct state *state) +{ + int err; + + if (state->started) + return 0; + + if (state->position) { + state->duration = state->position->clock.duration; + state->rate_denom = state->position->clock.rate.denom; + } + else { + spa_log_warn(state->log, "%s: no position set, using defaults", + state->props.device); + state->duration = 1024; + state->rate_denom = state->rate; + } + if (state->rate_denom == 0) { + spa_log_error(state->log, "%s: unset rate_denom", state->props.device); + return -EIO; + } + if (state->duration == 0) { + spa_log_error(state->log, "%s: unset duration", state->props.device); + return -EIO; + } + + state->following = is_following(state); + setup_matching(state); + + spa_dll_init(&state->dll); + state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom); + state->last_threshold = state->threshold; + state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f); + + spa_log_debug(state->log, "%p: start %d duration:%d rate:%d follower:%d match:%d resample:%d", + state, state->threshold, state->duration, state->rate_denom, + state->following, state->matching, state->resample); + + CHECK(set_swparams(state), "swparams"); + + if ((err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) { + spa_log_error(state->log, "%s: snd_pcm_prepare error: %s", + state->props.device, snd_strerror(err)); + return err; + } + + state->source.func = alsa_on_timeout_event; + state->source.data = state; + state->source.fd = state->timerfd; + state->source.mask = SPA_IO_IN; + state->source.rmask = 0; + spa_loop_add_source(state->data_loop, &state->source); + + reset_buffers(state); + state->alsa_sync = true; + state->alsa_sync_warning = false; + state->alsa_recovering = false; + state->alsa_started = false; + + /* start capture now, playback will start after first write */ + if (state->stream == SND_PCM_STREAM_PLAYBACK) + spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom); + else if ((err = do_start(state)) < 0) + return err; + + set_timers(state); + + state->started = true; + + return 0; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + set_timers(state); + spa_dll_init(&state->dll); + return 0; +} + +int spa_alsa_reassign_follower(struct state *state) +{ + bool following, freewheel; + + if (!state->started) + return 0; + + following = is_following(state); + if (following != state->following) { + spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following); + state->following = following; + spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + } + setup_matching(state); + + freewheel = state->position && + SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL); + + if (state->freewheel != freewheel) { + spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel); + state->freewheel = freewheel; + if (freewheel) + snd_pcm_pause(state->hndl, 1); + else + snd_pcm_pause(state->hndl, 0); + } + + state->alsa_sync_warning = false; + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + struct itimerspec ts; + + spa_loop_remove_source(state->data_loop, &state->source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, state->timerfd, 0, &ts, NULL); + + return 0; +} + +int spa_alsa_pause(struct state *state) +{ + int err; + + if (!state->started) + return 0; + + spa_log_debug(state->log, "%p: pause", state); + + spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + + if ((err = snd_pcm_drop(state->hndl)) < 0) + spa_log_error(state->log, "%s: snd_pcm_drop %s", state->props.device, + snd_strerror(err)); + + state->started = false; + + return 0; +} diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h new file mode 100644 index 0000000..9c4a868 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm.h @@ -0,0 +1,374 @@ +/* Spa ALSA Sink + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_ALSA_UTILS_H +#define SPA_ALSA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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 +#define DEFAULT_USE_CHMAP false + +struct props { + char device[64]; + char device_name[128]; + char card_name[128]; + bool use_chmap; +}; + +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +#define BW_MAX 0.128 +#define BW_MED 0.064 +#define BW_MIN 0.016 +#define BW_PERIOD (3 * SPA_NSEC_PER_SEC) + +struct channel_map { + uint32_t channels; + uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct card { + struct spa_list link; + int ref; + uint32_t index; + snd_use_case_mgr_t *ucm; + char *ucm_prefix; + int format_ref; + uint32_t rate; +}; + +struct ratelimit { + uint64_t interval; + uint64_t begin; + unsigned burst; + unsigned n_printed, n_missed; +}; + +struct state { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_system *data_system; + struct spa_loop *data_loop; + + FILE *log_file; + struct ratelimit rate_limit; + + uint32_t card_index; + struct card *card; + snd_pcm_stream_t stream; + snd_output_t *output; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint64_t info_all; + struct spa_node_info info; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_IO 2 +#define NODE_ProcessLatency 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + bool opened; + snd_pcm_t *hndl; + + bool have_format; + struct spa_audio_info current_format; + + uint32_t default_period_size; + uint32_t default_period_num; + uint32_t default_headroom; + uint32_t default_start_delay; + uint32_t default_format; + unsigned int default_channels; + unsigned int default_rate; + uint32_t allowed_rates[MAX_RATES]; + uint32_t n_allowed_rates; + struct channel_map default_pos; + unsigned int disable_mmap; + unsigned int disable_batch; + char clock_name[64]; + uint32_t quantum_limit; + + snd_pcm_uframes_t buffer_frames; + snd_pcm_uframes_t period_frames; + snd_pcm_format_t format; + int rate; + int channels; + size_t frame_size; + size_t frame_scale; + int blocks; + uint32_t rate_denom; + uint32_t delay; + uint32_t read_size; + + uint64_t port_info_all; + struct spa_port_info port_info; +#define PORT_EnumFormat 0 +#define PORT_Meta 1 +#define PORT_IO 2 +#define PORT_Format 3 +#define PORT_Buffers 4 +#define PORT_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info port_params[N_PORT_PARAMS]; + enum spa_direction port_direction; + struct spa_io_buffers *io; + struct spa_io_clock *clock; + struct spa_io_position *position; + struct spa_io_rate_match *rate_match; + + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; + + struct spa_list free; + struct spa_list ready; + + size_t ready_offset; + + bool started; + struct spa_source source; + int timerfd; + uint32_t threshold; + uint32_t last_threshold; + uint32_t headroom; + uint32_t start_delay; + uint32_t min_delay; + + uint32_t duration; + unsigned int alsa_started:1; + unsigned int alsa_sync:1; + unsigned int alsa_sync_warning:1; + unsigned int alsa_recovering:1; + unsigned int following:1; + unsigned int matching:1; + unsigned int resample:1; + unsigned int use_mmap:1; + unsigned int planar:1; + unsigned int freewheel:1; + unsigned int open_ucm:1; + unsigned int is_iec958:1; + unsigned int is_hdmi:1; + unsigned int multi_rate:1; + + uint64_t iec958_codecs; + + int64_t sample_count; + + int64_t sample_time; + uint64_t next_time; + uint64_t base_time; + + uint64_t underrun; + + struct spa_dll dll; + double max_error; + + struct spa_latency_info latency[2]; + struct spa_process_latency_info process_latency; +}; + +struct spa_pod *spa_alsa_enum_propinfo(struct state *state, + uint32_t idx, struct spa_pod_builder *b); +int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b); +int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params); + +int spa_alsa_enum_format(struct state *state, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter); + +int spa_alsa_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags); + +int spa_alsa_init(struct state *state, const struct spa_dict *info); +int spa_alsa_clear(struct state *state); + +int spa_alsa_open(struct state *state, const char *params); +int spa_alsa_start(struct state *state); +int spa_alsa_reassign_follower(struct state *state); +int spa_alsa_pause(struct state *state); +int spa_alsa_close(struct state *state); + +int spa_alsa_write(struct state *state); +int spa_alsa_read(struct state *state); +int spa_alsa_skip(struct state *state); + +void spa_alsa_recycle_buffer(struct state *state, uint32_t buffer_id); + +static inline uint32_t spa_alsa_format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static inline uint32_t spa_alsa_channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static inline void spa_alsa_parse_position(struct channel_map *map, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + map->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + map->channels < SPA_AUDIO_MAX_CHANNELS) { + map->pos[map->channels++] = spa_alsa_channel_from_name(v); + } +} + +static inline uint32_t spa_alsa_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + uint32_t count; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + count = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max) + rates[count++] = atoi(v); + return count; +} + +static inline uint32_t spa_alsa_iec958_codec_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_iec958_codec[i].name; i++) { + if (strcmp(name, spa_debug_type_short_name(spa_type_audio_iec958_codec[i].name)) == 0) + return spa_type_audio_iec958_codec[i].type; + } + return SPA_AUDIO_IEC958_CODEC_UNKNOWN; +} + +static inline void spa_alsa_parse_iec958_codecs(uint64_t *codecs, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + *codecs = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0) + *codecs |= 1ULL << spa_alsa_iec958_codec_from_name(v); +} + +static inline uint32_t spa_alsa_get_iec958_codecs(struct state *state, uint32_t *codecs, + uint32_t max_codecs) +{ + uint64_t mask = state->iec958_codecs; + uint32_t i = 0, j = 0; + if (!(state->is_iec958 || state->is_hdmi)) + return 0; + while (mask && i < max_codecs) { + if (mask & 1) + codecs[i++] = j; + mask >>= 1; + j++; + } + return i; +} + +static inline int ratelimit_test(struct ratelimit *r, uint64_t now) +{ + unsigned missed = 0; + if (r->begin + r->interval < now) { + missed = r->n_missed; + r->begin = now; + r->n_printed = 0; + r->n_missed = 0; + } else if (r->n_printed >= r->burst) { + r->n_missed++; + return -1; + } + r->n_printed++; + return missed; +} + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_ALSA_UTILS_H */ diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c new file mode 100644 index 0000000..ee50163 --- /dev/null +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -0,0 +1,1006 @@ +/* Spa ALSA Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static void reset_props(struct props *props) +{ + strncpy(props->device, DEFAULT_DEVICE, sizeof(props->device)); + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->disable_longname = 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct seq_state *this = object; + struct spa_pod *param; + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct props *p; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + p = &this->props; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Props: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct seq_state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) + spa_scnprintf(this->clock->name, sizeof(this->clock->name), + "%s", this->props.clock_name); + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + spa_alsa_seq_reassign_follower(this); + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct seq_state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + if (param == NULL) { + reset_props(p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device))); + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct seq_state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if ((res = spa_alsa_seq_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_alsa_seq_pause(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "alsa" }, + { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct seq_state *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static inline void clean_name(char *name) +{ + char *c; + for (c = name; *c; ++c) { + if (!isalnum(*c) && strchr(" /_:()[]", *c) == NULL) + *c = '-'; + } +} + +static void emit_port_info(struct seq_state *this, struct seq_port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[5]; + uint32_t n_items = 0; + int id; + snd_seq_port_info_t *info; + snd_seq_client_info_t *client_info; + char card[8]; + char name[256]; + char path[128]; + char alias[128]; + + snd_seq_port_info_alloca(&info); + snd_seq_get_any_port_info(this->sys.hndl, + port->addr.client, port->addr.port, info); + + snd_seq_client_info_alloca(&client_info); + snd_seq_get_any_client_info(this->sys.hndl, + port->addr.client, client_info); + + int card_id; + + // Failed to obtain card number (software device) or disabled + if (this->props.disable_longname || (card_id = snd_seq_client_info_get_card(client_info)) < 0) { + snprintf(name, sizeof(name), "%s:(%s_%d) %s", + snd_seq_client_info_get_name(client_info), + port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback", + port->addr.port, + snd_seq_port_info_get_name(info)); + } else { + char *longname; + if (snd_card_get_longname(card_id, &longname) == 0) { + snprintf(name, sizeof(name), "%s:(%s_%d) %s", + longname, + port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback", + port->addr.port, + snd_seq_port_info_get_name(info)); + free(longname); + } else { + // At least add card number to be distinct + snprintf(name, sizeof(name), "%s %d:(%s_%d) %s", + snd_seq_client_info_get_name(client_info), + card_id, + port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback", + port->addr.port, + snd_seq_port_info_get_name(info)); + } + } + clean_name(name); + + snprintf(path, sizeof(path), "alsa:seq:%s:client_%d:%s_%d", + this->props.device, + port->addr.client, + port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback", + port->addr.port); + clean_name(path); + + snprintf(alias, sizeof(alias), "%s:%s", + snd_seq_client_info_get_name(client_info), + snd_seq_port_info_get_name(info)); + clean_name(alias); + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias); + if ((id = snd_seq_client_info_get_card(client_info)) != -1) { + snprintf(card, sizeof(card), "%d", id); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card); + } + port->info.props = &SPA_DICT_INIT(items, n_items); + + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static void emit_stream_info(struct seq_state *this, struct seq_stream *stream, bool full) +{ + uint32_t i; + + for (i = 0; i < MAX_PORTS; i++) { + struct seq_port *port = &stream->ports[i]; + if (port->valid) + emit_port_info(this, port, full); + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct seq_state *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_stream_info(this, &this->streams[SPA_DIRECTION_INPUT], true); + emit_stream_info(this, &this->streams[SPA_DIRECTION_OUTPUT], true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct seq_state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct seq_state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static struct seq_port *find_port(struct seq_state *state, + struct seq_stream *stream, const snd_seq_addr_t *addr) +{ + uint32_t i; + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + if (port->valid && + port->addr.client == addr->client && + port->addr.port == addr->port) + return port; + } + return NULL; +} + +static struct seq_port *alloc_port(struct seq_state *state, struct seq_stream *stream) +{ + uint32_t i; + for (i = 0; i < MAX_PORTS; i++) { + struct seq_port *port = &stream->ports[i]; + if (!port->valid) { + port->id = i; + port->direction = stream->direction; + port->valid = true; + if (stream->last_port < i + 1) + stream->last_port = i + 1; + return port; + } + } + return NULL; +} + +static void free_port(struct seq_state *state, struct seq_stream *stream, struct seq_port *port) +{ + port->valid = false; + + if (port->id + 1 == stream->last_port) { + int i; + for (i = stream->last_port - 1; i >= 0; i--) + if (stream->ports[i].valid) + break; + stream->last_port = i + 1; + } + + spa_node_emit_port_info(&state->hooks, + port->direction, port->id, NULL); + spa_zero(*port); +} + +static void init_port(struct seq_state *state, struct seq_port *port, const snd_seq_addr_t *addr, + unsigned int type) +{ + enum spa_direction reverse = SPA_DIRECTION_REVERSE(port->direction); + + port->addr = *addr; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_LIVE; + if (type & (SND_SEQ_PORT_TYPE_HARDWARE|SND_SEQ_PORT_TYPE_PORT|SND_SEQ_PORT_TYPE_SPECIFIC)) + port->info.flags |= SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL; + port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + port->latency[port->direction] = SPA_LATENCY_INFO( + port->direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + port->latency[reverse] = SPA_LATENCY_INFO(reverse); + + spa_alsa_seq_activate_port(state, port, true); + + emit_port_info(state, port, true); +} + +static void update_stream_port(struct seq_state *state, struct seq_stream *stream, + const snd_seq_addr_t *addr, unsigned int caps, const snd_seq_port_info_t *info) +{ + struct seq_port *port = find_port(state, stream, addr); + + if (info == NULL) { + spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port); + if (port) + free_port(state, stream, port); + } else { + if (port == NULL && (caps & stream->caps) == stream->caps) { + spa_log_debug(state->log, "new port %d.%d", addr->client, addr->port); + port = alloc_port(state, stream); + if (port == NULL) + return; + init_port(state, port, addr, snd_seq_port_info_get_type(info)); + } else if (port != NULL) { + if ((caps & stream->caps) != stream->caps) { + spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port); + free_port(state, stream, port); + } + else { + spa_log_debug(state->log, "update port %d.%d", addr->client, addr->port); + port->info.change_mask = SPA_PORT_CHANGE_MASK_PROPS; + emit_port_info(state, port, false); + } + } + } +} + +static int on_port_info(void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info) +{ + struct seq_state *state = data; + + if (info == NULL) { + update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, 0, info); + update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, 0, info); + } else { + unsigned int caps = snd_seq_port_info_get_capability(info); + + if (caps & SND_SEQ_PORT_CAP_NO_EXPORT) + return 0; + + update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, caps, info); + update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, caps, info); + } + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct seq_state *this = object; + struct seq_port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (result.index > 0) + return 0; + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_Format, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + 4096, 4096, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + param = spa_latency_build(&b, id, &port->latency[result.index]); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct seq_state *this, struct seq_port *port) +{ + if (port->n_buffers > 0) { + spa_list_init(&port->free); + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static int port_set_format(void *object, struct seq_port *port, + uint32_t flags, const struct spa_pod *format) +{ + struct seq_state *this = object; + int err; + + if (format == NULL) { + if (!port->have_format) + return 0; + + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_application || + info.media_subtype != SPA_MEDIA_SUBTYPE_control) + return -EINVAL; + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, 1); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct seq_state *this = object; + struct seq_port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + port->latency[info.direction] = info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct seq_state *this = object; + struct seq_port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: port %d.%d buffers:%d format:%d", this, + direction, port_id, n_buffers, port->have_format); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + if (direction == SPA_DIRECTION_OUTPUT) + spa_alsa_seq_recycle_buffer(this, port, i); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct seq_state *this = object; + struct seq_port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: io %d.%d %d %p %zd", this, + direction, port_id, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct seq_state *this = object; + struct seq_port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(!CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id); + + if (port->n_buffers == 0) + return -EIO; + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + spa_alsa_seq_recycle_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct seq_state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return spa_alsa_seq_process(this); +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct seq_state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct seq_state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct seq_state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct seq_state *) handle; + + spa_alsa_seq_close(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct seq_state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct seq_state *this; + uint32_t i; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct seq_state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this); + + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + reset_props(&this->props); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) { + spa_scnprintf(this->props.device, + sizeof(this->props.device), "%s", s); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), "%s", s); + } else if (spa_streq(k, SPA_KEY_API_ALSA_DISABLE_LONGNAME)) { + this->props.disable_longname = spa_atob(s); + } + } + + this->port_info = on_port_info; + this->port_info_data = this; + + if ((res = spa_alsa_seq_open(this)) < 0) + return res; + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Bridge midi ports with the alsa sequencer 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_seq_bridge_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_ALSA_SEQ_BRIDGE, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c new file mode 100644 index 0000000..9cec44d --- /dev/null +++ b/spa/plugins/alsa/alsa-seq.c @@ -0,0 +1,983 @@ +/* Spa ALSA Sequencer + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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) +{ + struct props *props = &state->props; + int res; + + spa_log_debug(state->log, "%p: ALSA seq open '%s' duplex", state, props->device); + + if ((res = snd_seq_open(&conn->hndl, + props->device, + SND_SEQ_OPEN_DUPLEX, + 0)) < 0) { + return res; + } + return 0; +} + +static int seq_init(struct seq_state *state, struct seq_conn *conn, bool with_queue) +{ + struct pollfd pfd; + snd_seq_port_info_t *pinfo; + int res; + + /* client id */ + if ((res = snd_seq_client_id(conn->hndl)) < 0) { + spa_log_error(state->log, "failed to get client id: %d", res); + goto error_exit_close; + } + conn->addr.client = res; + + /* queue */ + if (with_queue) { + if ((res = snd_seq_alloc_queue(conn->hndl)) < 0) { + spa_log_error(state->log, "failed to create queue: %d", res); + goto error_exit_close; + } + conn->queue_id = res; + } else { + conn->queue_id = -1; + } + + if ((res = snd_seq_nonblock(conn->hndl, 1)) < 0) + spa_log_warn(state->log, "can't set nonblock mode: %s", snd_strerror(res)); + + /* port for receiving */ + snd_seq_port_info_alloca(&pinfo); + snd_seq_port_info_set_name(pinfo, "input"); + snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC); + snd_seq_port_info_set_capability(pinfo, + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_READ); + /* Enable timestamping for events sent by external subscribers. */ + snd_seq_port_info_set_timestamping(pinfo, 1); + snd_seq_port_info_set_timestamp_real(pinfo, 1); + if (with_queue) + snd_seq_port_info_set_timestamp_queue(pinfo, conn->queue_id); + + if ((res = snd_seq_create_port(conn->hndl, pinfo)) < 0) { + spa_log_error(state->log, "failed to create port: %s", snd_strerror(res)); + goto error_exit_close; + } + conn->addr.port = snd_seq_port_info_get_port(pinfo); + + spa_log_debug(state->log, "queue:%d client:%d port:%d", + conn->queue_id, conn->addr.client, conn->addr.port); + + snd_seq_poll_descriptors(conn->hndl, &pfd, 1, POLLIN); + conn->source.fd = pfd.fd; + conn->source.mask = SPA_IO_IN; + + return 0; + +error_exit_close: + snd_seq_close(conn->hndl); + return res; +} + +static int seq_close(struct seq_state *state, struct seq_conn *conn) +{ + int res; + spa_log_debug(state->log, "%p: Device '%s' closing", state, state->props.device); + if ((res = snd_seq_close(conn->hndl)) < 0) { + spa_log_warn(state->log, "close failed: %s", snd_strerror(res)); + } + return res; +} + +static int init_stream(struct seq_state *state, enum spa_direction direction) +{ + struct seq_stream *stream = &state->streams[direction]; + int res; + stream->direction = direction; + if (direction == SPA_DIRECTION_INPUT) { + stream->caps = SND_SEQ_PORT_CAP_SUBS_WRITE; + } else { + stream->caps = SND_SEQ_PORT_CAP_SUBS_READ; + } + if ((res = snd_midi_event_new(MAX_EVENT_SIZE, &stream->codec)) < 0) { + spa_log_error(state->log, "can make event decoder: %s", + snd_strerror(res)); + return res; + } + snd_midi_event_no_status(stream->codec, 1); + memset(stream->ports, 0, sizeof(stream->ports)); + return 0; +} + +static int uninit_stream(struct seq_state *state, enum spa_direction direction) +{ + struct seq_stream *stream = &state->streams[direction]; + if (stream->codec) + snd_midi_event_free(stream->codec); + stream->codec = NULL; + return 0; +} + +static void init_ports(struct seq_state *state) +{ + snd_seq_addr_t addr; + snd_seq_client_info_t *client_info; + snd_seq_port_info_t *port_info; + + snd_seq_client_info_alloca(&client_info); + snd_seq_port_info_alloca(&port_info); + snd_seq_client_info_set_client(client_info, -1); + + while (snd_seq_query_next_client(state->sys.hndl, client_info) >= 0) { + + addr.client = snd_seq_client_info_get_client(client_info); + if (addr.client == SND_SEQ_CLIENT_SYSTEM || + addr.client == state->sys.addr.client || + addr.client == state->event.addr.client) + continue; + + snd_seq_port_info_set_client(port_info, addr.client); + snd_seq_port_info_set_port(port_info, -1); + while (snd_seq_query_next_port(state->sys.hndl, port_info) >= 0) { + addr.port = snd_seq_port_info_get_port(port_info); + state->port_info(state->port_info_data, &addr, port_info); + } + } +} + +static void debug_event(struct seq_state *state, snd_seq_event_t *ev) +{ + if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) + return; + + spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags); + switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) { + case SND_SEQ_TIME_STAMP_TICK: + spa_log_trace(state->log, " time: %d ticks", ev->time.tick); + break; + case SND_SEQ_TIME_STAMP_REAL: + spa_log_trace(state->log, " time = %d.%09d", + (int)ev->time.time.tv_sec, + (int)ev->time.time.tv_nsec); + break; + } + spa_log_trace(state->log, " source:%d.%d dest:%d.%d queue:%d", + ev->source.client, + ev->source.port, + ev->dest.client, + ev->dest.port, + ev->queue); +} + +static void alsa_seq_on_sys(struct spa_source *source) +{ + struct seq_state *state = source->data; + snd_seq_event_t *ev; + int res; + + while (snd_seq_event_input(state->sys.hndl, &ev) > 0) { + const snd_seq_addr_t *addr = &ev->data.addr; + + if (addr->client == state->event.addr.client) + continue; + + debug_event(state, ev); + + switch (ev->type) { + case SND_SEQ_EVENT_CLIENT_START: + case SND_SEQ_EVENT_CLIENT_CHANGE: + spa_log_info(state->log, "client add/change %d", addr->client); + break; + case SND_SEQ_EVENT_CLIENT_EXIT: + spa_log_info(state->log, "client exit %d", addr->client); + break; + + case SND_SEQ_EVENT_PORT_START: + case SND_SEQ_EVENT_PORT_CHANGE: + { + snd_seq_port_info_t *info; + + snd_seq_port_info_alloca(&info); + + if ((res = snd_seq_get_any_port_info(state->sys.hndl, + addr->client, addr->port, info)) < 0) { + spa_log_warn(state->log, "can't get port info %d.%d: %s", + addr->client, addr->port, snd_strerror(res)); + } else { + spa_log_info(state->log, "port add/change %d:%d", + addr->client, addr->port); + state->port_info(state->port_info_data, addr, info); + } + break; + } + case SND_SEQ_EVENT_PORT_EXIT: + spa_log_info(state->log, "port_event: del %d:%d", + addr->client, addr->port); + state->port_info(state->port_info_data, addr, NULL); + break; + default: + spa_log_info(state->log, "unhandled event %d: %d:%d", + ev->type, addr->client, addr->port); + break; + + } + snd_seq_free_event(ev); + } +} + +int spa_alsa_seq_open(struct seq_state *state) +{ + int n, i, res; + snd_seq_port_subscribe_t *sub; + snd_seq_addr_t addr; + snd_seq_queue_timer_t *timer; + struct seq_conn reserve[16]; + + if (state->opened) + return 0; + + init_stream(state, SPA_DIRECTION_INPUT); + init_stream(state, SPA_DIRECTION_OUTPUT); + + spa_zero(reserve); + for (i = 0; i < 16; i++) { + spa_log_debug(state->log, "close %d", i); + if ((res = seq_open(state, &reserve[i], false)) < 0) + break; + } + if (i >= 2) { + state->event = reserve[--i]; + state->sys = reserve[--i]; + res = 0; + } + for (n = --i; n >= 0; n--) { + spa_log_debug(state->log, "close %d", n); + seq_close(state, &reserve[n]); + } + if (res < 0) { + spa_log_error(state->log, "open failed: %s", snd_strerror(res)); + return res; + } + + if ((res = seq_init(state, &state->sys, false)) < 0) + goto error_close; + + snd_seq_set_client_name(state->sys.hndl, "PipeWire-System"); + + if ((res = seq_init(state, &state->event, true)) < 0) + goto error_close; + + snd_seq_set_client_name(state->event.hndl, "PipeWire-RT-Event"); + + /* connect to system announce */ + snd_seq_port_subscribe_alloca(&sub); + addr.client = SND_SEQ_CLIENT_SYSTEM; + addr.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE; + snd_seq_port_subscribe_set_sender(sub, &addr); + snd_seq_port_subscribe_set_dest(sub, &state->sys.addr); + if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) { + spa_log_warn(state->log, "failed to connect announce port: %s", snd_strerror(res)); + } + + addr.client = SND_SEQ_CLIENT_SYSTEM; + addr.port = SND_SEQ_PORT_SYSTEM_TIMER; + snd_seq_port_subscribe_set_sender(sub, &addr); + if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) { + spa_log_warn(state->log, "failed to connect timer port: %s", snd_strerror(res)); + } + + state->sys.source.func = alsa_seq_on_sys; + state->sys.source.data = state; + spa_loop_add_source(state->main_loop, &state->sys.source); + + /* increase event queue timer resolution */ + snd_seq_queue_timer_alloca(&timer); + if ((res = snd_seq_get_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) { + spa_log_warn(state->log, "failed to get queue timer: %s", snd_strerror(res)); + } + snd_seq_queue_timer_set_resolution(timer, INT_MAX); + if ((res = snd_seq_set_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) { + spa_log_warn(state->log, "failed to set queue timer: %s", snd_strerror(res)); + } + + init_ports(state); + + if ((res = spa_system_timerfd_create(state->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_close; + + state->timerfd = res; + + state->opened = true; + + return 0; + +error_close: + seq_close(state, &state->event); + seq_close(state, &state->sys); + return res; +} + +int spa_alsa_seq_close(struct seq_state *state) +{ + int res = 0; + + if (!state->opened) + return 0; + + spa_loop_remove_source(state->main_loop, &state->sys.source); + + seq_close(state, &state->sys); + seq_close(state, &state->event); + + uninit_stream(state, SPA_DIRECTION_INPUT); + uninit_stream(state, SPA_DIRECTION_OUTPUT); + + spa_system_close(state->data_system, state->timerfd); + state->opened = false; + + return res; +} + +static int set_timeout(struct seq_state *state, uint64_t time) +{ + struct itimerspec ts; + + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, + state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); + return 0; +} + +static struct seq_port *find_port(struct seq_state *state, + struct seq_stream *stream, const snd_seq_addr_t *addr) +{ + uint32_t i; + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + if (port->valid && + port->addr.client == addr->client && + port->addr.port == addr->port) + return port; + } + return NULL; +} + +int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active) +{ + int res; + snd_seq_port_subscribe_t* sub; + + spa_log_debug(state->log, "activate: %d.%d: started:%d active:%d wanted:%d", + port->addr.client, port->addr.port, state->started, port->active, active); + + if (active && !state->started) + return 0; + if (port->active == active) + return 0; + + snd_seq_port_subscribe_alloca(&sub); + if (port->direction == SPA_DIRECTION_OUTPUT) { + snd_seq_port_subscribe_set_sender(sub, &port->addr); + snd_seq_port_subscribe_set_dest(sub, &state->event.addr); + } else { + snd_seq_port_subscribe_set_sender(sub, &state->event.addr); + snd_seq_port_subscribe_set_dest(sub, &port->addr); + } + + if (active) { + snd_seq_port_subscribe_set_time_update(sub, 1); + snd_seq_port_subscribe_set_time_real(sub, 1); + snd_seq_port_subscribe_set_queue(sub, state->event.queue_id); + if ((res = snd_seq_subscribe_port(state->event.hndl, sub)) < 0) { + spa_log_error(state->log, "can't subscribe to %d:%d - %s", + port->addr.client, port->addr.port, snd_strerror(res)); + active = false; + } + spa_log_info(state->log, "subscribe: %s port %d.%d", + port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input", + port->addr.client, port->addr.port); + } else { + if ((res = snd_seq_unsubscribe_port(state->event.hndl, sub)) < 0) { + spa_log_warn(state->log, "can't unsubscribe from %d:%d - %s", + port->addr.client, port->addr.port, snd_strerror(res)); + } + spa_log_info(state->log, "unsubscribe: %s port %d.%d", + port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input", + port->addr.client, port->addr.port); + } + port->active = active; + return res; +} + +static struct buffer *peek_buffer(struct seq_state *state, + struct seq_port *port) +{ + if (spa_list_is_empty(&port->free)) + return NULL; + return spa_list_first(&port->free, struct buffer, link); +} + +int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace_fp(state->log, "%p: recycle buffer port:%p buffer-id:%u", + state, port, buffer_id); + spa_list_append(&port->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } + return 0; +} + +static int prepare_buffer(struct seq_state *state, struct seq_port *port) +{ + if (port->buffer != NULL) + return 0; + + if ((port->buffer = peek_buffer(state, port)) == NULL) + return -EPIPE; + + spa_pod_builder_init(&port->builder, + port->buffer->buf->datas[0].data, + port->buffer->buf->datas[0].maxsize); + spa_pod_builder_push_sequence(&port->builder, &port->frame, 0); + + return 0; +} + +static int process_recycle(struct seq_state *state) +{ + struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; + uint32_t i; + + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + struct spa_io_buffers *io = port->io; + + if (!port->valid || io == NULL) + continue; + + if (io->status != SPA_STATUS_HAVE_DATA && + io->buffer_id < port->n_buffers) { + spa_alsa_seq_recycle_buffer(state, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + } + return 0; +} + +#define NSEC_TO_CLOCK(r,n) (((n) * (r)->denom) / ((r)->num * SPA_NSEC_PER_SEC)) +#define NSEC_FROM_CLOCK(r,n) (((n) * (r)->num * SPA_NSEC_PER_SEC) / (r)->denom) + +static int process_read(struct seq_state *state) +{ + snd_seq_event_t *ev; + struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; + uint32_t i; + long size; + uint8_t data[MAX_EVENT_SIZE]; + int res; + + /* copy all new midi events into their port buffers */ + while (snd_seq_event_input(state->event.hndl, &ev) > 0) { + const snd_seq_addr_t *addr = &ev->source; + struct seq_port *port; + uint64_t ev_time, diff; + uint32_t offset; + + debug_event(state, ev); + + if ((port = find_port(state, stream, addr)) == NULL) { + spa_log_debug(state->log, "unknown port %d.%d", + addr->client, addr->port); + continue; + } + if (port->io == NULL || port->n_buffers == 0) + continue; + + if ((res = prepare_buffer(state, port)) < 0) { + spa_log_debug(state->log, "can't prepare buffer port:%p %d.%d: %s", + port, addr->client, addr->port, spa_strerror(res)); + continue; + } + + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, data, MAX_EVENT_SIZE, ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } + + /* queue_time is the estimated current time of the queue as calculated by + * the DLL. Calculate the age of the event. */ + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + if (state->queue_time > ev_time) + diff = state->queue_time - ev_time; + else + diff = 0; + + /* convert the age to samples and convert to an offset */ + offset = NSEC_TO_CLOCK(&state->rate, diff); + if (state->duration > offset) + offset = state->duration - 1 - offset; + else + offset = 0; + + spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%ld port:%d.%d", + ev_time, offset, size, addr->client, addr->port); + + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&port->builder, data, size); + + snd_seq_free_event(ev); + } + + /* prepare a buffer on each port, some ports might have their + * buffer filled above */ + res = 0; + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + struct spa_io_buffers *io = port->io; + + if (!port->valid || io == NULL) + continue; + + if (prepare_buffer(state, port) >= 0) { + spa_pod_builder_pop(&port->builder, &port->frame); + + port->buffer->buf->datas[0].chunk->offset = 0; + port->buffer->buf->datas[0].chunk->size = port->builder.state.offset; + + /* move buffer to ready queue */ + spa_list_remove(&port->buffer->link); + SPA_FLAG_SET(port->buffer->flags, BUFFER_FLAG_OUT); + spa_list_append(&port->ready, &port->buffer->link); + port->buffer = NULL; + } + + /* if there is already data, continue */ + if (io->status == SPA_STATUS_HAVE_DATA) { + res |= SPA_STATUS_HAVE_DATA; + continue; + } + + if (io->buffer_id < port->n_buffers) + spa_alsa_seq_recycle_buffer(state, port, io->buffer_id); + + if (spa_list_is_empty(&port->ready)) { + /* we have no ready buffers */ + io->buffer_id = SPA_ID_INVALID; + io->status = -EPIPE; + } else { + struct buffer *b = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&b->link); + + /* dequeue ready buffer */ + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + res |= SPA_STATUS_HAVE_DATA; + } + } + return res; +} + +static int process_write(struct seq_state *state) +{ + struct seq_stream *stream = &state->streams[SPA_DIRECTION_INPUT]; + uint32_t i; + int err, res = 0; + + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + struct spa_io_buffers *io = port->io; + struct buffer *buffer; + struct spa_pod_sequence *pod; + struct spa_data *d; + struct spa_pod_control *c; + snd_seq_event_t ev; + uint64_t out_time; + snd_seq_real_time_t out_rt; + + if (!port->valid || io == NULL) + continue; + + if (io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= port->n_buffers) + continue; + + buffer = &port->buffers[io->buffer_id]; + d = &buffer->buf->datas[0]; + + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_reuse_buffer(&state->callbacks, i, io->buffer_id); + res |= SPA_STATUS_NEED_DATA; + + pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size); + if (pod == NULL) { + spa_log_warn(state->log, "invalid sequence in buffer max:%u offset:%u size:%u", + d->maxsize, d->chunk->offset, d->chunk->size); + continue; + } + + SPA_POD_SEQUENCE_FOREACH(pod, c) { + long size; + + if (c->type != SPA_CONTROL_Midi) + continue; + + snd_seq_ev_clear(&ev); + + snd_midi_event_reset_encode(stream->codec); + if ((size = snd_midi_event_encode(stream->codec, + SPA_POD_BODY(&c->value), + SPA_POD_BODY_SIZE(&c->value), &ev)) <= 0) { + spa_log_warn(state->log, "failed to encode event: %s", + snd_strerror(size)); + continue; + } + + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + + out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset); + + out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC; + out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC; + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%ld port:%d.%d", + out_time, c->offset, size, port->addr.client, port->addr.port); + + if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } + } + } + snd_seq_drain_output(state->event.hndl); + + return res; +} + +static void update_position(struct seq_state *state) +{ + if (state->position) { + struct spa_io_clock *clock = &state->position->clock; + state->rate = clock->rate; + if (state->rate.num == 0 || state->rate.denom == 0) + state->rate = SPA_FRACTION(1, 48000); + state->duration = clock->duration; + } else { + state->rate = SPA_FRACTION(1, 48000); + state->duration = 1024; + } + state->threshold = state->duration; +} + +static int update_time(struct seq_state *state, uint64_t nsec, bool follower) +{ + snd_seq_queue_status_t *status; + const snd_seq_real_time_t* queue_time; + uint64_t queue_real; + double err, corr; + uint64_t queue_elapsed; + + corr = 1.0 - (state->dll.z2 + state->dll.z3); + + /* take queue time */ + snd_seq_queue_status_alloca(&status); + snd_seq_get_queue_status(state->event.hndl, state->event.queue_id, status); + queue_time = snd_seq_queue_status_get_real_time(status); + queue_real = SPA_TIMESPEC_TO_NSEC(queue_time); + + if (state->queue_time == 0) + queue_elapsed = 0; + else + queue_elapsed = (queue_real - state->queue_time) / corr; + + state->queue_time = queue_real; + + queue_elapsed = NSEC_TO_CLOCK(&state->rate, queue_elapsed); + + err = ((int64_t)state->threshold - (int64_t) queue_elapsed); + err = SPA_CLAMP(err, -64, 64); + + if (state->dll.bw == 0.0) { + spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, + state->rate.denom); + state->next_time = nsec; + state->base_time = nsec; + } + corr = spa_dll_update(&state->dll, err); + + if ((state->next_time - state->base_time) > BW_PERIOD) { + state->base_time = state->next_time; + spa_log_debug(state->log, "%p: follower:%d rate:%f bw:%f err:%f (%f %f %f)", + state, follower, corr, state->dll.bw, err, + state->dll.z1, state->dll.z2, state->dll.z3); + } + + state->next_time += state->threshold / corr * 1e9 / state->rate.denom; + + if (!follower && state->clock) { + state->clock->nsec = nsec; + state->clock->position += state->duration; + state->clock->duration = state->duration; + state->clock->delay = state->duration * corr; + state->clock->rate_diff = corr; + state->clock->next_nsec = state->next_time; + } + + spa_log_trace_fp(state->log, "now:%"PRIu64" queue:%"PRIu64" err:%f corr:%f next:%"PRIu64" thr:%d", + nsec, queue_real, err, corr, state->next_time, state->threshold); + + return 0; +} + +int spa_alsa_seq_process(struct seq_state *state) +{ + int res; + + update_position(state); + + res = process_recycle(state); + + if (state->following && state->position) { + update_time(state, state->position->clock.nsec, true); + res |= process_read(state); + } + res |= process_write(state); + + return res; +} + +static void alsa_on_timeout_event(struct spa_source *source) +{ + struct seq_state *state = source->data; + uint64_t expire; + int res; + + if (state->started) { + if ((res = spa_system_timerfd_read(state->data_system, state->timerfd, &expire)) < 0) { + if (res != -EAGAIN) + spa_log_warn(state->log, "%p: error reading timerfd: %s", + state, spa_strerror(res)); + return; + } + } + + state->current_time = state->next_time; + + spa_log_trace(state->log, "timeout %"PRIu64, state->current_time); + + update_position(state); + + update_time(state, state->current_time, false); + + res = process_read(state); + if (res >= 0) + spa_node_call_ready(&state->callbacks, res | SPA_STATUS_NEED_DATA); + + set_timeout(state, state->next_time); +} + +static void reset_buffers(struct seq_state *this, struct seq_port *port) +{ + uint32_t i; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + if (port->direction == SPA_DIRECTION_INPUT) { + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + } else { + spa_list_append(&port->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } + } +} +static void reset_stream(struct seq_state *this, struct seq_stream *stream, bool active) +{ + uint32_t i; + for (i = 0; i < stream->last_port; i++) { + struct seq_port *port = &stream->ports[i]; + if (port->valid) { + reset_buffers(this, port); + spa_alsa_seq_activate_port(this, port, active); + } + } +} + +static int set_timers(struct seq_state *state) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0) + return res; + + state->next_time = SPA_TIMESPEC_TO_NSEC(&now); + if (state->following) { + set_timeout(state, 0); + } else { + set_timeout(state, state->next_time); + } + return 0; +} + +static inline bool is_following(struct seq_state *state) +{ + return state->position && state->clock && state->position->clock.id != state->clock->id; +} + +int spa_alsa_seq_start(struct seq_state *state) +{ + int res; + + if (state->started) + return 0; + + state->following = is_following(state); + + spa_log_debug(state->log, "alsa %p: start follower:%d", state, state->following); + + if ((res = snd_seq_start_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) { + spa_log_error(state->log, "failed to start queue: %s", snd_strerror(res)); + return res; + } + while (snd_seq_drain_output(state->event.hndl) > 0) + sleep(1); + + update_position(state); + + state->started = true; + + reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], true); + reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], true); + + state->source.func = alsa_on_timeout_event; + state->source.data = state; + state->source.fd = state->timerfd; + state->source.mask = SPA_IO_IN; + state->source.rmask = 0; + spa_loop_add_source(state->data_loop, &state->source); + + state->queue_time = 0; + spa_dll_init(&state->dll); + set_timers(state); + + return 0; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct seq_state *state = user_data; + set_timers(state); + return 0; +} + +int spa_alsa_seq_reassign_follower(struct seq_state *state) +{ + bool following; + + if (!state->started) + return 0; + + following = is_following(state); + if (following != state->following) { + spa_log_debug(state->log, "alsa %p: reassign follower %d->%d", state, state->following, following); + state->following = following; + spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + } + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct seq_state *state = user_data; + + spa_loop_remove_source(state->data_loop, &state->source); + set_timeout(state, 0); + + return 0; +} + +int spa_alsa_seq_pause(struct seq_state *state) +{ + int res; + + if (!state->started) + return 0; + + spa_log_debug(state->log, "alsa %p: pause", state); + + spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + + if ((res = snd_seq_stop_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) { + spa_log_warn(state->log, "failed to stop queue: %s", snd_strerror(res)); + } + while (snd_seq_drain_output(state->event.hndl) > 0) + sleep(1); + + state->started = false; + + reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], false); + reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], false); + + return 0; +} diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h new file mode 100644 index 0000000..5d5ed51 --- /dev/null +++ b/spa/plugins/alsa/alsa-seq.h @@ -0,0 +1,199 @@ +/* Spa ALSA Sequencer + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_ALSA_SEQ_H +#define SPA_ALSA_SEQ_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + +#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 1024 +#define MAX_PORTS 256 +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct seq_port { + uint32_t id; + enum spa_direction direction; + snd_seq_addr_t addr; + + uint64_t info_all; + struct spa_port_info info; +#define PORT_EnumFormat 0 +#define PORT_Meta 1 +#define PORT_IO 2 +#define PORT_Format 3 +#define PORT_Buffers 4 +#define PORT_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct spa_io_buffers *io; + + struct buffer buffers[MAX_BUFFERS]; + unsigned int n_buffers; + + struct spa_list free; + struct spa_list ready; + + struct buffer *buffer; + struct spa_pod_builder builder; + struct spa_pod_frame frame; + + struct spa_audio_info current_format; + unsigned int have_format:1; + unsigned int valid:1; + unsigned int active:1; + + struct spa_latency_info latency[2]; +}; + +struct seq_stream { + enum spa_direction direction; + unsigned int caps; + snd_midi_event_t *codec; + struct seq_port ports[MAX_PORTS]; + uint32_t last_port; +}; + +struct seq_conn { + snd_seq_t *hndl; + snd_seq_addr_t addr; + int queue_id; + int fd; + struct spa_source source; +}; + +#define BW_PERIOD (3 * SPA_NSEC_PER_SEC) + +struct seq_state { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_system *data_system; + struct spa_loop *data_loop; + struct spa_loop *main_loop; + + struct seq_conn sys; + struct seq_conn event; + int (*port_info) (void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info); + void *port_info_data; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint64_t info_all; + struct spa_node_info info; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_IO 2 +#define N_NODE_PARAMS 3 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + int rate_denom; + uint32_t duration; + uint32_t threshold; + struct spa_fraction rate; + + struct spa_source source; + int timerfd; + uint64_t current_time; + uint64_t next_time; + uint64_t base_time; + uint64_t queue_time; + + unsigned int opened:1; + unsigned int started:1; + unsigned int following:1; + + struct seq_stream streams[2]; + + struct spa_dll dll; +}; + +#define VALID_DIRECTION(this,d) ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT) +#define VALID_PORT(this,d,p) ((p) < MAX_PORTS && this->streams[d].ports[p].id == (p)) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && VALID_PORT(this,d,p)) +#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && VALID_PORT(this,d,p)) +#define CHECK_PORT(this,d,p) (VALID_DIRECTION(this,d) && VALID_PORT(this,d,p)) + +#define GET_PORT(this,d,p) (&this->streams[d].ports[p]) + +int spa_alsa_seq_open(struct seq_state *state); +int spa_alsa_seq_close(struct seq_state *state); + +int spa_alsa_seq_start(struct seq_state *state); +int spa_alsa_seq_pause(struct seq_state *state); +int spa_alsa_seq_reassign_follower(struct seq_state *state); + +int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active); +int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id); + +int spa_alsa_seq_process(struct seq_state *state); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_ALSA_SEQ_H */ diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c new file mode 100644 index 0000000..f89d863 --- /dev/null +++ b/spa/plugins/alsa/alsa-udev.c @@ -0,0 +1,1014 @@ +/* Spa ALSA udev + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 + +#define ACTION_ADD 0 +#define ACTION_REMOVE 1 +#define ACTION_DISABLE 2 + +struct device { + uint32_t id; + struct udev_device *dev; + unsigned int unavailable:1; + unsigned int accessible:1; + unsigned int ignored:1; + unsigned int emitted:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_system *main_system; + + struct spa_hook_list hooks; + + uint64_t info_all; + struct spa_device_info info; + + struct udev *udev; + struct udev_monitor *umonitor; + + struct device devices[MAX_DEVICES]; + uint32_t n_devices; + + struct spa_source source; + struct spa_source notify; + unsigned int use_acp:1; +}; + +static int impl_udev_open(struct impl *this) +{ + if (this->udev == NULL) { + this->udev = udev_new(); + if (this->udev == NULL) + return -ENOMEM; + } + return 0; +} + +static int impl_udev_close(struct impl *this) +{ + if (this->udev != NULL) + udev_unref(this->udev); + this->udev = NULL; + return 0; +} + +static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev) +{ + struct device *device; + + if (this->n_devices >= MAX_DEVICES) + return NULL; + device = &this->devices[this->n_devices++]; + spa_zero(*device); + device->id = id; + udev_device_ref(dev); + device->dev = dev; + return device; +} + +static struct device *find_device(struct impl *this, uint32_t id) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) { + if (this->devices[i].id == id) + return &this->devices[i]; + } + return NULL; +} + +static void remove_device(struct impl *this, struct device *device) +{ + udev_device_unref(device->dev); + *device = this->devices[--this->n_devices]; +} + +static void clear_devices(struct impl *this) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) + udev_device_unref(this->devices[i].dev); + this->n_devices = 0; +} + +static uint32_t get_card_id(struct impl *this, struct udev_device *dev) +{ + const char *e, *str; + + if (udev_device_get_property_value(dev, "ACP_IGNORE")) + return SPA_ID_INVALID; + + if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && spa_streq(str, "modem")) + return SPA_ID_INVALID; + + if (udev_device_get_property_value(dev, "SOUND_INITIALIZED") == NULL) + return SPA_ID_INVALID; + + if ((str = udev_device_get_property_value(dev, "DEVPATH")) == NULL) + return SPA_ID_INVALID; + + if ((e = strrchr(str, '/')) == NULL) + return SPA_ID_INVALID; + + if (strlen(e) <= 5 || strncmp(e, "/card", 5) != 0) + return SPA_ID_INVALID; + + return atoi(e + 5); +} + +static int dehex(char x) +{ + if (x >= '0' && x <= '9') + return x - '0'; + if (x >= 'A' && x <= 'F') + return x - 'A' + 10; + if (x >= 'a' && x <= 'f') + return x - 'a' + 10; + return -1; +} + +static void unescape(const char *src, char *dst) +{ + const char *s; + char *d; + int h1 = 0, h2 = 0; + enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT; + + for (s = src, d = dst; *s; s++) { + switch (state) { + case TEXT: + if (*s == '\\') + state = BACKSLASH; + else + *(d++) = *s; + break; + + case BACKSLASH: + if (*s == 'x') + state = EX; + else { + *(d++) = '\\'; + *(d++) = *s; + state = TEXT; + } + break; + + case EX: + h1 = dehex(*s); + if (h1 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *s; + state = TEXT; + } else + state = FIRST; + break; + + case FIRST: + h2 = dehex(*s); + if (h2 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + *(d++) = *s; + } else + *(d++) = (char) (h1 << 4) | h2; + state = TEXT; + break; + } + } + switch (state) { + case TEXT: + break; + case BACKSLASH: + *(d++) = '\\'; + break; + case EX: + *(d++) = '\\'; + *(d++) = 'x'; + break; + case FIRST: + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + break; + } + *d = 0; +} + +static int check_device_pcm_class(const char *devname) +{ + FILE *f; + char path[PATH_MAX]; + char buf[16]; + size_t sz; + + /* Check device class */ + spa_scnprintf(path, sizeof(path), "/sys/class/sound/%s/pcm_class", + devname); + f = fopen(path, "re"); + if (f == NULL) + return -errno; + sz = fread(buf, 1, sizeof(buf) - 1, f); + buf[sz] = '\0'; + fclose(f); + return spa_strstartswith(buf, "modem") ? -ENXIO : 0; +} + +static int get_num_pcm_devices(unsigned int card_id) +{ + char prefix[32]; + DIR *snd = NULL; + struct dirent *entry; + int num_dev = 0; + int res; + + /* Check if card has PCM devices, without opening them */ + + spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", card_id); + + if ((snd = opendir("/dev/snd")) == NULL) + return -errno; + + while ((errno = 0, entry = readdir(snd)) != NULL) { + if (!(entry->d_type == DT_CHR && + spa_strstartswith(entry->d_name, prefix))) + continue; + + res = check_device_pcm_class(entry->d_name); + if (res != -ENXIO) { + /* count device also if sysfs status file not accessible */ + ++num_dev; + } + } + if (errno != 0) + res = -errno; + else + res = num_dev; + + closedir(snd); + return res; +} + +static int check_device_available(struct impl *this, struct device *device, int *num_pcm) +{ + char path[PATH_MAX]; + DIR *card = NULL, *pcm = NULL; + FILE *f; + char buf[16]; + size_t sz; + struct dirent *entry, *entry_pcm; + int res; + + res = get_num_pcm_devices(device->id); + if (res < 0) { + spa_log_error(this->log, "Error finding PCM devices for ALSA card %u: %s", + (unsigned int)device->id, spa_strerror(res)); + return res; + } + *num_pcm = res; + + spa_log_debug(this->log, "card %u has %d pcm device(s)", (unsigned int)device->id, *num_pcm); + + /* + * Check if some pcm devices of the card are busy. Check it via /proc, as we + * don't want to actually open any devices using alsa-lib (generates uncontrolled + * number of inotify events), or replicate its subdevice logic. + * + * The /proc/asound directory might not exist if kernel is compiled with + * CONFIG_SND_PROCFS=n, and the pcmXX directories may be missing if compiled + * with CONFIG_SND_VERBOSE_PROCFS=n. In those cases, the busy check always succeeds. + */ + + res = 0; + + spa_scnprintf(path, sizeof(path), "/proc/asound/card%u", (unsigned int)device->id); + + if ((card = opendir(path)) == NULL) + goto done; + + while ((errno = 0, entry = readdir(card)) != NULL) { + if (!(entry->d_type == DT_DIR && + spa_strstartswith(entry->d_name, "pcm"))) + continue; + + spa_scnprintf(path, sizeof(path), "pcmC%uD%s", + (unsigned int)device->id, entry->d_name+3); + if (check_device_pcm_class(path) < 0) + continue; + + /* Check busy status */ + spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s", + (unsigned int)device->id, entry->d_name); + if ((pcm = opendir(path)) == NULL) + goto done; + + while ((errno = 0, entry_pcm = readdir(pcm)) != NULL) { + if (!(entry_pcm->d_type == DT_DIR && + spa_strstartswith(entry_pcm->d_name, "sub"))) + continue; + + spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s/%s/status", + (unsigned int)device->id, entry->d_name, entry_pcm->d_name); + + f = fopen(path, "re"); + if (f == NULL) + goto done; + sz = fread(buf, 1, 6, f); + buf[sz] = '\0'; + fclose(f); + + if (!spa_strstartswith(buf, "closed")) { + spa_log_debug(this->log, "card %u pcm device %s busy", + (unsigned int)device->id, entry->d_name); + res = -EBUSY; + goto done; + } + spa_log_debug(this->log, "card %u pcm device %s free", + (unsigned int)device->id, entry->d_name); + } + if (errno != 0) + goto done; + + closedir(pcm); + pcm = NULL; + } + if (errno != 0) + goto done; + +done: + if (errno != 0) { + spa_log_info(this->log, "card %u: failed to find busy status (%s)", + (unsigned int)device->id, spa_strerror(-errno)); + } + if (card) + closedir(card); + if (pcm) + closedir(pcm); + return res; +} + +static int emit_object_info(struct impl *this, struct device *device) +{ + struct spa_device_object_info info; + uint32_t id = device->id; + struct udev_device *dev = device->dev; + const char *str; + char path[32], *cn = NULL, *cln = NULL; + struct spa_dict_item items[25]; + uint32_t n_items = 0; + int res, pcm; + + /* + * inotify close events under /dev/snd must not be emitted, except after setting + * device->emitted to true. alsalib functions can be used after that. + */ + + snprintf(path, sizeof(path), "hw:%u", id); + + if ((res = check_device_available(this, device, &pcm)) < 0) + return res; + if (pcm == 0) { + spa_log_debug(this->log, "no pcm devices for %s", path); + device->ignored = true; + return -ENODEV; + } + + spa_log_debug(this->log, "emitting card %s", path); + device->emitted = true; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = this->use_acp ? + SPA_NAME_API_ALSA_ACP_DEVICE : + SPA_NAME_API_ALSA_PCM_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API, "udev"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, path+3); + if (snd_card_get_name(id, &cn) >= 0) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_NAME, cn); + if (snd_card_get_longname(id, &cln) >= 0) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_LONGNAME, cln); + + if ((str = udev_device_get_property_value(dev, "ACP_NAME")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, str); + + if ((str = udev_device_get_property_value(dev, "ACP_PROFILE_SET")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PROFILE_SET, str); + + if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CLASS, str); + + if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str); + + str = udev_device_get_property_value(dev, "ID_PATH"); + if (!(str && *str)) + str = udev_device_get_syspath(dev); + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str); + } + if ((str = udev_device_get_devpath(dev)) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str); + } + if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str); + } + if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str); + } + if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str); + } + if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffffffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec); + } + } + str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str); + } + if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffffffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec); + } + } + str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str); + + if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str); + } + if ((str = udev_device_get_property_value(dev, "SOUND_FORM_FACTOR")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, str); + } + info.props = &SPA_DICT_INIT(items, n_items); + + spa_device_emit_object_info(&this->hooks, id, &info); + free(cn); + free(cln); + + return 1; +} + +static bool check_access(struct impl *this, struct device *device) +{ + char path[128], prefix[32]; + DIR *snd = NULL; + struct dirent *entry; + bool accessible = false; + + snprintf(path, sizeof(path), "/dev/snd/controlC%u", device->id); + if (access(path, R_OK|W_OK) >= 0 && (snd = opendir("/dev/snd"))) { + /* + * It's possible that controlCX is accessible before pcmCX* or + * the other way around. Return true only if all devices are + * accessible. + */ + + accessible = true; + spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", device->id); + while ((entry = readdir(snd)) != NULL) { + if (!(entry->d_type == DT_CHR && + spa_strstartswith(entry->d_name, prefix))) + continue; + + snprintf(path, sizeof(path), "/dev/snd/%.32s", entry->d_name); + if (access(path, R_OK|W_OK) < 0) { + accessible = false; + break; + } + } + closedir(snd); + } + + if (accessible != device->accessible) + spa_log_debug(this->log, "%s accessible:%u", path, accessible); + device->accessible = accessible; + + return device->accessible; +} + +static void process_device(struct impl *this, uint32_t action, struct udev_device *dev) +{ + uint32_t id; + struct device *device; + bool emitted; + int res; + + if ((id = get_card_id(this, dev)) == SPA_ID_INVALID) + return; + + device = find_device(this, id); + if (device && device->ignored) + return; + + switch (action) { + case ACTION_ADD: + if (device == NULL) + device = add_device(this, id, dev); + if (device == NULL) + return; + if (!check_access(this, device)) + return; + res = emit_object_info(this, device); + if (res < 0) { + if (device->ignored) + spa_log_info(this->log, "ALSA card %u unavailable (%s): it is ignored", + device->id, spa_strerror(res)); + else if (!device->unavailable) + spa_log_info(this->log, "ALSA card %u unavailable (%s): wait for it", + device->id, spa_strerror(res)); + else + spa_log_debug(this->log, "ALSA card %u still unavailable (%s)", + device->id, spa_strerror(res)); + device->unavailable = true; + } else { + if (device->unavailable) + spa_log_info(this->log, "ALSA card %u now available", + device->id); + device->unavailable = false; + } + break; + + case ACTION_REMOVE: + if (device == NULL) + return; + emitted = device->emitted; + remove_device(this, device); + if (emitted) + spa_device_emit_object_info(&this->hooks, id, NULL); + break; + + case ACTION_DISABLE: + if (device == NULL) + return; + if (device->emitted) { + device->emitted = false; + spa_device_emit_object_info(&this->hooks, id, NULL); + } + break; + } +} + +static int stop_inotify(struct impl *this) +{ + if (this->notify.fd == -1) + return 0; + spa_log_info(this->log, "stop inotify"); + spa_loop_remove_source(this->main_loop, &this->notify); + close(this->notify.fd); + this->notify.fd = -1; + return 0; +} + +static void impl_on_notify_events(struct spa_source *source) +{ + bool deleted = false; + struct impl *this = source->data; + union { + struct inotify_event e; + char name[NAME_MAX+1+sizeof(struct inotify_event)]; + } buf; + + while (true) { + ssize_t len; + const struct inotify_event *event; + void *p, *e; + + len = read(source->fd, &buf, sizeof(buf)); + if (len < 0 && errno != EAGAIN) + break; + if (len <= 0) + break; + + e = SPA_PTROFF(&buf, len, void); + + for (p = &buf; p < e; + p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) { + unsigned int id; + struct device *device; + + event = (const struct inotify_event *) p; + spa_assert_se(SPA_PTRDIFF(e, p) >= (ptrdiff_t)sizeof(struct inotify_event) && + SPA_PTRDIFF(e, p) - sizeof(struct inotify_event) >= event->len && + "bad event from kernel"); + + /* Device becomes accessible or not busy */ + if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) { + bool access; + if (sscanf(event->name, "controlC%u", &id) != 1 && + sscanf(event->name, "pcmC%uD", &id) != 1) + continue; + if ((device = find_device(this, id)) == NULL) + continue; + + access = check_access(this, device); + if (access && !device->emitted) + process_device(this, ACTION_ADD, device->dev); + else if (!access && device->emitted) + process_device(this, ACTION_DISABLE, device->dev); + } + /* /dev/snd/ might have been removed */ + if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF))) + deleted = true; + } + } + if (deleted) + stop_inotify(this); +} + +static int start_inotify(struct impl *this) +{ + int res, notify_fd; + + if (this->notify.fd != -1) + return 0; + + if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0) + return -errno; + + res = inotify_add_watch(notify_fd, "/dev/snd", + IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF); + if (res < 0) { + res = -errno; + close(notify_fd); + + if (res == -ENOENT) { + spa_log_debug(this->log, "/dev/snd/ does not exist yet"); + return 0; + } + spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res)); + return res; + } + spa_log_info(this->log, "start inotify"); + this->notify.func = impl_on_notify_events; + this->notify.data = this; + this->notify.fd = notify_fd; + this->notify.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->notify); + + return 0; +} + +static void impl_on_fd_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct udev_device *dev; + const char *action; + + dev = udev_monitor_receive_device(this->umonitor); + if (dev == NULL) + return; + + if ((action = udev_device_get_action(dev)) == NULL) + action = "change"; + + spa_log_debug(this->log, "action %s", action); + + start_inotify(this); + + if (spa_streq(action, "change")) { + process_device(this, ACTION_ADD, dev); + } else if (spa_streq(action, "remove")) { + process_device(this, ACTION_REMOVE, dev); + } + udev_device_unref(dev); +} + +static int start_monitor(struct impl *this) +{ + int res; + + if (this->umonitor != NULL) + return 0; + + this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev"); + if (this->umonitor == NULL) + return -ENOMEM; + + udev_monitor_filter_add_match_subsystem_devtype(this->umonitor, + "sound", NULL); + udev_monitor_enable_receiving(this->umonitor); + + this->source.func = impl_on_fd_events; + this->source.data = this; + this->source.fd = udev_monitor_get_fd(this->umonitor); + this->source.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_log_debug(this->log, "monitor %p", this->umonitor); + spa_loop_add_source(this->main_loop, &this->source); + + if ((res = start_inotify(this)) < 0) + return res; + + return 0; +} + +static int stop_monitor(struct impl *this) +{ + if (this->umonitor == NULL) + return 0; + + clear_devices (this); + + spa_loop_remove_source(this->main_loop, &this->source); + udev_monitor_unref(this->umonitor); + this->umonitor = NULL; + + stop_inotify(this); + + return 0; +} + +static int enum_devices(struct impl *this) +{ + struct udev_enumerate *enumerate; + struct udev_list_entry *devices; + + enumerate = udev_enumerate_new(this->udev); + if (enumerate == NULL) + return -ENOMEM; + + udev_enumerate_add_match_subsystem(enumerate, "sound"); + udev_enumerate_scan_devices(enumerate); + + for (devices = udev_enumerate_get_list_entry(enumerate); devices; + devices = udev_list_entry_get_next(devices)) { + struct udev_device *dev; + + dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices)); + if (dev == NULL) + continue; + + process_device(this, ACTION_ADD, dev); + + udev_device_unref(dev); + } + udev_enumerate_unref(enumerate); + + return 0; +} + +static const struct spa_dict_item device_info_items[] = { + { SPA_KEY_DEVICE_API, "udev" }, + { SPA_KEY_DEVICE_NICK, "alsa-udev" }, + { SPA_KEY_API_UDEV_MATCH, "sound" }, +}; + +static void emit_device_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(device_info_items); + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void impl_hook_removed(struct spa_hook *hook) +{ + struct impl *this = hook->priv; + if (spa_hook_list_is_empty(&this->hooks)) { + stop_monitor(this); + impl_udev_close(this); + } +} + +static int +impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + int res; + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + if ((res = impl_udev_open(this)) < 0) + return res; + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_device_info(this, true); + + if ((res = start_monitor(this)) < 0) + return res; + + if ((res = enum_devices(this)) < 0) + return res; + + spa_hook_list_join(&this->hooks, &save); + + listener->removed = impl_hook_removed; + listener->priv = this; + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + stop_monitor(this); + impl_udev_close(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + this->notify.fd = -1; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main-loop is needed"); + return -EINVAL; + } + if (this->main_system == NULL) { + spa_log_error(this->log, "a main-system is needed"); + return -EINVAL; + } + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | + SPA_DEVICE_CHANGE_MASK_PROPS; + this->info.flags = 0; + + if (info) { + if ((str = spa_dict_lookup(info, "alsa.use-acp")) != NULL) + this->use_acp = spa_atob(str); + } + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_alsa_udev_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_ALSA_ENUM_UDEV, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/alsa/alsa.c b/spa/plugins/alsa/alsa.c new file mode 100644 index 0000000..ab4aa3a --- /dev/null +++ b/spa/plugins/alsa/alsa.c @@ -0,0 +1,80 @@ +/* Spa ALSA support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include + +#include +#include + +extern const struct spa_handle_factory spa_alsa_source_factory; +extern const struct spa_handle_factory spa_alsa_sink_factory; +extern const struct spa_handle_factory spa_alsa_udev_factory; +extern const struct spa_handle_factory spa_alsa_device_factory; +extern const struct spa_handle_factory spa_alsa_seq_bridge_factory; +extern const struct spa_handle_factory spa_alsa_acp_device_factory; +#ifdef HAVE_ALSA_COMPRESS_OFFLOAD +extern const struct spa_handle_factory spa_alsa_compress_offload_sink_factory; +#endif + +struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.alsa"); +struct spa_log_topic *alsa_log_topic = &log_topic; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_alsa_source_factory; + break; + case 1: + *factory = &spa_alsa_sink_factory; + break; + case 2: + *factory = &spa_alsa_udev_factory; + break; + case 3: + *factory = &spa_alsa_device_factory; + break; + case 4: + *factory = &spa_alsa_seq_bridge_factory; + break; + case 5: + *factory = &spa_alsa_acp_device_factory; + break; +#ifdef HAVE_ALSA_COMPRESS_OFFLOAD + case 6: + *factory = &spa_alsa_compress_offload_sink_factory; + break; +#endif + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/alsa/alsa.h b/spa/plugins/alsa/alsa.h new file mode 100644 index 0000000..ee18929 --- /dev/null +++ b/spa/plugins/alsa/alsa.h @@ -0,0 +1,39 @@ +/* Spa ALSA Source + * + * Copyright © 2021 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_ALSA_H +#define SPA_ALSA_H + +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT alsa_log_topic +extern struct spa_log_topic *alsa_log_topic; + +static inline void alsa_log_topic_init(struct spa_log *log) +{ + spa_log_topic_init(log, alsa_log_topic); +} + +#endif /* SPA_ALSA_H */ diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build new file mode 100644 index 0000000..7dedc87 --- /dev/null +++ b/spa/plugins/alsa/meson.build @@ -0,0 +1,60 @@ +subdir('acp') +subdir('mixer') + +spa_alsa_dependencies = [ spa_dep, alsa_dep, libudev_dep, mathlib, epoll_shim_dep, libinotify_dep ] + +spa_alsa_sources = ['alsa.c', + 'alsa.h', + 'alsa-udev.c', + 'alsa-acp-device.c', + 'alsa-pcm-device.c', + 'alsa-pcm-sink.c', + 'alsa-pcm-source.c', + 'alsa-pcm.c', + 'alsa-seq-bridge.c', + 'alsa-seq.c'] + +if tinycompress_dep.found() + spa_alsa_sources += [ 'alsa-compress-offload-sink.c' ] + spa_alsa_dependencies += tinycompress_dep +endif + +spa_alsa = shared_library( + 'spa-alsa', + [ spa_alsa_sources ], + c_args : acp_c_args, + include_directories : [configinc], + dependencies : spa_alsa_dependencies, + link_with : [ acp_lib ], + install : true, + install_dir : spa_plugindir / 'alsa' +) + +alsa_udevrules = [ + '90-pipewire-alsa.rules', +] + +executable('spa-acp-tool', + [ 'acp-tool.c' ], + c_args : acp_c_args, + dependencies : [ spa_dep, alsa_dep, mathlib, acp_dep ], + install : true, +) + +executable('test-timer', + [ 'test-timer.c' ], + dependencies : [ spa_dep, alsa_dep, mathlib, epoll_shim_dep ], + install : false, +) + +executable('test-hw-params', + [ 'test-hw-params.c' ], + dependencies : [ spa_dep, alsa_dep, mathlib ], + install : false, +) + +if libudev_dep.found() + install_data(alsa_udevrules, + install_dir : udevrulesdir, + ) +endif diff --git a/spa/plugins/alsa/mixer/meson.build b/spa/plugins/alsa/mixer/meson.build new file mode 100644 index 0000000..d4327b8 --- /dev/null +++ b/spa/plugins/alsa/mixer/meson.build @@ -0,0 +1,7 @@ +install_subdir('paths', + install_dir : alsadatadir +) + +install_subdir('profile-sets', + install_dir : alsadatadir +) diff --git a/spa/plugins/alsa/mixer/paths/analog-input-aux.conf b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf new file mode 100644 index 0000000..47e22c5 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf @@ -0,0 +1,65 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; 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/hdmi-output-0.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf new file mode 100644 index 0000000..bb3cec1 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort +type = hdmi +priority = 59 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf new file mode 100644 index 0000000..3389a72 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 2 +type = hdmi +priority = 58 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf new file mode 100644 index 0000000..7607f8f --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 11 +type = hdmi +priority = 49 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf new file mode 100644 index 0000000..316d810 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 3 +type = hdmi +priority = 57 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf new file mode 100644 index 0000000..0601ef7 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 4 +type = hdmi +priority = 56 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf new file mode 100644 index 0000000..ded155b --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 5 +type = hdmi +priority = 55 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf new file mode 100644 index 0000000..de31791 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 6 +type = hdmi +priority = 54 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf new file mode 100644 index 0000000..6d72176 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 7 +type = hdmi +priority = 53 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf new file mode 100644 index 0000000..d5d0771 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 8 +type = hdmi +priority = 52 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf new file mode 100644 index 0000000..0b8f9cd --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 9 +type = hdmi +priority = 51 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf new file mode 100644 index 0000000..f15797c --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf @@ -0,0 +1,12 @@ +[General] +description = HDMI / DisplayPort 10 +type = hdmi +priority = 50 +eld-device = auto + +[Properties] +device.icon_name = video-display + +[Jack HDMI/DP] +append-pcm-to-name = yes +required = ignore diff --git a/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf new file mode 100644 index 0000000..babc839 --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf @@ -0,0 +1,20 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +[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/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..3e42ea3 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf @@ -0,0 +1,93 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; ASUS Xonar SE card. +; This card has two devices for each rear and front panel jacks. +; +; See default.conf for an explanation on the directives used here. + +[General] +auto-profiles = yes + +[Mapping analog-stereo-front] +description = Analog Stereo Front +device-strings = hw:%f,1 +channel-map = left,right +paths-output = analog-output analog-output-headphones +paths-input = analog-input-mic analog-input-headphone-mic analog-input-headset-mic +priority = 15 + +[Mapping analog-stereo-rear] +description = Analog Stereo Rear +device-strings = hw:%f,0 +channel-map = left,right +paths-output = analog-output analog-output-speaker +paths-input = analog-input analog-input-mic analog-input-linein +priority = 14 + +[Mapping analog-surround-21] +device-strings = surround21:%f +channel-map = front-left,front-right,lfe +paths-output = analog-output-speaker +priority = 13 +direction = output + +[Mapping analog-surround-40] +device-strings = surround40:%f +channel-map = front-left,front-right,rear-left,rear-right +paths-output = analog-output-speaker +priority = 12 +direction = output + +[Mapping analog-surround-41] +device-strings = surround41:%f +channel-map = front-left,front-right,rear-left,rear-right,lfe +paths-output = analog-output-speaker +priority = 13 +direction = output + +[Mapping analog-surround-50] +device-strings = surround50:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center +paths-output = analog-output-speaker +priority = 12 +direction = output + +[Mapping analog-surround-51] +device-strings = surround51:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-output = analog-output-speaker +priority = 13 +direction = output + +[Mapping iec958-stereo] +device-strings = iec958:%f +channel-map = left,right +paths-output = iec958-stereo-output +priority = 5 + +[Mapping iec958-ac3-surround-40] +device-strings = a52:%f +channel-map = front-left,front-right,rear-left,rear-right +paths-output = iec958-stereo-output +priority = 2 +direction = output + +[Mapping iec958-ac3-surround-51] +device-strings = a52:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-output = iec958-stereo-output +priority = 3 +direction = output diff --git a/spa/plugins/alsa/mixer/profile-sets/audigy.conf b/spa/plugins/alsa/mixer/profile-sets/audigy.conf new file mode 100644 index 0000000..043596e --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/audigy.conf @@ -0,0 +1,94 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; Creative Sound Blaster Audigy product line +; +; These are just copies of the mappings we find in default.conf, with the +; small change of making analog-stereo and analog-mono non-fallback mappings. +; This is needed because these cards only support duplex profiles with mono +; inputs, and in the default configuration, with stereo being a fallback +; mapping, the mono mapping is never tried. +; +; See default.conf for an explanation on the directives used here. + +[General] +auto-profiles = yes + +# Based on stereo-fallback +[Mapping analog-stereo] +device-strings = hw:%f +channel-map = front-left,front-right +paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 +paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic +priority = 1 + +# Based on mono-fallback +[Mapping analog-mono] +device-strings = hw:%f +channel-map = mono +paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono +paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic +priority = 1 + +# The rest of these are identical to what's in default.conf +[Mapping analog-surround-21] +device-strings = surround21:%f +channel-map = front-left,front-right,lfe +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 +direction = output + +[Mapping analog-surround-40] +device-strings = surround40:%f +channel-map = front-left,front-right,rear-left,rear-right +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 +direction = output + +[Mapping analog-surround-41] +device-strings = surround41:%f +channel-map = front-left,front-right,rear-left,rear-right,lfe +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 +direction = output + +[Mapping analog-surround-50] +device-strings = surround50:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 +direction = output + +[Mapping analog-surround-51] +device-strings = surround51:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 +direction = output + +[Mapping analog-surround-71] +device-strings = surround71:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +description = Analog Surround 7.1 +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 +direction = output + +[Mapping iec958-stereo] +device-strings = iec958:%f +channel-map = left,right +paths-input = iec958-stereo-input +paths-output = iec958-stereo-output +priority = 5 diff --git a/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf new file mode 100644 index 0000000..1b6f61c --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf @@ -0,0 +1,66 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +# 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..f0c9d2a --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/default.conf @@ -0,0 +1,580 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; Default profile definitions for the ALSA backend of PulseAudio. This +; is used as fallback for all cards that have no special mapping +; assigned (and should be good enough for the vast majority of +; cards). If you want to assign a different profile set than this one +; to a device, either set the udev property ACP_PROFILE_SET for the +; card, or use the "profile_set" module argument when loading +; module-alsa-card. +; +; So what is this about? Simply, what we do here is map ALSA devices +; to how they are exposed in PA. We say which ALSA device string to +; use to open a device, which channel mapping to use then, and which +; mixer path to use. This is encoded in a 'mapping'. Multiple of these +; mappings can be bound together in a 'profile' which is then directly +; exposed in the UI as a card profile. Each mapping assigned to a +; profile will result in one sink/source to be created if the profile +; is selected for the card. +; +; Additionally, the path set configuration files can describe the +; decibel values assigned to the steps of the volume elements. This +; can be used to work around situations when the alsa driver doesn't +; provide any decibel information, or when the information is +; incorrect. + + +; [General] +; auto-profiles = no | yes # Instead of defining all profiles manually, autogenerate +; # them by combining every input mapping with every output mapping. +; +; [Mapping id] +; device-strings = ... # ALSA device string. %f will be replaced by the card identifier. +; channel-map = ... # Channel mapping to use for this device +; description = ... # Description for the mapping. Note that it's better to set the description +; # in the well_known_descriptions table in alsa-mixer.c than with this +; # option, because the descriptions in alsa-mixer.c are translatable. +; description-key = ... # A custom key for the well_known_descriptions table (by default the mapping +; # name is used). +; paths-input = ... # A list of mixer paths to use. Every path in this list will be probed. +; # If multiple are found to be working they will be available as device ports +; paths-output = ... +; element-input = ... # Instead of configuring a full mixer path simply configure a single +; # mixer element for volume/mute handling. The value can be an element +; # name, or name and index separated by a comma. +; element-output = ... +; priority = ... +; direction = any | input | output # Only useful for? +; +; exact-channels = yes | no # If no, and the exact number of channels is not supported, +; # allow device to be opened with another channel count +; fallback = no | yes # This mapping will only be considered if all non-fallback mappings fail +; intended-roles = ... # Set the device.intended_roles property for the sink/source. +; +; [Profile id] +; input-mappings = ... # Lists mappings for sources on this profile, those mapping must be +; # defined in this file too +; output-mappings = ... # Lists mappings for sinks on this profile, those mappings must be +; # defined in this file too +; description = ... +; priority = ... # Numeric value to deduce priority for this profile +; skip-probe = no | yes # Skip probing for availability? If this is yes then this profile +; # will be assumed as working without probing. Makes initialization +; # a bit faster but only works if the card is really known well. +; +; fallback = no | yes # This profile will only be considered if all non-fallback profiles fail +; [DecibelFix element] # Decibel fixes can be used to work around missing or incorrect dB +; # information from alsa. A decibel fix is a table that maps volume steps +; # to decibel values for one volume element. The "element" part in the +; # section title is the name of the volume element (or name and index +; # separated by a comma). +; # +; # NOTE: This feature is meant just as a help for figuring out the correct +; # decibel values. PulseAudio is not the correct place to maintain the +; # decibel mappings! +; # +; # If you need this feature, then you should make sure that when you have +; # the correct values figured out, the alsa driver developers get informed +; # too, so that they can fix the driver. +; +; db-values = ... # The option value consists of pairs of step numbers and decibel values. +; # The pairs are separated with whitespace, and steps are separated from +; # the corresponding decibel values with a colon. The values must be in an +; # increasing order. Here's an example of a valid string: +; # +; # "0:-40.50 1:-38.70 3:-33.00 11:0" +; # +; # The lowest step imposes a lower limit for hardware volume and the +; # highest step correspondingly imposes a higher limit. That means that +; # that the mixer will never be set outside those values - the rest of the +; # volume scale is done using software volume. +; # +; # As can be seen in the example, you don't need to specify a dB value for +; # each step. The dB values for skipped steps will be linearly interpolated +; # using the nearest steps that are given. + +[General] +auto-profiles = yes + +[Mapping analog-stereo] +device-strings = front:%f +channel-map = left,right +paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 +paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic +priority = 15 + +# If everything else fails, try to use hw:0 as a stereo device... +[Mapping stereo-fallback] +device-strings = hw:%f +fallback = yes +channel-map = front-left,front-right +paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 +paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic +priority = 1 + +# ...and if even that fails, try to use hw:0 as a mono device. +[Mapping mono-fallback] +device-strings = hw:%f +fallback = yes +channel-map = mono +paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono +paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic +priority = 1 + +[Mapping analog-surround-21] +device-strings = surround21:%f +channel-map = front-left,front-right,lfe +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 + +[Mapping analog-surround-40] +device-strings = surround40:%f +channel-map = front-left,front-right,rear-left,rear-right +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 + +[Mapping analog-surround-41] +device-strings = surround41:%f +channel-map = front-left,front-right,rear-left,rear-right,lfe +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 + +[Mapping analog-surround-50] +device-strings = surround50:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 + +[Mapping analog-surround-51] +device-strings = surround51:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 13 + +[Mapping analog-surround-71] +device-strings = surround71:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +description = Analog Surround 7.1 +paths-input = analog-input analog-input-linein analog-input-mic +paths-output = analog-output analog-output-lineout analog-output-speaker +priority = 12 + +[Mapping iec958-stereo] +device-strings = iec958:%f +channel-map = left,right +paths-input = iec958-stereo-input +paths-output = iec958-stereo-output +priority = 5 + +[Mapping iec958-ac3-surround-40] +device-strings = a52:%f +channel-map = front-left,front-right,rear-left,rear-right +paths-output = iec958-stereo-output +priority = 2 +direction = output + +[Mapping iec958-ac3-surround-51] +device-strings = a52:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-output = iec958-stereo-output +priority = 3 +direction = output + +[Mapping iec958-dts-surround-51] +device-strings = dca:%f +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +paths-output = iec958-stereo-output +priority = 3 +direction = output + +[Mapping hdmi-stereo] +description = Digital Stereo (HDMI) +device-strings = hdmi:%f +paths-output = hdmi-output-0 +channel-map = left,right +priority = 9 +direction = output + +[Mapping hdmi-surround] +description = Digital Surround 5.1 (HDMI) +device-strings = hdmi:%f +paths-output = hdmi-output-0 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 8 +direction = output + +[Mapping hdmi-surround71] +description = Digital Surround 7.1 (HDMI) +device-strings = hdmi:%f +paths-output = hdmi-output-0 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 8 +direction = output + +[Mapping hdmi-dts-surround] +description = Digital Surround 5.1 (HDMI/DTS) +device-strings = dcahdmi:%f +paths-output = hdmi-output-0 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra1] +description = Digital Stereo (HDMI 2) +device-strings = hdmi:%f,1 +paths-output = hdmi-output-1 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra1] +description = Digital Surround 5.1 (HDMI 2) +device-strings = hdmi:%f,1 +paths-output = hdmi-output-1 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra1] +description = Digital Surround 7.1 (HDMI 2) +device-strings = hdmi:%f,1 +paths-output = hdmi-output-1 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra1] +description = Digital Surround 5.1 (HDMI 2/DTS) +device-strings = dcahdmi:%f,1 +paths-output = hdmi-output-1 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra2] +description = Digital Stereo (HDMI 3) +device-strings = hdmi:%f,2 +paths-output = hdmi-output-2 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra2] +description = Digital Surround 5.1 (HDMI 3) +device-strings = hdmi:%f,2 +paths-output = hdmi-output-2 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra2] +description = Digital Surround 7.1 (HDMI 3) +device-strings = hdmi:%f,2 +paths-output = hdmi-output-2 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra2] +description = Digital Surround 5.1 (HDMI 3/DTS) +device-strings = dcahdmi:%f,2 +paths-output = hdmi-output-2 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra3] +description = Digital Stereo (HDMI 4) +device-strings = hdmi:%f,3 +paths-output = hdmi-output-3 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra3] +description = Digital Surround 5.1 (HDMI 4) +device-strings = hdmi:%f,3 +paths-output = hdmi-output-3 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra3] +description = Digital Surround 7.1 (HDMI 4) +device-strings = hdmi:%f,3 +paths-output = hdmi-output-3 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra3] +description = Digital Surround 5.1 (HDMI 4/DTS) +device-strings = dcahdmi:%f,3 +paths-output = hdmi-output-3 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra4] +description = Digital Stereo (HDMI 5) +device-strings = hdmi:%f,4 +paths-output = hdmi-output-4 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra4] +description = Digital Surround 5.1 (HDMI 5) +device-strings = hdmi:%f,4 +paths-output = hdmi-output-4 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra4] +description = Digital Surround 7.1 (HDMI 5) +device-strings = hdmi:%f,4 +paths-output = hdmi-output-4 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra4] +description = Digital Surround 5.1 (HDMI 5/DTS) +device-strings = dcahdmi:%f,4 +paths-output = hdmi-output-4 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra5] +description = Digital Stereo (HDMI 6) +device-strings = hdmi:%f,5 +paths-output = hdmi-output-5 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra5] +description = Digital Surround 5.1 (HDMI 6) +device-strings = hdmi:%f,5 +paths-output = hdmi-output-5 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra5] +description = Digital Surround 7.1 (HDMI 6) +device-strings = hdmi:%f,5 +paths-output = hdmi-output-5 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra5] +description = Digital Surround 5.1 (HDMI 6/DTS) +device-strings = dcahdmi:%f,5 +paths-output = hdmi-output-5 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra6] +description = Digital Stereo (HDMI 7) +device-strings = hdmi:%f,6 +paths-output = hdmi-output-6 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra6] +description = Digital Surround 5.1 (HDMI 7) +device-strings = hdmi:%f,6 +paths-output = hdmi-output-6 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra6] +description = Digital Surround 7.1 (HDMI 7) +device-strings = hdmi:%f,6 +paths-output = hdmi-output-6 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra6] +description = Digital Surround 5.1 (HDMI 7/DTS) +device-strings = dcahdmi:%f,6 +paths-output = hdmi-output-6 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra7] +description = Digital Stereo (HDMI 8) +device-strings = hdmi:%f,7 +paths-output = hdmi-output-7 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra7] +description = Digital Surround 5.1 (HDMI 8) +device-strings = hdmi:%f,7 +paths-output = hdmi-output-7 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra7] +description = Digital Surround 7.1 (HDMI 8) +device-strings = hdmi:%f,7 +paths-output = hdmi-output-7 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra7] +description = Digital Surround 5.1 (HDMI 8/DTS) +device-strings = dcahdmi:%f,7 +paths-output = hdmi-output-7 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra8] +description = Digital Stereo (HDMI 9) +device-strings = hdmi:%f,8 +paths-output = hdmi-output-8 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra8] +description = Digital Surround 5.1 (HDMI 9) +device-strings = hdmi:%f,8 +paths-output = hdmi-output-8 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra8] +description = Digital Surround 7.1 (HDMI 9) +device-strings = hdmi:%f,8 +paths-output = hdmi-output-8 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra8] +description = Digital Surround 5.1 (HDMI 9/DTS) +device-strings = dcahdmi:%f,8 +paths-output = hdmi-output-8 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra9] +description = Digital Stereo (HDMI 10) +device-strings = hdmi:%f,9 +paths-output = hdmi-output-9 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra9] +description = Digital Surround 5.1 (HDMI 10) +device-strings = hdmi:%f,9 +paths-output = hdmi-output-9 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra9] +description = Digital Surround 7.1 (HDMI 10) +device-strings = hdmi:%f,9 +paths-output = hdmi-output-9 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra9] +description = Digital Surround 5.1 (HDMI 10/DTS) +device-strings = dcahdmi:%f,9 +paths-output = hdmi-output-9 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-stereo-extra10] +description = Digital Stereo (HDMI 11) +device-strings = hdmi:%f,10 +paths-output = hdmi-output-10 +channel-map = left,right +priority = 7 +direction = output + +[Mapping hdmi-surround-extra10] +description = Digital Surround 5.1 (HDMI 11) +device-strings = hdmi:%f,10 +paths-output = hdmi-output-10 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping hdmi-surround71-extra10] +description = Digital Surround 7.1 (HDMI 11) +device-strings = hdmi:%f,10 +paths-output = hdmi-output-10 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +priority = 6 +direction = output + +[Mapping hdmi-dts-surround-extra10] +description = Digital Surround 5.1 (HDMI 11/DTS) +device-strings = dcahdmi:%f,10 +paths-output = hdmi-output-10 +channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe +priority = 6 +direction = output + +[Mapping multichannel-output] +device-strings = hw:%f +channel-map = left,right,rear-left,rear-right +exact-channels = false +fallback = yes +priority = 1 +direction = output + +[Mapping multichannel-input] +device-strings = hw:%f +channel-map = left,right,rear-left,rear-right +exact-channels = false +fallback = yes +priority = 1 +direction = input + +; An example for defining multiple-sink profiles +#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo] +#description = Foobar +#output-mappings = analog-stereo iec958-stereo +#input-mappings = analog-stereo diff --git a/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf new file mode 100644 index 0000000..1186552 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf @@ -0,0 +1,55 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PulseAudio; if not, see . + +; 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/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..7315061 --- /dev/null +++ b/spa/plugins/alsa/test-hw-params.c @@ -0,0 +1,173 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..cc5e5f1 --- /dev/null +++ b/spa/plugins/alsa/test-timer.c @@ -0,0 +1,310 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#define DEFAULT_DEVICE "hw:0" + +#define M_PI_M2 (M_PI + M_PI) + +#define BW_PERIOD (SPA_NSEC_PER_SEC * 3) + +struct state { + const char *device; + unsigned int format; + unsigned int rate; + unsigned int channels; + snd_pcm_uframes_t period; + snd_pcm_uframes_t buffer_frames; + + snd_pcm_t *hndl; + int timerfd; + + double max_error; + float accumulator; + + uint64_t next_time; + uint64_t prev_time; + + struct spa_dll dll; +}; + +static int set_timeout(struct state *state, uint64_t time) +{ + struct itimerspec ts; + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + return timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL); +} + +#define CHECK(s,msg,...) { \ + int __err; \ + if ((__err = (s)) < 0) { \ + fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err)); \ + return __err; \ + } \ +} + +#define LOOP(type,areas,scale) { \ + uint32_t i, j; \ + type *samples, v; \ + samples = (type*)((uint8_t*)areas[0].addr + (areas[0].first + offset*areas[0].step) / 8); \ + for (i = 0; i < frames; i++) { \ + state->accumulator += M_PI_M2 * 440 / state->rate; \ + if (state->accumulator >= M_PI_M2) \ + state->accumulator -= M_PI_M2; \ + v = sin(state->accumulator) * scale; \ + for (j = 0; j < state->channels; j++) \ + *samples++ = v; \ + } \ +} + +static int write_period(struct state *state) +{ + snd_pcm_uframes_t frames = state->period; + snd_pcm_uframes_t offset; + const snd_pcm_channel_area_t* areas; + + snd_pcm_mmap_begin(state->hndl, &areas, &offset, &frames); + + switch (state->format) { + case SND_PCM_FORMAT_S32_LE: + LOOP(int32_t, areas, 0x7fffffff); + break; + case SND_PCM_FORMAT_S16_LE: + LOOP(int16_t, areas, 0x7fff); + break; + default: + break; + } + + snd_pcm_mmap_commit(state->hndl, offset, frames) ; + + return 0; +} + +static int on_timer_wakeup(struct state *state) +{ + snd_pcm_sframes_t delay; + double error, corr; +#if 1 + snd_pcm_sframes_t avail; + CHECK(snd_pcm_avail_delay(state->hndl, &avail, &delay), "delay"); +#else + snd_pcm_uframes_t avail; + snd_htimestamp_t tstamp; + uint64_t then; + + CHECK(snd_pcm_htimestamp(state->hndl, &avail, &tstamp), "htimestamp"); + delay = state->buffer_frames - avail; + + then = SPA_TIMESPEC_TO_NSEC(&tstamp); + if (then != 0) { + if (then < state->next_time) { + delay -= (state->next_time - then) * state->rate / SPA_NSEC_PER_SEC; + } else { + delay += (then - state->next_time) * state->rate / SPA_NSEC_PER_SEC; + } + } +#endif + + /* calculate the error, we want to have exactly 1 period of + * samples remaining in the device when we wakeup. */ + error = (double)delay - (double)state->period; + if (error > state->max_error) + error = state->max_error; + else if (error < -state->max_error) + error = -state->max_error; + + /* update the dll with the error, this gives a rate correction */ + corr = spa_dll_update(&state->dll, error); + + /* set our new adjusted timeout. alternatively, this value can + * instead be used to drive a resampler if this device is + * slaved. */ + state->next_time += state->period / corr * 1e9 / state->rate; + set_timeout(state, state->next_time); + + if (state->next_time - state->prev_time > BW_PERIOD) { + state->prev_time = state->next_time; + fprintf(stdout, "corr:%f error:%f bw:%f\n", + corr, error, state->dll.bw); + } + /* pull in new samples write a new period */ + write_period(state); + + return 0; +} + +static unsigned int format_from_string(const char *str) +{ + if (strcmp(str, "S32_LE") == 0) + return SND_PCM_FORMAT_S32_LE; + else if (strcmp(str, "S32_BE") == 0) + return SND_PCM_FORMAT_S32_BE; + else if (strcmp(str, "S24_LE") == 0) + return SND_PCM_FORMAT_S24_LE; + else if (strcmp(str, "S24_BE") == 0) + return SND_PCM_FORMAT_S24_BE; + else if (strcmp(str, "S24_3LE") == 0) + return SND_PCM_FORMAT_S24_3LE; + else if (strcmp(str, "S24_3_BE") == 0) + return SND_PCM_FORMAT_S24_3BE; + else if (strcmp(str, "S16_LE") == 0) + return SND_PCM_FORMAT_S16_LE; + else if (strcmp(str, "S16_BE") == 0) + return SND_PCM_FORMAT_S16_BE; + return 0; +} + +static void show_help(const char *name, bool error) +{ + fprintf(error ? stderr : stdout, "%s [options]\n" + " -h, --help Show this help\n" + " -D, --device device name (default %s)\n", + name, DEFAULT_DEVICE); +} + +int main(int argc, char *argv[]) +{ + struct state state = { 0, }; + snd_pcm_hw_params_t *hparams; + snd_pcm_sw_params_t *sparams; + struct timespec now; + int c; + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "device", required_argument, NULL, 'D' }, + { "format", required_argument, NULL, 'f' }, + { "rate", required_argument, NULL, 'r' }, + { "channels", required_argument, NULL, 'c' }, + { NULL, 0, NULL, 0} + }; + state.device = DEFAULT_DEVICE; + state.format = SND_PCM_FORMAT_S16_LE; + state.rate = 44100; + state.channels = 2; + state.period = 1024; + + while ((c = getopt_long(argc, argv, "hD:f:r:c:", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(argv[0], false); + return 0; + case 'D': + state.device = optarg; + break; + case 'f': + state.format = format_from_string(optarg); + break; + case 'r': + state.rate = atoi(optarg); + break; + case 'c': + state.channels = atoi(optarg); + break; + default: + show_help(argv[0], true); + return -1; + } + } + + CHECK(snd_pcm_open(&state.hndl, state.device, SND_PCM_STREAM_PLAYBACK, 0), + "open %s failed", state.device); + + /* hw params */ + snd_pcm_hw_params_alloca(&hparams); + snd_pcm_hw_params_any(state.hndl, hparams); + CHECK(snd_pcm_hw_params_set_access(state.hndl, hparams, + SND_PCM_ACCESS_MMAP_INTERLEAVED), "set interleaved"); + CHECK(snd_pcm_hw_params_set_format(state.hndl, hparams, + state.format), "set format"); + CHECK(snd_pcm_hw_params_set_channels_near(state.hndl, hparams, + &state.channels), "set channels"); + CHECK(snd_pcm_hw_params_set_rate_near(state.hndl, hparams, + &state.rate, 0), "set rate"); + CHECK(snd_pcm_hw_params(state.hndl, hparams), "hw_params"); + + CHECK(snd_pcm_hw_params_get_buffer_size(hparams, &state.buffer_frames), "get_buffer_size_max"); + + fprintf(stdout, "opened format:%s rate:%u channels:%u\n", + snd_pcm_format_name(state.format), + state.rate, state.channels); + + snd_pcm_sw_params_alloca(&sparams); +#if 0 + CHECK(snd_pcm_sw_params_current(state.hndl, sparams), "sw_params_current"); + CHECK(snd_pcm_sw_params_set_tstamp_mode(state.hndl, sparams, SND_PCM_TSTAMP_ENABLE), + "sw_params_set_tstamp_type"); + CHECK(snd_pcm_sw_params_set_tstamp_type(state.hndl, sparams, SND_PCM_TSTAMP_TYPE_MONOTONIC), + "sw_params_set_tstamp_type"); + CHECK(snd_pcm_sw_params(state.hndl, sparams), "sw_params"); +#endif + + spa_dll_init(&state.dll); + spa_dll_set_bw(&state.dll, SPA_DLL_BW_MAX, state.period, state.rate); + state.max_error = SPA_MAX(256.0, state.period / 2.0f); + + if ((state.timerfd = timerfd_create(CLOCK_MONOTONIC, 0)) < 0) + perror("timerfd"); + + CHECK(snd_pcm_prepare(state.hndl), "prepare"); + + /* before we start, write one period */ + write_period(&state); + + /* set our first timeout for now */ + clock_gettime(CLOCK_MONOTONIC, &now); + state.prev_time = state.next_time = SPA_TIMESPEC_TO_NSEC(&now); + set_timeout(&state, state.next_time); + + /* and start playback */ + CHECK(snd_pcm_start(state.hndl), "start"); + + /* wait for timer to expire and call the wakeup function, + * this can be done in a poll loop as well */ + while (true) { + uint64_t expirations; + CHECK(read(state.timerfd, &expirations, sizeof(expirations)), "read"); + on_timer_wakeup(&state); + } + + snd_pcm_drain(state.hndl); + snd_pcm_close(state.hndl); + close(state.timerfd); + + return EXIT_SUCCESS; +} diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c new file mode 100644 index 0000000..2ccc2b7 --- /dev/null +++ b/spa/plugins/audioconvert/audioadapter.c @@ -0,0 +1,1733 @@ +/* SPA + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 +static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioadapter"); + +#define DEFAULT_ALIGN 16 + +#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1) + +/** \cond */ + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + + uint32_t max_align; + enum spa_direction direction; + + struct spa_node *target; + + struct spa_node *follower; + struct spa_hook follower_listener; + uint32_t follower_flags; + struct spa_audio_info follower_current_format; + struct spa_audio_info default_format; + + struct spa_handle *hnd_convert; + struct spa_node *convert; + struct spa_hook convert_listener; + uint32_t convert_flags; + + uint32_t n_buffers; + struct spa_buffer **buffers; + + struct spa_io_buffers io_buffers; + struct spa_io_rate_match io_rate_match; + struct spa_io_position *io_position; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumFormat 0 +#define IDX_PropInfo 1 +#define IDX_Props 2 +#define IDX_Format 3 +#define IDX_EnumPortConfig 4 +#define IDX_PortConfig 5 +#define IDX_Latency 6 +#define IDX_ProcessLatency 7 +#define N_NODE_PARAMS 8 + struct spa_param_info params[N_NODE_PARAMS]; + uint32_t convert_params_flags[N_NODE_PARAMS]; + uint32_t follower_params_flags[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + unsigned int add_listener:1; + unsigned int have_format:1; + unsigned int started:1; + unsigned int driver:1; + unsigned int async:1; + unsigned int passthrough:1; + unsigned int follower_removing:1; +}; + +/** \endcond */ + +static int follower_enum_params(struct impl *this, + uint32_t id, + uint32_t idx, + struct spa_result_node_params *result, + const struct spa_pod *filter, + struct spa_pod_builder *builder) +{ + int res; + if (result->next < 0x100000) { + if ((res = spa_node_enum_params_sync(this->convert, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + result->next = 0x100000; + } + if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + result->next &= 0xfffff; + if ((res = spa_node_enum_params_sync(this->follower, + id, &result->next, filter, &result->param, builder)) == 1) { + result->next |= 0x100000; + return res; + } + result->next = 0x200000; + } + return 0; +} + +static int convert_enum_port_config(struct impl *this, + int seq, uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter, struct spa_pod_builder *builder) +{ + struct spa_pod *f1, *f2 = NULL; + int res; + + f1 = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction)); + + if (filter) { + if ((res = spa_pod_filter(builder, &f2, f1, filter)) < 0) + return res; + } + else { + f2 = f1; + } + return spa_node_enum_params(this->convert, seq, id, start, num, f2); +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + uint8_t buffer[4096]; + struct spa_pod_dynamic_builder b; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + return convert_enum_port_config(this, seq, id, start, num, filter, &b.b); + case SPA_PARAM_PortConfig: + if (this->passthrough) { + switch (result.index) { + case 0: + result.param = spa_pod_builder_add_object(&b.b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id( + SPA_PARAM_PORT_CONFIG_MODE_passthrough)); + result.next++; + break; + default: + return 0; + } + } else { + return convert_enum_port_config(this, seq, id, start, num, filter, &b.b); + } + break; + case SPA_PARAM_PropInfo: + res = follower_enum_params(this, + id, IDX_PropInfo, &result, filter, &b.b); + break; + case SPA_PARAM_Props: + res = follower_enum_params(this, + id, IDX_Props, &result, filter, &b.b); + break; + case SPA_PARAM_ProcessLatency: + res = follower_enum_params(this, + id, IDX_ProcessLatency, &result, filter, &b.b); + break; + case SPA_PARAM_EnumFormat: + case SPA_PARAM_Format: + case SPA_PARAM_Latency: + res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + id, &result.next, filter, &result.param, &b.b); + break; + default: + return -ENOENT; + } + + if (res == 1) { + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + count++; + } + spa_pod_dynamic_builder_clean(&b); + + if (res != 1) + return res; + + if (count != num) + goto next; + + return 0; +} + +static int link_io(struct impl *this) +{ + int res; + + if (this->convert == NULL) + return 0; + + spa_log_debug(this->log, "%p: controls", this); + + spa_zero(this->io_rate_match); + this->io_rate_match.rate = 1.0; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_RateMatch, + &this->io_rate_match, sizeof(this->io_rate_match))) < 0) { + spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this, + res, spa_strerror(res)); + } + else if ((res = spa_node_port_set_io(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_RateMatch, + &this->io_rate_match, sizeof(this->io_rate_match))) < 0) { + spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this, + res, spa_strerror(res)); + } + + this->io_buffers = SPA_IO_BUFFERS_INIT; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_Buffers, + &this->io_buffers, sizeof(this->io_buffers))) < 0) { + spa_log_warn(this->log, "%p: set Buffers on follower failed %d %s", this, + res, spa_strerror(res)); + return res; + } + else if ((res = spa_node_port_set_io(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_Buffers, + &this->io_buffers, sizeof(this->io_buffers))) < 0) { + spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this, + res, spa_strerror(res)); + return res; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint32_t i; + uint64_t old = full ? this->info.change_mask : 0; + + spa_log_debug(this->log, "%p: info full:%d change:%08"PRIx64, + this, full, this->info.change_mask); + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + this->params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[i].user = 0; + spa_log_debug(this->log, "param %d flags:%08x", + i, this->params[i].flags); + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static int debug_params(struct impl *this, struct spa_node *node, + enum spa_direction direction, uint32_t port_id, uint32_t id, struct spa_pod *filter, + const char *debug, int err) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + uint32_t state; + struct spa_pod *param; + int res, count = 0; + + spa_log_error(this->log, "params %s: %d:%d (%s) %s", + spa_debug_type_find_name(spa_type_param, id), + direction, port_id, debug, err ? spa_strerror(err) : "no matching params"); + if (err == -EBUSY) + return 0; + + if (filter) { + spa_log_error(this->log, "with this filter:"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, filter); + } else { + spa_log_error(this->log, "there was no filter"); + } + + state = 0; + while (true) { + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + res = spa_node_port_enum_params_sync(node, + direction, port_id, + id, &state, + NULL, ¶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_DEBUG, 2, NULL, param); + count++; + } + if (count == 0) + spa_log_error(this->log, "could not get any %s", debug); + + return 0; +} + +static int negotiate_buffers(struct impl *this) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + uint32_t state; + struct spa_pod *param; + int res; + bool follower_alloc, conv_alloc; + uint32_t i, size, buffers, blocks, align, flags, stride = 0; + uint32_t *aligns; + struct spa_data *datas; + uint32_t follower_flags, conv_flags; + + spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); + + if (this->target == this->follower) + return 0; + + if (this->n_buffers > 0) + return 0; + + state = 0; + param = NULL; + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_Buffers, &state, + param, ¶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->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &b)) != 1) { + debug_params(this, this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "convert buffers", res); + return -ENOTSUP; + } + if (param == NULL) + return -ENOTSUP; + + spa_pod_fixate(param); + + follower_flags = this->follower_flags; + conv_flags = this->convert_flags; + + follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + + flags = 0; + if (conv_alloc || follower_alloc) { + flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA; + if (conv_alloc) + follower_alloc = false; + } + + align = DEFAULT_ALIGN; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamBuffers, NULL, + SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride), + SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align))) < 0) + return res; + + spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); + + align = SPA_MAX(align, this->max_align); + + datas = alloca(sizeof(struct spa_data) * blocks); + memset(datas, 0, sizeof(struct spa_data) * blocks); + aligns = alloca(sizeof(uint32_t) * blocks); + for (i = 0; i < blocks; i++) { + datas[i].type = SPA_DATA_MemPtr; + datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC; + datas[i].maxsize = size; + aligns[i] = align; + } + + free(this->buffers); + this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns); + if (this->buffers == NULL) + return -errno; + this->n_buffers = buffers; + + if ((res = spa_node_port_use_buffers(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + this->buffers, this->n_buffers)) < 0) + return res; + + if ((res = spa_node_port_use_buffers(this->follower, + this->direction, 0, + follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + this->buffers, this->n_buffers)) < 0) + return res; + + return 0; +} + +static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format) +{ + uint8_t buffer[4096]; + int res; + + if (format == NULL && !this->have_format) + return 0; + + spa_log_debug(this->log, "%p: configure format:", this); + if (format) + spa_debug_log_format(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + + if ((res = spa_node_port_set_param(this->follower, + this->direction, 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + + if (res > 0) { + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + uint32_t state = 0; + struct spa_pod *fmt; + + /* format was changed to nearest compatible format */ + + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_Format, &state, + NULL, &fmt, &b)) != 1) + return -EIO; + + format = fmt; + } + + if (this->target != this->follower && this->convert) { + if ((res = spa_node_port_set_param(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + } + + this->have_format = format != NULL; + if (format == NULL) { + this->n_buffers = 0; + } else { + res = negotiate_buffers(this); + } + + return res; +} + +static int configure_convert(struct impl *this, uint32_t mode) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_log_debug(this->log, "%p: configure convert %p", this, this->target); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode)); + + return spa_node_set_param(this->convert, SPA_PARAM_PortConfig, 0, param); +} + +extern const struct spa_handle_factory spa_audioconvert_factory; + +static const struct spa_node_events follower_node_events; + +static int reconfigure_mode(struct impl *this, bool passthrough, + enum spa_direction direction, struct spa_pod *format) +{ + int res = 0; + struct spa_hook l; + + spa_log_info(this->log, "%p: passthrough mode %d", this, passthrough); + + if (this->passthrough != passthrough) { + if (passthrough) { + /* remove converter split/merge ports */ + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); + } else { + /* remove follower ports */ + this->follower_removing = true; + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + this->follower_removing = false; + } + } + + /* set new target */ + this->target = passthrough ? this->follower : this->convert; + + if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0) + return res; + + if (this->passthrough != passthrough) { + this->passthrough = passthrough; + if (passthrough) { + /* add follower ports */ + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + } else { + /* add converter ports */ + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp); + link_io(this); + } + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_Props].user++; + + emit_node_info(this, false); + + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + int res = 0, res2 = 0; + struct impl *this = object; + struct spa_audio_info info = { 0 }; + + spa_log_debug(this->log, "%p: set param %d", this, id); + + switch (id) { + case SPA_PARAM_Format: + if (this->started) + return -EIO; + if (param == NULL) + return -EINVAL; + + if ((res = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0) + return res; + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + if (spa_format_audio_raw_parse(param, &info.info.raw) < 0) + return -EINVAL; + + this->follower_current_format = info; + break; + + case SPA_PARAM_PortConfig: + { + enum spa_direction dir; + enum spa_param_port_config_mode mode; + struct spa_pod *format = NULL; + + if (this->started) { + spa_log_error(this->log, "was started"); + return -EIO; + } + + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&dir), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; + + if (format) { + struct spa_audio_info info; + + spa_zero(info); + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -ENOTSUP; + + if (spa_format_audio_raw_parse(format, &info.info.raw) >= 0) { + info.info.raw.rate = 0; + this->default_format = info; + } + } + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_none: + return -ENOTSUP; + case SPA_PARAM_PORT_CONFIG_MODE_passthrough: + if ((res = reconfigure_mode(this, true, dir, format)) < 0) + return res; + break; + case SPA_PARAM_PORT_CONFIG_MODE_convert: + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + if ((res = reconfigure_mode(this, false, dir, NULL)) < 0) + return res; + break; + default: + return -EINVAL; + } + + if (this->target != this->follower) { + if ((res = spa_node_set_param(this->target, id, flags, param)) < 0) + return res; + } + break; + } + + case SPA_PARAM_Props: + if (this->target != this->follower) + res = spa_node_set_param(this->target, id, flags, param); + res2 = spa_node_set_param(this->follower, id, flags, param); + if (res < 0 && res2 < 0) + return res; + res = 0; + break; + case SPA_PARAM_ProcessLatency: + res = spa_node_set_param(this->follower, id, flags, param); + break; + default: + res = -ENOTSUP; + break; + } + return res; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + int res = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Position: + this->io_position = data; + break; + default: + break; + } + + if (this->target) + res = spa_node_set_io(this->target, id, data, size); + + if (this->target != this->follower) + res = spa_node_set_io(this->follower, id, data, size); + + return res; +} + +static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *b, uint32_t id, + struct spa_pod_object *o1, struct spa_pod_object *o2) +{ + const struct spa_pod_prop *p1, *p2; + struct spa_pod_frame f; + struct spa_pod_builder_state state; + int res = 0; + + if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2)) + return (struct spa_pod*)o1; + + spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id); + p2 = NULL; + SPA_POD_OBJECT_FOREACH(o1, p1) { + p2 = spa_pod_object_find_prop(o2, p2, p1->key); + if (p2 != NULL) { + spa_pod_builder_get_state(b, &state); + res = spa_pod_filter_prop(b, p1, p2); + if (res < 0) + spa_pod_builder_reset(b, &state); + } + if (p2 == NULL || res < 0) + spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1)); + } + p1 = NULL; + SPA_POD_OBJECT_FOREACH(o2, p2) { + p1 = spa_pod_object_find_prop(o1, p1, p2->key); + if (p1 != NULL) + continue; + spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2)); + } + return spa_pod_builder_pop(b, &f); +} + +static int negotiate_format(struct impl *this) +{ + uint32_t state; + struct spa_pod *format, *def; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + int res; + + spa_log_debug(this->log, "%p: have_format:%d", this, this->have_format); + + if (this->have_format) + return 0; + + if (this->target == this->follower) + return 0; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); + + state = 0; + format = NULL; + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &state, + format, &format, &b)) < 0) { + if (res == -ENOENT) + format = NULL; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_EnumFormat, format, "follower format", res); + goto done; + } + } + if (this->convert) { + state = 0; + if ((res = spa_node_port_enum_params_sync(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &state, + format, &format, &b)) != 1) { + debug_params(this, this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, format, "convert format", res); + res = -ENOTSUP; + goto done; + } + } + if (format == NULL) { + res = -ENOTSUP; + goto done; + } + + def = spa_format_audio_raw_build(&b, + SPA_PARAM_Format, &this->default_format.info.raw); + + format = merge_objects(this, &b, SPA_PARAM_Format, + (struct spa_pod_object*)format, + (struct spa_pod_object*)def); + + spa_pod_fixate(format); + + res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format); + +done: + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamEnd)); + + return res; +} + + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: command %d", this, SPA_NODE_COMMAND_ID(command)); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + spa_log_debug(this->log, "%p: starting %d", this, this->started); + if (this->started) + return 0; + if ((res = negotiate_format(this)) < 0) + return res; + if ((res = negotiate_buffers(this)) < 0) + return res; + this->started = true; + break; + case SPA_NODE_COMMAND_Suspend: + this->started = false; + spa_log_debug(this->log, "%p: suspending", this); + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + spa_log_debug(this->log, "%p: pausing", this); + break; + case SPA_NODE_COMMAND_Flush: + spa_log_debug(this->log, "%p: flushing", this); + this->io_buffers.status = SPA_STATUS_OK; + break; + default: + break; + } + + if ((res = spa_node_send_command(this->target, command)) < 0) { + spa_log_error(this->log, "%p: can't send command %d: %s", + this, SPA_NODE_COMMAND_ID(command), + spa_strerror(res)); + return res; + } + + if (this->target != this->follower) { + if ((res = spa_node_send_command(this->follower, command)) < 0) { + spa_log_error(this->log, "%p: can't send command %d: %s", + this, SPA_NODE_COMMAND_ID(command), + spa_strerror(res)); + return res; + } + } + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + spa_log_debug(this->log, "%p: started", this); + break; + case SPA_NODE_COMMAND_Suspend: + configure_format(this, 0, NULL); + spa_log_debug(this->log, "%p: suspended", this); + break; + case SPA_NODE_COMMAND_Pause: + spa_log_debug(this->log, "%p: paused", this); + break; + case SPA_NODE_COMMAND_Flush: + spa_log_debug(this->log, "%p: flushed", this); + break; + } + return res; +} + +static void convert_node_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + uint32_t i; + + spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, + info->change_mask); + + if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_EnumPortConfig: + idx = IDX_EnumPortConfig; + break; + case SPA_PARAM_PortConfig: + idx = IDX_PortConfig; + break; + case SPA_PARAM_PropInfo: + idx = IDX_PropInfo; + break; + case SPA_PARAM_Props: + idx = IDX_Props; + break; + default: + continue; + } + if (!this->add_listener && + this->convert_params_flags[idx] == info->params[i].flags) + continue; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->convert_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (this->add_listener) + continue; + + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + emit_node_info(this, false); +} + +static void convert_port_info(void *data, + enum spa_direction direction, uint32_t port_id, + const struct spa_port_info *info) +{ + struct impl *this = data; + + if (direction != this->direction) { + if (port_id == 0) + return; + else + port_id--; + } + + spa_log_debug(this->log, "%p: port info %d:%d", this, + direction, port_id); + + if (this->target != this->follower) + spa_node_emit_port_info(&this->hooks, direction, port_id, info); +} + +static void convert_result(void *data, int seq, int res, uint32_t type, const void *result) +{ + struct impl *this = data; + + if (this->target == this->follower) + return; + + spa_log_trace(this->log, "%p: result %d %d", this, seq, res); + spa_node_emit_result(&this->hooks, seq, res, type, result); +} + +static const struct spa_node_events convert_node_events = { + SPA_VERSION_NODE_EVENTS, + .info = convert_node_info, + .port_info = convert_port_info, + .result = convert_result, +}; + +static void follower_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + uint32_t i; + + spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, + info->change_mask); + + if (this->follower_removing) + return; + + this->async = (info->flags & SPA_NODE_FLAG_ASYNC) != 0; + + if (info->max_input_ports > 0) + this->direction = SPA_DIRECTION_INPUT; + else + this->direction = SPA_DIRECTION_OUTPUT; + + if (this->direction == SPA_DIRECTION_INPUT) { + this->info.flags |= SPA_NODE_FLAG_IN_PORT_CONFIG; + this->info.max_input_ports = MAX_PORTS; + } else { + this->info.flags |= SPA_NODE_FLAG_OUT_PORT_CONFIG; + this->info.max_output_ports = MAX_PORTS; + } + + spa_log_debug(this->log, "%p: follower info %s", this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output"); + + if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + this->info.props = info->props; + } + if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_PropInfo: + idx = IDX_PropInfo; + break; + case SPA_PARAM_Props: + idx = IDX_Props; + break; + case SPA_PARAM_ProcessLatency: + idx = IDX_ProcessLatency; + break; + default: + continue; + } + if (!this->add_listener && + this->follower_params_flags[idx] == info->params[i].flags) + continue; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->follower_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (this->add_listener) + continue; + + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + emit_node_info(this, false); + + spa_zero(this->info.props); + this->info.change_mask &= ~SPA_NODE_CHANGE_MASK_PROPS; + +} + +static int recalc_latency(struct impl *this, enum spa_direction direction, uint32_t port_id) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + uint32_t index = 0; + struct spa_latency_info latency; + int res; + + spa_log_debug(this->log, "%p: ", this); + + if (this->target == this->follower) + return 0; + + while (true) { + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_port_enum_params_sync(this->follower, + direction, port_id, SPA_PARAM_Latency, + &index, NULL, ¶m, &b)) != 1) + return res; + if ((res = spa_latency_parse(param, &latency)) < 0) + return res; + if (latency.direction == direction) + break; + } + if ((res = spa_node_port_set_param(this->target, + SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Latency, 0, param)) < 0) + return res; + + return 0; +} + +static void follower_port_info(void *data, + enum spa_direction direction, uint32_t port_id, + const struct spa_port_info *info) +{ + struct impl *this = data; + uint32_t i; + int res; + + if (this->follower_removing) { + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return; + } + + spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64, this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask); + + if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_EnumFormat: + idx = IDX_EnumFormat; + break; + case SPA_PARAM_Format: + idx = IDX_Format; + break; + case SPA_PARAM_Latency: + idx = IDX_Latency; + break; + default: + continue; + } + + if (!this->add_listener && + this->follower_params_flags[idx] == info->params[i].flags) + continue; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->follower_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (this->add_listener) + continue; + + if (idx == IDX_Latency) { + res = recalc_latency(this, direction, port_id); + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + if (idx == IDX_EnumFormat) { + spa_log_debug(this->log, "new formats"); + configure_format(this, 0, NULL); + } + + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + emit_node_info(this, false); + + if (this->target == this->follower) + spa_node_emit_port_info(&this->hooks, direction, port_id, info); +} + +static void follower_result(void *data, int seq, int res, uint32_t type, const void *result) +{ + struct impl *this = data; + + if (this->target != this->follower) + return; + + spa_log_trace(this->log, "%p: result %d %d", this, seq, res); + spa_node_emit_result(&this->hooks, seq, res, type, result); +} + +static void follower_event(void *data, const struct spa_event *event) +{ + struct impl *this = data; + + spa_log_trace(this->log, "%p: event %d", this, SPA_EVENT_TYPE(event)); + + switch (SPA_NODE_EVENT_ID(event)) { + case SPA_NODE_EVENT_Error: + /* Forward errors */ + spa_node_emit_event(&this->hooks, event); + break; + default: + /* Ignore other events */ + break; + } +} + +static const struct spa_node_events follower_node_events = { + SPA_VERSION_NODE_EVENTS, + .info = follower_info, + .port_info = follower_port_info, + .result = follower_result, + .event = follower_event, +}; + +static int follower_ready(void *data, int status) +{ + struct impl *this = data; + + spa_log_trace_fp(this->log, "%p: ready %d", this, status); + + if (!this->started) { + spa_log_info(this->log, "%p: ready stopped node", this); + return -EIO; + } + + if (this->target != this->follower) { + this->driver = true; + + if (this->direction == SPA_DIRECTION_OUTPUT) { + int retry = 8; + while (retry--) { + status = spa_node_process(this->convert); + if (status & SPA_STATUS_HAVE_DATA) + break; + + if (status & SPA_STATUS_NEED_DATA) { + status = spa_node_process(this->follower); + if (!(status & SPA_STATUS_HAVE_DATA)) + break; + } + } + + } + } + + return spa_node_call_ready(&this->callbacks, status); +} + +static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id) +{ + int res; + struct impl *this = data; + + if (this->target != this->follower && this->convert) + res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id); + else + res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id); + + return res; +} + +static int follower_xrun(void *data, uint64_t trigger, uint64_t delay, struct spa_pod *info) +{ + struct impl *this = data; + return spa_node_call_xrun(&this->callbacks, trigger, delay, info); +} + +static const struct spa_node_callbacks follower_node_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = follower_ready, + .reuse_buffer = follower_reuse_buffer, + .xrun = follower_xrun, +}; + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook l; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + + if (events->info || events->port_info) { + this->add_listener = true; + + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + + if (this->convert) { + spa_zero(l); + spa_node_add_listener(this->convert, &l, &convert_node_events, this); + spa_hook_remove(&l); + } + this->add_listener = false; + + emit_node_info(this, true); + } + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return spa_node_sync(this->follower, seq); +} + +static int +impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + return -EINVAL; + + return spa_node_add_port(this->target, direction, port_id, props); +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + return -EINVAL; + + return spa_node_remove_port(this->target, direction, port_id); +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + if (direction != this->direction) + port_id++; + + spa_log_debug(this->log, "%p: %d %u", this, seq, id); + + return spa_node_port_enum_params(this->target, seq, direction, port_id, id, + start, num, filter); +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, " %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + if ((res = spa_node_port_set_param(this->target, direction, port_id, id, + flags, param)) < 0) + return res; + + if ((id == SPA_PARAM_Latency) && + direction == this->direction) { + if ((res = spa_node_port_set_param(this->follower, direction, 0, id, + flags, param)) < 0) + return res; + } + + return res; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "set io %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + return spa_node_port_set_io(this->target, direction, port_id, id, data, size); +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + port_id++; + + spa_log_debug(this->log, "%p: %d %d:%d", this, + n_buffers, direction, port_id); + + if ((res = spa_node_port_use_buffers(this->target, + direction, port_id, flags, buffers, n_buffers)) < 0) + return res; + + return res; +} + +static int +impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return spa_node_port_reuse_buffer(this->target, port_id, buffer_id); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + int status = 0, fstatus, retry = 8; + + if (!this->started) { + spa_log_warn(this->log, "%p: scheduling stopped node", this); + return -EIO; + } + + spa_log_trace_fp(this->log, "%p: process convert:%p driver:%d", + this, this->convert, this->driver); + + if (this->target == this->follower) { + if (this->io_position) + this->io_rate_match.size = this->io_position->clock.duration; + return spa_node_process(this->follower); + } + + if (this->direction == SPA_DIRECTION_INPUT) { + /* an input node (sink). + * First we run the converter to process the input for the follower + * then if it produced data, we run the follower. */ + while (retry--) { + status = this->convert ? spa_node_process(this->convert) : 0; + /* schedule the follower when the converter needed + * a recycled buffer */ + if (status == -EPIPE || status == 0) + status = SPA_STATUS_HAVE_DATA; + else if (status < 0) + break; + + if (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) { + /* as long as the converter produced something or + * is drained, process the follower. */ + fstatus = spa_node_process(this->follower); + if (fstatus < 0) { + status = fstatus; + break; + } + /* if the follower doesn't need more data or is + * drained we can stop */ + if ((fstatus & SPA_STATUS_NEED_DATA) == 0 || + (fstatus & SPA_STATUS_DRAINED)) + break; + } + /* the converter needs more data */ + if ((status & SPA_STATUS_NEED_DATA)) + break; + } + } else if (!this->driver) { + bool done = false; + while (retry--) { + /* output node (source). First run the converter to make + * sure we push out any queued data. Then when it needs + * more data, schedule the follower. */ + status = this->convert ? spa_node_process(this->convert) : 0; + if (status == 0) + status = SPA_STATUS_NEED_DATA; + else if (status < 0) + break; + + done = (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)); + + /* when not async, we can return the data when we are done. + * In async mode we might first need to wake up the follower + * to asynchronously provide more data for the next round. */ + if (!this->async && done) + break; + + if (status & SPA_STATUS_NEED_DATA) { + /* the converter needs more data, schedule the + * follower */ + fstatus = spa_node_process(this->follower); + if (fstatus < 0) { + status = fstatus; + break; + } + /* if the follower didn't produce more data or is + * not drained we can stop now */ + if ((fstatus & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) == 0) + break; + } + /* converter produced something or is drained and we + * scheduled the follower above, we can stop now*/ + if (done) + break; + } + if (!done) + spa_node_call_xrun(&this->callbacks, 0, 0, NULL); + + } else { + status = spa_node_process(this->follower); + } + spa_log_trace_fp(this->log, "%p: process status:%d", this, status); + + this->driver = false; + + return status; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_hook_remove(&this->follower_listener); + spa_node_set_callbacks(this->follower, NULL, NULL); + + spa_handle_clear(this->hnd_convert); + + if (this->buffers) + free(this->buffers); + this->buffers = NULL; + + return 0; +} + + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + size_t size; + + size = spa_handle_factory_get_size(&spa_audioconvert_factory, params); + size += sizeof(struct impl); + + return size; +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + void *iface; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + + if (info == NULL || + (str = spa_dict_lookup(info, "audio.adapt.follower")) == NULL) + return -EINVAL; + + sscanf(str, "pointer:%p", &this->follower); + if (this->follower == NULL) + return -EINVAL; + + if (this->cpu) + this->max_align = spa_cpu_get_max_align(this->cpu); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->hnd_convert = SPA_PTROFF(this, sizeof(struct impl), struct spa_handle); + spa_handle_factory_init(&spa_audioconvert_factory, + this->hnd_convert, + info, support, n_support); + + spa_handle_get_interface(this->hnd_convert, SPA_TYPE_INTERFACE_Node, &iface); + this->convert = iface; + this->target = this->convert; + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->params[IDX_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + spa_node_add_listener(this->follower, + &this->follower_listener, &follower_node_events, this); + spa_node_set_callbacks(this->follower, &follower_node_callbacks, this); + + spa_node_add_listener(this->convert, + &this->convert_listener, &convert_node_events, this); + + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp); + + link_io(this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_Node, }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_audioadapter_factory = { + .version = SPA_VERSION_HANDLE_FACTORY, + .name = SPA_NAME_AUDIO_ADAPT, + .get_size = impl_get_size, + .init = impl_init, + .enum_interface_info = impl_enum_interface_info, +}; diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c new file mode 100644 index 0000000..970df16 --- /dev/null +++ b/spa/plugins/audioconvert/audioconvert.c @@ -0,0 +1,2945 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT log_topic +static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audioconvert"); + +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 + +#define MAX_ALIGN FMT_OPS_MAX_ALIGN +#define MAX_BUFFERS 32 +#define MAX_DATAS SPA_AUDIO_MAX_CHANNELS +#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1) + +#define DEFAULT_MUTE false +#define DEFAULT_VOLUME VOLUME_NORM + +struct volumes { + bool mute; + uint32_t n_volumes; + float volumes[SPA_AUDIO_MAX_CHANNELS]; +}; + +static void init_volumes(struct volumes *vol) +{ + uint32_t i; + vol->mute = DEFAULT_MUTE; + vol->n_volumes = 0; + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + vol->volumes[i] = DEFAULT_VOLUME; +} + +struct props { + float volume; + uint32_t n_channels; + uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS]; + struct volumes channel; + struct volumes soft; + struct volumes monitor; + unsigned int have_soft_volume:1; + unsigned int mix_disabled:1; + unsigned int resample_disabled:1; + unsigned int resample_quality; + double rate; +}; + +static void props_reset(struct props *props) +{ + uint32_t i; + props->volume = DEFAULT_VOLUME; + props->n_channels = 0; + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN; + init_volumes(&props->channel); + init_volumes(&props->soft); + init_volumes(&props->monitor); + props->have_soft_volume = false; + props->mix_disabled = false; + props->resample_disabled = false; + props->resample_quality = RESAMPLE_DEFAULT_QUALITY; + props->rate = 1.0; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1<<0) + uint32_t flags; + struct spa_list link; + struct spa_buffer *buf; + void *datas[MAX_DATAS]; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + char position[16]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_audio_info format; + unsigned int have_format:1; + unsigned int is_dsp:1; + unsigned int is_monitor:1; + unsigned int is_control:1; + + uint32_t blocks; + uint32_t stride; + + const struct spa_pod_sequence *ctrl; + uint32_t ctrl_offset; + + struct spa_list queue; +}; + +struct dir { + struct port *ports[MAX_PORTS]; + uint32_t n_ports; + + enum spa_direction direction; + enum spa_param_port_config_mode mode; + + struct spa_audio_info format; + unsigned int have_format:1; + unsigned int have_profile:1; + struct spa_latency_info latency; + + uint32_t remap[MAX_PORTS]; + + struct convert conv; + unsigned int need_remap:1; + unsigned int is_passthrough:1; + unsigned int control:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + + uint32_t cpu_flags; + uint32_t max_align; + uint32_t quantum_limit; + enum spa_direction direction; + + struct props props; + + struct spa_io_position *io_position; + struct spa_io_rate_match *io_rate_match; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumPortConfig 0 +#define IDX_PortConfig 1 +#define IDX_PropInfo 2 +#define IDX_Props 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + + unsigned int monitor:1; + unsigned int monitor_channel_volumes:1; + + struct dir dir[2]; + struct channelmix mix; + struct resample resample; + struct volume volume; + double rate_scale; + + uint32_t in_offset; + uint32_t out_offset; + unsigned int started:1; + unsigned int setup:1; + unsigned int resample_peaks:1; + unsigned int is_passthrough:1; + unsigned int drained:1; + unsigned int rate_adjust:1; + + uint32_t empty_size; + float *empty; + float *scratch; + float *tmp[2]; + float *tmp_datas[2][MAX_PORTS]; +}; + +#define CHECK_PORT(this,d,p) ((p) < this->dir[d].n_ports) +#define GET_PORT(this,d,p) (this->dir[d].ports[p]) +#define GET_IN_PORT(this,p) GET_PORT(this,SPA_DIRECTION_INPUT,p) +#define GET_OUT_PORT(this,p) GET_PORT(this,SPA_DIRECTION_OUTPUT,p) + +#define PORT_IS_DSP(this,d,p) (GET_PORT(this,d,p)->is_dsp) +#define PORT_IS_CONTROL(this,d,p) (GET_PORT(this,d,p)->is_control) + +static void set_volume(struct impl *this); + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(this->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[3]; + uint32_t n_items = 0; + + if (PORT_IS_DSP(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNEL, port->position); + if (port->is_monitor) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_MONITOR, "true"); + } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); + } + port->info.props = &SPA_DICT_INIT(items, n_items); + + if (port->info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + SPA_FOR_EACH_ELEMENT_VAR(port->params, p) { + if (p->user > 0) { + p->flags ^= SPA_PARAM_INFO_SERIAL; + p->user = 0; + } + } + } + spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id, + uint32_t position, bool is_dsp, bool is_monitor, bool is_control) +{ + struct port *port = GET_PORT(this, direction, port_id); + const char *name; + + spa_assert(port_id < MAX_PORTS); + + if (port == NULL) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + this->dir[direction].ports[port_id] = port; + } + port->direction = direction; + port->id = port_id; + + name = spa_debug_type_find_short_name(spa_type_audio_channel, position); + snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK"); + + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_DYNAMIC_DATA; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->n_buffers = 0; + port->have_format = false; + port->is_monitor = is_monitor; + port->is_dsp = is_dsp; + if (port->is_dsp) { + port->format.media_type = SPA_MEDIA_TYPE_audio; + port->format.media_subtype = SPA_MEDIA_SUBTYPE_dsp; + port->format.info.dsp.format = SPA_AUDIO_FORMAT_DSP_F32; + port->blocks = 1; + port->stride = 4; + } + port->is_control = is_control; + if (port->is_control) { + port->format.media_type = SPA_MEDIA_TYPE_application; + port->format.media_subtype = SPA_MEDIA_SUBTYPE_control; + port->blocks = 1; + port->stride = 1; + } + spa_list_init(&port->queue); + + spa_log_info(this->log, "%p: add port %d:%d position:%s %d %d %d", + this, direction, port_id, port->position, is_dsp, is_monitor, is_control); + emit_port_info(this, port, true); + + return 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + { + struct dir *dir; + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_CHOICE_ENUM_Id(4, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_dsp, + SPA_PARAM_PORT_CONFIG_MODE_convert), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_CHOICE_Bool(false), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_CHOICE_Bool(false)); + break; + } + case SPA_PARAM_PortConfig: + { + struct dir *dir; + struct spa_pod_frame f[1]; + + switch (result.index) { + case 0: + dir = &this->dir[SPA_DIRECTION_INPUT];; + break; + case 1: + dir = &this->dir[SPA_DIRECTION_OUTPUT];; + break; + default: + return 0; + } + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); + spa_pod_builder_add(&b, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(this->monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(dir->control), + 0); + + if (dir->have_format) { + spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0); + spa_format_audio_raw_build(&b, SPA_PARAM_PORT_CONFIG_format, + &dir->format.info.raw); + } + param = spa_pod_builder_pop(&b, &f[0]); + break; + } + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), + SPA_PROP_INFO_description, SPA_POD_String("Volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute), + SPA_PROP_INFO_description, SPA_POD_String("Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute)); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 3: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelMap), + SPA_PROP_INFO_description, SPA_POD_String("Channel Map"), + SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 4: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorMute), + SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute)); + break; + case 5: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 6: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softMute), + SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute)); + break; + case 7: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 8: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("monitor.channel-volumes"), + SPA_PROP_INFO_description, SPA_POD_String("Monitor channel volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + this->monitor_channel_volumes), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 9: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mix_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 10: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"), + SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 11: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"), + SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 12: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"), + SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 13: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"), + SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.lfe_cutoff, 0.0, 1000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 14: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"), + SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.fc_cutoff, 0.0, 48000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 15: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"), + SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.rear_delay, 0.0, 1000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 16: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"), + SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.widen, 0.0, 1.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 17: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"), + SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int( + this->mix.hilbert_taps, 0, MAX_TAPS), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 18: + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(&b, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"), + SPA_PROP_INFO_description, SPA_POD_String("Upmix method to use"), + SPA_PROP_INFO_type, SPA_POD_String( + channelmix_upmix_info[this->mix.upmix].label), + SPA_PROP_INFO_params, SPA_POD_Bool(true), + 0); + + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(&b, &f[1]); + SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) { + spa_pod_builder_string(&b, i->label); + spa_pod_builder_string(&b, i->description); + } + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + case 19: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate), + SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0)); + break; + case 20: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality), + SPA_PROP_INFO_name, SPA_POD_String("resample.quality"), + SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 21: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("resample.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 22: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("dither.noise"), + SPA_PROP_INFO_description, SPA_POD_String("Add noise bits"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 23: + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(&b, + SPA_PROP_INFO_name, SPA_POD_String("dither.method"), + SPA_PROP_INFO_description, SPA_POD_String("The dithering method"), + SPA_PROP_INFO_type, SPA_POD_String( + dither_method_info[this->dir[1].conv.method].label), + SPA_PROP_INFO_params, SPA_POD_Bool(true), + 0); + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(&b, &f[1]); + SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) { + spa_pod_builder_string(&b, i->label); + spa_pod_builder_string(&b, i->description); + } + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + default: + return 0; + } + break; + } + + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (result.index) { + case 0: + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(&b, + SPA_PROP_volume, SPA_POD_Float(p->volume), + SPA_PROP_mute, SPA_POD_Bool(p->channel.mute), + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->channel.n_volumes, + p->channel.volumes), + SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, + p->n_channels, + p->channel_map), + SPA_PROP_softMute, SPA_POD_Bool(p->soft.mute), + SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->soft.n_volumes, + p->soft.volumes), + SPA_PROP_monitorMute, SPA_POD_Bool(p->monitor.mute), + SPA_PROP_monitorVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->monitor.n_volumes, + p->monitor.volumes), + 0); + spa_pod_builder_prop(&b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(&b, &f[1]); + spa_pod_builder_string(&b, "monitor.channel-volumes"); + spa_pod_builder_bool(&b, this->monitor_channel_volumes); + spa_pod_builder_string(&b, "channelmix.disable"); + spa_pod_builder_bool(&b, this->props.mix_disabled); + spa_pod_builder_string(&b, "channelmix.normalize"); + spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_NORMALIZE)); + spa_pod_builder_string(&b, "channelmix.mix-lfe"); + spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_MIX_LFE)); + spa_pod_builder_string(&b, "channelmix.upmix"); + spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_UPMIX)); + spa_pod_builder_string(&b, "channelmix.lfe-cutoff"); + spa_pod_builder_float(&b, this->mix.lfe_cutoff); + spa_pod_builder_string(&b, "channelmix.fc-cutoff"); + spa_pod_builder_float(&b, this->mix.fc_cutoff); + spa_pod_builder_string(&b, "channelmix.rear-delay"); + spa_pod_builder_float(&b, this->mix.rear_delay); + spa_pod_builder_string(&b, "channelmix.stereo-widen"); + spa_pod_builder_float(&b, this->mix.widen); + spa_pod_builder_string(&b, "channelmix.hilbert-taps"); + spa_pod_builder_int(&b, this->mix.hilbert_taps); + spa_pod_builder_string(&b, "channelmix.upmix-method"); + spa_pod_builder_string(&b, channelmix_upmix_info[this->mix.upmix].label); + spa_pod_builder_string(&b, "resample.quality"); + spa_pod_builder_int(&b, p->resample_quality); + spa_pod_builder_string(&b, "resample.disable"); + spa_pod_builder_bool(&b, p->resample_disabled); + spa_pod_builder_string(&b, "dither.noise"); + spa_pod_builder_int(&b, this->dir[1].conv.noise_bits); + spa_pod_builder_string(&b, "dither.method"); + spa_pod_builder_string(&b, dither_method_info[this->dir[1].conv.method].label); + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + default: + return 0; + } + break; + } + default: + return 0; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size); + + switch (id) { + case SPA_IO_Position: + this->io_position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int audioconvert_set_param(struct impl *this, const char *k, const char *s) +{ + if (spa_streq(k, "monitor.channel-volumes")) + this->monitor_channel_volumes = spa_atob(s); + else if (spa_streq(k, "channelmix.disable")) + this->props.mix_disabled = spa_atob(s); + else if (spa_streq(k, "channelmix.normalize")) + SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_NORMALIZE, spa_atob(s)); + else if (spa_streq(k, "channelmix.mix-lfe")) + SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_MIX_LFE, spa_atob(s)); + else if (spa_streq(k, "channelmix.upmix")) + SPA_FLAG_UPDATE(this->mix.options, CHANNELMIX_OPTION_UPMIX, spa_atob(s)); + else if (spa_streq(k, "channelmix.lfe-cutoff")) + spa_atof(s, &this->mix.lfe_cutoff); + else if (spa_streq(k, "channelmix.fc-cutoff")) + spa_atof(s, &this->mix.fc_cutoff); + else if (spa_streq(k, "channelmix.rear-delay")) + spa_atof(s, &this->mix.rear_delay); + else if (spa_streq(k, "channelmix.stereo-widen")) + spa_atof(s, &this->mix.widen); + else if (spa_streq(k, "channelmix.hilbert-taps")) + spa_atou32(s, &this->mix.hilbert_taps, 0); + else if (spa_streq(k, "channelmix.upmix-method")) + this->mix.upmix = channelmix_upmix_from_label(s); + else if (spa_streq(k, "resample.quality")) + this->props.resample_quality = atoi(s); + else if (spa_streq(k, "resample.disable")) + this->props.resample_disabled = spa_atob(s); + else if (spa_streq(k, "dither.noise")) + spa_atou32(s, &this->dir[1].conv.noise_bits, 0); + else if (spa_streq(k, "dither.method")) + this->dir[1].conv.method = dither_method_from_label(s); + else + return 0; + return 1; +} + +static int parse_prop_params(struct impl *this, struct spa_pod *params) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + int changed = 0; + + spa_pod_parser_pod(&prs, params); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + struct spa_pod *pod; + char value[512]; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + + if (spa_pod_parser_get_pod(&prs, &pod) < 0) + break; + + if (spa_pod_is_string(pod)) { + spa_pod_copy_string(pod, sizeof(value), value); + } else if (spa_pod_is_float(pod)) { + spa_dtoa(value, sizeof(value), + SPA_POD_VALUE(struct spa_pod_float, pod)); + } else if (spa_pod_is_double(pod)) { + spa_dtoa(value, sizeof(value), + SPA_POD_VALUE(struct spa_pod_double, pod)); + } else if (spa_pod_is_int(pod)) { + snprintf(value, sizeof(value), "%d", + SPA_POD_VALUE(struct spa_pod_int, pod)); + } else if (spa_pod_is_bool(pod)) { + snprintf(value, sizeof(value), "%s", + SPA_POD_VALUE(struct spa_pod_bool, pod) ? + "true" : "false"); + } else if (spa_pod_is_none(pod)) { + spa_zero(value); + } else + continue; + + spa_log_info(this->log, "key:'%s' val:'%s'", name, value); + changed += audioconvert_set_param(this, name, value); + } + if (changed) { + channelmix_init(&this->mix); + } + return changed; +} + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + struct props *p = &this->props; + bool have_channel_volume = false; + bool have_soft_volume = false; + int changed = 0; + uint32_t n; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + if (spa_pod_get_float(&prop->value, &p->volume) == 0) + changed++; + break; + case SPA_PROP_mute: + if (spa_pod_get_bool(&prop->value, &p->channel.mute) == 0) { + have_channel_volume = true; + changed++; + } + break; + case SPA_PROP_channelVolumes: + if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + have_channel_volume = true; + p->channel.n_volumes = n; + changed++; + } + break; + case SPA_PROP_channelMap: + if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, + p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->n_channels = n; + changed++; + } + break; + case SPA_PROP_softMute: + if (spa_pod_get_bool(&prop->value, &p->soft.mute) == 0) { + have_soft_volume = true; + changed++; + } + break; + case SPA_PROP_softVolumes: + if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + have_soft_volume = true; + p->soft.n_volumes = n; + changed++; + } + break; + case SPA_PROP_monitorMute: + if (spa_pod_get_bool(&prop->value, &p->monitor.mute) == 0) + changed++; + break; + case SPA_PROP_monitorVolumes: + if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->monitor.n_volumes = n; + changed++; + } + break; + case SPA_PROP_rate: + spa_pod_get_double(&prop->value, &p->rate); + if (!this->rate_adjust && p->rate != 1.0) { + this->rate_adjust = true; + spa_log_info(this->log, "%p: activating adaptive resampler", + this); + } + break; + case SPA_PROP_params: + changed += parse_prop_params(this, &prop->value); + break; + default: + break; + } + } + if (changed) { + if (have_soft_volume) + p->have_soft_volume = true; + else if (have_channel_volume) + p->have_soft_volume = false; + + set_volume(this); + } + return changed; +} + +static int apply_midi(struct impl *this, const struct spa_pod *value) +{ + const uint8_t *val = SPA_POD_BODY(value); + uint32_t size = SPA_POD_BODY_SIZE(value); + struct props *p = &this->props; + + if (size < 3) + return -EINVAL; + + if ((val[0] & 0xf0) != 0xb0 || val[1] != 7) + return 0; + + p->volume = val[2] / 127.0f; + set_volume(this); + return 1; +} + +static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode mode, + enum spa_direction direction, bool monitor, bool control, struct spa_audio_info *info) +{ + struct dir *dir; + uint32_t i; + + dir = &this->dir[direction]; + + if (dir->have_profile && this->monitor == monitor && dir->mode == mode && + dir->control == control && + (info == NULL || memcmp(&dir->format, info, sizeof(*info)) == 0)) + return 0; + + spa_log_info(this->log, "%p: port config direction:%d monitor:%d control:%d mode:%d %d", this, + direction, monitor, control, mode, dir->n_ports); + + for (i = 0; i < dir->n_ports; i++) { + spa_node_emit_port_info(&this->hooks, direction, i, NULL); + if (this->monitor && direction == SPA_DIRECTION_INPUT) + spa_node_emit_port_info(&this->hooks, SPA_DIRECTION_OUTPUT, i+1, NULL); + } + + this->monitor = monitor; + this->setup = false; + dir->control = control; + dir->have_profile = true; + dir->mode = mode; + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + { + if (info) { + dir->n_ports = info->info.raw.channels; + dir->format = *info; + dir->format.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32; + dir->format.info.raw.rate = 0; + dir->have_format = true; + } else { + dir->n_ports = 0; + } + + if (this->monitor && direction == SPA_DIRECTION_INPUT) + this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1; + + for (i = 0; i < dir->n_ports; i++) { + init_port(this, direction, i, info->info.raw.position[i], true, false, false); + if (this->monitor && direction == SPA_DIRECTION_INPUT) + init_port(this, SPA_DIRECTION_OUTPUT, i+1, + info->info.raw.position[i], true, true, false); + } + break; + } + case SPA_PARAM_PORT_CONFIG_MODE_convert: + { + dir->n_ports = 1; + dir->have_format = false; + init_port(this, direction, 0, 0, false, false, false); + break; + } + case SPA_PARAM_PORT_CONFIG_MODE_none: + break; + default: + return -ENOTSUP; + } + if (direction == SPA_DIRECTION_INPUT && dir->control) { + i = dir->n_ports++; + init_port(this, direction, i, 0, false, false, true); + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_Props].user++; + this->params[IDX_PortConfig].user++; + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (param == NULL) + return 0; + + switch (id) { + case SPA_PARAM_PortConfig: + { + struct spa_audio_info info = { 0, }, *infop = NULL; + struct spa_pod *format = NULL; + enum spa_direction direction; + enum spa_param_port_config_mode mode; + bool monitor = false, control = false; + int res; + + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; + + if (format) { + if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) + return -EINVAL; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format == 0 || + info.info.raw.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + infop = &info; + } + + if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0) + return res; + + emit_node_info(this, false); + break; + } + case SPA_PARAM_Props: + if (apply_props(this, param) > 0) + emit_node_info(this, false); + break; + default: + return -ENOENT; + } + return 0; +} + +static int int32_cmp(const void *v1, const void *v2) +{ + int32_t a1 = *(int32_t*)v1; + int32_t a2 = *(int32_t*)v2; + if (a1 == 0 && a2 != 0) + return 1; + if (a2 == 0 && a1 != 0) + return -1; + return a1 - a2; +} + +static int setup_in_convert(struct impl *this) +{ + uint32_t i, j; + struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; + struct spa_audio_info src_info, dst_info; + int res; + bool remap = false; + + src_info = in->format; + dst_info = src_info; + dst_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32; + + spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this, + spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format), + src_info.info.raw.channels, + src_info.info.raw.rate, + spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format), + dst_info.info.raw.channels, + dst_info.info.raw.rate); + + qsort(dst_info.info.raw.position, dst_info.info.raw.channels, + sizeof(uint32_t), int32_cmp); + + for (i = 0; i < src_info.info.raw.channels; i++) { + for (j = 0; j < dst_info.info.raw.channels; j++) { + if (src_info.info.raw.position[i] != + dst_info.info.raw.position[j]) + continue; + in->remap[i] = j; + if (i != j) + remap = true; + spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this, + i, in->remap[i], j, + spa_debug_type_find_short_name(spa_type_audio_channel, + src_info.info.raw.position[i]), + spa_debug_type_find_short_name(spa_type_audio_channel, + dst_info.info.raw.position[j])); + dst_info.info.raw.position[j] = -1; + break; + } + } + if (in->conv.free) + convert_free(&in->conv); + + in->conv.src_fmt = src_info.info.raw.format; + in->conv.dst_fmt = dst_info.info.raw.format; + in->conv.n_channels = dst_info.info.raw.channels; + in->conv.cpu_flags = this->cpu_flags; + in->need_remap = remap; + + if ((res = convert_init(&in->conv)) < 0) + return res; + + spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d remap:%d %s", this, + this->cpu_flags, in->conv.cpu_flags, in->conv.is_passthrough, + remap, in->conv.func_name); + + return 0; +} + +static void fix_volumes(struct impl *this, struct volumes *vols, uint32_t channels) +{ + float s; + uint32_t i; + spa_log_debug(this->log, "%p %d -> %d", this, vols->n_volumes, channels); + if (vols->n_volumes > 0) { + s = 0.0f; + for (i = 0; i < vols->n_volumes; i++) + s += vols->volumes[i]; + s /= vols->n_volumes; + } else { + s = 1.0f; + } + vols->n_volumes = channels; + for (i = 0; i < vols->n_volumes; i++) + vols->volumes[i] = s; +} + +static int remap_volumes(struct impl *this, const struct spa_audio_info *info) +{ + struct props *p = &this->props; + uint32_t i, j, target = info->info.raw.channels; + + for (i = 0; i < p->n_channels; i++) { + for (j = i; j < target; j++) { + spa_log_debug(this->log, "%d %d: %d <-> %d", i, j, + p->channel_map[i], info->info.raw.position[j]); + if (p->channel_map[i] != info->info.raw.position[j]) + continue; + if (i != j) { + SPA_SWAP(p->channel_map[i], p->channel_map[j]); + SPA_SWAP(p->channel.volumes[i], p->channel.volumes[j]); + SPA_SWAP(p->soft.volumes[i], p->soft.volumes[j]); + SPA_SWAP(p->monitor.volumes[i], p->monitor.volumes[j]); + } + break; + } + } + p->n_channels = target; + for (i = 0; i < p->n_channels; i++) + p->channel_map[i] = info->info.raw.position[i]; + + if (target == 0) + return 0; + if (p->channel.n_volumes != target) + fix_volumes(this, &p->channel, target); + if (p->soft.n_volumes != target) + fix_volumes(this, &p->soft, target); + if (p->monitor.n_volumes != target) + fix_volumes(this, &p->monitor, target); + + return 1; +} + +static void set_volume(struct impl *this) +{ + struct volumes *vol; + uint32_t i; + float volumes[SPA_AUDIO_MAX_CHANNELS]; + struct dir *dir = &this->dir[this->direction]; + + spa_log_debug(this->log, "%p have_format:%d", this, dir->have_format); + + if (dir->have_format) + remap_volumes(this, &dir->format); + + if (this->mix.set_volume == NULL) + return; + + if (this->props.have_soft_volume) + vol = &this->props.soft; + else + vol = &this->props.channel; + + for (i = 0; i < vol->n_volumes; i++) + volumes[i] = vol->volumes[dir->remap[i]]; + + channelmix_set_volume(&this->mix, this->props.volume, vol->mute, + vol->n_volumes, volumes); + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].user++; +} + +static char *format_position(char *str, size_t len, uint32_t channels, uint32_t *position) +{ + uint32_t i, idx = 0; + for (i = 0; i < channels; i++) + idx += snprintf(str + idx, len - idx, "%s%s", i == 0 ? "" : " ", + spa_debug_type_find_short_name(spa_type_audio_channel, + position[i])); + return str; +} + +static int setup_channelmix(struct impl *this) +{ + struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; + struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; + uint32_t i, src_chan, dst_chan, p; + uint64_t src_mask, dst_mask; + char str[1024]; + int res; + + src_chan = in->format.info.raw.channels; + dst_chan = out->format.info.raw.channels; + + for (i = 0, src_mask = 0; i < src_chan; i++) { + p = in->format.info.raw.position[i]; + src_mask |= 1ULL << (p < 64 ? p : 0); + } + for (i = 0, dst_mask = 0; i < dst_chan; i++) { + p = out->format.info.raw.position[i]; + dst_mask |= 1ULL << (p < 64 ? p : 0); + } + + spa_log_info(this->log, "in %s (%016"PRIx64")", format_position(str, sizeof(str), + src_chan, in->format.info.raw.position), src_mask); + spa_log_info(this->log, "out %s (%016"PRIx64")", format_position(str, sizeof(str), + dst_chan, out->format.info.raw.position), dst_mask); + + spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d %08"PRIx64":%08"PRIx64, this, + spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), + src_chan, + in->format.info.raw.rate, + spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), + dst_chan, + in->format.info.raw.rate, + src_mask, dst_mask); + + this->mix.src_chan = src_chan; + this->mix.src_mask = src_mask; + this->mix.dst_chan = dst_chan; + this->mix.dst_mask = dst_mask; + this->mix.cpu_flags = this->cpu_flags; + this->mix.log = this->log; + this->mix.freq = in->format.info.raw.rate; + + if ((res = channelmix_init(&this->mix)) < 0) + return res; + + set_volume(this); + + spa_log_debug(this->log, "%p: got channelmix features %08x:%08x flags:%08x %s", + this, this->cpu_flags, this->mix.cpu_flags, + this->mix.flags, this->mix.func_name); + return 0; +} + +static int setup_resample(struct impl *this) +{ + struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; + struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; + int res; + + spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this, + spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), + out->format.info.raw.channels, + in->format.info.raw.rate, + spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), + out->format.info.raw.channels, + out->format.info.raw.rate); + + if (this->resample.free) + resample_free(&this->resample); + + this->resample.channels = out->format.info.raw.channels; + this->resample.i_rate = in->format.info.raw.rate; + this->resample.o_rate = out->format.info.raw.rate; + this->resample.log = this->log; + this->resample.quality = this->props.resample_quality; + this->resample.cpu_flags = this->cpu_flags; + + this->rate_adjust = this->props.rate != 1.0; + + if (this->resample_peaks) + res = resample_peaks_init(&this->resample); + else + res = resample_native_init(&this->resample); + + spa_log_debug(this->log, "%p: got resample features %08x:%08x %s", + this, this->cpu_flags, this->resample.cpu_flags, + this->resample.func_name); + return res; +} + +static int calc_width(struct spa_audio_info *info) +{ + switch (info->info.raw.format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_U8P: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_S8P: + case SPA_AUDIO_FORMAT_ULAW: + case SPA_AUDIO_FORMAT_ALAW: + return 1; + case SPA_AUDIO_FORMAT_S16P: + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + return 2; + case SPA_AUDIO_FORMAT_S24P: + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + return 3; + case SPA_AUDIO_FORMAT_F64P: + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return 8; + default: + return 4; + } +} + +static int setup_out_convert(struct impl *this) +{ + uint32_t i, j; + struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; + struct spa_audio_info src_info, dst_info; + int res; + bool remap = false; + + dst_info = out->format; + src_info = dst_info; + src_info.info.raw.format = SPA_AUDIO_FORMAT_DSP_F32; + + spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this, + spa_debug_type_find_name(spa_type_audio_format, src_info.info.raw.format), + src_info.info.raw.channels, + src_info.info.raw.rate, + spa_debug_type_find_name(spa_type_audio_format, dst_info.info.raw.format), + dst_info.info.raw.channels, + dst_info.info.raw.rate); + + qsort(src_info.info.raw.position, src_info.info.raw.channels, + sizeof(uint32_t), int32_cmp); + + for (i = 0; i < src_info.info.raw.channels; i++) { + for (j = 0; j < dst_info.info.raw.channels; j++) { + if (src_info.info.raw.position[i] != + dst_info.info.raw.position[j]) + continue; + out->remap[i] = j; + if (i != j) + remap = true; + + spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this, + i, out->remap[i], j, + spa_debug_type_find_short_name(spa_type_audio_channel, + src_info.info.raw.position[i]), + spa_debug_type_find_short_name(spa_type_audio_channel, + dst_info.info.raw.position[j])); + dst_info.info.raw.position[j] = -1; + break; + } + } + if (out->conv.free) + convert_free(&out->conv); + + out->conv.src_fmt = src_info.info.raw.format; + out->conv.dst_fmt = dst_info.info.raw.format; + out->conv.rate = dst_info.info.raw.rate; + out->conv.n_channels = dst_info.info.raw.channels; + out->conv.cpu_flags = this->cpu_flags; + out->need_remap = remap; + + if ((res = convert_init(&out->conv)) < 0) + return res; + + spa_log_debug(this->log, "%p: got converter features %08x:%08x quant:%d:%d" + " passthrough:%d remap:%d %s", this, + this->cpu_flags, out->conv.cpu_flags, out->conv.method, + out->conv.noise_bits, out->conv.is_passthrough, remap, out->conv.func_name); + + return 0; +} + +static int setup_convert(struct impl *this) +{ + struct dir *in, *out; + uint32_t i, rate; + int res; + + in = &this->dir[SPA_DIRECTION_INPUT]; + out = &this->dir[SPA_DIRECTION_OUTPUT]; + + spa_log_debug(this->log, "%p: setup:%d in_format:%d out_format:%d", this, + this->setup, in->have_format, out->have_format); + + if (this->setup) + return 0; + + if (!in->have_format || !out->have_format) + return -EINVAL; + + rate = this->io_position ? this->io_position->clock.rate.denom : DEFAULT_RATE; + + /* in DSP mode we always convert to the DSP rate */ + if (in->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) + in->format.info.raw.rate = rate; + if (out->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) + out->format.info.raw.rate = rate; + + /* try to passthrough the rates */ + if (in->format.info.raw.rate == 0) + in->format.info.raw.rate = out->format.info.raw.rate; + else if (out->format.info.raw.rate == 0) + out->format.info.raw.rate = in->format.info.raw.rate; + + /* try to passthrough the channels */ + if (in->format.info.raw.channels == 0) + in->format.info.raw.channels = out->format.info.raw.channels; + else if (out->format.info.raw.channels == 0) + out->format.info.raw.channels = in->format.info.raw.channels; + + if (in->format.info.raw.rate == 0 || out->format.info.raw.rate == 0) + return -EINVAL; + if (in->format.info.raw.channels == 0 || out->format.info.raw.channels == 0) + return -EINVAL; + + if ((res = setup_in_convert(this)) < 0) + return res; + if ((res = setup_channelmix(this)) < 0) + return res; + if ((res = setup_resample(this)) < 0) + return res; + if ((res = setup_out_convert(this)) < 0) + return res; + + for (i = 0; i < MAX_PORTS; i++) { + this->tmp_datas[0][i] = SPA_PTROFF(this->tmp[0], this->empty_size * i, void); + this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp_datas[0][i], MAX_ALIGN, void); + this->tmp_datas[1][i] = SPA_PTROFF(this->tmp[1], this->empty_size * i, void); + this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp_datas[1][i], MAX_ALIGN, void); + } + this->setup = true; + + emit_node_info(this, false); + + return 0; +} + +static void reset_node(struct impl *this) +{ + if (this->resample.reset) + resample_reset(&this->resample); + this->in_offset = 0; + this->out_offset = 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (this->started) + return 0; + if ((res = setup_convert(this)) < 0) + return res; + this->started = true; + break; + case SPA_NODE_COMMAND_Suspend: + this->setup = false; + SPA_FALLTHROUGH; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + case SPA_NODE_COMMAND_Flush: + reset_node(this); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + uint32_t i; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + emit_port_info(this, GET_IN_PORT(this, i), true); + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + emit_port_info(this, GET_OUT_PORT(this, i), true); + } + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + + switch (index) { + case 0: + if (PORT_IS_DSP(this, direction, port_id)) { + struct spa_audio_info_dsp info; + info.format = SPA_AUDIO_FORMAT_DSP_F32; + *param = spa_format_audio_dsp_build(builder, + SPA_PARAM_EnumFormat, &info); + } else if (PORT_IS_CONTROL(this, direction, port_id)) { + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + } else { + uint32_t rate = this->io_position ? + this->io_position->clock.rate.denom : DEFAULT_RATE; + + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(25, + SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_F32, + SPA_AUDIO_FORMAT_F32_OE, + SPA_AUDIO_FORMAT_F64P, + SPA_AUDIO_FORMAT_F64, + SPA_AUDIO_FORMAT_F64_OE, + SPA_AUDIO_FORMAT_S32P, + SPA_AUDIO_FORMAT_S32, + SPA_AUDIO_FORMAT_S32_OE, + SPA_AUDIO_FORMAT_S24_32P, + SPA_AUDIO_FORMAT_S24_32, + SPA_AUDIO_FORMAT_S24_32_OE, + SPA_AUDIO_FORMAT_S24P, + SPA_AUDIO_FORMAT_S24, + SPA_AUDIO_FORMAT_S24_OE, + SPA_AUDIO_FORMAT_S16P, + SPA_AUDIO_FORMAT_S16, + SPA_AUDIO_FORMAT_S16_OE, + SPA_AUDIO_FORMAT_S8P, + SPA_AUDIO_FORMAT_S8, + SPA_AUDIO_FORMAT_U8P, + SPA_AUDIO_FORMAT_U8, + SPA_AUDIO_FORMAT_ULAW, + SPA_AUDIO_FORMAT_ALAW), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + rate, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_CHANNELS, 1, SPA_AUDIO_MAX_CHANNELS)); + } + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[2048]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_log_debug(this->log, "%p: enum params port %d.%d %d %u", + this, direction, port_id, seq, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(object, direction, port_id, result.index, ¶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)); + else + param = spa_format_audio_raw_build(&b, id, &port->format.info.raw); + break; + case SPA_PARAM_Buffers: + { + uint32_t size; + + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + if (PORT_IS_DSP(this, direction, port_id)) { + /* DSP ports always use the quantum_limit as the buffer + * size. */ + size = this->quantum_limit; + } else { + uint32_t irate, orate; + struct dir *dir = &this->dir[direction]; + + /* Convert ports are scaled so that they can always + * provide one quantum of data */ + irate = dir->format.info.raw.rate; + + /* collect the other port rate */ + dir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; + if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) + orate = this->io_position ? this->io_position->clock.rate.denom : DEFAULT_RATE; + else + orate = dir->format.info.raw.rate; + + /* always keep some extra room for adaptive resampling */ + size = this->quantum_limit * 2; + /* scale the buffer size when we can. */ + if (irate != 0 && orate != 0) + size = SPA_SCALE32_UP(size, irate, orate); + } + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + size * port->stride, + 16 * port->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + param = spa_latency_build(&b, id, &this->dir[result.index].latency); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, "%p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int port_set_latency(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *latency) +{ + struct impl *this = object; + struct port *port, *oport; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + uint32_t i; + + spa_log_debug(this->log, "%p: set latency direction:%d id:%d", + this, direction, port_id); + + port = GET_PORT(this, direction, port_id); + if (port->is_monitor) + return 0; + + if (latency == NULL) { + this->dir[other].latency = SPA_LATENCY_INFO(other); + } else { + struct spa_latency_info info; + if (spa_latency_parse(latency, &info) < 0 || + info.direction != other) + return -EINVAL; + this->dir[other].latency = info; + } + + for (i = 0; i < this->dir[other].n_ports; i++) { + oport = GET_PORT(this, other, i); + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_Latency].user++; + emit_port_info(this, oport, false); + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].user++; + emit_port_info(this, port, false); + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: set format", this); + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + if (PORT_IS_DSP(this, direction, port_id)) { + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + if ((res = spa_format_audio_dsp_parse(format, &info.info.dsp)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) { + spa_log_error(this->log, "unexpected format %d<->%d", + info.info.dsp.format, SPA_AUDIO_FORMAT_DSP_F32); + return -EINVAL; + } + port->blocks = 1; + port->stride = 4; + } + else if (PORT_IS_CONTROL(this, direction, port_id)) { + if (info.media_type != SPA_MEDIA_TYPE_application || + info.media_subtype != SPA_MEDIA_SUBTYPE_control) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + port->blocks = 1; + port->stride = 1; + } + else { + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) { + spa_log_error(this->log, "unexpected types %d/%d", + info.media_type, info.media_subtype); + return -EINVAL; + } + if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0) { + spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); + return res; + } + if (info.info.raw.format == 0 || + info.info.raw.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) { + spa_log_error(this->log, "invalid format:%d rate:%d channels:%d", + info.info.raw.format, info.info.raw.rate, + info.info.raw.channels); + return -EINVAL; + } + port->stride = calc_width(&info); + if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) { + port->blocks = info.info.raw.channels; + } else { + port->stride *= info.info.raw.channels; + port->blocks = 1; + } + this->dir[direction].format = info; + this->dir[direction].have_format = true; + this->setup = false; + } + port->format = info; + port->have_format = true; + + spa_log_debug(this->log, "%p: %d %d %d", this, + port_id, port->stride, port->blocks); + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set param port %d.%d %u", + this, direction, port_id, id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Latency: + return port_set_latency(this, direction, port_id, flags, param); + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } +} + +static void queue_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + spa_log_trace_fp(this->log, "%p: queue buffer %d on port %d %d", + this, id, port->id, b->flags); + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); +} + +static struct buffer *peek_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_log_trace_fp(this->log, "%p: peek buffer %d on port %d %u", + this, b->id, port->id, b->flags); + return b; +} + +static void dequeue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, "%p: dequeue buffer %d on port %d %u", + this, b->id, port->id, b->flags); +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i, j, maxsize; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", + this, n_buffers, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + maxsize = this->quantum_limit * sizeof(float); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + uint32_t n_datas = buffers[i]->n_datas; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->buf = buffers[i]; + + if (n_datas != port->blocks) { + spa_log_error(this->log, "%p: invalid blocks %d on buffer %d", + this, n_datas, i); + return -EINVAL; + } + + for (j = 0; j < n_datas; j++) { + if (d[j].data == NULL) { + spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p", + this, j, i, d[j].type, d[j].data); + return -EINVAL; + } + if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) { + spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned", + this, j, i); + } + if (direction == SPA_DIRECTION_OUTPUT && + !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC)) + this->is_passthrough = false; + + b->datas[j] = d[j].data; + + maxsize = SPA_MAX(maxsize, d[j].maxsize); + } + if (direction == SPA_DIRECTION_OUTPUT) + queue_buffer(this, port, i); + } + if (maxsize > this->empty_size) { + this->empty = realloc(this->empty, maxsize + MAX_ALIGN); + this->scratch = realloc(this->scratch, maxsize + MAX_ALIGN); + this->tmp[0] = realloc(this->tmp[0], (maxsize + MAX_ALIGN) * MAX_PORTS); + this->tmp[1] = realloc(this->tmp[1], (maxsize + MAX_ALIGN) * MAX_PORTS); + if (this->empty == NULL || this->scratch == NULL || + this->tmp[0] == NULL || this->tmp[1] == NULL) + return -errno; + memset(this->empty, 0, maxsize + MAX_ALIGN); + this->empty_size = maxsize; + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: set io %d on port %d:%d %p", + this, id, direction, port_id, data); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + this->io_rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_OUT_PORT(this, port_id); + queue_buffer(this, port, buffer_id); + + return 0; +} + +static int channelmix_process_control(struct impl *this, struct port *ctrlport, + void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + struct spa_pod_control *c, *prev = NULL; + uint32_t avail_samples = n_samples; + uint32_t i; + const float *s[MAX_PORTS], **ss = (const float**) src; + float *d[MAX_PORTS], **sd = (float **) dst; + const struct spa_pod_sequence_body *body = &(ctrlport->ctrl)->body; + uint32_t size = SPA_POD_BODY_SIZE(ctrlport->ctrl); + bool end = false; + + c = spa_pod_control_first(body); + while (true) { + uint32_t chunk; + + if (c == NULL || !spa_pod_control_is_inside(body, size, c)) { + c = NULL; + end = true; + } + if (avail_samples == 0) + break; + + /* ignore old control offsets */ + if (c != NULL) { + if (c->offset <= ctrlport->ctrl_offset) { + prev = c; + if (c != NULL) + c = spa_pod_control_next(c); + continue; + } + chunk = SPA_MIN(avail_samples, c->offset - ctrlport->ctrl_offset); + spa_log_trace_fp(this->log, "%p: process %d-%d %d/%d", this, + ctrlport->ctrl_offset, c->offset, chunk, avail_samples); + } else { + chunk = avail_samples; + spa_log_trace_fp(this->log, "%p: process remain %d", this, chunk); + } + + if (prev) { + switch (prev->type) { + case SPA_CONTROL_Midi: + apply_midi(this, &prev->value); + break; + case SPA_CONTROL_Properties: + apply_props(this, &prev->value); + break; + default: + continue; + } + } + if (ss == (const float**)src && chunk != avail_samples) { + for (i = 0; i < this->mix.src_chan; i++) + s[i] = ss[i]; + for (i = 0; i < this->mix.dst_chan; i++) + d[i] = sd[i]; + ss = s; + sd = d; + } + + channelmix_process(&this->mix, (void**)sd, (const void**)ss, chunk); + + if (chunk != avail_samples) { + for (i = 0; i < this->mix.src_chan; i++) + ss[i] += chunk; + for (i = 0; i < this->mix.dst_chan; i++) + sd[i] += chunk; + } + avail_samples -= chunk; + ctrlport->ctrl_offset += chunk; + } + return end ? 1 : 0; +} + +static uint32_t resample_get_in_size(struct impl *this, bool passthrough, uint32_t out_size) +{ + uint32_t match_size = passthrough ? out_size : resample_in_len(&this->resample, out_size); + spa_log_trace_fp(this->log, "%p: current match %u", this, match_size); + return match_size; +} + +static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t out_size, uint32_t in_queued) +{ + uint32_t delay, match_size; + + if (passthrough) { + delay = 0; + match_size = out_size; + } else { + double rate = this->rate_scale / this->props.rate; + if (this->io_rate_match && + SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE)) + rate *= this->io_rate_match->rate; + resample_update_rate(&this->resample, rate); + delay = resample_delay(&this->resample); + match_size = resample_in_len(&this->resample, out_size); + } + match_size -= SPA_MIN(match_size, in_queued); + + spa_log_trace_fp(this->log, "%p: next match %u", this, match_size); + + if (this->io_rate_match) { + this->io_rate_match->delay = delay; + this->io_rate_match->size = match_size; + } + return match_size; +} + +static inline bool resample_is_passthrough(struct impl *this) +{ + return this->resample.i_rate == this->resample.o_rate && this->rate_scale == 1.0 && + !this->rate_adjust && (this->io_rate_match == NULL || + !SPA_FLAG_IS_SET(this->io_rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE)); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + const void *src_datas[MAX_PORTS], **in_datas; + void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS]; + void **out_datas, **dst_remap; + uint32_t i, j, n_src_datas = 0, n_dst_datas = 0, n_mon_datas = 0, remap; + uint32_t n_samples, max_in, n_out, max_out, quant_samples; + struct port *port, *ctrlport = NULL; + struct buffer *buf, *out_bufs[MAX_PORTS]; + struct spa_data *bd; + struct dir *dir; + int tmp = 0, res = 0; + bool in_passthrough, mix_passthrough, resample_passthrough, out_passthrough; + bool in_avail = false, flush_in = false, flush_out = false, draining = false, in_empty = true; + struct spa_io_buffers *io, *ctrlio = NULL; + const struct spa_pod_sequence *ctrl = NULL; + + /* calculate quantum scale, this is how many samples we need to produce or + * consume. Also update the rate scale, this is sent to the resampler to adjust + * the rate, either when the graph clock changed or when the user adjusted the + * rate. */ + if (SPA_LIKELY(this->io_position)) { + double r = this->rate_scale; + + quant_samples = this->io_position->clock.duration; + if (this->direction == SPA_DIRECTION_INPUT) { + if (this->io_position->clock.rate.denom != this->resample.o_rate) + r = (double) this->io_position->clock.rate.denom / this->resample.o_rate; + else + r = 1.0; + } else { + if (this->io_position->clock.rate.denom != this->resample.i_rate) + r = (double) this->resample.i_rate / this->io_position->clock.rate.denom; + else + r = 1.0; + } + if (this->rate_scale != r) { + spa_log_info(this->log, "scale %f->%f", this->rate_scale, r); + this->rate_scale = r; + } + } + else + quant_samples = this->quantum_limit; + + dir = &this->dir[SPA_DIRECTION_INPUT]; + in_passthrough = dir->conv.is_passthrough; + max_in = UINT32_MAX; + + /* collect input port data */ + for (i = 0; i < dir->n_ports; i++) { + port = GET_IN_PORT(this, i); + + if (SPA_UNLIKELY((io = port->io) == NULL)) { + spa_log_trace_fp(this->log, "%p: no io on input port %d", + this, port->id); + buf = NULL; + } else if (SPA_UNLIKELY(io->status != SPA_STATUS_HAVE_DATA)) { + if (io->status & SPA_STATUS_DRAINED) { + spa_log_debug(this->log, "%p: port %d drained", this, port->id); + in_avail = flush_in = draining = true; + } else { + spa_log_trace_fp(this->log, "%p: empty input port %d %p %d %d %d", + this, port->id, io, io->status, io->buffer_id, + port->n_buffers); + this->drained = false; + } + buf = NULL; + } else if (SPA_UNLIKELY(io->buffer_id >= port->n_buffers)) { + spa_log_trace_fp(this->log, "%p: invalid input buffer port %d %p %d %d %d", + this, port->id, io, io->status, io->buffer_id, + port->n_buffers); + io->status = -EINVAL; + buf = NULL; + } else { + spa_log_trace_fp(this->log, "%p: input buffer port %d io:%p status:%d id:%d n:%d", + this, port->id, io, io->status, io->buffer_id, + port->n_buffers); + buf = &port->buffers[io->buffer_id]; + } + + if (SPA_UNLIKELY(buf == NULL)) { + for (j = 0; j < port->blocks; j++) { + if (port->is_control) { + spa_log_trace_fp(this->log, "%p: empty control %d", this, + i * port->blocks + j); + } else { + remap = n_src_datas++; + src_datas[remap] = SPA_PTR_ALIGN(this->empty, MAX_ALIGN, void); + spa_log_trace_fp(this->log, "%p: empty input %d->%d", this, + i * port->blocks + j, remap); + max_in = SPA_MIN(max_in, this->empty_size / port->stride); + } + } + } else { + in_avail = true; + for (j = 0; j < port->blocks; j++) { + uint32_t offs, size; + + bd = &buf->buf->datas[j]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) + in_empty = false; + + if (SPA_UNLIKELY(port->is_control)) { + spa_log_trace_fp(this->log, "%p: control %d", this, + i * port->blocks + j); + ctrlport = port; + ctrlio = io; + ctrl = spa_pod_from_data(bd->data, bd->maxsize, + bd->chunk->offset, bd->chunk->size); + if (ctrl && !spa_pod_is_sequence(&ctrl->pod)) + ctrl = NULL; + if (ctrl != ctrlport->ctrl) { + ctrlport->ctrl = ctrl; + ctrlport->ctrl_offset = 0; + } + } else { + max_in = SPA_MIN(max_in, size / port->stride); + + remap = n_src_datas++; + offs += this->in_offset * port->stride; + src_datas[remap] = SPA_PTROFF(bd->data, offs, void); + + spa_log_trace_fp(this->log, "%p: input %d:%d:%d %d %d %d->%d", this, + offs, size, port->stride, this->in_offset, max_in, + i * port->blocks + j, remap); + } + } + } + } + + /* calculate how many samples we are going to produce. */ + if (this->direction == SPA_DIRECTION_INPUT) { + /* in split mode we need to output exactly the size of the + * duration so we don't try to flush early */ + max_out = quant_samples; + flush_out = false; + } else { + /* in merge mode we consume one duration of samples and + * always output the resulting data */ + max_out = this->quantum_limit; + flush_out = true; + } + + dir = &this->dir[SPA_DIRECTION_OUTPUT]; + /* collect output ports and monitor ports data */ + for (i = 0; i < dir->n_ports; i++) { + port = GET_OUT_PORT(this, i); + + if (SPA_UNLIKELY((io = port->io) == NULL || + io->status == SPA_STATUS_HAVE_DATA)) { + buf = NULL; + } + else { + if (SPA_LIKELY(io->buffer_id < port->n_buffers)) + queue_buffer(this, port, io->buffer_id); + + buf = peek_buffer(this, port); + } + out_bufs[i] = buf; + + if (SPA_UNLIKELY(buf == NULL)) { + for (j = 0; j < port->blocks; j++) { + if (port->is_monitor) { + remap = n_mon_datas++; + spa_log_trace_fp(this->log, "%p: empty monitor %d", this, + remap); + } else if (port->is_control) { + spa_log_trace_fp(this->log, "%p: empty control %d", this, j); + } else { + remap = n_dst_datas++; + dst_datas[remap] = SPA_PTR_ALIGN(this->scratch, MAX_ALIGN, void); + spa_log_trace_fp(this->log, "%p: empty output %d->%d", this, + i * port->blocks + j, remap); + max_out = SPA_MIN(max_out, this->empty_size / port->stride); + } + } + } else { + for (j = 0; j < port->blocks; j++) { + bd = &buf->buf->datas[j]; + + bd->chunk->offset = 0; + bd->chunk->size = 0; + if (port->is_monitor) { + float volume; + uint32_t mon_max; + + remap = n_mon_datas++; + volume = this->props.monitor.mute ? 0.0f : this->props.monitor.volumes[remap]; + if (this->monitor_channel_volumes) + volume *= this->props.channel.mute ? 0.0f : + this->props.channel.volumes[remap]; + + mon_max = SPA_MIN(bd->maxsize / port->stride, max_in); + + volume_process(&this->volume, bd->data, src_datas[remap], + volume, mon_max); + + bd->chunk->size = mon_max * port->stride; + bd->chunk->stride = port->stride; + + spa_log_trace_fp(this->log, "%p: monitor %d %d", this, + remap, mon_max); + + dequeue_buffer(this, port, buf); + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = buf->id; + res |= SPA_STATUS_HAVE_DATA; + } else if (SPA_UNLIKELY(port->is_control)) { + spa_log_trace_fp(this->log, "%p: control %d", this, j); + } else { + remap = n_dst_datas++; + dst_datas[remap] = SPA_PTROFF(bd->data, + this->out_offset * port->stride, void); + max_out = SPA_MIN(max_out, bd->maxsize / port->stride); + + spa_log_trace_fp(this->log, "%p: output %d offs:%d %d->%d", this, + max_out, this->out_offset, + i * port->blocks + j, remap); + } + } + } + } + + + /* calculate how many samples at most we are going to consume. If we're + * draining, we consume as much as we can. Otherwise we consume what is + * left. */ + if (SPA_UNLIKELY(draining)) + n_samples = SPA_MIN(max_in, this->quantum_limit); + else { + n_samples = max_in - SPA_MIN(max_in, this->in_offset); + } + /* we only need to output the remaining samples */ + n_out = max_out - SPA_MIN(max_out, this->out_offset); + + resample_passthrough = resample_is_passthrough(this); + + /* calculate how many samples we are going to consume. */ + if (this->direction == SPA_DIRECTION_INPUT) { + if (!in_avail || this->drained) { + /* no input, ask for more, update rate-match first */ + resample_update_rate_match(this, resample_passthrough, n_out, 0); + spa_log_trace_fp(this->log, "%p: no input drained:%d", this, this->drained); + res |= this->drained ? SPA_STATUS_DRAINED : SPA_STATUS_NEED_DATA; + return res; + } + /* else figure out how much input samples we need to consume */ + n_samples = SPA_MIN(n_samples, + resample_get_in_size(this, resample_passthrough, n_out)); + } else { + /* in merge mode we consume one duration of samples */ + n_samples = SPA_MIN(n_samples, quant_samples); + flush_in = true; + } + + mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) && + (ctrlport == NULL || ctrlport->ctrl == NULL); + + out_passthrough = dir->conv.is_passthrough; + if (in_passthrough && mix_passthrough && resample_passthrough) + out_passthrough = false; + + if (out_passthrough && dir->need_remap) { + for (i = 0; i < dir->conv.n_channels; i++) { + remap_dst_datas[i] = dst_datas[dir->remap[i]]; + spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]); + } + dst_remap = (void **)remap_dst_datas; + } else { + dst_remap = (void **)dst_datas; + } + + dir = &this->dir[SPA_DIRECTION_INPUT]; + if (!in_passthrough) { + if (mix_passthrough && resample_passthrough && out_passthrough) + out_datas = (void **)dst_remap; + else + out_datas = (void **)this->tmp_datas[(tmp++) & 1]; + + if (dir->need_remap) { + for (i = 0; i < dir->conv.n_channels; i++) { + remap_src_datas[i] = out_datas[dir->remap[i]]; + spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i); + } + } else { + for (i = 0; i < dir->conv.n_channels; i++) + remap_src_datas[i] = out_datas[i]; + } + + spa_log_trace_fp(this->log, "%p: input convert %d", this, n_samples); + convert_process(&dir->conv, remap_src_datas, src_datas, n_samples); + } else { + if (dir->need_remap) { + for (i = 0; i < dir->conv.n_channels; i++) { + remap_src_datas[dir->remap[i]] = (void *)src_datas[i]; + spa_log_trace_fp(this->log, "%p: input remap %d -> %d", this, dir->remap[i], i); + } + out_datas = (void **)remap_src_datas; + } else { + out_datas = (void **)src_datas; + } + } + + if (!mix_passthrough) { + in_datas = (const void**)out_datas; + if (resample_passthrough && out_passthrough) { + out_datas = (void **)dst_remap; + n_samples = SPA_MIN(n_samples, n_out); + } else { + out_datas = (void **)this->tmp_datas[(tmp++) & 1]; + } + spa_log_trace_fp(this->log, "%p: channelmix %d %d %d", this, n_samples, + resample_passthrough, out_passthrough); + if (ctrlport != NULL && ctrlport->ctrl != NULL) { + if (channelmix_process_control(this, ctrlport, out_datas, + in_datas, n_samples) == 1) { + ctrlio->status = SPA_STATUS_OK; + ctrlport->ctrl = NULL; + } + } else { + channelmix_process(&this->mix, out_datas, in_datas, n_samples); + } + } + if (!resample_passthrough) { + uint32_t in_len, out_len; + + in_datas = (const void**)out_datas; + if (out_passthrough) + out_datas = (void **)dst_remap; + else + out_datas = (void **)this->tmp_datas[(tmp++) & 1]; + + in_len = n_samples; + out_len = n_out; + resample_process(&this->resample, in_datas, &in_len, out_datas, &out_len); + spa_log_trace_fp(this->log, "%p: resample %d/%d -> %d/%d %d", this, + n_samples, in_len, n_out, out_len, out_passthrough); + this->in_offset += in_len; + n_samples = out_len; + } else { + n_samples = SPA_MIN(n_samples, n_out); + this->in_offset += n_samples; + } + this->out_offset += n_samples; + + if (!out_passthrough) { + dir = &this->dir[SPA_DIRECTION_OUTPUT]; + if (dir->need_remap) { + for (i = 0; i < dir->conv.n_channels; i++) { + remap_dst_datas[dir->remap[i]] = out_datas[i]; + spa_log_trace_fp(this->log, "%p: output remap %d -> %d", this, i, dir->remap[i]); + } + in_datas = (const void**)remap_dst_datas; + } else { + in_datas = (const void**)out_datas; + } + spa_log_trace_fp(this->log, "%p: output convert %d", this, n_samples); + convert_process(&dir->conv, dst_datas, in_datas, n_samples); + } + + spa_log_trace_fp(this->log, "%d/%d %d/%d %d->%d", this->in_offset, max_in, + this->out_offset, max_out, n_samples, n_out); + + dir = &this->dir[SPA_DIRECTION_INPUT]; + if (SPA_LIKELY(this->in_offset >= max_in || flush_in)) { + /* return input buffers */ + for (i = 0; i < dir->n_ports; i++) { + port = GET_IN_PORT(this, i); + if (port->is_control) + continue; + if (SPA_UNLIKELY((io = port->io) == NULL)) + continue; + spa_log_trace_fp(this->log, "return: input %d %d", port->id, io->buffer_id); + if (!draining) + io->status = SPA_STATUS_NEED_DATA; + } + this->in_offset = 0; + max_in = 0; + res |= SPA_STATUS_NEED_DATA; + } + + dir = &this->dir[SPA_DIRECTION_OUTPUT]; + if (SPA_LIKELY(n_samples > 0 && (this->out_offset >= max_out || flush_out))) { + /* queue output buffers */ + for (i = 0; i < dir->n_ports; i++) { + port = GET_OUT_PORT(this, i); + if (SPA_UNLIKELY(port->is_monitor || port->is_control)) + continue; + if (SPA_UNLIKELY((io = port->io) == NULL)) + continue; + + if (SPA_UNLIKELY((buf = out_bufs[i]) == NULL)) + continue; + + dequeue_buffer(this, port, buf); + + for (j = 0; j < port->blocks; j++) { + bd = &buf->buf->datas[j]; + bd->chunk->size = this->out_offset * port->stride; + bd->chunk->stride = port->stride; + SPA_FLAG_UPDATE(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY, in_empty); + spa_log_trace_fp(this->log, "out: offs:%d stride:%d size:%d", + this->out_offset, port->stride, bd->chunk->size); + } + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = buf->id; + } + res |= SPA_STATUS_HAVE_DATA; + this->drained = draining; + this->out_offset = 0; + } + else if (n_samples == 0 && this->resample_peaks) { + for (i = 0; i < dir->n_ports; i++) { + port = GET_OUT_PORT(this, i); + if (port->is_monitor || port->is_control) + continue; + if (SPA_UNLIKELY((io = port->io) == NULL)) + continue; + + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = SPA_ID_INVALID; + res |= SPA_STATUS_HAVE_DATA; + spa_log_trace_fp(this->log, "%p: no output buffer", this); + } + } + if (resample_update_rate_match(this, resample_passthrough, + max_out - this->out_offset, + max_in - this->in_offset) > 0) + res |= SPA_STATUS_NEED_DATA; + + return res; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + for (i = 0; i < MAX_PORTS; i++) + free(this->dir[SPA_DIRECTION_INPUT].ports[i]); + for (i = 0; i < MAX_PORTS; i++) + free(this->dir[SPA_DIRECTION_OUTPUT].ports[i]); + free(this->empty); + free(this->scratch); + free(this->tmp[0]); + free(this->tmp[1]); + + if (this->resample.free) + resample_free(&this->resample); + if (this->dir[0].conv.free) + convert_free(&this->dir[0].conv); + if (this->dir[1].conv.free) + convert_free(&this->dir[1].conv); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static inline uint32_t parse_position(uint32_t *pos, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + uint32_t i = 0; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + i < SPA_AUDIO_MAX_CHANNELS) { + pos[i++] = channel_from_name(v); + } + return i; +} + + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + if (this->cpu) { + this->cpu_flags = spa_cpu_get_flags(this->cpu); + this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); + } + + props_reset(&this->props); + + this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE; + this->mix.upmix = CHANNELMIX_UPMIX_PSD; + this->mix.log = this->log; + this->mix.lfe_cutoff = 150.0f; + this->mix.fc_cutoff = 12000.0f; + this->mix.rear_delay = 12.0f; + this->mix.widen = 0.0f; + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + else if (spa_streq(k, "resample.peaks")) + this->resample_peaks = spa_atob(s); + else if (spa_streq(k, "resample.prefill")) + SPA_FLAG_UPDATE(this->resample.options, + RESAMPLE_OPTION_PREFILL, spa_atob(s)); + else if (spa_streq(k, "factory.mode")) { + if (spa_streq(s, "merge")) + this->direction = SPA_DIRECTION_OUTPUT; + else + this->direction = SPA_DIRECTION_INPUT; + } + else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + if (s != NULL) + this->props.n_channels = parse_position(this->props.channel_map, s, strlen(s)); + } + else + audioconvert_set_param(this, k, s); + } + + this->props.channel.n_volumes = this->props.n_channels; + this->props.soft.n_volumes = this->props.n_channels; + this->props.monitor.n_volumes = this->props.n_channels; + + this->dir[SPA_DIRECTION_INPUT].direction = SPA_DIRECTION_INPUT; + this->dir[SPA_DIRECTION_INPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + this->dir[SPA_DIRECTION_OUTPUT].direction = SPA_DIRECTION_OUTPUT; + this->dir[SPA_DIRECTION_OUTPUT].latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_OUT_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + this->volume.cpu_flags = this->cpu_flags; + volume_init(&this->volume); + + this->rate_scale = 1.0; + + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL); + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_audioconvert_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_AUDIO_CONVERT, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c new file mode 100644 index 0000000..2a0d4e8 --- /dev/null +++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c @@ -0,0 +1,323 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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 + run_test("test_f32_s16d", "c", true, false, conv_f32_to_s16d_c); + run_test("test_f32d_s16d", "c", false, false, conv_f32d_to_s16d_c); +} + +static void test_s16_f32(void) +{ + run_test("test_s16_f32", "c", true, true, conv_s16_to_f32_c); + run_test("test_s16d_f32", "c", false, true, conv_s16d_to_f32_c); + run_test("test_s16_f32d", "c", true, false, conv_s16_to_f32d_c); +#if defined (HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s16_f32d", "sse2", true, false, conv_s16_to_f32d_sse2); + run_testc("test_s16_f32d_2", "sse2", true, false, conv_s16_to_f32d_2_sse2, 2); + } +#endif +#if defined (HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s16_f32d", "avx2", true, false, conv_s16_to_f32d_avx2); + run_testc("test_s16_f32d_2", "avx2", true, false, conv_s16_to_f32d_2_avx2, 2); + } +#endif + run_test("test_s16d_f32d", "c", false, false, conv_s16d_to_f32d_c); +} + +static void test_f32_s32(void) +{ + run_test("test_f32_s32", "c", true, true, conv_f32_to_s32_c); + run_test("test_f32d_s32", "c", false, true, conv_f32d_to_s32_c); +#if defined (HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_f32d_s32", "sse2", false, true, conv_f32d_to_s32_sse2); + } +#endif +#if defined (HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_f32d_s32", "avx2", false, true, conv_f32d_to_s32_avx2); + } +#endif + run_test("test_f32_s32d", "c", true, false, conv_f32_to_s32d_c); + run_test("test_f32d_s32d", "c", false, false, conv_f32d_to_s32d_c); +} + +static void test_s32_f32(void) +{ + run_test("test_s32_f32", "c", true, true, conv_s32_to_f32_c); + run_test("test_s32d_f32", "c", false, true, conv_s32d_to_f32_c); +#if defined (HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s32_f32d", "sse2", true, false, conv_s32_to_f32d_sse2); + } +#endif +#if defined (HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s32_f32d", "avx2", true, false, conv_s32_to_f32d_avx2); + } +#endif + run_test("test_s32_f32d", "c", true, false, conv_s32_to_f32d_c); + run_test("test_s32d_f32d", "c", false, false, conv_s32d_to_f32d_c); +} + +static void test_f32_s24(void) +{ + run_test("test_f32_s24", "c", true, true, conv_f32_to_s24_c); + run_test("test_f32d_s24", "c", false, true, conv_f32d_to_s24_c); + run_test("test_f32_s24d", "c", true, false, conv_f32_to_s24d_c); + run_test("test_f32d_s24d", "c", false, false, conv_f32d_to_s24d_c); +} + +static void test_s24_f32(void) +{ + run_test("test_s24_f32", "c", true, true, conv_s24_to_f32_c); + run_test("test_s24d_f32", "c", false, true, conv_s24d_to_f32_c); + run_test("test_s24_f32d", "c", true, false, conv_s24_to_f32d_c); +#if defined (HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s24_f32d", "sse2", true, false, conv_s24_to_f32d_sse2); + } +#endif +#if defined (HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s24_f32d", "avx2", true, false, conv_s24_to_f32d_avx2); + } +#endif +#if defined (HAVE_SSSE3) + if (cpu_flags & SPA_CPU_FLAG_SSSE3) { + run_test("test_s24_f32d", "ssse3", true, false, conv_s24_to_f32d_ssse3); + } +#endif +#if defined (HAVE_SSE41) + if (cpu_flags & SPA_CPU_FLAG_SSE41) { + run_test("test_s24_f32d", "sse41", true, false, conv_s24_to_f32d_sse41); + } +#endif + run_test("test_s24d_f32d", "c", false, false, conv_s24d_to_f32d_c); +} + +static void test_f32_s24_32(void) +{ + run_test("test_f32_s24_32", "c", true, true, conv_f32_to_s24_32_c); + run_test("test_f32d_s24_32", "c", false, true, conv_f32d_to_s24_32_c); + run_test("test_f32_s24_32d", "c", true, false, conv_f32_to_s24_32d_c); + run_test("test_f32d_s24_32d", "c", false, false, conv_f32d_to_s24_32d_c); +} + +static void test_s24_32_f32(void) +{ + run_test("test_s24_32_f32", "c", true, true, conv_s24_32_to_f32_c); + run_test("test_s24_32d_f32", "c", false, true, conv_s24_32d_to_f32_c); + run_test("test_s24_32_f32d", "c", true, false, conv_s24_32_to_f32d_c); + run_test("test_s24_32d_f32d", "c", false, false, conv_s24_32d_to_f32d_c); +} + +static void test_interleave(void) +{ + run_test("test_8d_to_8", "c", false, true, conv_8d_to_8_c); + run_test("test_16d_to_16", "c", false, true, conv_16d_to_16_c); + run_test("test_24d_to_24", "c", false, true, conv_24d_to_24_c); + run_test("test_32d_to_32", "c", false, true, conv_32d_to_32_c); +} + +static void test_deinterleave(void) +{ + run_test("test_8_to_8d", "c", true, false, conv_8_to_8d_c); + run_test("test_16_to_16d", "c", true, false, conv_16_to_16d_c); + run_test("test_24_to_24d", "c", true, false, conv_24_to_24d_c); + run_test("test_32_to_32d", "c", true, false, conv_32_to_32d_c); +} + +static int compare_func(const void *_a, const void *_b) +{ + const struct stats *a = _a, *b = _b; + int diff; + if ((diff = strcmp(a->name, b->name)) != 0) return diff; + if ((diff = a->n_samples - b->n_samples) != 0) return diff; + if ((diff = a->n_channels - b->n_channels) != 0) return diff; + if ((diff = b->perf - a->perf) != 0) return diff; + return 0; +} + +int main(int argc, char *argv[]) +{ + uint32_t i; + + cpu_flags = get_cpu_flags(); + printf("got get CPU flags %d\n", cpu_flags); + + test_f32_u8(); + test_u8_f32(); + test_f32_s16(); + test_s16_f32(); + test_f32_s32(); + test_s32_f32(); + test_f32_s24(); + test_s24_f32(); + test_f32_s24_32(); + test_s24_32_f32(); + test_interleave(); + test_deinterleave(); + + qsort(results, n_results, sizeof(struct stats), compare_func); + + for (i = 0; i < n_results; i++) { + struct stats *s = &results[i]; + fprintf(stderr, "%-12."PRIu64" \t%-32.32s %s \t samples %d, channels %d\n", + s->perf, s->name, s->impl, s->n_samples, s->n_channels); + } + return 0; +} diff --git a/spa/plugins/audioconvert/benchmark-resample.c b/spa/plugins/audioconvert/benchmark-resample.c new file mode 100644 index 0000000..597f9ac --- /dev/null +++ b/spa/plugins/audioconvert/benchmark-resample.c @@ -0,0 +1,204 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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..409b673 --- /dev/null +++ b/spa/plugins/audioconvert/biquad.c @@ -0,0 +1,111 @@ +/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +/* Copyright (C) 2010 Google Inc. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE.WEBKIT file. + */ + + +#include + +#include +#include "biquad.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#ifndef M_SQRT2 +#define M_SQRT2 1.41421356237309504880 +#endif + +static void set_coefficient(struct biquad *bq, double b0, double b1, double b2, + double a0, double a1, double a2) +{ + double a0_inv = 1 / a0; + bq->b0 = b0 * a0_inv; + bq->b1 = b1 * a0_inv; + bq->b2 = b2 * a0_inv; + bq->a1 = a1 * a0_inv; + bq->a2 = a2 * a0_inv; +} + +static void biquad_lowpass(struct biquad *bq, double cutoff) +{ + /* Limit cutoff to 0 to 1. */ + cutoff = SPA_CLAMP(cutoff, 0.0, 1.0); + + if (cutoff >= 1.0) { + /* When cutoff is 1, the z-transform is 1. */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + } else if (cutoff > 0) { + /* Compute biquad coefficients for lowpass filter */ + double theta = M_PI * cutoff; + double sn = 0.5 * M_SQRT2 * sin(theta); + double beta = 0.5 * (1 - sn) / (1 + sn); + double gamma_coeff = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta - gamma_coeff); + + double b0 = 2 * alpha; + double b1 = 2 * 2 * alpha; + double b2 = 2 * alpha; + double a1 = 2 * -gamma_coeff; + double a2 = 2 * beta; + + set_coefficient(bq, b0, b1, b2, 1, a1, a2); + } else { + /* When cutoff is zero, nothing gets through the filter, so set + * coefficients up correctly. + */ + set_coefficient(bq, 0, 0, 0, 1, 0, 0); + } +} + +static void biquad_highpass(struct biquad *bq, double cutoff) +{ + /* Limit cutoff to 0 to 1. */ + cutoff = SPA_CLAMP(cutoff, 0.0, 1.0); + + if (cutoff >= 1.0) { + /* The z-transform is 0. */ + set_coefficient(bq, 0, 0, 0, 1, 0, 0); + } else if (cutoff > 0) { + /* Compute biquad coefficients for highpass filter */ + double theta = M_PI * cutoff; + double sn = 0.5 * M_SQRT2 * sin(theta); + double beta = 0.5 * (1 - sn) / (1 + sn); + double gamma_coeff = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta + gamma_coeff); + + double b0 = 2 * alpha; + double b1 = 2 * -2 * alpha; + double b2 = 2 * alpha; + double a1 = 2 * -gamma_coeff; + double a2 = 2 * beta; + + set_coefficient(bq, b0, b1, b2, 1, a1, a2); + } else { + /* When cutoff is zero, we need to be careful because the above + * gives a quadratic divided by the same quadratic, with poles + * and zeros on the unit circle in the same place. When cutoff + * is zero, the z-transform is 1. + */ + set_coefficient(bq, 1, 0, 0, 1, 0, 0); + } +} + +void biquad_set(struct biquad *bq, enum biquad_type type, double freq) +{ + + switch (type) { + case BQ_LOWPASS: + biquad_lowpass(bq, freq); + break; + case BQ_HIGHPASS: + biquad_highpass(bq, freq); + break; + } +} diff --git a/spa/plugins/audioconvert/biquad.h b/spa/plugins/audioconvert/biquad.h new file mode 100644 index 0000000..8b7eccc --- /dev/null +++ b/spa/plugins/audioconvert/biquad.h @@ -0,0 +1,45 @@ +/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef BIQUAD_H_ +#define BIQUAD_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* The biquad filter parameters. The transfer function H(z) is (b0 + b1 * z^(-1) + * + b2 * z^(-2)) / (1 + a1 * z^(-1) + a2 * z^(-2)). The previous two inputs + * are stored in x1 and x2, and the previous two outputs are stored in y1 and + * y2. + * + * We use double during the coefficients calculation for better accuracy, but + * float is used during the actual filtering for faster computation. + */ +struct biquad { + float b0, b1, b2; + float a1, a2; +}; + +/* The type of the biquad filters */ +enum biquad_type { + BQ_LOWPASS, + BQ_HIGHPASS, +}; + +/* Initialize a biquad filter parameters from its type and parameters. + * Args: + * bq - The biquad filter we want to set. + * type - The type of the biquad filter. + * frequency - The value should be in the range [0, 1]. It is relative to + * half of the sampling rate. + */ +void biquad_set(struct biquad *bq, enum biquad_type type, double freq); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* BIQUAD_H_ */ diff --git a/spa/plugins/audioconvert/channelmix-ops-c.c b/spa/plugins/audioconvert/channelmix-ops-c.c new file mode 100644 index 0000000..f12f35f --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops-c.c @@ -0,0 +1,533 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "channelmix-ops.h" + +static inline void clear_c(float *d, uint32_t n_samples) +{ + memset(d, 0, n_samples * sizeof(float)); +} + +static inline void copy_c(float *d, const float *s, uint32_t n_samples) +{ + spa_memcpy(d, s, n_samples * sizeof(float)); +} + +static inline void vol_c(float *d, const float *s, float vol, uint32_t n_samples) +{ + uint32_t n; + if (vol == 0.0f) { + clear_c(d, n_samples); + } else if (vol == 1.0f) { + copy_c(d, s, n_samples); + } else { + for (n = 0; n < n_samples; n++) + d[n] = s[n] * vol; + } +} +static inline void conv_c(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples) +{ + uint32_t n, j; + for (n = 0; n < n_samples; n++) { + float sum = 0.0f; + for (j = 0; j < n_c; j++) + sum += s[j][n] * c[j]; + d[n] = sum; + } +} + +static inline void avg_c(float *d, const float *s0, const float *s1, uint32_t n_samples) +{ + uint32_t n; + for (n = 0; n < n_samples; n++) + d[n] = (s0[n] + s1[n]) * 0.5f; +} + +static inline void sub_c(float *d, const float *s0, const float *s1, uint32_t n_samples) +{ + uint32_t n; + for (n = 0; n < n_samples; n++) + d[n] = s0[n] - s1[n]; +} + +void +channelmix_copy_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + for (i = 0; i < n_dst; i++) + vol_c(d[i], s[i], mix->matrix[i][i], n_samples); +} + +#define _M(ch) (1UL << SPA_AUDIO_CHANNEL_ ## ch) + +void +channelmix_f32_n_m_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_COPY)) { + uint32_t copy = SPA_MIN(n_dst, n_src); + for (i = 0; i < copy; i++) + copy_c(d[i], s[i], n_samples); + for (; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + for (i = 0; i < n_dst; i++) { + float *di = d[i]; + float mj[n_src]; + const float *sj[n_src]; + uint32_t n_j = 0; + + for (j = 0; j < n_src; j++) { + if (mix->matrix[i][j] == 0.0f) + continue; + mj[n_j] = mix->matrix[i][j]; + sj[n_j++] = s[j]; + } + if (n_j == 0) { + clear_c(di, n_samples); + } else if (n_j == 1) { + lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples); + } else { + conv_c(di, sj, mj, n_j, n_samples); + lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples); + } + } + } +} + +#define MASK_MONO _M(FC)|_M(MONO)|_M(UNKNOWN) +#define MASK_STEREO _M(FL)|_M(FR)|_M(UNKNOWN) + +void +channelmix_f32_1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][0]; + + vol_c(d[0], s[0], v0, n_samples); + vol_c(d[1], s[0], v1, n_samples); +} + +void +channelmix_f32_2_1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[0][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_c(d[0], n_samples); + } else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_EQUAL)) { + for (n = 0; n < n_samples; n++) + d[0][n] = (s[0][n] + s[1][n]) * v0; + } + else { + for (n = 0; n < n_samples; n++) + d[0][n] = s[0][n] * v0 + s[1][n] * v1; + } +} + +void +channelmix_f32_4_1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[0][1]; + const float v2 = mix->matrix[0][2]; + const float v3 = mix->matrix[0][3]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_c(d[0], n_samples); + } + else if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_EQUAL)) { + for (n = 0; n < n_samples; n++) + d[0][n] = (s[0][n] + s[1][n] + s[2][n] + s[3][n]) * v0; + } + else { + for (n = 0; n < n_samples; n++) + d[0][n] = s[0][n] * v0 + s[1][n] * v1 + + s[2][n] * v2 + s[3][n] * v3; + } +} + +#define MASK_QUAD _M(FL)|_M(FR)|_M(RL)|_M(RR)|_M(UNKNOWN) + +void +channelmix_f32_2_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float v2 = mix->matrix[2][0]; + const float v3 = mix->matrix[3][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + vol_c(d[0], s[0], v0, n_samples); + vol_c(d[1], s[1], v1, n_samples); + if (mix->upmix != CHANNELMIX_UPMIX_PSD) { + vol_c(d[2], s[0], v2, n_samples); + vol_c(d[3], s[1], v3, n_samples); + } else { + sub_c(d[2], s[0], s[1], n_samples); + + delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[3], d[2], -v3, n_samples); + delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[2], d[2], v2, n_samples); + } + } +} + +#define MASK_3_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE) +void +channelmix_f32_2_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float v2 = (mix->matrix[2][0] + mix->matrix[2][1]) * 0.5f; + const float v3 = (mix->matrix[3][0] + mix->matrix[3][1]) * 0.5f; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + if (mix->widen == 0.0f) { + vol_c(d[0], s[0], v0, n_samples); + vol_c(d[1], s[1], v1, n_samples); + avg_c(d[2], s[0], s[1], n_samples); + } else { + for (n = 0; n < n_samples; n++) { + float c = s[0][n] + s[1][n]; + float w = c * mix->widen; + d[0][n] = (s[0][n] - w) * v0; + d[1][n] = (s[1][n] - w) * v1; + d[2][n] = c * 0.5f; + } + } + lr4_process(&mix->lr4[3], d[3], d[2], v3, n_samples); + lr4_process(&mix->lr4[2], d[2], d[2], v2, n_samples); + } +} + +#define MASK_5_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR) +void +channelmix_f32_2_5p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v4 = mix->matrix[4][0]; + const float v5 = mix->matrix[5][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + channelmix_f32_2_3p1_c(mix, dst, src, n_samples); + + if (mix->upmix != CHANNELMIX_UPMIX_PSD) { + vol_c(d[4], s[0], v4, n_samples); + vol_c(d[5], s[1], v5, n_samples); + } else { + sub_c(d[4], s[0], s[1], n_samples); + + delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples); + delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[4], d[4], v4, n_samples); + } + } +} + +void +channelmix_f32_2_7p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v4 = mix->matrix[4][0]; + const float v5 = mix->matrix[5][1]; + const float v6 = mix->matrix[6][0]; + const float v7 = mix->matrix[7][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + channelmix_f32_2_3p1_c(mix, dst, src, n_samples); + + vol_c(d[4], s[0], v4, n_samples); + vol_c(d[5], s[1], v5, n_samples); + + if (mix->upmix != CHANNELMIX_UPMIX_PSD) { + vol_c(d[6], s[0], v6, n_samples); + vol_c(d[7], s[1], v7, n_samples); + } else { + sub_c(d[6], s[0], s[1], n_samples); + + delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples); + delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[6], d[6], v6, n_samples); + } + } +} + +/* FL+FR+FC+LFE -> FL+FR */ +void +channelmix_f32_3p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f; + const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_c(d[0], n_samples); + clear_c(d[1], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + const float ctr = clev * s[2][n] + llev * s[3][n]; + d[0][n] = s[0][n] * v0 + ctr; + d[1][n] = s[1][n] * v1 + ctr; + } + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR */ +void +channelmix_f32_5p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f; + const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f; + const float slev0 = mix->matrix[0][4]; + const float slev1 = mix->matrix[1][5]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_c(d[0], n_samples); + clear_c(d[1], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + const float ctr = clev * s[2][n] + llev * s[3][n]; + d[0][n] = s[0][n] * v0 + ctr + (slev0 * s[4][n]); + d[1][n] = s[1][n] * v1 + ctr + (slev1 * s[5][n]); + } + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR+FC+LFE*/ +void +channelmix_f32_5p1_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float v2 = mix->matrix[2][2]; + const float v3 = mix->matrix[3][3]; + const float v4 = mix->matrix[0][4]; + const float v5 = mix->matrix[1][5]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + d[0][n] = s[0][n] * v0 + s[4][n] * v4; + d[1][n] = s[1][n] * v1 + s[5][n] * v5; + } + vol_c(d[2], s[2], v2, n_samples); + vol_c(d[3], s[3], v3, n_samples); + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR+RL+RR*/ +void +channelmix_f32_5p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v4 = mix->matrix[2][4]; + const float v5 = mix->matrix[3][5]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + channelmix_f32_3p1_2_c(mix, dst, src, n_samples); + + vol_c(d[2], s[4], v4, n_samples); + vol_c(d[3], s[5], v5, n_samples); + } +} + +#define MASK_7_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR) + +/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR */ +void +channelmix_f32_7p1_2_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f; + const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f; + const float slev0 = mix->matrix[0][4]; + const float slev1 = mix->matrix[1][5]; + const float rlev0 = mix->matrix[0][6]; + const float rlev1 = mix->matrix[1][7]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_c(d[0], n_samples); + clear_c(d[1], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + const float ctr = clev * s[2][n] + llev * s[3][n]; + d[0][n] = s[0][n] * v0 + ctr + s[4][n] * slev0 + s[6][n] * rlev0; + d[1][n] = s[1][n] * v1 + ctr + s[5][n] * slev1 + s[7][n] * rlev1; + } + } +} + +/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR+FC+LFE*/ +void +channelmix_f32_7p1_3p1_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float v2 = mix->matrix[2][2]; + const float v3 = mix->matrix[3][3]; + const float v4 = (mix->matrix[0][4] + mix->matrix[0][6]) * 0.5f; + const float v5 = (mix->matrix[1][5] + mix->matrix[1][7]) * 0.5f; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + d[0][n] = s[0][n] * v0 + (s[4][n] + s[6][n]) * v4; + d[1][n] = s[1][n] * v1 + (s[5][n] + s[7][n]) * v5; + } + vol_c(d[2], s[2], v2, n_samples); + vol_c(d[3], s[3], v3, n_samples); + } +} + +/* FL+FR+FC+LFE+SL+SR+RL+RR -> FL+FR+RL+RR*/ +void +channelmix_f32_7p1_4_c(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float clev = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f; + const float llev = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f; + const float slev0 = mix->matrix[2][4]; + const float slev1 = mix->matrix[3][5]; + const float rlev0 = mix->matrix[2][6]; + const float rlev1 = mix->matrix[3][7]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_c(d[i], n_samples); + } + else { + for (n = 0; n < n_samples; n++) { + const float ctr = s[2][n] * clev + s[3][n] * llev; + const float sl = s[4][n] * slev0; + const float sr = s[5][n] * slev1; + d[0][n] = s[0][n] * v0 + ctr + sl; + d[1][n] = s[1][n] * v1 + ctr + sr; + d[2][n] = s[6][n] * rlev0 + sl; + d[3][n] = s[7][n] * rlev1 + sr; + } + } +} diff --git a/spa/plugins/audioconvert/channelmix-ops-sse.c b/spa/plugins/audioconvert/channelmix-ops-sse.c new file mode 100644 index 0000000..8311fb4 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops-sse.c @@ -0,0 +1,522 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "channelmix-ops.h" + +#include + +static inline void clear_sse(float *d, uint32_t n_samples) +{ + memset(d, 0, n_samples * sizeof(float)); +} + +static inline void copy_sse(float *d, const float *s, uint32_t n_samples) +{ + spa_memcpy(d, s, n_samples * sizeof(float)); +} + +static inline void vol_sse(float *d, const float *s, float vol, uint32_t n_samples) +{ + uint32_t n, unrolled; + if (vol == 0.0f) { + clear_sse(d, n_samples); + } else if (vol == 1.0f) { + copy_sse(d, s, n_samples); + } else { + __m128 t[4]; + const __m128 v = _mm_set1_ps(vol); + + if (SPA_IS_ALIGNED(d, 16) && + SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + t[0] = _mm_load_ps(&s[n]); + t[1] = _mm_load_ps(&s[n+4]); + t[2] = _mm_load_ps(&s[n+8]); + t[3] = _mm_load_ps(&s[n+12]); + _mm_store_ps(&d[n], _mm_mul_ps(t[0], v)); + _mm_store_ps(&d[n+4], _mm_mul_ps(t[1], v)); + _mm_store_ps(&d[n+8], _mm_mul_ps(t[2], v)); + _mm_store_ps(&d[n+12], _mm_mul_ps(t[3], v)); + } + for(; n < n_samples; n++) + _mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), v)); + } +} + +static inline void conv_sse(float *d, const float **s, float *c, uint32_t n_c, uint32_t n_samples) +{ + __m128 mi[n_c], sum[2]; + uint32_t n, j, unrolled; + bool aligned = true; + + for (j = 0; j < n_c; j++) { + mi[j] = _mm_set1_ps(c[j]); + aligned &= SPA_IS_ALIGNED(s[j], 16); + } + + if (aligned && SPA_IS_ALIGNED(d, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for (n = 0; n < unrolled; n += 8) { + sum[0] = sum[1] = _mm_setzero_ps(); + for (j = 0; j < n_c; j++) { + sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(_mm_load_ps(&s[j][n + 0]), mi[j])); + sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(_mm_load_ps(&s[j][n + 4]), mi[j])); + } + _mm_store_ps(&d[n + 0], sum[0]); + _mm_store_ps(&d[n + 4], sum[1]); + } + for (; n < n_samples; n++) { + sum[0] = _mm_setzero_ps(); + for (j = 0; j < n_c; j++) + sum[0] = _mm_add_ss(sum[0], _mm_mul_ss(_mm_load_ss(&s[j][n]), mi[j])); + _mm_store_ss(&d[n], sum[0]); + } +} + +static inline void avg_sse(float *d, const float *s0, const float *s1, uint32_t n_samples) +{ + uint32_t n, unrolled; + __m128 half = _mm_set1_ps(0.5f); + + if (SPA_IS_ALIGNED(d, 16) && + SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for (n = 0; n < unrolled; n += 8) { + _mm_store_ps(&d[n + 0], + _mm_mul_ps( + _mm_add_ps( + _mm_load_ps(&s0[n + 0]), + _mm_load_ps(&s1[n + 0])), + half)); + _mm_store_ps(&d[n + 4], + _mm_mul_ps( + _mm_add_ps( + _mm_load_ps(&s0[n + 4]), + _mm_load_ps(&s1[n + 4])), + half)); + } + + for (; n < n_samples; n++) + _mm_store_ss(&d[n], + _mm_mul_ss( + _mm_add_ss( + _mm_load_ss(&s0[n]), + _mm_load_ss(&s1[n])), + half)); +} + +static inline void sub_sse(float *d, const float *s0, const float *s1, uint32_t n_samples) +{ + uint32_t n, unrolled; + + if (SPA_IS_ALIGNED(d, 16) && + SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for (n = 0; n < unrolled; n += 8) { + _mm_store_ps(&d[n + 0], + _mm_sub_ps(_mm_load_ps(&s0[n + 0]), _mm_load_ps(&s1[n + 0]))); + _mm_store_ps(&d[n + 4], + _mm_sub_ps(_mm_load_ps(&s0[n + 4]), _mm_load_ps(&s1[n + 4]))); + } + for (; n < n_samples; n++) + _mm_store_ss(&d[n], + _mm_sub_ss(_mm_load_ss(&s0[n]), _mm_load_ss(&s1[n]))); +} + +void channelmix_copy_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + for (i = 0; i < n_dst; i++) + vol_sse(d[i], s[i], mix->matrix[i][i], n_samples); +} + +void +channelmix_f32_n_m_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + float **d = (float **) dst; + const float **s = (const float **) src; + uint32_t i, j, n_dst = mix->dst_chan, n_src = mix->src_chan; + + for (i = 0; i < n_dst; i++) { + float *di = d[i]; + float mj[n_src]; + const float *sj[n_src]; + uint32_t n_j = 0; + + for (j = 0; j < n_src; j++) { + if (mix->matrix[i][j] == 0.0f) + continue; + mj[n_j] = mix->matrix[i][j]; + sj[n_j++] = s[j]; + } + if (n_j == 0) { + clear_sse(di, n_samples); + } else if (n_j == 1) { + if (mix->lr4[i].active) + lr4_process(&mix->lr4[i], di, sj[0], mj[0], n_samples); + else + vol_sse(di, sj[0], mj[0], n_samples); + } else { + conv_sse(di, sj, mj, n_j, n_samples); + lr4_process(&mix->lr4[i], di, di, 1.0f, n_samples); + } + } +} + +void +channelmix_f32_2_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, unrolled, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v0 = mix->matrix[0][0]; + const float v1 = mix->matrix[1][1]; + const float v2 = (mix->matrix[2][0] + mix->matrix[2][1]) * 0.5f; + const float v3 = (mix->matrix[3][0] + mix->matrix[3][1]) * 0.5f; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_sse(d[i], n_samples); + } + else { + if (mix->widen == 0.0f) { + vol_sse(d[0], s[0], v0, n_samples); + vol_sse(d[1], s[1], v1, n_samples); + avg_sse(d[2], s[0], s[1], n_samples); + } else { + const __m128 mv0 = _mm_set1_ps(mix->matrix[0][0]); + const __m128 mv1 = _mm_set1_ps(mix->matrix[1][1]); + const __m128 mw = _mm_set1_ps(mix->widen); + const __m128 mh = _mm_set1_ps(0.5f); + __m128 t0[1], t1[1], w[1], c[1]; + + if (SPA_IS_ALIGNED(s[0], 16) && + SPA_IS_ALIGNED(s[1], 16) && + SPA_IS_ALIGNED(d[0], 16) && + SPA_IS_ALIGNED(d[1], 16) && + SPA_IS_ALIGNED(d[2], 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + t0[0] = _mm_load_ps(&s[0][n]); + t1[0] = _mm_load_ps(&s[1][n]); + c[0] = _mm_add_ps(t0[0], t1[0]); + w[0] = _mm_mul_ps(c[0], mw); + _mm_store_ps(&d[0][n], _mm_mul_ps(_mm_sub_ps(t0[0], w[0]), mv0)); + _mm_store_ps(&d[1][n], _mm_mul_ps(_mm_sub_ps(t1[0], w[0]), mv1)); + _mm_store_ps(&d[2][n], _mm_mul_ps(c[0], mh)); + } + for (; n < n_samples; n++) { + t0[0] = _mm_load_ss(&s[0][n]); + t1[0] = _mm_load_ss(&s[1][n]); + c[0] = _mm_add_ss(t0[0], t1[0]); + w[0] = _mm_mul_ss(c[0], mw); + _mm_store_ss(&d[0][n], _mm_mul_ss(_mm_sub_ss(t0[0], w[0]), mv0)); + _mm_store_ss(&d[1][n], _mm_mul_ss(_mm_sub_ss(t1[0], w[0]), mv1)); + _mm_store_ss(&d[2][n], _mm_mul_ss(c[0], mh)); + } + } + lr4_process(&mix->lr4[3], d[3], d[2], v3, n_samples); + lr4_process(&mix->lr4[2], d[2], d[2], v2, n_samples); + } +} + +void +channelmix_f32_2_5p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v4 = mix->matrix[4][0]; + const float v5 = mix->matrix[5][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_sse(d[i], n_samples); + } + else { + channelmix_f32_2_3p1_sse(mix, dst, src, n_samples); + + if (mix->upmix != CHANNELMIX_UPMIX_PSD) { + vol_sse(d[4], s[0], v4, n_samples); + vol_sse(d[5], s[1], v5, n_samples); + } else { + sub_sse(d[4], s[0], s[1], n_samples); + + delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[5], d[4], -v5, n_samples); + delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[4], d[4], v4, n_samples); + } + } +} + +void +channelmix_f32_2_7p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + const float v4 = mix->matrix[4][0]; + const float v5 = mix->matrix[5][1]; + const float v6 = mix->matrix[6][0]; + const float v7 = mix->matrix[7][1]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_sse(d[i], n_samples); + } + else { + channelmix_f32_2_3p1_sse(mix, dst, src, n_samples); + + vol_sse(d[4], s[0], v4, n_samples); + vol_sse(d[5], s[1], v5, n_samples); + + if (mix->upmix != CHANNELMIX_UPMIX_PSD) { + vol_sse(d[6], s[0], v6, n_samples); + vol_sse(d[7], s[1], v7, n_samples); + } else { + sub_sse(d[6], s[0], s[1], n_samples); + + delay_convolve_run(mix->buffer[1], &mix->pos[1], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[7], d[6], -v7, n_samples); + delay_convolve_run(mix->buffer[0], &mix->pos[0], BUFFER_SIZE, mix->delay, + mix->taps, mix->n_taps, d[6], d[6], v6, n_samples); + } + } +} +/* FL+FR+FC+LFE -> FL+FR */ +void +channelmix_f32_3p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + float **d = (float **) dst; + const float **s = (const float **) src; + const float m0 = mix->matrix[0][0]; + const float m1 = mix->matrix[1][1]; + const float m2 = (mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f; + const float m3 = (mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f; + + if (m0 == 0.0f && m1 == 0.0f && m2 == 0.0f && m3 == 0.0f) { + clear_sse(d[0], n_samples); + clear_sse(d[1], n_samples); + } + else { + uint32_t n, unrolled; + const __m128 v0 = _mm_set1_ps(m0); + const __m128 v1 = _mm_set1_ps(m1); + const __m128 clev = _mm_set1_ps(m2); + const __m128 llev = _mm_set1_ps(m3); + __m128 ctr; + + if (SPA_IS_ALIGNED(s[0], 16) && + SPA_IS_ALIGNED(s[1], 16) && + SPA_IS_ALIGNED(s[2], 16) && + SPA_IS_ALIGNED(s[3], 16) && + SPA_IS_ALIGNED(d[0], 16) && + SPA_IS_ALIGNED(d[1], 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + ctr = _mm_add_ps( + _mm_mul_ps(_mm_load_ps(&s[2][n]), clev), + _mm_mul_ps(_mm_load_ps(&s[3][n]), llev)); + _mm_store_ps(&d[0][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[0][n]), v0), ctr)); + _mm_store_ps(&d[1][n], _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[1][n]), v1), ctr)); + } + for(; n < n_samples; n++) { + ctr = _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[2][n]), clev), + _mm_mul_ss(_mm_load_ss(&s[3][n]), llev)); + _mm_store_ss(&d[0][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[0][n]), v0), ctr)); + _mm_store_ss(&d[1][n], _mm_add_ss(_mm_mul_ss(_mm_load_ss(&s[1][n]), v1), ctr)); + } + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR */ +void +channelmix_f32_5p1_2_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t n, unrolled; + float **d = (float **) dst; + const float **s = (const float **) src; + const float m00 = mix->matrix[0][0]; + const float m11 = mix->matrix[1][1]; + const __m128 clev = _mm_set1_ps((mix->matrix[0][2] + mix->matrix[1][2]) * 0.5f); + const __m128 llev = _mm_set1_ps((mix->matrix[0][3] + mix->matrix[1][3]) * 0.5f); + const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]); + const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]); + __m128 in, ctr; + + if (SPA_IS_ALIGNED(s[0], 16) && + SPA_IS_ALIGNED(s[1], 16) && + SPA_IS_ALIGNED(s[2], 16) && + SPA_IS_ALIGNED(s[3], 16) && + SPA_IS_ALIGNED(s[4], 16) && + SPA_IS_ALIGNED(s[5], 16) && + SPA_IS_ALIGNED(d[0], 16) && + SPA_IS_ALIGNED(d[1], 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + clear_sse(d[0], n_samples); + clear_sse(d[1], n_samples); + } + else { + const __m128 v0 = _mm_set1_ps(m00); + const __m128 v1 = _mm_set1_ps(m11); + for(n = 0; n < unrolled; n += 4) { + ctr = _mm_add_ps(_mm_mul_ps(_mm_load_ps(&s[2][n]), clev), + _mm_mul_ps(_mm_load_ps(&s[3][n]), llev)); + in = _mm_mul_ps(_mm_load_ps(&s[4][n]), slev0); + in = _mm_add_ps(in, ctr); + in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[0][n]), v0)); + _mm_store_ps(&d[0][n], in); + in = _mm_mul_ps(_mm_load_ps(&s[5][n]), slev1); + in = _mm_add_ps(in, ctr); + in = _mm_add_ps(in, _mm_mul_ps(_mm_load_ps(&s[1][n]), v1)); + _mm_store_ps(&d[1][n], in); + } + for(; n < n_samples; n++) { + ctr = _mm_mul_ss(_mm_load_ss(&s[2][n]), clev); + ctr = _mm_add_ss(ctr, _mm_mul_ss(_mm_load_ss(&s[3][n]), llev)); + in = _mm_mul_ss(_mm_load_ss(&s[4][n]), slev0); + in = _mm_add_ss(in, ctr); + in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[0][n]), v0)); + _mm_store_ss(&d[0][n], in); + in = _mm_mul_ss(_mm_load_ss(&s[5][n]), slev1); + in = _mm_add_ss(in, ctr); + in = _mm_add_ss(in, _mm_mul_ss(_mm_load_ss(&s[1][n]), v1)); + _mm_store_ss(&d[1][n], in); + } + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR+FC+LFE*/ +void +channelmix_f32_5p1_3p1_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n, unrolled, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + + if (SPA_IS_ALIGNED(s[0], 16) && + SPA_IS_ALIGNED(s[1], 16) && + SPA_IS_ALIGNED(s[2], 16) && + SPA_IS_ALIGNED(s[3], 16) && + SPA_IS_ALIGNED(s[4], 16) && + SPA_IS_ALIGNED(s[5], 16) && + SPA_IS_ALIGNED(d[0], 16) && + SPA_IS_ALIGNED(d[1], 16) && + SPA_IS_ALIGNED(d[2], 16) && + SPA_IS_ALIGNED(d[3], 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_sse(d[i], n_samples); + } + else { + const __m128 v0 = _mm_set1_ps(mix->matrix[0][0]); + const __m128 v1 = _mm_set1_ps(mix->matrix[1][1]); + const __m128 slev0 = _mm_set1_ps(mix->matrix[0][4]); + const __m128 slev1 = _mm_set1_ps(mix->matrix[1][5]); + + for(n = 0; n < unrolled; n += 4) { + _mm_store_ps(&d[0][n], _mm_add_ps( + _mm_mul_ps(_mm_load_ps(&s[0][n]), v0), + _mm_mul_ps(_mm_load_ps(&s[4][n]), slev0))); + + _mm_store_ps(&d[1][n], _mm_add_ps( + _mm_mul_ps(_mm_load_ps(&s[1][n]), v1), + _mm_mul_ps(_mm_load_ps(&s[5][n]), slev1))); + } + for(; n < n_samples; n++) { + _mm_store_ss(&d[0][n], _mm_add_ss( + _mm_mul_ss(_mm_load_ss(&s[0][n]), v0), + _mm_mul_ss(_mm_load_ss(&s[4][n]), slev0))); + + _mm_store_ss(&d[1][n], _mm_add_ss( + _mm_mul_ss(_mm_load_ss(&s[1][n]), v1), + _mm_mul_ss(_mm_load_ss(&s[5][n]), slev1))); + } + vol_sse(d[2], s[2], mix->matrix[2][2], n_samples); + vol_sse(d[3], s[3], mix->matrix[3][3], n_samples); + } +} + +/* FL+FR+FC+LFE+SL+SR -> FL+FR+RL+RR*/ +void +channelmix_f32_5p1_4_sse(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **) dst; + const float **s = (const float **) src; + const float v4 = mix->matrix[2][4]; + const float v5 = mix->matrix[3][5]; + + if (SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_ZERO)) { + for (i = 0; i < n_dst; i++) + clear_sse(d[i], n_samples); + } + else { + channelmix_f32_3p1_2_sse(mix, dst, src, n_samples); + + vol_sse(d[2], s[4], v4, n_samples); + vol_sse(d[3], s[5], v5, n_samples); + } +} diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c new file mode 100644 index 0000000..527763b --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -0,0 +1,668 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 _CH(ch) ((SPA_AUDIO_CHANNEL_ ## ch)-3) +#define _MASK(ch) (1ULL << _CH(ch)) +#define FRONT (_MASK(FC)) +#define STEREO (_MASK(FL)|_MASK(FR)) +#define REAR (_MASK(RL)|_MASK(RR)) +#define SIDE (_MASK(SL)|_MASK(SR)) + +static int make_matrix(struct channelmix *mix) +{ + float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS] = {{ 0.0f }}; + uint64_t src_mask = mix->src_mask; + uint64_t dst_mask = mix->dst_mask; + uint32_t src_chan = mix->src_chan; + uint32_t dst_chan = mix->dst_chan; + uint64_t unassigned, keep; + uint32_t i, j, ic, jc, matrix_encoding = MATRIX_NORMAL; + float clev = SQRT1_2; + float slev = SQRT1_2; + float llev = 0.5f; + float maxsum = 0.0f; + bool filter_fc = false, filter_lfe = false; +#define _MATRIX(s,d) matrix[_CH(s)][_CH(d)] + + spa_log_debug(mix->log, "src-mask:%08"PRIx64" dst-mask:%08"PRIx64 + " options:%08x", src_mask, dst_mask, mix->options); + + /* move the MONO mask to FRONT so that the lower bits can be shifted + * away. */ + if ((src_mask & (1Ull << SPA_AUDIO_CHANNEL_MONO)) != 0) { + if (src_chan == 1) + src_mask = 0; + else + src_mask |= (1ULL << SPA_AUDIO_CHANNEL_FC); + } + if ((dst_mask & (1Ull << SPA_AUDIO_CHANNEL_MONO)) != 0) + dst_mask |= (1ULL << SPA_AUDIO_CHANNEL_FC); + + /* shift so that bit 0 is FL */ + src_mask >>= 3; + dst_mask >>= 3; + + /* unknown channels or just 1 channel */ + if (src_mask == 0 || dst_mask == 0) { + if (src_chan == 1) { + /* one FC/MONO src goes everywhere */ + spa_log_debug(mix->log, "distribute FC/MONO (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][0]= 1.0f; + } else if (dst_chan == 1) { + /* one FC/MONO dst get average of everything */ + spa_log_debug(mix->log, "average FC/MONO (%f)", 1.0f / src_chan); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[0][i]= 1.0f / src_chan; + } else { + /* just pair channels */ + spa_log_debug(mix->log, "pairing channels (%f)", 1.0f); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + matrix[i][i]= 1.0f; + } + if (dst_mask & FRONT) + filter_fc = true; + if (dst_mask & _MASK(LFE)) + filter_lfe = true; + src_mask = dst_mask = ~0LU; + goto done; + } else { + spa_log_debug(mix->log, "matching channels"); + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + if ((src_mask & dst_mask & (1ULL << i))) { + spa_log_debug(mix->log, "matched channel %u (%f)", i, 1.0f); + matrix[i][i]= 1.0f; + } + } + } + + unassigned = src_mask & ~dst_mask; + keep = dst_mask & ~src_mask; + + if (!SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_UPMIX)) { + keep = 0; + } else { + if (mix->upmix == CHANNELMIX_UPMIX_NONE) + keep = 0; + keep |= FRONT; + if (mix->lfe_cutoff > 0.0f) + keep |= _MASK(LFE); + else + keep &= ~_MASK(LFE); + } + + spa_log_debug(mix->log, "unassigned downmix %08" PRIx64 " %08" PRIx64, unassigned, keep); + + if (unassigned & FRONT){ + if ((dst_mask & STEREO) == STEREO){ + if(src_mask & STEREO) { + spa_log_debug(mix->log, "assign FC to STEREO (%f)", clev); + _MATRIX(FL,FC) += clev; + _MATRIX(FR,FC) += clev; + } else { + spa_log_debug(mix->log, "assign FC to STEREO (%f)", SQRT1_2); + _MATRIX(FL,FC) += SQRT1_2; + _MATRIX(FR,FC) += SQRT1_2; + } + } else { + spa_log_warn(mix->log, "can't assign FC"); + } + } + + if (unassigned & STEREO){ + if (dst_mask & FRONT) { + spa_log_debug(mix->log, "assign STEREO to FC (%f)", SQRT1_2); + _MATRIX(FC,FL) += SQRT1_2; + _MATRIX(FC,FR) += SQRT1_2; + if (src_mask & FRONT) { + spa_log_debug(mix->log, "assign FC to FC (%f)", clev * SQRT2); + _MATRIX(FC,FC) = clev * SQRT2; + } + keep &= ~FRONT; + } else { + spa_log_warn(mix->log, "can't assign STEREO"); + } + } + + if (unassigned & _MASK(RC)) { + if (dst_mask & REAR){ + spa_log_debug(mix->log, "assign RC to RL+RR (%f)", SQRT1_2); + _MATRIX(RL,RC) += SQRT1_2; + _MATRIX(RR,RC) += SQRT1_2; + } else if (dst_mask & SIDE) { + spa_log_debug(mix->log, "assign RC to SL+SR (%f)", SQRT1_2); + _MATRIX(SL,RC) += SQRT1_2; + _MATRIX(SR,RC) += SQRT1_2; + } else if(dst_mask & STEREO) { + spa_log_debug(mix->log, "assign RC to FL+FR"); + if (matrix_encoding == MATRIX_DOLBY || + matrix_encoding == MATRIX_DPLII) { + if (unassigned & (_MASK(RL)|_MASK(RR))) { + _MATRIX(FL,RC) -= slev * SQRT1_2; + _MATRIX(FR,RC) += slev * SQRT1_2; + } else { + _MATRIX(FL,RC) -= slev; + _MATRIX(FR,RC) += slev; + } + } else { + _MATRIX(FL,RC) += slev * SQRT1_2; + _MATRIX(FR,RC) += slev * SQRT1_2; + } + } else if (dst_mask & FRONT) { + spa_log_debug(mix->log, "assign RC to FC (%f)", slev * SQRT1_2); + _MATRIX(FC,RC) += slev * SQRT1_2; + } else { + spa_log_warn(mix->log, "can't assign RC"); + } + } + + if (unassigned & REAR) { + if (dst_mask & _MASK(RC)) { + spa_log_debug(mix->log, "assign RL+RR to RC"); + _MATRIX(RC,RL) += SQRT1_2; + _MATRIX(RC,RR) += SQRT1_2; + } else if (dst_mask & SIDE) { + spa_log_debug(mix->log, "assign RL+RR to SL+SR"); + if (src_mask & SIDE) { + _MATRIX(SL,RL) += SQRT1_2; + _MATRIX(SR,RR) += SQRT1_2; + } else { + _MATRIX(SL,RL) += 1.0f; + _MATRIX(SR,RR) += 1.0f; + } + keep &= ~SIDE; + } else if (dst_mask & STEREO) { + spa_log_debug(mix->log, "assign RL+RR to FL+FR (%f)", slev); + if (matrix_encoding == MATRIX_DOLBY) { + _MATRIX(FL,RL) -= slev * SQRT1_2; + _MATRIX(FL,RR) -= slev * SQRT1_2; + _MATRIX(FR,RL) += slev * SQRT1_2; + _MATRIX(FR,RR) += slev * SQRT1_2; + } else if (matrix_encoding == MATRIX_DPLII) { + _MATRIX(FL,RL) -= slev * SQRT3_2; + _MATRIX(FL,RR) -= slev * SQRT1_2; + _MATRIX(FR,RL) += slev * SQRT1_2; + _MATRIX(FR,RR) += slev * SQRT3_2; + } else { + _MATRIX(FL,RL) += slev; + _MATRIX(FR,RR) += slev; + } + } else if (dst_mask & FRONT) { + spa_log_debug(mix->log, "assign RL+RR to FC (%f)", + slev * SQRT1_2); + _MATRIX(FC,RL)+= slev * SQRT1_2; + _MATRIX(FC,RR)+= slev * SQRT1_2; + } else { + spa_log_warn(mix->log, "can't assign RL"); + } + } + + if (unassigned & SIDE) { + if (dst_mask & REAR) { + if (src_mask & _MASK(RL)) { + spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", SQRT1_2); + _MATRIX(RL,SL) += SQRT1_2; + _MATRIX(RR,SR) += SQRT1_2; + } else { + spa_log_debug(mix->log, "assign SL+SR to RL+RR (%f)", 1.0f); + _MATRIX(RL,SL) += 1.0f; + _MATRIX(RR,SR) += 1.0f; + } + keep &= ~REAR; + } else if (dst_mask & _MASK(RC)) { + spa_log_debug(mix->log, "assign SL+SR to RC (%f)", SQRT1_2); + _MATRIX(RC,SL)+= SQRT1_2; + _MATRIX(RC,SR)+= SQRT1_2; + } else if (dst_mask & STEREO) { + if (matrix_encoding == MATRIX_DOLBY) { + spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)", + slev * SQRT1_2); + _MATRIX(FL,SL) -= slev * SQRT1_2; + _MATRIX(FL,SR) -= slev * SQRT1_2; + _MATRIX(FR,SL) += slev * SQRT1_2; + _MATRIX(FR,SR) += slev * SQRT1_2; + } else if (matrix_encoding == MATRIX_DPLII) { + spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f / %f)", + slev * SQRT3_2, slev * SQRT1_2); + _MATRIX(FL,SL) -= slev * SQRT3_2; + _MATRIX(FL,SR) -= slev * SQRT1_2; + _MATRIX(FR,SL) += slev * SQRT1_2; + _MATRIX(FR,SR) += slev * SQRT3_2; + } else { + spa_log_debug(mix->log, "assign SL+SR to FL+FR (%f)", slev); + _MATRIX(FL,SL) += slev; + _MATRIX(FR,SR) += slev; + } + } else if (dst_mask & FRONT) { + spa_log_debug(mix->log, "assign SL+SR to FC (%f)", slev * SQRT1_2); + _MATRIX(FC,SL) += slev * SQRT1_2; + _MATRIX(FC,SR) += slev * SQRT1_2; + } else { + spa_log_warn(mix->log, "can't assign SL"); + } + } + + if (unassigned & _MASK(FLC)) { + if (dst_mask & STEREO) { + spa_log_debug(mix->log, "assign FLC+FRC to FL+FR (%f)", 1.0f); + _MATRIX(FL,FLC)+= 1.0f; + _MATRIX(FR,FRC)+= 1.0f; + } else if(dst_mask & FRONT) { + spa_log_debug(mix->log, "assign FLC+FRC to FC (%f)", SQRT1_2); + _MATRIX(FC,FLC)+= SQRT1_2; + _MATRIX(FC,FRC)+= SQRT1_2; + } else { + spa_log_warn(mix->log, "can't assign FLC"); + } + } + if (unassigned & _MASK(LFE) && + SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_MIX_LFE)) { + if (dst_mask & FRONT) { + spa_log_debug(mix->log, "assign LFE to FC (%f)", llev); + _MATRIX(FC,LFE) += llev; + } else if (dst_mask & STEREO) { + spa_log_debug(mix->log, "assign LFE to FL+FR (%f)", + llev * SQRT1_2); + _MATRIX(FL,LFE) += llev * SQRT1_2; + _MATRIX(FR,LFE) += llev * SQRT1_2; + } else { + spa_log_warn(mix->log, "can't assign LFE"); + } + } + + unassigned = dst_mask & ~src_mask & keep; + + spa_log_debug(mix->log, "unassigned upmix %08"PRIx64" lfe:%f", + unassigned, mix->lfe_cutoff); + + if (unassigned & STEREO) { + if ((src_mask & FRONT) == FRONT) { + spa_log_debug(mix->log, "produce STEREO from FC (%f)", clev); + _MATRIX(FL,FC) += clev; + _MATRIX(FR,FC) += clev; + } else { + spa_log_warn(mix->log, "can't produce STEREO"); + } + } + if (unassigned & FRONT) { + if ((src_mask & STEREO) == STEREO) { + spa_log_debug(mix->log, "produce FC from STEREO (%f)", clev); + _MATRIX(FC,FL) += clev; + _MATRIX(FC,FR) += clev; + filter_fc = true; + } else { + spa_log_warn(mix->log, "can't produce FC"); + } + } + if (unassigned & _MASK(LFE)) { + if ((src_mask & STEREO) == STEREO) { + spa_log_debug(mix->log, "produce LFE from STEREO (%f)", llev); + _MATRIX(LFE,FL) += llev; + _MATRIX(LFE,FR) += llev; + filter_lfe = true; + } else if ((src_mask & FRONT) == FRONT) { + spa_log_debug(mix->log, "produce LFE from FC (%f)", llev); + _MATRIX(LFE,FC) += llev; + filter_lfe = true; + } else { + spa_log_warn(mix->log, "can't produce LFE"); + } + } + if (unassigned & SIDE) { + if ((src_mask & REAR) == REAR) { + spa_log_debug(mix->log, "produce SIDE from REAR (%f)", 1.0f); + _MATRIX(SL,RL) += 1.0f; + _MATRIX(SR,RR) += 1.0f; + } else if ((src_mask & STEREO) == STEREO) { + spa_log_debug(mix->log, "produce SIDE from STEREO (%f)", slev); + _MATRIX(SL,FL) += slev; + _MATRIX(SR,FR) += slev; + } else if ((src_mask & FRONT) == FRONT && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_debug(mix->log, "produce SIDE from FC (%f)", clev); + _MATRIX(SL,FC) += clev; + _MATRIX(SR,FC) += clev; + } else { + spa_log_debug(mix->log, "won't produce SIDE"); + } + } + if (unassigned & REAR) { + if ((src_mask & SIDE) == SIDE) { + spa_log_debug(mix->log, "produce REAR from SIDE (%f)", 1.0f); + _MATRIX(RL,SL) += 1.0f; + _MATRIX(RR,SR) += 1.0f; + } else if ((src_mask & STEREO) == STEREO) { + spa_log_debug(mix->log, "produce REAR from STEREO (%f)", slev); + _MATRIX(RL,FL) += slev; + _MATRIX(RR,FR) += slev; + } else if ((src_mask & FRONT) == FRONT && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_debug(mix->log, "produce REAR from FC (%f)", clev); + _MATRIX(RL,FC) += clev; + _MATRIX(RR,FC) += clev; + } else { + spa_log_debug(mix->log, "won't produce SIDE"); + } + } + if (unassigned & _MASK(RC)) { + if ((src_mask & REAR) == REAR) { + spa_log_debug(mix->log, "produce RC from REAR (%f)", 0.5f); + _MATRIX(RC,RL) += 0.5f; + _MATRIX(RC,RR) += 0.5f; + } else if ((src_mask & SIDE) == SIDE) { + spa_log_debug(mix->log, "produce RC from SIDE (%f)", 0.5f); + _MATRIX(RC,SL) += 0.5f; + _MATRIX(RC,SR) += 0.5f; + } else if ((src_mask & STEREO) == STEREO) { + spa_log_debug(mix->log, "produce RC from STEREO (%f)", 0.5f); + _MATRIX(RC,FL) += 0.5f; + _MATRIX(RC,FR) += 0.5f; + } else if ((src_mask & FRONT) == FRONT && + mix->upmix == CHANNELMIX_UPMIX_SIMPLE) { + spa_log_debug(mix->log, "produce RC from FC (%f)", slev); + _MATRIX(RC,FC) += slev; + } else { + spa_log_debug(mix->log, "won't produce RC"); + } + } + +done: + for (jc = 0, ic = 0, i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + float sum = 0.0f; + char str[1024], str2[1024]; + int idx = 0, idx2 = 0; + if ((dst_mask & (1UL << i)) == 0) + continue; + for (jc = 0, j = 0; j < SPA_AUDIO_MAX_CHANNELS; j++) { + if ((src_mask & (1UL << j)) == 0) + continue; + if (ic >= dst_chan || jc >= src_chan) + continue; + + if (ic == 0) + idx2 += snprintf(str2 + idx2, sizeof(str2) - idx2, "%-4.4s ", + src_mask == ~0LU ? "MONO" : + spa_debug_type_find_short_name(spa_type_audio_channel, j + 3)); + + mix->matrix_orig[ic][jc++] = matrix[i][j]; + sum += fabs(matrix[i][j]); + + if (matrix[i][j] == 0.0f) + idx += snprintf(str + idx, sizeof(str) - idx, " "); + else + idx += snprintf(str + idx, sizeof(str) - idx, "%1.3f ", matrix[i][j]); + } + if (idx2 > 0) + spa_log_info(mix->log, " %s", str2); + if (idx > 0) { + spa_log_info(mix->log, "%-4.4s %s %f", + dst_mask == ~0LU ? "MONO" : + spa_debug_type_find_short_name(spa_type_audio_channel, i + 3), + str, sum); + } + + maxsum = SPA_MAX(maxsum, sum); + if (i == _CH(LFE) && mix->lfe_cutoff > 0.0f && filter_lfe) { + spa_log_info(mix->log, "channel %d is LFE cutoff:%f", ic, mix->lfe_cutoff); + lr4_set(&mix->lr4[ic], BQ_LOWPASS, mix->lfe_cutoff / mix->freq); + } else if (i == _CH(FC) && mix->fc_cutoff > 0.0f && filter_fc) { + spa_log_info(mix->log, "channel %d is FC cutoff:%f", ic, mix->fc_cutoff); + lr4_set(&mix->lr4[ic], BQ_LOWPASS, mix->fc_cutoff / mix->freq); + } else { + mix->lr4[ic].active = false; + } + ic++; + } + if (SPA_FLAG_IS_SET(mix->options, CHANNELMIX_OPTION_NORMALIZE) && + maxsum > 1.0f) { + spa_log_debug(mix->log, "normalize %f", maxsum); + for (i = 0; i < dst_chan; i++) + for (j = 0; j < src_chan; j++) + mix->matrix_orig[i][j] /= maxsum; + } + return 0; +} + +static void impl_channelmix_set_volume(struct channelmix *mix, float volume, bool mute, + uint32_t n_channel_volumes, float *channel_volumes) +{ + float volumes[SPA_AUDIO_MAX_CHANNELS]; + float vol = mute ? 0.0f : volume, t; + uint32_t i, j; + uint32_t src_chan = mix->src_chan; + uint32_t dst_chan = mix->dst_chan; + + spa_log_debug(mix->log, "volume:%f mute:%d n_volumes:%d", volume, mute, n_channel_volumes); + + /** apply global volume to channels */ + for (i = 0; i < n_channel_volumes; i++) { + volumes[i] = channel_volumes[i] * vol; + spa_log_debug(mix->log, "%d: %f * %f = %f", i, channel_volumes[i], vol, volumes[i]); + } + + /** apply volumes per channel */ + if (n_channel_volumes == src_chan) { + for (i = 0; i < dst_chan; i++) { + for (j = 0; j < src_chan; j++) { + mix->matrix[i][j] = mix->matrix_orig[i][j] * volumes[j]; + } + } + } else if (n_channel_volumes == dst_chan) { + for (i = 0; i < dst_chan; i++) { + for (j = 0; j < src_chan; j++) { + mix->matrix[i][j] = mix->matrix_orig[i][j] * volumes[i]; + } + } + } else if (n_channel_volumes == 0) { + for (i = 0; i < dst_chan; i++) { + for (j = 0; j < src_chan; j++) { + mix->matrix[i][j] = mix->matrix_orig[i][j] * vol; + } + } + } + + SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_ZERO); + SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_EQUAL); + SPA_FLAG_SET(mix->flags, CHANNELMIX_FLAG_COPY); + + t = 0.0; + for (i = 0; i < dst_chan; i++) { + for (j = 0; j < src_chan; j++) { + float v = mix->matrix[i][j]; + spa_log_debug(mix->log, "%d %d: %f", i, j, v); + if (i == 0 && j == 0) + t = v; + else if (t != v) + SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_EQUAL); + if (v != 0.0) + SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_ZERO); + if ((i == j && v != 1.0f) || + (i != j && v != 0.0f)) + SPA_FLAG_CLEAR(mix->flags, CHANNELMIX_FLAG_COPY); + } + } + SPA_FLAG_UPDATE(mix->flags, CHANNELMIX_FLAG_IDENTITY, + dst_chan == src_chan && SPA_FLAG_IS_SET(mix->flags, CHANNELMIX_FLAG_COPY)); + + spa_log_debug(mix->log, "flags:%08x", mix->flags); +} + +static void impl_channelmix_free(struct channelmix *mix) +{ + mix->process = NULL; +} + +int channelmix_init(struct channelmix *mix) +{ + const struct channelmix_info *info; + + if (mix->src_chan > SPA_AUDIO_MAX_CHANNELS || + mix->dst_chan > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + info = find_channelmix_info(mix->src_chan, mix->src_mask, mix->dst_chan, mix->dst_mask, + mix->cpu_flags); + if (info == NULL) + return -ENOTSUP; + + mix->free = impl_channelmix_free; + mix->process = info->process; + mix->set_volume = impl_channelmix_set_volume; + mix->cpu_flags = info->cpu_flags; + mix->delay = mix->rear_delay * mix->freq / 1000.0f; + mix->func_name = info->name; + + spa_log_debug(mix->log, "selected %s delay:%d options:%08x", info->name, mix->delay, + mix->options); + + if (mix->hilbert_taps > 0) { + mix->n_taps = SPA_CLAMP(mix->hilbert_taps, 15u, 255u) | 1; + blackman_window(mix->taps, mix->n_taps); + hilbert_generate(mix->taps, mix->n_taps); + } else { + mix->n_taps = 1; + mix->taps[0] = 1.0f; + } + return make_matrix(mix); +} diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h new file mode 100644 index 0000000..c134a96 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops.h @@ -0,0 +1,160 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include +#include +#include + +#include "crossover.h" +#include "delay.h" + +#define VOLUME_MIN 0.0f +#define VOLUME_NORM 1.0f + +#define _M(ch) (1UL << SPA_AUDIO_CHANNEL_ ## ch) +#define MASK_MONO _M(FC)|_M(MONO)|_M(UNKNOWN) +#define MASK_STEREO _M(FL)|_M(FR)|_M(UNKNOWN) +#define MASK_QUAD _M(FL)|_M(FR)|_M(RL)|_M(RR)|_M(UNKNOWN) +#define MASK_3_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE) +#define MASK_5_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR) +#define MASK_7_1 _M(FL)|_M(FR)|_M(FC)|_M(LFE)|_M(SL)|_M(SR)|_M(RL)|_M(RR) + +#define BUFFER_SIZE 4096 +#define MAX_TAPS 255 + +struct channelmix { + uint32_t src_chan; + uint32_t dst_chan; + uint64_t src_mask; + uint64_t dst_mask; + uint32_t cpu_flags; +#define CHANNELMIX_OPTION_MIX_LFE (1<<0) /**< mix LFE */ +#define CHANNELMIX_OPTION_NORMALIZE (1<<1) /**< normalize volumes */ +#define CHANNELMIX_OPTION_UPMIX (1<<2) /**< do simple upmixing */ + uint32_t options; +#define CHANNELMIX_UPMIX_NONE 0 /**< disable upmixing */ +#define CHANNELMIX_UPMIX_SIMPLE 1 /**< simple upmixing */ +#define CHANNELMIX_UPMIX_PSD 2 /**< Passive Surround Decoding upmixing */ + uint32_t upmix; + + struct spa_log *log; + const char *func_name; + +#define CHANNELMIX_FLAG_ZERO (1<<0) /**< all zero components */ +#define CHANNELMIX_FLAG_IDENTITY (1<<1) /**< identity matrix */ +#define CHANNELMIX_FLAG_EQUAL (1<<2) /**< all values are equal */ +#define CHANNELMIX_FLAG_COPY (1<<3) /**< 1 on diagonal, can be nxm */ + uint32_t flags; + float matrix_orig[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]; + float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]; + + float freq; /* sample frequency */ + float lfe_cutoff; /* in Hz, 0 is disabled */ + float fc_cutoff; /* in Hz, 0 is disabled */ + float rear_delay; /* in ms, 0 is disabled */ + float widen; /* stereo widen. 0 is disabled */ + uint32_t hilbert_taps; /* to phase shift, 0 disabled */ + struct lr4 lr4[SPA_AUDIO_MAX_CHANNELS]; + + float buffer[2][BUFFER_SIZE]; + uint32_t pos[2]; + uint32_t delay; + float taps[MAX_TAPS]; + uint32_t n_taps; + + void (*process) (struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples); + void (*set_volume) (struct channelmix *mix, float volume, bool mute, + uint32_t n_channel_volumes, float *channel_volumes); + void (*free) (struct channelmix *mix); + + void *data; +}; + +int channelmix_init(struct channelmix *mix); + +static const struct channelmix_upmix_info { + const char *label; + const char *description; + uint32_t upmix; +} channelmix_upmix_info[] = { + [CHANNELMIX_UPMIX_NONE] = { "none", "Disabled", CHANNELMIX_UPMIX_NONE }, + [CHANNELMIX_UPMIX_SIMPLE] = { "simple", "Simple upmixing", CHANNELMIX_UPMIX_SIMPLE }, + [CHANNELMIX_UPMIX_PSD] = { "psd", "Passive Surround Decoding", CHANNELMIX_UPMIX_PSD } +}; + +static inline uint32_t channelmix_upmix_from_label(const char *label) +{ + SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) { + if (spa_streq(i->label, label)) + return i->upmix; + } + return CHANNELMIX_UPMIX_NONE; +} + +#define channelmix_process(mix,...) (mix)->process(mix, __VA_ARGS__) +#define channelmix_set_volume(mix,...) (mix)->set_volume(mix, __VA_ARGS__) +#define channelmix_free(mix) (mix)->free(mix) + +#define DEFINE_FUNCTION(name,arch) \ +void channelmix_##name##_##arch(struct channelmix *mix, \ + void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], \ + uint32_t n_samples); + +#define CHANNELMIX_OPS_MAX_ALIGN 16 + +DEFINE_FUNCTION(copy, c); +DEFINE_FUNCTION(f32_n_m, c); +DEFINE_FUNCTION(f32_1_2, c); +DEFINE_FUNCTION(f32_2_1, c); +DEFINE_FUNCTION(f32_4_1, c); +DEFINE_FUNCTION(f32_2_4, c); +DEFINE_FUNCTION(f32_2_3p1, c); +DEFINE_FUNCTION(f32_2_5p1, c); +DEFINE_FUNCTION(f32_2_7p1, c); +DEFINE_FUNCTION(f32_3p1_2, c); +DEFINE_FUNCTION(f32_5p1_2, c); +DEFINE_FUNCTION(f32_5p1_3p1, c); +DEFINE_FUNCTION(f32_5p1_4, c); +DEFINE_FUNCTION(f32_7p1_2, c); +DEFINE_FUNCTION(f32_7p1_3p1, c); +DEFINE_FUNCTION(f32_7p1_4, c); + +#if defined (HAVE_SSE) +DEFINE_FUNCTION(copy, sse); +DEFINE_FUNCTION(f32_n_m, sse); +DEFINE_FUNCTION(f32_2_3p1, sse); +DEFINE_FUNCTION(f32_2_5p1, sse); +DEFINE_FUNCTION(f32_2_7p1, sse); +DEFINE_FUNCTION(f32_3p1_2, sse); +DEFINE_FUNCTION(f32_5p1_2, sse); +DEFINE_FUNCTION(f32_5p1_3p1, sse); +DEFINE_FUNCTION(f32_5p1_4, sse); +DEFINE_FUNCTION(f32_7p1_4, sse); +#endif + +#undef DEFINE_FUNCTION diff --git a/spa/plugins/audioconvert/crossover.c b/spa/plugins/audioconvert/crossover.c new file mode 100644 index 0000000..7575833 --- /dev/null +++ b/spa/plugins/audioconvert/crossover.c @@ -0,0 +1,70 @@ +/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include +#include + +#include "crossover.h" + +void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq) +{ + biquad_set(&lr4->bq, type, freq); + lr4->x1 = 0; + lr4->x2 = 0; + lr4->y1 = 0; + lr4->y2 = 0; + lr4->z1 = 0; + lr4->z2 = 0; + lr4->active = true; +} + +void lr4_process(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples) +{ + float lx1 = lr4->x1; + float lx2 = lr4->x2; + float ly1 = lr4->y1; + float ly2 = lr4->y2; + float lz1 = lr4->z1; + float lz2 = lr4->z2; + float lb0 = lr4->bq.b0; + float lb1 = lr4->bq.b1; + float lb2 = lr4->bq.b2; + float la1 = lr4->bq.a1; + float la2 = lr4->bq.a2; + int i; + + if (vol == 0.0f) { + memset(dst, 0, samples * sizeof(float)); + return; + } else if (!lr4->active) { + if (src != dst || vol != 1.0f) { + for (i = 0; i < samples; i++) + dst[i] = src[i] * vol; + } + return; + } + + for (i = 0; i < samples; i++) { + float x, y, z; + x = src[i]; + y = lb0*x + lb1*lx1 + lb2*lx2 - la1*ly1 - la2*ly2; + z = lb0*y + lb1*ly1 + lb2*ly2 - la1*lz1 - la2*lz2; + lx2 = lx1; + lx1 = x; + ly2 = ly1; + ly1 = y; + lz2 = lz1; + lz1 = z; + dst[i] = z * vol; + } +#define F(x) (-FLT_MIN < (x) && (x) < FLT_MIN ? 0.0f : (x)) + lr4->x1 = F(lx1); + lr4->x2 = F(lx2); + lr4->y1 = F(ly1); + lr4->y2 = F(ly2); + lr4->z1 = F(lz1); + lr4->z2 = F(lz2); +#undef F +} diff --git a/spa/plugins/audioconvert/crossover.h b/spa/plugins/audioconvert/crossover.h new file mode 100644 index 0000000..b6f458b --- /dev/null +++ b/spa/plugins/audioconvert/crossover.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef CROSSOVER_H_ +#define CROSSOVER_H_ + +#include + +#include "biquad.h" +/* An LR4 filter is two biquads with the same parameters connected in series: + * + * x -- [BIQUAD] -- y -- [BIQUAD] -- z + * + * Both biquad filter has the same parameter b[012] and a[12], + * The variable [xyz][12] keep the history values. + */ +struct lr4 { + struct biquad bq; + float x1, x2; + float y1, y2; + float z1, z2; + bool active; +}; + +void lr4_set(struct lr4 *lr4, enum biquad_type type, float freq); + +void lr4_process(struct lr4 *lr4, float *dst, const float *src, const float vol, int samples); + +#endif /* CROSSOVER_H_ */ diff --git a/spa/plugins/audioconvert/delay.h b/spa/plugins/audioconvert/delay.h new file mode 100644 index 0000000..16e189f --- /dev/null +++ b/spa/plugins/audioconvert/delay.h @@ -0,0 +1,72 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef DELAY_H +#define DELAY_H + +#ifdef __cplusplus +extern "C" { +#endif + +static inline void delay_run(float *buffer, uint32_t *pos, + uint32_t n_buffer, uint32_t delay, + float *dst, const float *src, const float vol, uint32_t n_samples) +{ + uint32_t i; + uint32_t p = *pos; + + for (i = 0; i < n_samples; i++) { + buffer[p] = src[i]; + dst[i] = buffer[(p - delay) & (n_buffer-1)] * vol; + p = (p + 1) & (n_buffer-1); + } + *pos = p; +} + +static inline void delay_convolve_run(float *buffer, uint32_t *pos, + uint32_t n_buffer, uint32_t delay, + const float *taps, uint32_t n_taps, + float *dst, const float *src, const float vol, uint32_t n_samples) +{ + uint32_t i, j; + uint32_t p = *pos; + + for (i = 0; i < n_samples; i++) { + float sum = 0.0f; + + buffer[p] = src[i]; + for (j = 0; j < n_taps; j++) + sum += (taps[j] * buffer[((p - delay) - j) & (n_buffer-1)]); + dst[i] = sum * vol; + + p = (p + 1) & (n_buffer-1); + } + *pos = p; +} + +#ifdef __cplusplus +} +#endif + +#endif /* DELAY_H */ diff --git a/spa/plugins/audioconvert/fmt-ops-avx2.c b/spa/plugins/audioconvert/fmt-ops-avx2.c new file mode 100644 index 0000000..087f027 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-avx2.c @@ -0,0 +1,1043 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "fmt-ops.h" + +#include +// GCC: workaround for missing AVX intrinsic: "_mm256_setr_m128()" +// (see https://stackoverflow.com/questions/32630458/setting-m256i-to-the-value-of-two-m128i-values) +#ifndef _mm256_setr_m128i +# ifndef _mm256_set_m128i +# define _mm256_set_m128i(v0, v1) _mm256_insertf128_si256(_mm256_castsi128_si256(v1), (v0), 1) +# endif +# define _mm256_setr_m128i(v0, v1) _mm256_set_m128i((v1), (v0)) +#endif + +#define _MM_CLAMP_PS(r,min,max) \ + _mm_min_ps(_mm_max_ps(r, min), max) + +#define _MM256_CLAMP_PS(r,min,max) \ + _mm256_min_ps(_mm256_max_ps(r, min), max) + +#define _MM_CLAMP_SS(r,min,max) \ + _mm_min_ss(_mm_max_ss(r, min), max) + +static void +conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int16_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m256i in = _mm256_setzero_si256(); + __m256 out, factor = _mm256_set1_ps(1.0f / S16_SCALE); + + if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 32))) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in = _mm256_insert_epi16(in, s[0*n_channels], 1); + in = _mm256_insert_epi16(in, s[1*n_channels], 3); + in = _mm256_insert_epi16(in, s[2*n_channels], 5); + in = _mm256_insert_epi16(in, s[3*n_channels], 7); + in = _mm256_insert_epi16(in, s[4*n_channels], 9); + in = _mm256_insert_epi16(in, s[5*n_channels], 11); + in = _mm256_insert_epi16(in, s[6*n_channels], 13); + in = _mm256_insert_epi16(in, s[7*n_channels], 15); + + in = _mm256_srai_epi32(in, 16); + out = _mm256_cvtepi32_ps(in); + out = _mm256_mul_ps(out, factor); + _mm256_store_ps(&d0[n], out); + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE); + out = _mm_cvtsi32_ss(factor, s[0]); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s16_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int16_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_s16_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +void +conv_s16_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int16_t *s = src[0]; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m256i in[2], t[4]; + __m256 out[4], factor = _mm256_set1_ps(1.0f / S16_SCALE); + + if (SPA_IS_ALIGNED(s, 32) && + SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + in[0] = _mm256_load_si256((__m256i*)(s + 0)); + in[1] = _mm256_load_si256((__m256i*)(s + 16)); + + t[0] = _mm256_slli_epi32(in[0], 16); + t[0] = _mm256_srai_epi32(t[0], 16); + out[0] = _mm256_cvtepi32_ps(t[0]); + out[0] = _mm256_mul_ps(out[0], factor); + + t[1] = _mm256_srai_epi32(in[0], 16); + out[1] = _mm256_cvtepi32_ps(t[1]); + out[1] = _mm256_mul_ps(out[1], factor); + + t[2] = _mm256_slli_epi32(in[1], 16); + t[2] = _mm256_srai_epi32(t[2], 16); + out[2] = _mm256_cvtepi32_ps(t[2]); + out[2] = _mm256_mul_ps(out[2], factor); + + t[3] = _mm256_srai_epi32(in[1], 16); + out[3] = _mm256_cvtepi32_ps(t[3]); + out[3] = _mm256_mul_ps(out[3], factor); + + _mm256_store_ps(&d0[n + 0], out[0]); + _mm256_store_ps(&d1[n + 0], out[1]); + _mm256_store_ps(&d0[n + 8], out[2]); + _mm256_store_ps(&d1[n + 8], out[3]); + + s += 32; + } + for(; n < n_samples; n++) { + __m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE); + out[0] = _mm_cvtsi32_ss(factor, s[0]); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_cvtsi32_ss(factor, s[1]); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += 2; + } +} + +void +conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int8_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in; + __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE); + __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels); + + if (SPA_IS_ALIGNED(d0, 16) && n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_i32gather_epi32((int*)s, mask1, 1); + in = _mm_slli_epi32(in, 8); + in = _mm_srai_epi32(in, 8); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 12 * n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, s24_to_s32(*(int24_t*)s)); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += 3 * n_channels; + } +} + +static void +conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int8_t *s = src; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m128i in[2]; + __m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE); + __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1); + in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1); + + in[0] = _mm_slli_epi32(in[0], 8); + in[1] = _mm_slli_epi32(in[1], 8); + + in[0] = _mm_srai_epi32(in[0], 8); + in[1] = _mm_srai_epi32(in[1], 8); + + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + + s += 12 * n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0))); + out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1))); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += 3 * n_channels; + } +} +static void +conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int8_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128i in[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE); + __m128i mask1 = _mm_setr_epi32(0*n_channels, 3*n_channels, 6*n_channels, 9*n_channels); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16) && + n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_i32gather_epi32((int*)&s[0], mask1, 1); + in[1] = _mm_i32gather_epi32((int*)&s[3], mask1, 1); + in[2] = _mm_i32gather_epi32((int*)&s[6], mask1, 1); + in[3] = _mm_i32gather_epi32((int*)&s[9], mask1, 1); + + in[0] = _mm_slli_epi32(in[0], 8); + in[1] = _mm_slli_epi32(in[1], 8); + in[2] = _mm_slli_epi32(in[2], 8); + in[3] = _mm_slli_epi32(in[3], 8); + + in[0] = _mm_srai_epi32(in[0], 8); + in[1] = _mm_srai_epi32(in[1], 8); + in[2] = _mm_srai_epi32(in[2], 8); + in[3] = _mm_srai_epi32(in[3], 8); + + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + out[2] = _mm_cvtepi32_ps(in[2]); + out[3] = _mm_cvtepi32_ps(in[3]); + + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + out[2] = _mm_mul_ps(out[2], factor); + out[3] = _mm_mul_ps(out[3], factor); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + + s += 12 * n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0))); + out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1))); + out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+2))); + out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+3))); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + out[2] = _mm_mul_ss(out[2], factor); + out[3] = _mm_mul_ss(out[3], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + _mm_store_ss(&d2[n], out[2]); + _mm_store_ss(&d3[n], out[3]); + s += 3 * n_channels; + } +} + +void +conv_s24_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int8_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_s24_to_f32d_4s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s24_to_f32d_2s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s24_to_f32d_1s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); +} + + +void +conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m256i in[4]; + __m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + if (SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32) && + SPA_IS_ALIGNED(d2, 32) && + SPA_IS_ALIGNED(d3, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4); + in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4); + in[2] = _mm256_i32gather_epi32((int*)&s[2], mask1, 4); + in[3] = _mm256_i32gather_epi32((int*)&s[3], mask1, 4); + + in[0] = _mm256_srai_epi32(in[0], 8); + in[1] = _mm256_srai_epi32(in[1], 8); + in[2] = _mm256_srai_epi32(in[2], 8); + in[3] = _mm256_srai_epi32(in[3], 8); + + out[0] = _mm256_cvtepi32_ps(in[0]); + out[1] = _mm256_cvtepi32_ps(in[1]); + out[2] = _mm256_cvtepi32_ps(in[2]); + out[3] = _mm256_cvtepi32_ps(in[3]); + + out[0] = _mm256_mul_ps(out[0], factor); + out[1] = _mm256_mul_ps(out[1], factor); + out[2] = _mm256_mul_ps(out[2], factor); + out[3] = _mm256_mul_ps(out[3], factor); + + _mm256_store_ps(&d0[n], out[0]); + _mm256_store_ps(&d1[n], out[1]); + _mm256_store_ps(&d2[n], out[2]); + _mm256_store_ps(&d3[n], out[3]); + + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE); + out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8); + out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8); + out[2] = _mm_cvtsi32_ss(factor, s[2] >> 8); + out[3] = _mm_cvtsi32_ss(factor, s[3] >> 8); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + out[2] = _mm_mul_ss(out[2], factor); + out[3] = _mm_mul_ss(out[3], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + _mm_store_ss(&d2[n], out[2]); + _mm_store_ss(&d3[n], out[3]); + s += n_channels; + } +} + +void +conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m256i in[4]; + __m256 out[4], factor = _mm256_set1_ps(1.0f / S24_SCALE); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + if (SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_i32gather_epi32((int*)&s[0], mask1, 4); + in[1] = _mm256_i32gather_epi32((int*)&s[1], mask1, 4); + + in[0] = _mm256_srai_epi32(in[0], 8); + in[1] = _mm256_srai_epi32(in[1], 8); + + out[0] = _mm256_cvtepi32_ps(in[0]); + out[1] = _mm256_cvtepi32_ps(in[1]); + + out[0] = _mm256_mul_ps(out[0], factor); + out[1] = _mm256_mul_ps(out[1], factor); + + _mm256_store_ps(&d0[n], out[0]); + _mm256_store_ps(&d1[n], out[1]); + + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE); + out[0] = _mm_cvtsi32_ss(factor, s[0] >> 8); + out[1] = _mm_cvtsi32_ss(factor, s[1] >> 8); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += n_channels; + } +} + +void +conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m256i in[2]; + __m256 out[2], factor = _mm256_set1_ps(1.0f / S24_SCALE); + __m256i mask1 = _mm256_setr_epi32(0*n_channels, 1*n_channels, 2*n_channels, 3*n_channels, + 4*n_channels, 5*n_channels, 6*n_channels, 7*n_channels); + + if (SPA_IS_ALIGNED(d0, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + in[0] = _mm256_i32gather_epi32(&s[0*n_channels], mask1, 4); + in[1] = _mm256_i32gather_epi32(&s[8*n_channels], mask1, 4); + + in[0] = _mm256_srai_epi32(in[0], 8); + in[1] = _mm256_srai_epi32(in[1], 8); + + out[0] = _mm256_cvtepi32_ps(in[0]); + out[1] = _mm256_cvtepi32_ps(in[1]); + + out[0] = _mm256_mul_ps(out[0], factor); + out[1] = _mm256_mul_ps(out[1], factor); + + _mm256_store_ps(&d0[n+0], out[0]); + _mm256_store_ps(&d0[n+8], out[1]); + + s += 16*n_channels; + } + for(; n < n_samples; n++) { + __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE); + out = _mm_cvtsi32_ss(factor, s[0] >> 8); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s32_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int32_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_s32_to_f32d_4s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s32_to_f32d_2s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s32_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +static void +conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[1]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_max = _mm_set1_ps(S24_MAX); + __m128 int_min = _mm_set1_ps(S24_MIN); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + d[0*n_channels] = _mm_cvtsi128_si32(out[0]); + d[1*n_channels] = _mm_cvtsi128_si32(out[1]); + d[2*n_channels] = _mm_cvtsi128_si32(out[2]); + d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_load_ss(&s0[n]); + in[0] = _mm_mul_ss(in[0], scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]) << 8; + d += n_channels; + } +} + +static void +conv_f32d_to_s32_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int32_t *d = dst; + uint32_t n, unrolled; + __m256 in[2]; + __m256i out[2], t[2]; + __m256 scale = _mm256_set1_ps(S24_SCALE); + __m256 int_min = _mm256_set1_ps(S24_MIN); + __m256 int_max = _mm256_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), scale); + + in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max); + in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max); + + out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + out[0] = _mm256_slli_epi32(out[0], 8); + out[1] = _mm256_slli_epi32(out[1], 8); + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + +#ifdef __x86_64__ + *((int64_t*)(d + 0*n_channels)) = _mm256_extract_epi64(t[0], 0); + *((int64_t*)(d + 1*n_channels)) = _mm256_extract_epi64(t[0], 1); + *((int64_t*)(d + 2*n_channels)) = _mm256_extract_epi64(t[1], 0); + *((int64_t*)(d + 3*n_channels)) = _mm256_extract_epi64(t[1], 1); + *((int64_t*)(d + 4*n_channels)) = _mm256_extract_epi64(t[0], 2); + *((int64_t*)(d + 5*n_channels)) = _mm256_extract_epi64(t[0], 3); + *((int64_t*)(d + 6*n_channels)) = _mm256_extract_epi64(t[1], 2); + *((int64_t*)(d + 7*n_channels)) = _mm256_extract_epi64(t[1], 3); +#else + _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)_mm256_extracti128_si256(t[0], 0)); + _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)_mm256_extracti128_si256(t[0], 0)); + _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)_mm256_extracti128_si256(t[1], 0)); + _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)_mm256_extracti128_si256(t[1], 0)); + _mm_storel_pi((__m64*)(d + 4*n_channels), (__m128)_mm256_extracti128_si256(t[0], 1)); + _mm_storeh_pi((__m64*)(d + 5*n_channels), (__m128)_mm256_extracti128_si256(t[0], 1)); + _mm_storel_pi((__m64*)(d + 6*n_channels), (__m128)_mm256_extracti128_si256(t[1], 1)); + _mm_storeh_pi((__m64*)(d + 7*n_channels), (__m128)_mm256_extracti128_si256(t[1], 1)); +#endif + d += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 in[2]; + __m128i out[2]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + + in[0] = _mm_unpacklo_ps(in[0], in[1]); + + in[0] = _mm_mul_ps(in[0], scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + _mm_storel_epi64((__m128i*)d, out[0]); + d += n_channels; + } +} + +static void +conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + int32_t *d = dst; + uint32_t n, unrolled; + __m256 in[4]; + __m256i out[4], t[4]; + __m256 scale = _mm256_set1_ps(S24_SCALE); + __m256 int_min = _mm256_set1_ps(S24_MIN); + __m256 int_max = _mm256_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32) && + SPA_IS_ALIGNED(s2, 32) && + SPA_IS_ALIGNED(s3, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), scale); + in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), scale); + in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), scale); + + in[0] = _MM256_CLAMP_PS(in[0], int_min, int_max); + in[1] = _MM256_CLAMP_PS(in[1], int_min, int_max); + in[2] = _MM256_CLAMP_PS(in[2], int_min, int_max); + in[3] = _MM256_CLAMP_PS(in[3], int_min, int_max); + + out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + out[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */ + out[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */ + out[0] = _mm256_slli_epi32(out[0], 8); + out[1] = _mm256_slli_epi32(out[1], 8); + out[2] = _mm256_slli_epi32(out[2], 8); + out[3] = _mm256_slli_epi32(out[3], 8); + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + t[2] = _mm256_unpacklo_epi32(out[2], out[3]); /* c0 d0 c1 d1 c4 d4 c5 d5 */ + t[3] = _mm256_unpackhi_epi32(out[2], out[3]); /* c2 d2 c3 d3 c6 d6 c7 d7 */ + + out[0] = _mm256_unpacklo_epi64(t[0], t[2]); /* a0 b0 c0 d0 a4 b4 c4 d4 */ + out[1] = _mm256_unpackhi_epi64(t[0], t[2]); /* a1 b1 c1 d1 a5 b5 c5 d5 */ + out[2] = _mm256_unpacklo_epi64(t[1], t[3]); /* a2 b2 c2 d2 a6 b6 c6 d6 */ + out[3] = _mm256_unpackhi_epi64(t[1], t[3]); /* a3 b3 c3 d3 a7 b7 c7 d7 */ + + _mm_storeu_si128((__m128i*)(d + 0*n_channels), _mm256_extracti128_si256(out[0], 0)); + _mm_storeu_si128((__m128i*)(d + 1*n_channels), _mm256_extracti128_si256(out[1], 0)); + _mm_storeu_si128((__m128i*)(d + 2*n_channels), _mm256_extracti128_si256(out[2], 0)); + _mm_storeu_si128((__m128i*)(d + 3*n_channels), _mm256_extracti128_si256(out[3], 0)); + _mm_storeu_si128((__m128i*)(d + 4*n_channels), _mm256_extracti128_si256(out[0], 1)); + _mm_storeu_si128((__m128i*)(d + 5*n_channels), _mm256_extracti128_si256(out[1], 1)); + _mm_storeu_si128((__m128i*)(d + 6*n_channels), _mm256_extracti128_si256(out[2], 1)); + _mm_storeu_si128((__m128i*)(d + 7*n_channels), _mm256_extracti128_si256(out[3], 1)); + d += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 in[4]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + in[2] = _mm_load_ss(&s2[n]); + in[3] = _mm_load_ss(&s3[n]); + + in[0] = _mm_unpacklo_ps(in[0], in[2]); + in[1] = _mm_unpacklo_ps(in[1], in[3]); + in[0] = _mm_unpacklo_ps(in[0], in[1]); + + in[0] = _mm_mul_ps(in[0], scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + _mm_storeu_si128((__m128i*)d, out[0]); + d += n_channels; + } +} + +void +conv_f32d_to_s32_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int32_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_f32d_to_s32_4s_avx2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_f32d_to_s32_2s_avx2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_f32d_to_s32_1s_avx2(conv, &d[i], &src[i], n_channels, n_samples); +} + +static void +conv_f32d_to_s16_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0]; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + + d[0*n_channels] = _mm_extract_epi16(out[0], 0); + d[1*n_channels] = _mm_extract_epi16(out[0], 1); + d[2*n_channels] = _mm_extract_epi16(out[0], 2); + d[3*n_channels] = _mm_extract_epi16(out[0], 3); + d[4*n_channels] = _mm_extract_epi16(out[0], 4); + d[5*n_channels] = _mm_extract_epi16(out[0], 5); + d[6*n_channels] = _mm_extract_epi16(out[0], 6); + d[7*n_channels] = _mm_extract_epi16(out[0], 7); + d += 8*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]); + d += n_channels; + } +} + +static void +conv_f32d_to_s16_2s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int16_t *d = dst; + uint32_t n, unrolled; + __m256 in[2]; + __m256i out[4], t[2]; + __m256 int_scale = _mm256_set1_ps(S16_SCALE); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale); + + out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + + out[0] = _mm256_packs_epi32(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + + *((int32_t*)(d + 0*n_channels)) = _mm256_extract_epi32(out[0],0); + *((int32_t*)(d + 1*n_channels)) = _mm256_extract_epi32(out[0],1); + *((int32_t*)(d + 2*n_channels)) = _mm256_extract_epi32(out[0],2); + *((int32_t*)(d + 3*n_channels)) = _mm256_extract_epi32(out[0],3); + *((int32_t*)(d + 4*n_channels)) = _mm256_extract_epi32(out[0],4); + *((int32_t*)(d + 5*n_channels)) = _mm256_extract_epi32(out[0],5); + *((int32_t*)(d + 6*n_channels)) = _mm256_extract_epi32(out[0],6); + *((int32_t*)(d + 7*n_channels)) = _mm256_extract_epi32(out[0],7); + + d += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 in[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d += n_channels; + } +} + +static void +conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + int16_t *d = dst; + uint32_t n, unrolled; + __m256 in[4]; + __m256i out[4], t[4]; + __m256 int_scale = _mm256_set1_ps(S16_SCALE); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32) && + SPA_IS_ALIGNED(s2, 32) && + SPA_IS_ALIGNED(s3, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale); + in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale); + in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale); + + t[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + t[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + t[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */ + t[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */ + + t[0] = _mm256_packs_epi32(t[0], t[2]); /* a0 a1 a2 a3 c0 c1 c2 c3 a4 a5 a6 a7 c4 c5 c6 c7 */ + t[1] = _mm256_packs_epi32(t[1], t[3]); /* b0 b1 b2 b3 d0 d1 d2 d3 b4 b5 b6 b7 d4 d5 d6 d7 */ + + out[0] = _mm256_unpacklo_epi16(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + out[1] = _mm256_unpackhi_epi16(t[0], t[1]); /* c0 d0 c1 d1 c2 d2 c3 d3 c4 d4 c5 d5 c6 d6 c7 d7 */ + + out[2] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 c0 d0 a1 b1 c1 d1 a4 b4 c4 d4 a5 b5 c5 d5 */ + out[3] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 c2 d2 a3 b3 c3 d3 a6 b6 c6 d6 a7 b7 c7 d7 */ + +#ifdef __x86_64__ + *(int64_t*)(d + 0*n_channels) = _mm256_extract_epi64(out[2], 0); /* a0 b0 c0 d0 */ + *(int64_t*)(d + 1*n_channels) = _mm256_extract_epi64(out[2], 1); /* a1 b1 c1 d1 */ + *(int64_t*)(d + 2*n_channels) = _mm256_extract_epi64(out[3], 0); /* a2 b2 c2 d2 */ + *(int64_t*)(d + 3*n_channels) = _mm256_extract_epi64(out[3], 1); /* a3 b3 c3 d3 */ + *(int64_t*)(d + 4*n_channels) = _mm256_extract_epi64(out[2], 2); /* a4 b4 c4 d4 */ + *(int64_t*)(d + 5*n_channels) = _mm256_extract_epi64(out[2], 3); /* a5 b5 c5 d5 */ + *(int64_t*)(d + 6*n_channels) = _mm256_extract_epi64(out[3], 2); /* a6 b6 c6 d6 */ + *(int64_t*)(d + 7*n_channels) = _mm256_extract_epi64(out[3], 3); /* a7 b7 c7 d7 */ +#else + _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)_mm256_extracti128_si256(out[2], 0)); + _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)_mm256_extracti128_si256(out[2], 0)); + _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)_mm256_extracti128_si256(out[3], 0)); + _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)_mm256_extracti128_si256(out[3], 0)); + _mm_storel_pi((__m64*)(d + 4*n_channels), (__m128)_mm256_extracti128_si256(out[2], 1)); + _mm_storeh_pi((__m64*)(d + 5*n_channels), (__m128)_mm256_extracti128_si256(out[2], 1)); + _mm_storel_pi((__m64*)(d + 6*n_channels), (__m128)_mm256_extracti128_si256(out[3], 1)); + _mm_storeh_pi((__m64*)(d + 7*n_channels), (__m128)_mm256_extracti128_si256(out[3], 1)); +#endif + + d += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 in[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale); + in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + in[2] = _MM_CLAMP_SS(in[2], int_min, int_max); + in[3] = _MM_CLAMP_SS(in[3], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d[2] = _mm_cvtss_si32(in[2]); + d[3] = _mm_cvtss_si32(in[3]); + d += n_channels; + } +} + +void +conv_f32d_to_s16_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int16_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_f32d_to_s16_4s_avx2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_f32d_to_s16_2s_avx2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_f32d_to_s16_1s_avx2(conv, &d[i], &src[i], n_channels, n_samples); +} + +void +conv_f32d_to_s16_4_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + int16_t *d = dst[0]; + uint32_t n, unrolled; + __m256 in[4]; + __m256i out[4], t[4]; + __m256 int_scale = _mm256_set1_ps(S16_SCALE); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32) && + SPA_IS_ALIGNED(s2, 32) && + SPA_IS_ALIGNED(s3, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n]), int_scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n]), int_scale); + in[2] = _mm256_mul_ps(_mm256_load_ps(&s2[n]), int_scale); + in[3] = _mm256_mul_ps(_mm256_load_ps(&s3[n]), int_scale); + + t[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + t[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + t[2] = _mm256_cvtps_epi32(in[2]); /* c0 c1 c2 c3 c4 c5 c6 c7 */ + t[3] = _mm256_cvtps_epi32(in[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */ + + t[0] = _mm256_packs_epi32(t[0], t[2]); /* a0 a1 a2 a3 c0 c1 c2 c3 a4 a5 a6 a7 c4 c5 c6 c7 */ + t[1] = _mm256_packs_epi32(t[1], t[3]); /* b0 b1 b2 b3 d0 d1 d2 d3 b4 b5 b6 b7 d4 d5 d6 d7 */ + + out[0] = _mm256_unpacklo_epi16(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + out[1] = _mm256_unpackhi_epi16(t[0], t[1]); /* c0 d0 c1 d1 c2 d2 c3 d3 c4 d4 c5 d5 c6 d6 c7 d7 */ + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 c0 d0 a1 b1 c1 d1 a4 b4 c4 d4 a5 b5 c5 d5 */ + t[2] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 c2 d2 a3 b3 c3 d3 a6 b6 c6 d6 a7 b7 c7 d7 */ + + out[0] = _mm256_inserti128_si256(t[0], _mm256_extracti128_si256(t[2], 0), 1); + out[2] = _mm256_inserti128_si256(t[2], _mm256_extracti128_si256(t[0], 1), 0); + + _mm256_store_si256((__m256i*)(d+0), out[0]); + _mm256_store_si256((__m256i*)(d+16), out[2]); + d += 32; + } + for(; n < n_samples; n++) { + __m128 in[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale); + in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + in[2] = _MM_CLAMP_SS(in[2], int_min, int_max); + in[3] = _MM_CLAMP_SS(in[3], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d[2] = _mm_cvtss_si32(in[2]); + d[3] = _mm_cvtss_si32(in[3]); + d += 4; + } +} +void +conv_f32d_to_s16_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int16_t *d = dst[0]; + uint32_t n, unrolled; + __m256 in[4]; + __m256i out[4], t[4]; + __m256 int_scale = _mm256_set1_ps(S16_SCALE); + + if (SPA_IS_ALIGNED(s0, 32) && + SPA_IS_ALIGNED(s1, 32)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + in[0] = _mm256_mul_ps(_mm256_load_ps(&s0[n+0]), int_scale); + in[1] = _mm256_mul_ps(_mm256_load_ps(&s1[n+0]), int_scale); + in[2] = _mm256_mul_ps(_mm256_load_ps(&s0[n+8]), int_scale); + in[3] = _mm256_mul_ps(_mm256_load_ps(&s1[n+8]), int_scale); + + out[0] = _mm256_cvtps_epi32(in[0]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_cvtps_epi32(in[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + out[2] = _mm256_cvtps_epi32(in[2]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[3] = _mm256_cvtps_epi32(in[3]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + + t[0] = _mm256_unpacklo_epi32(out[0], out[1]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[1] = _mm256_unpackhi_epi32(out[0], out[1]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + t[2] = _mm256_unpacklo_epi32(out[2], out[3]); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + t[3] = _mm256_unpackhi_epi32(out[2], out[3]); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + + out[0] = _mm256_packs_epi32(t[0], t[1]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + out[1] = _mm256_packs_epi32(t[2], t[3]); /* a0 b0 a1 b1 a2 b2 a3 b3 a4 b4 a5 b5 a6 b6 a7 b7 */ + + _mm256_store_si256((__m256i*)(d+0), out[0]); + _mm256_store_si256((__m256i*)(d+16), out[1]); + + d += 32; + } + for(; n < n_samples; n++) { + __m128 in[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d += 2; + } +} diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c new file mode 100644 index 0000000..92ecb5a --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-c.c @@ -0,0 +1,433 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..e6c8b84 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-neon.c @@ -0,0 +1,487 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 { v2.s }[0], [%[s1]], #4\n" + " subs %[remainder], %[remainder], #1\n" + " sqadd v0.4s, v0.4s, v2.4s\n" + " sqadd v1.4s, v1.4s, v2.4s\n" + " fcvtns v0.4s, v0.4s\n" + " fcvtns v1.4s, v1.4s\n" + " sqxtn v0.4h, v0.4s\n" + " sqxtn v1.4h, v1.4s\n" + " st2 { v0.h, v1.h }[0], [%[d]], %[stride]\n" + " bne 3b\n" + "4:" + : [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples), + [remainder] "+r" (remainder) + : [stride] "r" (stride), + [scale] "r" (15 << 23) + : "cc", "v0", "v1"); +#else + float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE); + float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE); + + asm volatile( + " veor q2, q2, q2\n" + " cmp %[n_samples], #0\n" + " beq 2f\n" + "1:" + " vld1.32 { q0 }, [%[s0]]!\n" + " vld1.32 { q1 }, [%[s1]]!\n" + " subs %[n_samples], %[n_samples], #4\n" + " vcgt.f32 q3, q0, q2\n" + " vcgt.f32 q4, q0, q2\n" + " vbsl q3, %q[pos], %q[neg]\n" + " vbsl q4, %q[pos], %q[neg]\n" + " vadd.f32 q0, q0, q3\n" + " vadd.f32 q1, q1, q4\n" + " vcvt.s32.f32 q0, q0, #15\n" + " vcvt.s32.f32 q1, q1, #15\n" + " vqmovn.s32 d0, q0\n" + " vqmovn.s32 d1, q1\n" + " vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n" + " vst2.16 { d0[1], d1[1] }, [%[d]], %[stride]\n" + " vst2.16 { d0[2], d1[2] }, [%[d]], %[stride]\n" + " vst2.16 { d0[3], d1[3] }, [%[d]], %[stride]\n" + " bne 1b\n" + "2:" + " cmp %[remainder], #0\n" + " beq 4f\n" + "3:" + " vld1.32 { d0[0] }, [%[s0]]!\n" + " vld1.32 { d2[0] }, [%[s1]]!\n" + " subs %[remainder], %[remainder], #1\n" + " vcgt.f32 q3, q0, q2\n" + " vcgt.f32 q4, q0, q2\n" + " vbsl q3, %q[pos], %q[neg]\n" + " vbsl q4, %q[pos], %q[neg]\n" + " vadd.f32 q0, q0, q3\n" + " vadd.f32 q1, q1, q4\n" + " vcvt.s32.f32 q0, q0, #15\n" + " vcvt.s32.f32 q1, q1, #15\n" + " vqmovn.s32 d0, q0\n" + " vqmovn.s32 d1, q1\n" + " vst2.16 { d0[0], d1[0] }, [%[d]], %[stride]\n" + " bne 3b\n" + "4:" + : [d] "+r" (d), [s0] "+r" (s0), [s1] "+r" (s1), [n_samples] "+r" (n_samples), + [remainder] "+r" (remainder) + : [stride] "r" (stride), + [pos]"w"(pos), + [neg]"w"(neg) + : "cc", "q0", "q1", "q2", "q3", "q4"); +#endif +} + +static void +conv_f32d_to_s16_1s_neon(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src[0]; + int16_t *d = dst; + uint32_t stride = n_channels << 1; + uint32_t remainder = n_samples & 3; + n_samples -= remainder; + +#ifdef __aarch64__ + asm volatile( + " dup v2.4s, %w[scale]\n" + " cmp %[n_samples], #0\n" + " beq 2f\n" + "1:" + " ld1 { v0.4s }, [%[s]], #16\n" + " subs %[n_samples], %[n_samples], #4\n" + " sqadd v0.4s, v0.4s, v2.4s\n" + " fcvtns v0.4s, v0.4s\n" + " sqxtn v0.4h, v0.4s\n" + " st1 { v0.h }[0], [%[d]], %[stride]\n" + " st1 { v0.h }[1], [%[d]], %[stride]\n" + " st1 { v0.h }[2], [%[d]], %[stride]\n" + " st1 { v0.h }[3], [%[d]], %[stride]\n" + " bne 1b\n" + "2:" + " cmp %[remainder], #0\n" + " beq 4f\n" + "3:" + " ld1 { v0.s }[0], [%[s]], #4\n" + " subs %[remainder], %[remainder], #1\n" + " sqadd v0.4s, v0.4s, v2.4s\n" + " fcvtns v0.4s, v0.4s\n" + " sqxtn v0.4h, v0.4s\n" + " st1 { v0.h }[0], [%[d]], %[stride]\n" + " bne 3b\n" + "4:" + : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples), + [remainder] "+r" (remainder) + : [stride] "r" (stride), + [scale] "r" (15 << 23) + : "cc", "v0"); +#else + float32x4_t pos = vdupq_n_f32(0.4999999f / S16_SCALE); + float32x4_t neg = vdupq_n_f32(-0.4999999f / S16_SCALE); + + asm volatile( + " veor q1, q1, q1\n" + " cmp %[n_samples], #0\n" + " beq 2f\n" + "1:" + " vld1.32 { q0 }, [%[s]]!\n" + " subs %[n_samples], %[n_samples], #4\n" + " vcgt.f32 q2, q0, q1\n" + " vbsl q2, %q[pos], %q[neg]\n" + " vadd.f32 q0, q0, q2\n" + " vcvt.s32.f32 q0, q0, #15\n" + " vqmovn.s32 d0, q0\n" + " vst1.16 { d0[0] }, [%[d]], %[stride]\n" + " vst1.16 { d0[1] }, [%[d]], %[stride]\n" + " vst1.16 { d0[2] }, [%[d]], %[stride]\n" + " vst1.16 { d0[3] }, [%[d]], %[stride]\n" + " bne 1b\n" + "2:" + " cmp %[remainder], #0\n" + " beq 4f\n" + "3:" + " vld1.32 { d0[0] }, [%[s]]!\n" + " subs %[remainder], %[remainder], #1\n" + " vcgt.f32 q2, q0, q1\n" + " vbsl q2, %q[pos], %q[neg]\n" + " vadd.f32 q0, q0, q2\n" + " vcvt.s32.f32 q0, q0, #15\n" + " vqmovn.s32 d0, q0\n" + " vst1.16 { d0[0] }, [%[d]], %[stride]\n" + " bne 3b\n" + "4:" + : [d] "+r" (d), [s] "+r" (s), [n_samples] "+r" (n_samples), + [remainder] "+r" (remainder) + : [stride] "r" (stride), + [pos]"w"(pos), + [neg]"w"(neg) + : "cc", "q0", "q1", "q2"); +#endif +} + +void +conv_f32d_to_s16_neon(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int16_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 1 < n_channels; i += 2) + conv_f32d_to_s16_2s_neon(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_f32d_to_s16_1s_neon(conv, &d[i], &src[i], n_channels, n_samples); +} diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c new file mode 100644 index 0000000..4e2fce7 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-sse2.c @@ -0,0 +1,1438 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "fmt-ops.h" + +#include + +#define _MM_CLAMP_PS(r,min,max) \ + _mm_min_ps(_mm_max_ps(r, min), max) + +#define _MM_CLAMP_SS(r,min,max) \ + _mm_min_ss(_mm_max_ss(r, min), max) + +static void +conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int16_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in = _mm_setzero_si128(); + __m128 out, factor = _mm_set1_ps(1.0f / S16_SCALE); + + if (SPA_LIKELY(SPA_IS_ALIGNED(d0, 16))) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_insert_epi16(in, s[0*n_channels], 1); + in = _mm_insert_epi16(in, s[1*n_channels], 3); + in = _mm_insert_epi16(in, s[2*n_channels], 5); + in = _mm_insert_epi16(in, s[3*n_channels], 7); + in = _mm_srai_epi32(in, 16); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 4*n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, s[0]); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s16_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int16_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_s16_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +void +conv_s16_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int16_t *s = src[0]; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m128i in[2], t[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S16_SCALE); + + if (SPA_IS_ALIGNED(s, 16) && + SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_load_si128((__m128i*)(s + 0)); + in[1] = _mm_load_si128((__m128i*)(s + 8)); + + t[0] = _mm_slli_epi32(in[0], 16); + t[0] = _mm_srai_epi32(t[0], 16); + out[0] = _mm_cvtepi32_ps(t[0]); + out[0] = _mm_mul_ps(out[0], factor); + + t[1] = _mm_srai_epi32(in[0], 16); + out[1] = _mm_cvtepi32_ps(t[1]); + out[1] = _mm_mul_ps(out[1], factor); + + t[2] = _mm_slli_epi32(in[1], 16); + t[2] = _mm_srai_epi32(t[2], 16); + out[2] = _mm_cvtepi32_ps(t[2]); + out[2] = _mm_mul_ps(out[2], factor); + + t[3] = _mm_srai_epi32(in[1], 16); + out[3] = _mm_cvtepi32_ps(t[3]); + out[3] = _mm_mul_ps(out[3], factor); + + _mm_store_ps(&d0[n + 0], out[0]); + _mm_store_ps(&d1[n + 0], out[1]); + _mm_store_ps(&d0[n + 4], out[2]); + _mm_store_ps(&d1[n + 4], out[3]); + + s += 16; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s[0]); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_cvtsi32_ss(factor, s[1]); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += 2; + } +} + +void +conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int24_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in; + __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE); + + if (SPA_IS_ALIGNED(d0, 16) && n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_setr_epi32( + *((uint32_t*)&s[0 * n_channels]), + *((uint32_t*)&s[1 * n_channels]), + *((uint32_t*)&s[2 * n_channels]), + *((uint32_t*)&s[3 * n_channels])); + in = _mm_slli_epi32(in, 8); + in = _mm_srai_epi32(in, 8); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +static void +conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int24_t *s = src; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m128i in[2]; + __m128 out[2], factor = _mm_set1_ps(1.0f / S24_SCALE); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_setr_epi32( + *((uint32_t*)&s[0 + 0*n_channels]), + *((uint32_t*)&s[0 + 1*n_channels]), + *((uint32_t*)&s[0 + 2*n_channels]), + *((uint32_t*)&s[0 + 3*n_channels])); + in[1] = _mm_setr_epi32( + *((uint32_t*)&s[1 + 0*n_channels]), + *((uint32_t*)&s[1 + 1*n_channels]), + *((uint32_t*)&s[1 + 2*n_channels]), + *((uint32_t*)&s[1 + 3*n_channels])); + + in[0] = _mm_slli_epi32(in[0], 8); + in[1] = _mm_slli_epi32(in[1], 8); + + in[0] = _mm_srai_epi32(in[0], 8); + in[1] = _mm_srai_epi32(in[1], 8); + + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); + out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1))); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += n_channels; + } +} +static void +conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int24_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128i in[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16) && + n_samples > 0) { + unrolled = n_samples & ~3; + if ((n_samples & 3) == 0) + unrolled -= 4; + } + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_setr_epi32( + *((uint32_t*)&s[0 + 0*n_channels]), + *((uint32_t*)&s[0 + 1*n_channels]), + *((uint32_t*)&s[0 + 2*n_channels]), + *((uint32_t*)&s[0 + 3*n_channels])); + in[1] = _mm_setr_epi32( + *((uint32_t*)&s[1 + 0*n_channels]), + *((uint32_t*)&s[1 + 1*n_channels]), + *((uint32_t*)&s[1 + 2*n_channels]), + *((uint32_t*)&s[1 + 3*n_channels])); + in[2] = _mm_setr_epi32( + *((uint32_t*)&s[2 + 0*n_channels]), + *((uint32_t*)&s[2 + 1*n_channels]), + *((uint32_t*)&s[2 + 2*n_channels]), + *((uint32_t*)&s[2 + 3*n_channels])); + in[3] = _mm_setr_epi32( + *((uint32_t*)&s[3 + 0*n_channels]), + *((uint32_t*)&s[3 + 1*n_channels]), + *((uint32_t*)&s[3 + 2*n_channels]), + *((uint32_t*)&s[3 + 3*n_channels])); + + in[0] = _mm_slli_epi32(in[0], 8); + in[1] = _mm_slli_epi32(in[1], 8); + in[2] = _mm_slli_epi32(in[2], 8); + in[3] = _mm_slli_epi32(in[3], 8); + + in[0] = _mm_srai_epi32(in[0], 8); + in[1] = _mm_srai_epi32(in[1], 8); + in[2] = _mm_srai_epi32(in[2], 8); + in[3] = _mm_srai_epi32(in[3], 8); + + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + out[2] = _mm_cvtepi32_ps(in[2]); + out[3] = _mm_cvtepi32_ps(in[3]); + + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + out[2] = _mm_mul_ps(out[2], factor); + out[3] = _mm_mul_ps(out[3], factor); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); + out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1))); + out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2))); + out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3))); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + out[2] = _mm_mul_ss(out[2], factor); + out[3] = _mm_mul_ss(out[3], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + _mm_store_ss(&d2[n], out[2]); + _mm_store_ss(&d3[n], out[3]); + s += n_channels; + } +} + +void +conv_s24_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int8_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_s24_to_f32d_4s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s24_to_f32d_2s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s24_to_f32d_1s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); +} + + +void +conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in; + __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE); + + if (SPA_IS_ALIGNED(d0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_setr_epi32(s[0*n_channels], + s[1*n_channels], + s[2*n_channels], + s[3*n_channels]); + in = _mm_srai_epi32(in, 8); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 4*n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, s[0]>>8); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +void +conv_s32_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int32_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i < n_channels; i++) + conv_s32_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +static void +conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[1]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + d[0*n_channels] = _mm_cvtsi128_si32(out[0]); + d[1*n_channels] = _mm_cvtsi128_si32(out[1]); + d[2*n_channels] = _mm_cvtsi128_si32(out[2]); + d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_load_ss(&s0[n]); + in[0] = _mm_mul_ss(in[0], scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]) << 8; + d += n_channels; + } +} + +static void +conv_f32d_to_s32_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2], t[2]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), scale); + + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_PS(in[1], int_min, int_max); + + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_slli_epi32(out[0], 8); + out[1] = _mm_slli_epi32(out[1], 8); + + t[0] = _mm_unpacklo_epi32(out[0], out[1]); + t[1] = _mm_unpackhi_epi32(out[0], out[1]); + + _mm_storel_pd((double*)(d + 0*n_channels), (__m128d)t[0]); + _mm_storeh_pd((double*)(d + 1*n_channels), (__m128d)t[0]); + _mm_storel_pd((double*)(d + 2*n_channels), (__m128d)t[1]); + _mm_storeh_pd((double*)(d + 3*n_channels), (__m128d)t[1]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + + in[0] = _mm_unpacklo_ps(in[0], in[1]); + + in[0] = _mm_mul_ps(in[0], scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + _mm_storel_epi64((__m128i*)d, out[0]); + d += n_channels; + } +} + +static void +conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[4]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16) && + SPA_IS_ALIGNED(s2, 16) && + SPA_IS_ALIGNED(s3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), scale); + in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), scale); + in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), scale); + + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_PS(in[1], int_min, int_max); + in[2] = _MM_CLAMP_PS(in[2], int_min, int_max); + in[3] = _MM_CLAMP_PS(in[3], int_min, int_max); + + _MM_TRANSPOSE4_PS(in[0], in[1], in[2], in[3]); + + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[2] = _mm_cvtps_epi32(in[2]); + out[3] = _mm_cvtps_epi32(in[3]); + out[0] = _mm_slli_epi32(out[0], 8); + out[1] = _mm_slli_epi32(out[1], 8); + out[2] = _mm_slli_epi32(out[2], 8); + out[3] = _mm_slli_epi32(out[3], 8); + + _mm_storeu_si128((__m128i*)(d + 0*n_channels), out[0]); + _mm_storeu_si128((__m128i*)(d + 1*n_channels), out[1]); + _mm_storeu_si128((__m128i*)(d + 2*n_channels), out[2]); + _mm_storeu_si128((__m128i*)(d + 3*n_channels), out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_load_ss(&s0[n]); + in[1] = _mm_load_ss(&s1[n]); + in[2] = _mm_load_ss(&s2[n]); + in[3] = _mm_load_ss(&s3[n]); + + in[0] = _mm_unpacklo_ps(in[0], in[2]); + in[1] = _mm_unpacklo_ps(in[1], in[3]); + in[0] = _mm_unpacklo_ps(in[0], in[1]); + + in[0] = _mm_mul_ps(in[0], scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + _mm_storeu_si128((__m128i*)d, out[0]); + d += n_channels; + } +} + +void +conv_f32d_to_s32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int32_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_f32d_to_s32_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_f32d_to_s32_2s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_f32d_to_s32_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples); +} + +/* 32 bit xorshift PRNG, see https://en.wikipedia.org/wiki/Xorshift */ +#define _MM_XORSHIFT_EPI32(r) \ +({ \ + __m128i i, t; \ + i = _mm_load_si128((__m128i*)r); \ + t = _mm_slli_epi32(i, 13); \ + i = _mm_xor_si128(i, t); \ + t = _mm_srli_epi32(i, 17); \ + i = _mm_xor_si128(i, t); \ + t = _mm_slli_epi32(i, 5); \ + i = _mm_xor_si128(i, t); \ + _mm_store_si128((__m128i*)r, i); \ + i; \ +}) + +void conv_noise_rect_sse2(struct convert *conv, float *noise, uint32_t n_samples) +{ + uint32_t n; + const uint32_t *r = conv->random; + __m128 scale = _mm_set1_ps(conv->scale); + __m128i in[1]; + __m128 out[1]; + + for (n = 0; n < n_samples; n += 4) { + in[0] = _MM_XORSHIFT_EPI32(r); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], scale); + _mm_store_ps(&noise[n], out[0]); + } +} + +void conv_noise_tri_sse2(struct convert *conv, float *noise, uint32_t n_samples) +{ + uint32_t n; + const uint32_t *r = conv->random; + __m128 scale = _mm_set1_ps(conv->scale); + __m128i in[1]; + __m128 out[1]; + + for (n = 0; n < n_samples; n += 4) { + in[0] = _mm_sub_epi32( _MM_XORSHIFT_EPI32(r), _MM_XORSHIFT_EPI32(r)); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], scale); + _mm_store_ps(&noise[n], out[0]); + } +} + +void conv_noise_tri_hf_sse2(struct convert *conv, float *noise, uint32_t n_samples) +{ + uint32_t n; + int32_t *p = conv->prev; + const uint32_t *r = conv->random; + __m128 scale = _mm_set1_ps(conv->scale); + __m128i in[1], old[1], new[1]; + __m128 out[1]; + + old[0] = _mm_load_si128((__m128i*)p); + for (n = 0; n < n_samples; n += 4) { + new[0] = _MM_XORSHIFT_EPI32(r); + in[0] = _mm_sub_epi32(old[0], new[0]); + old[0] = new[0]; + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], scale); + _mm_store_ps(&noise[n], out[0]); + } + _mm_store_si128((__m128i*)p, old[0]); +} + +static void +conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + float *noise, uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + int32_t *d = dst; + uint32_t n, unrolled; + __m128 in[1]; + __m128i out[4]; + __m128 scale = _mm_set1_ps(S24_SCALE); + __m128 int_min = _mm_set1_ps(S24_MIN); + __m128 int_max = _mm_set1_ps(S24_MAX); + + if (SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), scale); + in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n])); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + out[0] = _mm_cvtps_epi32(in[0]); + out[0] = _mm_slli_epi32(out[0], 8); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + d[0*n_channels] = _mm_cvtsi128_si32(out[0]); + d[1*n_channels] = _mm_cvtsi128_si32(out[1]); + d[2*n_channels] = _mm_cvtsi128_si32(out[2]); + d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_load_ss(&s[n]); + in[0] = _mm_mul_ss(in[0], scale); + in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n])); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]) << 8; + d += n_channels; + } +} + +void +conv_f32d_to_s32_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int32_t *d = dst[0]; + uint32_t i, k, chunk, n_channels = conv->n_channels; + float *noise = conv->noise; + + convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size)); + + for(i = 0; i < n_channels; i++) { + const float *s = src[i]; + for(k = 0; k < n_samples; k += chunk) { + chunk = SPA_MIN(n_samples - k, conv->noise_size); + conv_f32d_to_s32_1s_noise_sse2(conv, &d[i + k*n_channels], + &s[k], noise, n_channels, chunk); + } + } +} + +static void +conv_interleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s0 = src[0]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128i out[4]; + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_load_si128((__m128i*)&s0[n]); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + d[0*n_channels] = _mm_cvtsi128_si32(out[0]); + d[1*n_channels] = _mm_cvtsi128_si32(out[1]); + d[2*n_channels] = _mm_cvtsi128_si32(out[2]); + d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + *d = s0[n]; + d += n_channels; + } +} +static void +conv_interleave_32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + float *d = dst; + uint32_t n, unrolled; + __m128 out[4]; + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16) && + SPA_IS_ALIGNED(s2, 16) && + SPA_IS_ALIGNED(s3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_load_ps(&s0[n]); + out[1] = _mm_load_ps(&s1[n]); + out[2] = _mm_load_ps(&s2[n]); + out[3] = _mm_load_ps(&s3[n]); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + _mm_storeu_ps((d + 0*n_channels), out[0]); + _mm_storeu_ps((d + 1*n_channels), out[1]); + _mm_storeu_ps((d + 2*n_channels), out[2]); + _mm_storeu_ps((d + 3*n_channels), out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); + _mm_storeu_ps(d, out[0]); + d += n_channels; + } +} + +void +conv_32d_to_32_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int32_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_interleave_32_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_interleave_32_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples); +} + +#define _MM_BSWAP_EPI32(x) \ +({ \ + __m128i a = _mm_or_si128( \ + _mm_slli_epi16(x, 8), \ + _mm_srli_epi16(x, 8)); \ + a = _mm_shufflelo_epi16(a, _MM_SHUFFLE(2, 3, 0, 1)); \ + a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(2, 3, 0, 1)); \ +}) + +static void +conv_interleave_32s_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s0 = src[0]; + int32_t *d = dst; + uint32_t n, unrolled; + __m128i out[4]; + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_load_si128((__m128i*)&s0[n]); + out[0] = _MM_BSWAP_EPI32(out[0]); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + d[0*n_channels] = _mm_cvtsi128_si32(out[0]); + d[1*n_channels] = _mm_cvtsi128_si32(out[1]); + d[2*n_channels] = _mm_cvtsi128_si32(out[2]); + d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + *d = bswap_32(s0[n]); + d += n_channels; + } +} +static void +conv_interleave_32s_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + float *d = dst; + uint32_t n, unrolled; + __m128 out[4]; + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16) && + SPA_IS_ALIGNED(s2, 16) && + SPA_IS_ALIGNED(s3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_load_ps(&s0[n]); + out[1] = _mm_load_ps(&s1[n]); + out[2] = _mm_load_ps(&s2[n]); + out[3] = _mm_load_ps(&s3[n]); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]); + out[1] = (__m128) _MM_BSWAP_EPI32((__m128i)out[1]); + out[2] = (__m128) _MM_BSWAP_EPI32((__m128i)out[2]); + out[3] = (__m128) _MM_BSWAP_EPI32((__m128i)out[3]); + + _mm_storeu_ps(&d[0*n_channels], out[0]); + _mm_storeu_ps(&d[1*n_channels], out[1]); + _mm_storeu_ps(&d[2*n_channels], out[2]); + _mm_storeu_ps(&d[3*n_channels], out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); + out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]); + _mm_storeu_ps(d, out[0]); + d += n_channels; + } +} + +void +conv_32d_to_32s_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int32_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_interleave_32s_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_interleave_32s_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples); +} + +static void +conv_deinterleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128 out; + + if (SPA_IS_ALIGNED(d0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out = _mm_setr_ps(s[0*n_channels], + s[1*n_channels], + s[2*n_channels], + s[3*n_channels]); + _mm_store_ps(&d0[n], out); + s += 4*n_channels; + } + for(; n < n_samples; n++) { + d0[n] = *s; + s += n_channels; + } +} + +static void +conv_deinterleave_32_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128 out[4]; + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_loadu_ps(&s[0 * n_channels]); + out[1] = _mm_loadu_ps(&s[1 * n_channels]); + out[2] = _mm_loadu_ps(&s[2 * n_channels]); + out[3] = _mm_loadu_ps(&s[3 * n_channels]); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + d0[n] = s[0]; + d1[n] = s[1]; + d2[n] = s[2]; + d3[n] = s[3]; + s += n_channels; + } +} + +void +conv_32_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_deinterleave_32_4s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_deinterleave_32_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +static void +conv_deinterleave_32s_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128 out; + + if (SPA_IS_ALIGNED(d0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out = _mm_setr_ps(s[0*n_channels], + s[1*n_channels], + s[2*n_channels], + s[3*n_channels]); + out = (__m128) _MM_BSWAP_EPI32((__m128i)out); + _mm_store_ps(&d0[n], out); + s += 4*n_channels; + } + for(; n < n_samples; n++) { + d0[n] = bswap_32(*s); + s += n_channels; + } +} + +static void +conv_deinterleave_32s_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const float *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128 out[4]; + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + out[0] = _mm_loadu_ps(&s[0 * n_channels]); + out[1] = _mm_loadu_ps(&s[1 * n_channels]); + out[2] = _mm_loadu_ps(&s[2 * n_channels]); + out[3] = _mm_loadu_ps(&s[3 * n_channels]); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + out[0] = (__m128) _MM_BSWAP_EPI32((__m128i)out[0]); + out[1] = (__m128) _MM_BSWAP_EPI32((__m128i)out[1]); + out[2] = (__m128) _MM_BSWAP_EPI32((__m128i)out[2]); + out[3] = (__m128) _MM_BSWAP_EPI32((__m128i)out[3]); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + d0[n] = bswap_32(s[0]); + d1[n] = bswap_32(s[1]); + d2[n] = bswap_32(s[2]); + d3[n] = bswap_32(s[3]); + s += n_channels; + } +} + +void +conv_32s_to_32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_deinterleave_32s_4s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_deinterleave_32s_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); +} + +static void +conv_f32_to_s16_1_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + uint32_t n_samples) +{ + const float *s = src; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + _mm_storeu_si128((__m128i*)(d+0), out[0]); + d += 8; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d++ = _mm_cvtss_si32(in[0]); + } +} + +void +conv_f32d_to_s16d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + uint32_t i, n_channels = conv->n_channels; + for(i = 0; i < n_channels; i++) + conv_f32_to_s16_1_sse2(conv, dst[i], src[i], n_samples); +} + +void +conv_f32_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + conv_f32_to_s16_1_sse2(conv, dst[0], src[0], n_samples * conv->n_channels); +} + +static void +conv_f32d_to_s16_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0]; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + + d[0*n_channels] = _mm_extract_epi16(out[0], 0); + d[1*n_channels] = _mm_extract_epi16(out[0], 1); + d[2*n_channels] = _mm_extract_epi16(out[0], 2); + d[3*n_channels] = _mm_extract_epi16(out[0], 3); + d[4*n_channels] = _mm_extract_epi16(out[0], 4); + d[5*n_channels] = _mm_extract_epi16(out[0], 5); + d[6*n_channels] = _mm_extract_epi16(out[0], 6); + d[7*n_channels] = _mm_extract_epi16(out[0], 7); + d += 8*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]); + d += n_channels; + } +} + +static void +conv_f32d_to_s16_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[4], t[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale); + + t[0] = _mm_cvtps_epi32(in[0]); + t[1] = _mm_cvtps_epi32(in[1]); + + t[0] = _mm_packs_epi32(t[0], t[0]); + t[1] = _mm_packs_epi32(t[1], t[1]); + + out[0] = _mm_unpacklo_epi16(t[0], t[1]); + out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); + out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); + out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); + + *((int32_t*)(d + 0*n_channels)) = _mm_cvtsi128_si32(out[0]); + *((int32_t*)(d + 1*n_channels)) = _mm_cvtsi128_si32(out[1]); + *((int32_t*)(d + 2*n_channels)) = _mm_cvtsi128_si32(out[2]); + *((int32_t*)(d + 3*n_channels)) = _mm_cvtsi128_si32(out[3]); + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d += n_channels; + } +} + +static void +conv_f32d_to_s16_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src[], + uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1], *s2 = src[2], *s3 = src[3]; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[4]; + __m128i out[4], t[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16) && + SPA_IS_ALIGNED(s2, 16) && + SPA_IS_ALIGNED(s3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n]), int_scale); + in[2] = _mm_mul_ps(_mm_load_ps(&s2[n]), int_scale); + in[3] = _mm_mul_ps(_mm_load_ps(&s3[n]), int_scale); + + t[0] = _mm_cvtps_epi32(in[0]); + t[1] = _mm_cvtps_epi32(in[1]); + t[2] = _mm_cvtps_epi32(in[2]); + t[3] = _mm_cvtps_epi32(in[3]); + + t[0] = _mm_packs_epi32(t[0], t[2]); + t[1] = _mm_packs_epi32(t[1], t[3]); + + out[0] = _mm_unpacklo_epi16(t[0], t[1]); + out[1] = _mm_unpackhi_epi16(t[0], t[1]); + out[2] = _mm_unpacklo_epi32(out[0], out[1]); + out[3] = _mm_unpackhi_epi32(out[0], out[1]); + + _mm_storel_pi((__m64*)(d + 0*n_channels), (__m128)out[2]); + _mm_storeh_pi((__m64*)(d + 1*n_channels), (__m128)out[2]); + _mm_storel_pi((__m64*)(d + 2*n_channels), (__m128)out[3]); + _mm_storeh_pi((__m64*)(d + 3*n_channels), (__m128)out[3]); + + d += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale); + in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + in[2] = _MM_CLAMP_SS(in[2], int_min, int_max); + in[3] = _MM_CLAMP_SS(in[3], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d[2] = _mm_cvtss_si32(in[2]); + d[3] = _mm_cvtss_si32(in[3]); + d += n_channels; + } +} + +void +conv_f32d_to_s16_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int16_t *d = dst[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_f32d_to_s16_4s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_f32d_to_s16_2s_sse2(conv, &d[i], &src[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_f32d_to_s16_1s_sse2(conv, &d[i], &src[i], n_channels, n_samples); +} + +static void +conv_f32d_to_s16_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + const float *noise, uint32_t n_channels, uint32_t n_samples) +{ + const float *s0 = src; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n])); + in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4])); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + + d[0*n_channels] = _mm_extract_epi16(out[0], 0); + d[1*n_channels] = _mm_extract_epi16(out[0], 1); + d[2*n_channels] = _mm_extract_epi16(out[0], 2); + d[3*n_channels] = _mm_extract_epi16(out[0], 3); + d[4*n_channels] = _mm_extract_epi16(out[0], 4); + d[5*n_channels] = _mm_extract_epi16(out[0], 5); + d[6*n_channels] = _mm_extract_epi16(out[0], 6); + d[7*n_channels] = _mm_extract_epi16(out[0], 7); + d += 8*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n])); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + *d = _mm_cvtss_si32(in[0]); + d += n_channels; + } +} + +void +conv_f32d_to_s16_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + int16_t *d = dst[0]; + uint32_t i, k, chunk, n_channels = conv->n_channels; + float *noise = conv->noise; + + convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size)); + + for(i = 0; i < n_channels; i++) { + const float *s = src[i]; + for(k = 0; k < n_samples; k += chunk) { + chunk = SPA_MIN(n_samples - k, conv->noise_size); + conv_f32d_to_s16_1s_noise_sse2(conv, &d[i + k*n_channels], + &s[k], noise, n_channels, chunk); + } + } +} + +static void +conv_f32_to_s16_1_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, const void * SPA_RESTRICT src, + const float *noise, uint32_t n_samples) +{ + const float *s = src; + int16_t *d = dst; + uint32_t n, unrolled; + __m128 in[2]; + __m128i out[2]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s[n]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s[n+4]), int_scale); + in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n])); + in[1] = _mm_add_ps(in[1], _mm_load_ps(&noise[n+4])); + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[0] = _mm_packs_epi32(out[0], out[1]); + _mm_storeu_si128((__m128i*)(&d[n]), out[0]); + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s[n]), int_scale); + in[0] = _mm_add_ss(in[0], _mm_load_ss(&noise[n])); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + d[n] = _mm_cvtss_si32(in[0]); + } +} + +void +conv_f32d_to_s16d_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + uint32_t i, k, chunk, n_channels = conv->n_channels; + float *noise = conv->noise; + + convert_update_noise(conv, noise, SPA_MIN(n_samples, conv->noise_size)); + + for(i = 0; i < n_channels; i++) { + const float *s = src[i]; + int16_t *d = dst[i]; + for(k = 0; k < n_samples; k += chunk) { + chunk = SPA_MIN(n_samples - k, conv->noise_size); + conv_f32_to_s16_1_noise_sse2(conv, &d[k], &s[k], noise, chunk); + } + } +} + +void +conv_f32d_to_s16_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const float *s0 = src[0], *s1 = src[1]; + int16_t *d = dst[0]; + uint32_t n, unrolled; + __m128 in[4]; + __m128i out[4]; + __m128 int_scale = _mm_set1_ps(S16_SCALE); + __m128 int_max = _mm_set1_ps(S16_MAX); + __m128 int_min = _mm_set1_ps(S16_MIN); + + if (SPA_IS_ALIGNED(s0, 16) && + SPA_IS_ALIGNED(s1, 16)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm_mul_ps(_mm_load_ps(&s0[n+0]), int_scale); + in[1] = _mm_mul_ps(_mm_load_ps(&s1[n+0]), int_scale); + in[2] = _mm_mul_ps(_mm_load_ps(&s0[n+4]), int_scale); + in[3] = _mm_mul_ps(_mm_load_ps(&s1[n+4]), int_scale); + + out[0] = _mm_cvtps_epi32(in[0]); + out[1] = _mm_cvtps_epi32(in[1]); + out[2] = _mm_cvtps_epi32(in[2]); + out[3] = _mm_cvtps_epi32(in[3]); + + out[0] = _mm_packs_epi32(out[0], out[2]); + out[1] = _mm_packs_epi32(out[1], out[3]); + + out[2] = _mm_unpacklo_epi16(out[0], out[1]); + out[3] = _mm_unpackhi_epi16(out[0], out[1]); + + _mm_storeu_si128((__m128i*)(d+0), out[2]); + _mm_storeu_si128((__m128i*)(d+8), out[3]); + + d += 16; + } + for(; n < n_samples; n++) { + in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); + in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); + in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); + in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); + d[0] = _mm_cvtss_si32(in[0]); + d[1] = _mm_cvtss_si32(in[1]); + d += 2; + } +} diff --git a/spa/plugins/audioconvert/fmt-ops-sse41.c b/spa/plugins/audioconvert/fmt-ops-sse41.c new file mode 100644 index 0000000..042294d --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-sse41.c @@ -0,0 +1,86 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "fmt-ops.h" + +#include + +static void +conv_s24_to_f32d_1s_sse41(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int24_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m128i in = _mm_setzero_si128(); + __m128 out, factor = _mm_set1_ps(1.0f / S24_SCALE); + + if (SPA_IS_ALIGNED(d0, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in = _mm_insert_epi32(in, *((uint32_t*)&s[0 * n_channels]), 0); + in = _mm_insert_epi32(in, *((uint32_t*)&s[1 * n_channels]), 1); + in = _mm_insert_epi32(in, *((uint32_t*)&s[2 * n_channels]), 2); + in = _mm_insert_epi32(in, *((uint32_t*)&s[3 * n_channels]), 3); + in = _mm_slli_epi32(in, 8); + in = _mm_srai_epi32(in, 8); + out = _mm_cvtepi32_ps(in); + out = _mm_mul_ps(out, factor); + _mm_store_ps(&d0[n], out); + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + out = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +extern void conv_s24_to_f32d_2s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples); +extern void conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples); + +void +conv_s24_to_f32d_sse41(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int8_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + +#if defined (HAVE_SSSE3) + for(; i + 3 < n_channels; i += 4) + conv_s24_to_f32d_4s_ssse3(conv, &dst[i], &s[3*i], n_channels, n_samples); +#endif +#if defined (HAVE_SSE2) + for(; i + 1 < n_channels; i += 2) + conv_s24_to_f32d_2s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); +#endif + for(; i < n_channels; i++) + conv_s24_to_f32d_1s_sse41(conv, &dst[i], &s[3*i], n_channels, n_samples); +} diff --git a/spa/plugins/audioconvert/fmt-ops-ssse3.c b/spa/plugins/audioconvert/fmt-ops-ssse3.c new file mode 100644 index 0000000..c35c7c9 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops-ssse3.c @@ -0,0 +1,111 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "fmt-ops.h" + +#include + +static void +conv_s24_to_f32d_4s_ssse3(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int24_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128i in[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S24_SCALE); + const __m128i mask = _mm_setr_epi8(-1, 0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11); + //const __m128i mask = _mm_set_epi8(15, 14, 13, -1, 12, 11, 10, -1, 9, 8, 7, -1, 6, 5, 4, -1); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16)) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_loadu_si128((__m128i*)(s + 0*n_channels)); + in[1] = _mm_loadu_si128((__m128i*)(s + 1*n_channels)); + in[2] = _mm_loadu_si128((__m128i*)(s + 2*n_channels)); + in[3] = _mm_loadu_si128((__m128i*)(s + 3*n_channels)); + in[0] = _mm_shuffle_epi8(in[0], mask); + in[1] = _mm_shuffle_epi8(in[1], mask); + in[2] = _mm_shuffle_epi8(in[2], mask); + in[3] = _mm_shuffle_epi8(in[3], mask); + in[0] = _mm_srai_epi32(in[0], 8); + in[1] = _mm_srai_epi32(in[1], 8); + in[2] = _mm_srai_epi32(in[2], 8); + in[3] = _mm_srai_epi32(in[3], 8); + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + out[2] = _mm_cvtepi32_ps(in[2]); + out[3] = _mm_cvtepi32_ps(in[3]); + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + out[2] = _mm_mul_ps(out[2], factor); + out[3] = _mm_mul_ps(out[3], factor); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + s += 4 * n_channels; + } + for(; n < n_samples; n++) { + out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); + out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1))); + out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2))); + out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3))); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + out[2] = _mm_mul_ss(out[2], factor); + out[3] = _mm_mul_ss(out[3], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + _mm_store_ss(&d2[n], out[2]); + _mm_store_ss(&d3[n], out[3]); + s += n_channels; + } +} + +void +conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples); + +void +conv_s24_to_f32d_ssse3(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples) +{ + const int8_t *s = src[0]; + uint32_t i = 0, n_channels = conv->n_channels; + + for(; i + 3 < n_channels; i += 4) + conv_s24_to_f32d_4s_ssse3(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s24_to_f32d_1s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); +} diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c new file mode 100644 index 0000000..4999a20 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops.c @@ -0,0 +1,564 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 + MAKE(S16, F32P, 0, conv_s16_to_f32d_c), + MAKE(S16P, F32, 0, conv_s16d_to_f32_c), + + MAKE(S16_OE, F32P, 0, conv_s16s_to_f32d_c), + + MAKE(F32, F32, 0, conv_copy32_c), + MAKE(F32P, F32P, 0, conv_copy32d_c), +#if defined (HAVE_SSE2) + MAKE(F32, F32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32, F32P, 0, conv_32_to_32d_c), +#if defined (HAVE_SSE2) + MAKE(F32P, F32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, F32, 0, conv_32d_to_32_c), + +#if defined (HAVE_SSE2) + MAKE(F32_OE, F32P, 0, conv_32s_to_32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32_OE, F32P, 0, conv_32s_to_32d_c), +#if defined (HAVE_SSE2) + MAKE(F32P, F32_OE, 0, conv_32d_to_32s_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, F32_OE, 0, conv_32d_to_32s_c), + + MAKE(U32, F32, 0, conv_u32_to_f32_c), + MAKE(U32, F32P, 0, conv_u32_to_f32d_c), + +#if defined (HAVE_AVX2) + MAKE(S32, F32P, 0, conv_s32_to_f32d_avx2, SPA_CPU_FLAG_AVX2), +#endif +#if defined (HAVE_SSE2) + MAKE(S32, F32P, 0, conv_s32_to_f32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S32, F32, 0, conv_s32_to_f32_c), + MAKE(S32P, F32P, 0, conv_s32d_to_f32d_c), + MAKE(S32, F32P, 0, conv_s32_to_f32d_c), + MAKE(S32P, F32, 0, conv_s32d_to_f32_c), + + MAKE(S32_OE, F32P, 0, conv_s32s_to_f32d_c), + + MAKE(U24, F32, 0, conv_u24_to_f32_c), + MAKE(U24, F32P, 0, conv_u24_to_f32d_c), + + MAKE(S24, F32, 0, conv_s24_to_f32_c), + MAKE(S24P, F32P, 0, conv_s24d_to_f32d_c), +#if defined (HAVE_AVX2) + MAKE(S24, F32P, 0, conv_s24_to_f32d_avx2, SPA_CPU_FLAG_AVX2), +#endif +#if defined (HAVE_SSSE3) +// MAKE(S24, F32P, 0, conv_s24_to_f32d_ssse3, SPA_CPU_FLAG_SSSE3), +#endif +#if defined (HAVE_SSE41) + MAKE(S24, F32P, 0, conv_s24_to_f32d_sse41, SPA_CPU_FLAG_SSE41), +#endif +#if defined (HAVE_SSE2) + MAKE(S24, F32P, 0, conv_s24_to_f32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S24, F32P, 0, conv_s24_to_f32d_c), + MAKE(S24P, F32, 0, conv_s24d_to_f32_c), + + MAKE(S24_OE, F32P, 0, conv_s24s_to_f32d_c), + + MAKE(U24_32, F32, 0, conv_u24_32_to_f32_c), + MAKE(U24_32, F32P, 0, conv_u24_32_to_f32d_c), + + MAKE(S24_32, F32, 0, conv_s24_32_to_f32_c), + MAKE(S24_32P, F32P, 0, conv_s24_32d_to_f32d_c), + MAKE(S24_32, F32P, 0, conv_s24_32_to_f32d_c), + MAKE(S24_32P, F32, 0, conv_s24_32d_to_f32_c), + + MAKE(S24_32_OE, F32P, 0, conv_s24_32s_to_f32d_c), + + MAKE(F64, F32, 0, conv_f64_to_f32_c), + MAKE(F64P, F32P, 0, conv_f64d_to_f32d_c), + MAKE(F64, F32P, 0, conv_f64_to_f32d_c), + MAKE(F64P, F32, 0, conv_f64d_to_f32_c), + + MAKE(F64_OE, F32P, 0, conv_f64s_to_f32d_c), + + /* from f32 */ + MAKE(F32, U8, 0, conv_f32_to_u8_c), + MAKE(F32P, U8P, 0, conv_f32d_to_u8d_shaped_c, 0, CONV_SHAPE), + MAKE(F32P, U8P, 0, conv_f32d_to_u8d_noise_c, 0, CONV_NOISE), + MAKE(F32P, U8P, 0, conv_f32d_to_u8d_c), + MAKE(F32, U8P, 0, conv_f32_to_u8d_c), + MAKE(F32P, U8, 0, conv_f32d_to_u8_shaped_c, 0, CONV_SHAPE), + MAKE(F32P, U8, 0, conv_f32d_to_u8_noise_c, 0, CONV_NOISE), + MAKE(F32P, U8, 0, conv_f32d_to_u8_c), + + MAKE(F32, S8, 0, conv_f32_to_s8_c), + MAKE(F32P, S8P, 0, conv_f32d_to_s8d_shaped_c, 0, CONV_SHAPE), + MAKE(F32P, S8P, 0, conv_f32d_to_s8d_noise_c, 0, CONV_NOISE), + MAKE(F32P, S8P, 0, conv_f32d_to_s8d_c), + MAKE(F32, S8P, 0, conv_f32_to_s8d_c), + MAKE(F32P, S8, 0, conv_f32d_to_s8_shaped_c, 0, CONV_SHAPE), + MAKE(F32P, S8, 0, conv_f32d_to_s8_noise_c, 0, CONV_NOISE), + MAKE(F32P, S8, 0, conv_f32d_to_s8_c), + + MAKE(F32P, ALAW, 0, conv_f32d_to_alaw_c), + MAKE(F32P, ULAW, 0, conv_f32d_to_ulaw_c), + + MAKE(F32, U16, 0, conv_f32_to_u16_c), + MAKE(F32P, U16, 0, conv_f32d_to_u16_c), + +#if defined (HAVE_SSE2) + MAKE(F32, S16, 0, conv_f32_to_s16_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32, S16, 0, conv_f32_to_s16_c), + + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_shaped_c, 0, CONV_SHAPE), +#if defined (HAVE_SSE2) + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE), +#endif + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_noise_c, 0, CONV_NOISE), +#if defined (HAVE_SSE2) + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, S16P, 0, conv_f32d_to_s16d_c), + + MAKE(F32, S16P, 0, conv_f32_to_s16d_c), + + MAKE(F32P, S16, 0, conv_f32d_to_s16_shaped_c, 0, CONV_SHAPE), +#if defined (HAVE_SSE2) + MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE), +#endif + MAKE(F32P, S16, 0, conv_f32d_to_s16_noise_c, 0, CONV_NOISE), +#if defined (HAVE_NEON) + MAKE(F32P, S16, 0, conv_f32d_to_s16_neon, SPA_CPU_FLAG_NEON), +#endif +#if defined (HAVE_AVX2) + MAKE(F32P, S16, 4, conv_f32d_to_s16_4_avx2, SPA_CPU_FLAG_AVX2), + MAKE(F32P, S16, 2, conv_f32d_to_s16_2_avx2, SPA_CPU_FLAG_AVX2), + MAKE(F32P, S16, 0, conv_f32d_to_s16_avx2, SPA_CPU_FLAG_AVX2), +#endif +#if defined (HAVE_SSE2) + MAKE(F32P, S16, 2, conv_f32d_to_s16_2_sse2, SPA_CPU_FLAG_SSE2), + MAKE(F32P, S16, 0, conv_f32d_to_s16_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, S16, 0, conv_f32d_to_s16_c), + + MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_shaped_c, 0, CONV_SHAPE), + MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_noise_c, 0, CONV_NOISE), + MAKE(F32P, S16_OE, 0, conv_f32d_to_s16s_c), + + MAKE(F32, U32, 0, conv_f32_to_u32_c), + MAKE(F32P, U32, 0, conv_f32d_to_u32_c), + + MAKE(F32, S32, 0, conv_f32_to_s32_c), + MAKE(F32P, S32P, 0, conv_f32d_to_s32d_noise_c, 0, CONV_NOISE), + MAKE(F32P, S32P, 0, conv_f32d_to_s32d_c), + MAKE(F32, S32P, 0, conv_f32_to_s32d_c), + +#if defined (HAVE_SSE2) + MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_sse2, SPA_CPU_FLAG_SSE2, CONV_NOISE), +#endif + MAKE(F32P, S32, 0, conv_f32d_to_s32_noise_c, 0, CONV_NOISE), + +#if defined (HAVE_AVX2) + MAKE(F32P, S32, 0, conv_f32d_to_s32_avx2, SPA_CPU_FLAG_AVX2), +#endif +#if defined (HAVE_SSE2) + MAKE(F32P, S32, 0, conv_f32d_to_s32_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(F32P, S32, 0, conv_f32d_to_s32_c), + + MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_noise_c, 0, CONV_NOISE), + MAKE(F32P, S32_OE, 0, conv_f32d_to_s32s_c), + + MAKE(F32, U24, 0, conv_f32_to_u24_c), + MAKE(F32P, U24, 0, conv_f32d_to_u24_c), + + MAKE(F32, S24, 0, conv_f32_to_s24_c), + MAKE(F32P, S24P, 0, conv_f32d_to_s24d_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24P, 0, conv_f32d_to_s24d_c), + MAKE(F32, S24P, 0, conv_f32_to_s24d_c), + MAKE(F32P, S24, 0, conv_f32d_to_s24_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24, 0, conv_f32d_to_s24_c), + + MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24_OE, 0, conv_f32d_to_s24s_c), + + MAKE(F32, U24_32, 0, conv_f32_to_u24_32_c), + MAKE(F32P, U24_32, 0, conv_f32d_to_u24_32_c), + + MAKE(F32, S24_32, 0, conv_f32_to_s24_32_c), + MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24_32P, 0, conv_f32d_to_s24_32d_c), + MAKE(F32, S24_32P, 0, conv_f32_to_s24_32d_c), + MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24_32, 0, conv_f32d_to_s24_32_c), + + MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_noise_c, 0, CONV_NOISE), + MAKE(F32P, S24_32_OE, 0, conv_f32d_to_s24_32s_c), + + MAKE(F32, F64, 0, conv_f32_to_f64_c), + MAKE(F32P, F64P, 0, conv_f32d_to_f64d_c), + MAKE(F32, F64P, 0, conv_f32_to_f64d_c), + MAKE(F32P, F64, 0, conv_f32d_to_f64_c), + + MAKE(F32P, F64_OE, 0, conv_f32d_to_f64s_c), + + /* u8 */ + MAKE(U8, U8, 0, conv_copy8_c), + MAKE(U8P, U8P, 0, conv_copy8d_c), + MAKE(U8, U8P, 0, conv_8_to_8d_c), + MAKE(U8P, U8, 0, conv_8d_to_8_c), + + /* s8 */ + MAKE(S8, S8, 0, conv_copy8_c), + MAKE(S8P, S8P, 0, conv_copy8d_c), + MAKE(S8, S8P, 0, conv_8_to_8d_c), + MAKE(S8P, S8, 0, conv_8d_to_8_c), + + /* alaw */ + MAKE(ALAW, ALAW, 0, conv_copy8_c), + /* ulaw */ + MAKE(ULAW, ULAW, 0, conv_copy8_c), + + /* s16 */ + MAKE(S16, S16, 0, conv_copy16_c), + MAKE(S16P, S16P, 0, conv_copy16d_c), + MAKE(S16, S16P, 0, conv_16_to_16d_c), + MAKE(S16P, S16, 0, conv_16d_to_16_c), + + /* s32 */ + MAKE(S32, S32, 0, conv_copy32_c), + MAKE(S32P, S32P, 0, conv_copy32d_c), +#if defined (HAVE_SSE2) + MAKE(S32, S32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S32, S32P, 0, conv_32_to_32d_c), +#if defined (HAVE_SSE2) + MAKE(S32P, S32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S32P, S32, 0, conv_32d_to_32_c), + + /* s24 */ + MAKE(S24, S24, 0, conv_copy24_c), + MAKE(S24P, S24P, 0, conv_copy24d_c), + MAKE(S24, S24P, 0, conv_24_to_24d_c), + MAKE(S24P, S24, 0, conv_24d_to_24_c), + + /* s24_32 */ + MAKE(S24_32, S24_32, 0, conv_copy32_c), + MAKE(S24_32P, S24_32P, 0, conv_copy32d_c), +#if defined (HAVE_SSE2) + MAKE(S24_32, S24_32P, 0, conv_32_to_32d_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S24_32, S24_32P, 0, conv_32_to_32d_c), +#if defined (HAVE_SSE2) + MAKE(S24_32P, S24_32, 0, conv_32d_to_32_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(S24_32P, S24_32, 0, conv_32d_to_32_c), + + /* F64 */ + MAKE(F64, F64, 0, conv_copy64_c), + MAKE(F64P, F64P, 0, conv_copy64d_c), + MAKE(F64, F64P, 0, conv_64_to_64d_c), + MAKE(F64P, F64, 0, conv_64d_to_64_c), +}; +#undef MAKE + +#define MATCH_CHAN(a,b) ((a) == 0 || (a) == (b)) +#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a) +#define MATCH_DITHER(a,b) ((a) == 0 || ((a) & (b)) == a) + +static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt, + uint32_t n_channels, uint32_t cpu_flags, uint32_t conv_flags) +{ + SPA_FOR_EACH_ELEMENT_VAR(conv_table, c) { + if (c->src_fmt == src_fmt && + c->dst_fmt == dst_fmt && + MATCH_CHAN(c->n_channels, n_channels) && + MATCH_CPU_FLAGS(c->cpu_flags, cpu_flags) && + MATCH_DITHER(c->conv_flags, conv_flags)) + return c; + } + return NULL; +} + +typedef void (*noise_func_t) (struct convert *conv, float * noise, uint32_t n_samples); + +struct noise_info { + uint32_t method; + + noise_func_t noise; + const char *name; + + uint32_t cpu_flags; +}; + +#define MAKE(method,func,...) \ + { NOISE_METHOD_ ##method, func, #func , __VA_ARGS__ } + +static struct noise_info noise_table[] = +{ +#if defined (HAVE_SSE2) + MAKE(RECTANGULAR, conv_noise_rect_sse2, SPA_CPU_FLAG_SSE2), + MAKE(TRIANGULAR, conv_noise_tri_sse2, SPA_CPU_FLAG_SSE2), + MAKE(TRIANGULAR_HF, conv_noise_tri_hf_sse2, SPA_CPU_FLAG_SSE2), +#endif + MAKE(NONE, conv_noise_none_c), + MAKE(RECTANGULAR, conv_noise_rect_c), + MAKE(TRIANGULAR, conv_noise_tri_c), + MAKE(TRIANGULAR_HF, conv_noise_tri_hf_c), + MAKE(PATTERN, conv_noise_pattern_c), +}; +#undef MAKE + +static const struct noise_info *find_noise_info(uint32_t method, + uint32_t cpu_flags) +{ + SPA_FOR_EACH_ELEMENT_VAR(noise_table, t) { + if (t->method == method && + MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags)) + return t; + } + return NULL; +} + +static void impl_convert_free(struct convert *conv) +{ + conv->process = NULL; + free(conv->data); + conv->data = NULL; +} + +static bool need_dither(uint32_t format) +{ + switch (format) { + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_U8P: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_S8P: + case SPA_AUDIO_FORMAT_ULAW: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_S16P: + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + return true; + } + return false; +} + +/* filters based on F-weighted curves + * from 'Psychoacoustically Optimal Noise Shaping' (**) + * this filter is the "F-Weighted" noise filter described by Wannamaker + * It is designed to produce minimum audibility: */ +static const float wan3[] = { /* Table 3; 3 Coefficients */ + 1.623f, -0.982f, 0.109f +}; +/* Noise shaping coefficients from[1], moves most power of the + * error noise into inaudible frequency ranges. + * + * [1] + * "Minimally Audible Noise Shaping", Stanley P. Lipshitz, + * John Vanderkooy, and Robert A. Wannamaker, + * J. Audio Eng. Soc., Vol. 39, No. 11, November 1991. */ +static const float lips44[] = { /* improved E-weighted (appendix: 5) */ + 2.033f, -2.165f, 1.959f, -1.590f, 0.6149f +}; + +static const struct dither_info { + uint32_t method; + uint32_t noise_method; + uint32_t rate; + const float *ns; + uint32_t n_ns; +} dither_info[] = { + { DITHER_METHOD_NONE, NOISE_METHOD_NONE, }, + { DITHER_METHOD_RECTANGULAR, NOISE_METHOD_RECTANGULAR, }, + { DITHER_METHOD_TRIANGULAR, NOISE_METHOD_TRIANGULAR, }, + { DITHER_METHOD_TRIANGULAR_HF, NOISE_METHOD_TRIANGULAR_HF, }, + { DITHER_METHOD_WANNAMAKER_3, NOISE_METHOD_TRIANGULAR_HF, 44100, wan3, SPA_N_ELEMENTS(wan3) }, + { DITHER_METHOD_LIPSHITZ, NOISE_METHOD_TRIANGULAR, 44100, lips44, SPA_N_ELEMENTS(lips44) } +}; + +static const struct dither_info *find_dither_info(uint32_t method, uint32_t rate) +{ + SPA_FOR_EACH_ELEMENT_VAR(dither_info, di) { + if (di->method != method) + continue; + /* don't use shaped for too low rates, it moves the noise to + * audible ranges */ + if (di->ns != NULL && rate < di->rate * 3 / 4) + return find_dither_info(DITHER_METHOD_TRIANGULAR_HF, rate); + return di; + } + return NULL; +} + +int convert_init(struct convert *conv) +{ + const struct conv_info *info; + const struct dither_info *dinfo; + const struct noise_info *ninfo; + uint32_t i, conv_flags, data_size[3]; + + conv->scale = 1.0f / (float)(INT32_MAX); + + /* disable dither if not needed */ + if (!need_dither(conv->dst_fmt)) + conv->method = DITHER_METHOD_NONE; + + dinfo = find_dither_info(conv->method, conv->rate); + if (dinfo == NULL) + return -EINVAL; + + conv->noise_method = dinfo->noise_method; + if (conv->noise_bits > 0) { + switch (conv->noise_method) { + case NOISE_METHOD_NONE: + conv->noise_method = NOISE_METHOD_PATTERN; + conv->scale = -1.0f * (1 << (conv->noise_bits-1)); + break; + case NOISE_METHOD_RECTANGULAR: + conv->noise_method = NOISE_METHOD_TRIANGULAR; + SPA_FALLTHROUGH; + case NOISE_METHOD_TRIANGULAR: + case NOISE_METHOD_TRIANGULAR_HF: + conv->scale *= (1 << (conv->noise_bits-1)); + break; + } + } + if (conv->noise_method < NOISE_METHOD_TRIANGULAR) + conv->scale *= 0.5f; + + conv_flags = 0; + if (conv->noise_method != NOISE_METHOD_NONE) + conv_flags |= CONV_NOISE; + if (dinfo->n_ns > 0) { + conv_flags |= CONV_SHAPE; + conv->n_ns = dinfo->n_ns; + conv->ns = dinfo->ns; + } + + info = find_conv_info(conv->src_fmt, conv->dst_fmt, conv->n_channels, + conv->cpu_flags, conv_flags); + if (info == NULL) + return -ENOTSUP; + + ninfo = find_noise_info(conv->noise_method, conv->cpu_flags); + if (ninfo == NULL) + return -ENOTSUP; + + conv->noise_size = NOISE_SIZE; + + data_size[0] = SPA_ROUND_UP(conv->noise_size * sizeof(float), FMT_OPS_MAX_ALIGN); + data_size[1] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(uint32_t), FMT_OPS_MAX_ALIGN); + data_size[2] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(int32_t), FMT_OPS_MAX_ALIGN); + + conv->data = calloc(FMT_OPS_MAX_ALIGN + + data_size[0] + data_size[1] + data_size[2], 1); + if (conv->data == NULL) + return -errno; + + conv->noise = SPA_PTR_ALIGN(conv->data, FMT_OPS_MAX_ALIGN, float); + conv->random = SPA_PTROFF(conv->noise, data_size[0], uint32_t); + conv->prev = SPA_PTROFF(conv->random, data_size[1], int32_t); + + for (i = 0; i < RANDOM_SIZE; i++) + conv->random[i] = random(); + + conv->is_passthrough = conv->src_fmt == conv->dst_fmt; + conv->cpu_flags = info->cpu_flags; + conv->update_noise = ninfo->noise; + conv->process = info->process; + conv->free = impl_convert_free; + conv->func_name = info->name; + + return 0; +} diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h new file mode 100644 index 0000000..c2afaa2 --- /dev/null +++ b/spa/plugins/audioconvert/fmt-ops.h @@ -0,0 +1,488 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#include +#define bswap_16 bswap16 +#define bswap_32 bswap32 +#define bswap_64 bswap64 +#else +#include +#endif + +#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 32 + +#define U8_MIN 0u +#define U8_MAX 255u +#define U8_SCALE 128.f +#define U8_OFFS 128.f +#define U8_TO_F32(v) ITOF(uint8_t, v, U8_SCALE, 1.0f) +#define F32_TO_U8_D(v,d) FTOI(uint8_t, v, U8_SCALE, U8_OFFS, d, U8_MIN, U8_MAX) +#define F32_TO_U8(v) F32_TO_U8_D(v, 0.0f) + +#define S8_MIN -128 +#define S8_MAX 127 +#define S8_SCALE 128.0f +#define S8_TO_F32(v) ITOF(int8_t, v, S8_SCALE, 0.0f) +#define F32_TO_S8_D(v,d) FTOI(int8_t, v, S8_SCALE, 0.0f, d, S8_MIN, S8_MAX) +#define F32_TO_S8(v) F32_TO_S8_D(v, 0.0f); + +#define U16_MIN 0u +#define U16_MAX 65535u +#define U16_SCALE 32768.f +#define U16_OFFS 32768.f +#define U16_TO_F32(v) ITOF(uint16_t, v, U16_SCALE, 1.0f) +#define U16S_TO_F32(v) U16_TO_F32(bswap_16(v)) +#define F32_TO_U16_D(v,d) FTOI(uint16_t, v, U16_SCALE, U16_OFFS, d, U16_MIN, U16_MAX) +#define F32_TO_U16(v) F32_TO_U16_D(v, 0.0f); +#define F32_TO_U16S_D(v,d) bswap_16(F32_TO_U16_D(v,d)) +#define F32_TO_U16S(v) bswap_16(F32_TO_U16(v)) + +#define S16_MIN -32768 +#define S16_MAX 32767 +#define S16_SCALE 32768.0f +#define S16_TO_F32(v) ITOF(int16_t, v, S16_SCALE, 0.0f) +#define S16S_TO_F32(v) S16_TO_F32(bswap_16(v)) +#define F32_TO_S16_D(v,d) FTOI(int16_t, v, S16_SCALE, 0.0f, d, S16_MIN, S16_MAX) +#define F32_TO_S16(v) F32_TO_S16_D(v, 0.0f) +#define F32_TO_S16S_D(v,d) bswap_16(F32_TO_S16_D(v,d)) +#define F32_TO_S16S(v) bswap_16(F32_TO_S16(v)) + +#define U24_MIN 0u +#define U24_MAX 16777215u +#define U24_SCALE 8388608.f +#define U24_OFFS 8388608.f +#define U24_TO_F32(v) ITOF(uint32_t, u24_to_u32(v), U24_SCALE, 1.0f) +#define F32_TO_U24_D(v,d) u32_to_u24(FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX)) +#define F32_TO_U24(v) F32_TO_U24_D(v, 0.0f) + +#define S24_MIN -8388608 +#define S24_MAX 8388607 +#define S24_SCALE 8388608.0f +#define S24_TO_F32(v) ITOF(int32_t, s24_to_s32(v), S24_SCALE, 0.0f) +#define S24S_TO_F32(v) S24_TO_F32(bswap_s24(v)) +#define F32_TO_S24_D(v,d) s32_to_s24(FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX)) +#define F32_TO_S24(v) F32_TO_S24_D(v, 0.0f) +#define F32_TO_S24S(v) bswap_s24(F32_TO_S24(v)) + +#define U24_32_TO_U32(v) (((uint32_t)(v)) << 8) + +#define U24_32_TO_F32(v) U32_TO_F32(U24_32_TO_U32(v)) +#define U24_32S_TO_F32(v) U24_32_TO_F32(bswap_32(v)) +#define F32_TO_U24_32_D(v,d) FTOI(uint32_t, v, U24_SCALE, U24_OFFS, d, U24_MIN, U24_MAX) +#define F32_TO_U24_32(v) F32_TO_U24_32_D(v, 0.0f) +#define F32_TO_U24_32S(v) bswap_32(F32_TO_U24_32(v)) +#define F32_TO_U24_32S_D(v,d) bswap_32(F32_TO_U24_32_D(v,d)) + +#define U32_TO_U24_32(v) (((uint32_t)(v)) >> 8) + +#define U32_MIN 0u +#define U32_MAX 4294967295u +#define U32_SCALE 2147483648.f +#define U32_OFFS 2147483648.f +#define U32_TO_F32(v) ITOF(uint32_t, U32_TO_U24_32(v), U24_SCALE, 1.0f) +#define F32_TO_U32(v) U24_32_TO_U32(F32_TO_U24_32(v)) +#define F32_TO_U32_D(v,d) U24_32_TO_U32(F32_TO_U24_32_D(v,d)) + +#define S24_32_TO_S32(v) ((int32_t)(((uint32_t)(v)) << 8)) + +#define S24_32_TO_F32(v) S32_TO_F32(S24_32_TO_S32(v)) +#define S24_32S_TO_F32(v) S24_32_TO_F32(bswap_32(v)) +#define F32_TO_S24_32_D(v,d) FTOI(int32_t, v, S24_SCALE, 0.0f, d, S24_MIN, S24_MAX) +#define F32_TO_S24_32(v) F32_TO_S24_32_D(v, 0.0f) +#define F32_TO_S24_32S(v) bswap_32(F32_TO_S24_32(v)) +#define F32_TO_S24_32S_D(v,d) bswap_32(F32_TO_S24_32_D(v,d)) + +#define S32_TO_S24_32(v) (((int32_t)(v)) >> 8) + +#define S32_MIN (S24_MIN * 256) +#define S32_MAX (S24_MAX * 256) +#define S32_TO_F32(v) ITOF(int32_t, S32_TO_S24_32(v), S24_SCALE, 0.0f) +#define S32S_TO_F32(v) S32_TO_F32(bswap_32(v)) +#define F32_TO_S32(v) S24_32_TO_S32(F32_TO_S24_32(v)) +#define F32_TO_S32_D(v,d) S24_32_TO_S32(F32_TO_S24_32_D(v,d)) +#define F32_TO_S32S(v) bswap_32(F32_TO_S32(v)) +#define F32_TO_S32S_D(v,d) bswap_32(F32_TO_S32_D(v,d)) + +typedef struct { +#if __BYTE_ORDER == __LITTLE_ENDIAN + uint8_t v3; + uint8_t v2; + uint8_t v1; +#else + uint8_t v1; + uint8_t v2; + uint8_t v3; +#endif +} __attribute__ ((packed)) uint24_t; + +typedef struct { +#if __BYTE_ORDER == __LITTLE_ENDIAN + uint8_t v3; + uint8_t v2; + int8_t v1; +#else + int8_t v1; + uint8_t v2; + uint8_t v3; +#endif +} __attribute__ ((packed)) int24_t; + +static inline uint32_t u24_to_u32(uint24_t src) +{ + return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3; +} + +#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \ + .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) } + +static inline uint24_t u32_to_u24(uint32_t src) +{ + return U32_TO_U24(src); +} + +static inline int32_t s24_to_s32(int24_t src) +{ + return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3; +} + +#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \ + .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) } + +static inline int24_t s32_to_s24(int32_t src) +{ + return S32_TO_S24(src); +} + +static inline uint24_t bswap_u24(uint24_t src) +{ + return (uint24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 }; +} +static inline int24_t bswap_s24(int24_t src) +{ + return (int24_t) { .v1 = src.v3, .v2 = src.v2, .v3 = src.v1 }; +} + +#define F32_TO_F32S(v) \ + bswap_32((union { uint32_t i; float f; }){ .f = (v) }.i) +#define F32S_TO_F32(v) \ + ((union { uint32_t i; float f; }){ .i = bswap_32(v) }.f) + +#define F64_TO_F64S(v) \ + bswap_32((union { uint64_t i; double d; }){ .d = (v) }.i) +#define F64S_TO_F64(v) \ + ((union { uint64_t i; double d; }){ .i = bswap_32(v) }.d) + +#define NS_MAX 8 +#define NS_MASK (NS_MAX-1) + +struct shaper { + float e[NS_MAX * 2]; + uint32_t idx; + float r; +}; + +struct convert { + uint32_t noise_bits; +#define DITHER_METHOD_NONE 0 +#define DITHER_METHOD_RECTANGULAR 1 +#define DITHER_METHOD_TRIANGULAR 2 +#define DITHER_METHOD_TRIANGULAR_HF 3 +#define DITHER_METHOD_WANNAMAKER_3 4 +#define DITHER_METHOD_LIPSHITZ 5 + uint32_t method; + + uint32_t src_fmt; + uint32_t dst_fmt; + uint32_t n_channels; + uint32_t rate; + uint32_t cpu_flags; + const char *func_name; + + unsigned int is_passthrough:1; + + float scale; + uint32_t *random; + int32_t *prev; +#define NOISE_METHOD_NONE 0 +#define NOISE_METHOD_RECTANGULAR 1 +#define NOISE_METHOD_TRIANGULAR 2 +#define NOISE_METHOD_TRIANGULAR_HF 3 +#define NOISE_METHOD_PATTERN 4 + uint32_t noise_method; + float *noise; + uint32_t noise_size; + const float *ns; + uint32_t n_ns; + struct shaper shaper[64]; + + void (*update_noise) (struct convert *conv, float *noise, uint32_t n_samples); + void (*process) (struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], + uint32_t n_samples); + void (*free) (struct convert *conv); + + void *data; +}; + +int convert_init(struct convert *conv); + +static const struct dither_method_info { + uint32_t method; + const char *label; + const char *description; +} dither_method_info[] = { + [DITHER_METHOD_NONE] = { DITHER_METHOD_NONE, + "none", "Disabled", }, + [DITHER_METHOD_RECTANGULAR] = { DITHER_METHOD_RECTANGULAR, + "rectangular", "Rectangular dithering", }, + [DITHER_METHOD_TRIANGULAR] = { DITHER_METHOD_TRIANGULAR, + "triangular", "Triangular dithering", }, + [DITHER_METHOD_TRIANGULAR_HF] = { DITHER_METHOD_TRIANGULAR_HF, + "triangular-hf", "Sloped Triangular dithering", }, + [DITHER_METHOD_WANNAMAKER_3] = { DITHER_METHOD_WANNAMAKER_3, + "wannamaker3", "Wannamaker 3 dithering", }, + [DITHER_METHOD_LIPSHITZ] = { DITHER_METHOD_LIPSHITZ, + "shaped5", "Lipshitz 5 dithering", }, +}; + +static inline uint32_t dither_method_from_label(const char *label) +{ + SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) { + if (spa_streq(i->label, label)) + return i->method; + } + return DITHER_METHOD_NONE; +} + +#define convert_update_noise(conv,...) (conv)->update_noise(conv, __VA_ARGS__) +#define convert_process(conv,...) (conv)->process(conv, __VA_ARGS__) +#define convert_free(conv) (conv)->free(conv) + +#define DEFINE_NOISE_FUNCTION(name,arch) \ +void conv_noise_##name##_##arch(struct convert *conv, float *noise, \ + uint32_t n_samples) + +DEFINE_NOISE_FUNCTION(none, c); +DEFINE_NOISE_FUNCTION(rect, c); +DEFINE_NOISE_FUNCTION(tri, c); +DEFINE_NOISE_FUNCTION(tri_hf, c); +DEFINE_NOISE_FUNCTION(pattern, c); +#if defined(HAVE_SSE2) +DEFINE_NOISE_FUNCTION(rect, sse2); +DEFINE_NOISE_FUNCTION(tri, sse2); +DEFINE_NOISE_FUNCTION(tri_hf, sse2); +#endif + +#undef DEFINE_NOISE_FUNCTION + +#define DEFINE_FUNCTION(name,arch) \ +void conv_##name##_##arch(struct convert *conv, void * SPA_RESTRICT dst[], \ + const void * SPA_RESTRICT src[], uint32_t n_samples) + +DEFINE_FUNCTION(copy8d, c); +DEFINE_FUNCTION(copy8, c); +DEFINE_FUNCTION(copy16d, c); +DEFINE_FUNCTION(copy16, c); +DEFINE_FUNCTION(copy24d, c); +DEFINE_FUNCTION(copy24, c); +DEFINE_FUNCTION(copy32d, c); +DEFINE_FUNCTION(copy32, c); +DEFINE_FUNCTION(copy64d, c); +DEFINE_FUNCTION(copy64, c); +DEFINE_FUNCTION(u8d_to_f32d, c); +DEFINE_FUNCTION(u8_to_f32, c); +DEFINE_FUNCTION(u8_to_f32d, c); +DEFINE_FUNCTION(u8d_to_f32, c); +DEFINE_FUNCTION(s8d_to_f32d, c); +DEFINE_FUNCTION(s8_to_f32, c); +DEFINE_FUNCTION(s8_to_f32d, c); +DEFINE_FUNCTION(s8d_to_f32, c); +DEFINE_FUNCTION(ulaw_to_f32d, c); +DEFINE_FUNCTION(alaw_to_f32d, c); +DEFINE_FUNCTION(u16_to_f32, c); +DEFINE_FUNCTION(u16_to_f32d, c); +DEFINE_FUNCTION(s16d_to_f32d, c); +DEFINE_FUNCTION(s16_to_f32, c); +DEFINE_FUNCTION(s16_to_f32d, c); +DEFINE_FUNCTION(s16s_to_f32d, c); +DEFINE_FUNCTION(s16d_to_f32, c); +DEFINE_FUNCTION(u32_to_f32, c); +DEFINE_FUNCTION(u32_to_f32d, c); +DEFINE_FUNCTION(s32d_to_f32d, c); +DEFINE_FUNCTION(s32_to_f32, c); +DEFINE_FUNCTION(s32_to_f32d, c); +DEFINE_FUNCTION(s32s_to_f32d, c); +DEFINE_FUNCTION(s32d_to_f32, c); +DEFINE_FUNCTION(u24_to_f32, c); +DEFINE_FUNCTION(u24_to_f32d, c); +DEFINE_FUNCTION(s24d_to_f32d, c); +DEFINE_FUNCTION(s24_to_f32, c); +DEFINE_FUNCTION(s24_to_f32d, c); +DEFINE_FUNCTION(s24s_to_f32d, c); +DEFINE_FUNCTION(s24d_to_f32, c); +DEFINE_FUNCTION(u24_32_to_f32, c); +DEFINE_FUNCTION(u24_32_to_f32d, c); +DEFINE_FUNCTION(s24_32d_to_f32d, c); +DEFINE_FUNCTION(s24_32_to_f32, c); +DEFINE_FUNCTION(s24_32_to_f32d, c); +DEFINE_FUNCTION(s24_32s_to_f32d, c); +DEFINE_FUNCTION(s24_32d_to_f32, c); +DEFINE_FUNCTION(f64d_to_f32d, c); +DEFINE_FUNCTION(f64_to_f32, c); +DEFINE_FUNCTION(f64_to_f32d, c); +DEFINE_FUNCTION(f64s_to_f32d, c); +DEFINE_FUNCTION(f64d_to_f32, c); +DEFINE_FUNCTION(f32d_to_u8d, c); +DEFINE_FUNCTION(f32d_to_u8d_noise, c); +DEFINE_FUNCTION(f32d_to_u8d_shaped, c); +DEFINE_FUNCTION(f32_to_u8, c); +DEFINE_FUNCTION(f32_to_u8d, c); +DEFINE_FUNCTION(f32d_to_u8, c); +DEFINE_FUNCTION(f32d_to_u8_noise, c); +DEFINE_FUNCTION(f32d_to_u8_shaped, c); +DEFINE_FUNCTION(f32d_to_s8d, c); +DEFINE_FUNCTION(f32d_to_s8d_noise, c); +DEFINE_FUNCTION(f32d_to_s8d_shaped, c); +DEFINE_FUNCTION(f32_to_s8, c); +DEFINE_FUNCTION(f32_to_s8d, c); +DEFINE_FUNCTION(f32d_to_s8, c); +DEFINE_FUNCTION(f32d_to_s8_noise, c); +DEFINE_FUNCTION(f32d_to_s8_shaped, c); +DEFINE_FUNCTION(f32d_to_alaw, c); +DEFINE_FUNCTION(f32d_to_ulaw, c); +DEFINE_FUNCTION(f32_to_u16, c); +DEFINE_FUNCTION(f32d_to_u16, c); +DEFINE_FUNCTION(f32d_to_s16d, c); +DEFINE_FUNCTION(f32d_to_s16d_noise, c); +DEFINE_FUNCTION(f32d_to_s16d_shaped, c); +DEFINE_FUNCTION(f32_to_s16, c); +DEFINE_FUNCTION(f32_to_s16d, c); +DEFINE_FUNCTION(f32d_to_s16, c); +DEFINE_FUNCTION(f32d_to_s16_noise, c); +DEFINE_FUNCTION(f32d_to_s16_shaped, c); +DEFINE_FUNCTION(f32d_to_s16s, c); +DEFINE_FUNCTION(f32d_to_s16s_noise, c); +DEFINE_FUNCTION(f32d_to_s16s_shaped, c); +DEFINE_FUNCTION(f32_to_u32, c); +DEFINE_FUNCTION(f32d_to_u32, c); +DEFINE_FUNCTION(f32d_to_s32d, c); +DEFINE_FUNCTION(f32d_to_s32d_noise, c); +DEFINE_FUNCTION(f32_to_s32, c); +DEFINE_FUNCTION(f32_to_s32d, c); +DEFINE_FUNCTION(f32d_to_s32, c); +DEFINE_FUNCTION(f32d_to_s32_noise, c); +DEFINE_FUNCTION(f32d_to_s32s, c); +DEFINE_FUNCTION(f32d_to_s32s_noise, c); +DEFINE_FUNCTION(f32_to_u24, c); +DEFINE_FUNCTION(f32d_to_u24, c); +DEFINE_FUNCTION(f32d_to_s24d, c); +DEFINE_FUNCTION(f32d_to_s24d_noise, c); +DEFINE_FUNCTION(f32_to_s24, c); +DEFINE_FUNCTION(f32_to_s24d, c); +DEFINE_FUNCTION(f32d_to_s24, c); +DEFINE_FUNCTION(f32d_to_s24_noise, c); +DEFINE_FUNCTION(f32d_to_s24s, c); +DEFINE_FUNCTION(f32d_to_s24s_noise, c); +DEFINE_FUNCTION(f32_to_u24_32, c); +DEFINE_FUNCTION(f32d_to_u24_32, c); +DEFINE_FUNCTION(f32d_to_s24_32d, c); +DEFINE_FUNCTION(f32d_to_s24_32d_noise, c); +DEFINE_FUNCTION(f32_to_s24_32, c); +DEFINE_FUNCTION(f32_to_s24_32d, c); +DEFINE_FUNCTION(f32d_to_s24_32, c); +DEFINE_FUNCTION(f32d_to_s24_32_noise, c); +DEFINE_FUNCTION(f32d_to_s24_32s, c); +DEFINE_FUNCTION(f32d_to_s24_32s_noise, c); +DEFINE_FUNCTION(f32d_to_f64d, c); +DEFINE_FUNCTION(f32_to_f64, c); +DEFINE_FUNCTION(f32_to_f64d, c); +DEFINE_FUNCTION(f32d_to_f64, c); +DEFINE_FUNCTION(f32d_to_f64s, c); +DEFINE_FUNCTION(8_to_8d, c); +DEFINE_FUNCTION(16_to_16d, c); +DEFINE_FUNCTION(24_to_24d, c); +DEFINE_FUNCTION(32_to_32d, c); +DEFINE_FUNCTION(32s_to_32d, c); +DEFINE_FUNCTION(64_to_64d, c); +DEFINE_FUNCTION(64s_to_64sd, c); +DEFINE_FUNCTION(8d_to_8, c); +DEFINE_FUNCTION(16d_to_16, c); +DEFINE_FUNCTION(24d_to_24, c); +DEFINE_FUNCTION(32d_to_32, c); +DEFINE_FUNCTION(32d_to_32s, c); +DEFINE_FUNCTION(64d_to_64, c); +DEFINE_FUNCTION(64sd_to_64s, c); + +#if defined(HAVE_NEON) +DEFINE_FUNCTION(s16_to_f32d_2, neon); +DEFINE_FUNCTION(s16_to_f32d, neon); +DEFINE_FUNCTION(f32d_to_s16, neon); +#endif +#if defined(HAVE_SSE2) +DEFINE_FUNCTION(s16_to_f32d_2, sse2); +DEFINE_FUNCTION(s16_to_f32d, sse2); +DEFINE_FUNCTION(s24_to_f32d, sse2); +DEFINE_FUNCTION(s32_to_f32d, sse2); +DEFINE_FUNCTION(f32d_to_s32, sse2); +DEFINE_FUNCTION(f32d_to_s32_noise, sse2); +DEFINE_FUNCTION(f32_to_s16, sse2); +DEFINE_FUNCTION(f32d_to_s16_2, sse2); +DEFINE_FUNCTION(f32d_to_s16, sse2); +DEFINE_FUNCTION(f32d_to_s16_noise, sse2); +DEFINE_FUNCTION(f32d_to_s16d, sse2); +DEFINE_FUNCTION(f32d_to_s16d_noise, sse2); +DEFINE_FUNCTION(32_to_32d, sse2); +DEFINE_FUNCTION(32s_to_32d, sse2); +DEFINE_FUNCTION(32d_to_32, sse2); +DEFINE_FUNCTION(32d_to_32s, sse2); +#endif +#if defined(HAVE_SSSE3) +DEFINE_FUNCTION(s24_to_f32d, ssse3); +#endif +#if defined(HAVE_SSE41) +DEFINE_FUNCTION(s24_to_f32d, sse41); +#endif +#if defined(HAVE_AVX2) +DEFINE_FUNCTION(s16_to_f32d_2, avx2); +DEFINE_FUNCTION(s16_to_f32d, avx2); +DEFINE_FUNCTION(s24_to_f32d, avx2); +DEFINE_FUNCTION(s32_to_f32d, avx2); +DEFINE_FUNCTION(f32d_to_s32, avx2); +DEFINE_FUNCTION(f32d_to_s16_4, avx2); +DEFINE_FUNCTION(f32d_to_s16_2, avx2); +DEFINE_FUNCTION(f32d_to_s16, avx2); +#endif + +#undef DEFINE_FUNCTION diff --git a/spa/plugins/audioconvert/hilbert.h b/spa/plugins/audioconvert/hilbert.h new file mode 100644 index 0000000..103e6e9 --- /dev/null +++ b/spa/plugins/audioconvert/hilbert.h @@ -0,0 +1,69 @@ +/* Hilbert function + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef HILBERT_H +#define HILBERT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +static inline void blackman_window(float *taps, int n_taps) +{ + int n; + for (n = 0; n < n_taps; n++) { + float w = 2 * M_PI * n / (n_taps-1); + taps[n] = 0.3635819 - 0.4891775 * cos(w) + + 0.1365995 * cos(2 * w) - 0.0106411 * cos(3 * w); + } +} + +static inline int hilbert_generate(float *taps, int n_taps) +{ + int i; + + if ((n_taps & 1) == 0) + return -EINVAL; + + for (i = 0; i < n_taps; i++) { + int k = -(n_taps / 2) + i; + if (k & 1) { + float pk = M_PI * k; + taps[i] *= (1.0f - cosf(pk)) / pk; + } else { + taps[i] = 0.0f; + } + } + return 0; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* HILBERT_H */ diff --git a/spa/plugins/audioconvert/law.h b/spa/plugins/audioconvert/law.h new file mode 100644 index 0000000..f5273d8 --- /dev/null +++ b/spa/plugins/audioconvert/law.h @@ -0,0 +1,2163 @@ +/* Spa + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +static inline float alaw_to_f32(uint8_t alawbyte) +{ + static int16_t alaw_decode[256] = { + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944, + -30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136, + -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, + -15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848 + }; + return S16_TO_F32(alaw_decode[alawbyte]); +} + +static inline float ulaw_to_f32(uint8_t ulawbyte) +{ + static int16_t ulaw_decode[256] = { + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, 0, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0 + }; + return S16_TO_F32(ulaw_decode[ulawbyte]); +} + +static inline uint8_t f32_to_alaw(float val) +{ + static uint8_t alaw_encode[0x2000] = { + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6a, 0x6b, 0x6b, 0x6b, 0x6b, + 0x6b, 0x6b, 0x6b, 0x6b, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, + 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x6e, 0x6e, 0x6e, 0x6e, + 0x6e, 0x6e, 0x6e, 0x6e, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, + 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6c, 0x6d, 0x6d, 0x6d, 0x6d, + 0x6d, 0x6d, 0x6d, 0x6d, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x60, 0x60, 0x60, 0x60, + 0x60, 0x60, 0x60, 0x60, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x67, 0x67, 0x67, 0x67, + 0x67, 0x67, 0x67, 0x67, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x7a, 0x7a, 0x7a, 0x7a, + 0x7b, 0x7b, 0x7b, 0x7b, 0x78, 0x78, 0x78, 0x78, 0x79, 0x79, 0x79, 0x79, + 0x7e, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f, 0x7f, 0x7c, 0x7c, 0x7c, 0x7c, + 0x7d, 0x7d, 0x7d, 0x7d, 0x72, 0x72, 0x72, 0x72, 0x73, 0x73, 0x73, 0x73, + 0x70, 0x70, 0x70, 0x70, 0x71, 0x71, 0x71, 0x71, 0x76, 0x76, 0x76, 0x76, + 0x77, 0x77, 0x77, 0x77, 0x74, 0x74, 0x74, 0x74, 0x75, 0x75, 0x75, 0x75, + 0x4a, 0x4a, 0x4b, 0x4b, 0x48, 0x48, 0x49, 0x49, 0x4e, 0x4e, 0x4f, 0x4f, + 0x4c, 0x4c, 0x4d, 0x4d, 0x42, 0x42, 0x43, 0x43, 0x40, 0x40, 0x41, 0x41, + 0x46, 0x46, 0x47, 0x47, 0x44, 0x44, 0x45, 0x45, 0x5a, 0x5a, 0x5b, 0x5b, + 0x58, 0x58, 0x59, 0x59, 0x5e, 0x5e, 0x5f, 0x5f, 0x5c, 0x5c, 0x5d, 0x5d, + 0x52, 0x52, 0x53, 0x53, 0x50, 0x50, 0x51, 0x51, 0x56, 0x56, 0x57, 0x57, + 0x54, 0x54, 0x55, 0x55, 0xd5, 0xd5, 0xd4, 0xd4, 0xd7, 0xd7, 0xd6, 0xd6, + 0xd1, 0xd1, 0xd0, 0xd0, 0xd3, 0xd3, 0xd2, 0xd2, 0xdd, 0xdd, 0xdc, 0xdc, + 0xdf, 0xdf, 0xde, 0xde, 0xd9, 0xd9, 0xd8, 0xd8, 0xdb, 0xdb, 0xda, 0xda, + 0xc5, 0xc5, 0xc4, 0xc4, 0xc7, 0xc7, 0xc6, 0xc6, 0xc1, 0xc1, 0xc0, 0xc0, + 0xc3, 0xc3, 0xc2, 0xc2, 0xcd, 0xcd, 0xcc, 0xcc, 0xcf, 0xcf, 0xce, 0xce, + 0xc9, 0xc9, 0xc8, 0xc8, 0xcb, 0xcb, 0xca, 0xca, 0xf5, 0xf5, 0xf5, 0xf5, + 0xf4, 0xf4, 0xf4, 0xf4, 0xf7, 0xf7, 0xf7, 0xf7, 0xf6, 0xf6, 0xf6, 0xf6, + 0xf1, 0xf1, 0xf1, 0xf1, 0xf0, 0xf0, 0xf0, 0xf0, 0xf3, 0xf3, 0xf3, 0xf3, + 0xf2, 0xf2, 0xf2, 0xf2, 0xfd, 0xfd, 0xfd, 0xfd, 0xfc, 0xfc, 0xfc, 0xfc, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0xfe, 0xfe, 0xf9, 0xf9, 0xf9, 0xf9, + 0xf8, 0xf8, 0xf8, 0xf8, 0xfb, 0xfb, 0xfb, 0xfb, 0xfa, 0xfa, 0xfa, 0xfa, + 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe5, 0xe4, 0xe4, 0xe4, 0xe4, + 0xe4, 0xe4, 0xe4, 0xe4, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, + 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe6, 0xe1, 0xe1, 0xe1, 0xe1, + 0xe1, 0xe1, 0xe1, 0xe1, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, 0xe0, + 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe3, 0xe2, 0xe2, 0xe2, 0xe2, + 0xe2, 0xe2, 0xe2, 0xe2, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed, 0xed, + 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xec, 0xef, 0xef, 0xef, 0xef, + 0xef, 0xef, 0xef, 0xef, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, + 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe9, 0xe8, 0xe8, 0xe8, 0xe8, + 0xe8, 0xe8, 0xe8, 0xe8, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, 0xeb, + 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0xea, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa + }; + return alaw_encode[(F32_TO_S16(val)>>3) + 0x1000]; +} + +static inline uint8_t f32_to_ulaw(float val) +{ + static uint8_t ulaw_encode[0x4000] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, + 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, + 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, + 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, + 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0e, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, + 0x0f, 0x0f, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, + 0x12, 0x12, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, + 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, + 0x15, 0x15, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, + 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, + 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, + 0x1b, 0x1b, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, + 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1d, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, + 0x1e, 0x1e, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, + 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, + 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, + 0x23, 0x23, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, + 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, + 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, 0x26, + 0x26, 0x26, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, + 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, + 0x29, 0x29, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, + 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2a, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, + 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2b, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, 0x2c, + 0x2c, 0x2c, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, + 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, + 0x2f, 0x2f, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, + 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, + 0x32, 0x32, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, + 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, + 0x35, 0x35, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, + 0x37, 0x37, 0x37, 0x37, 0x37, 0x37, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x38, 0x38, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, + 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3a, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, 0x3b, + 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, + 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3c, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, 0x3e, + 0x3e, 0x3e, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x40, 0x40, + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x40, 0x40, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x43, 0x43, + 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, + 0x43, 0x43, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, + 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x45, 0x46, 0x46, + 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, + 0x46, 0x46, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, + 0x47, 0x47, 0x47, 0x47, 0x47, 0x47, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, + 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x49, 0x49, + 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, 0x49, + 0x49, 0x49, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, + 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, + 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4c, 0x4c, + 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, + 0x4c, 0x4c, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, + 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4d, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, + 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4e, 0x4f, 0x4f, + 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, + 0x4f, 0x4f, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x51, 0x51, + 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, + 0x52, 0x52, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x54, 0x54, + 0x54, 0x54, 0x54, 0x54, 0x54, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, + 0x55, 0x55, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x57, 0x57, + 0x57, 0x57, 0x57, 0x57, 0x57, 0x57, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, + 0x58, 0x58, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x5a, 0x5a, + 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, + 0x5b, 0x5b, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5d, 0x5d, + 0x5d, 0x5d, 0x5d, 0x5d, 0x5d, 0x5d, 0x5e, 0x5e, 0x5e, 0x5e, 0x5e, 0x5e, + 0x5e, 0x5e, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x60, 0x60, + 0x60, 0x60, 0x61, 0x61, 0x61, 0x61, 0x62, 0x62, 0x62, 0x62, 0x63, 0x63, + 0x63, 0x63, 0x64, 0x64, 0x64, 0x64, 0x65, 0x65, 0x65, 0x65, 0x66, 0x66, + 0x66, 0x66, 0x67, 0x67, 0x67, 0x67, 0x68, 0x68, 0x68, 0x68, 0x69, 0x69, + 0x69, 0x69, 0x6a, 0x6a, 0x6a, 0x6a, 0x6b, 0x6b, 0x6b, 0x6b, 0x6c, 0x6c, + 0x6c, 0x6c, 0x6d, 0x6d, 0x6d, 0x6d, 0x6e, 0x6e, 0x6e, 0x6e, 0x6f, 0x6f, + 0x6f, 0x6f, 0x70, 0x70, 0x71, 0x71, 0x72, 0x72, 0x73, 0x73, 0x74, 0x74, + 0x75, 0x75, 0x76, 0x76, 0x77, 0x77, 0x78, 0x78, 0x79, 0x79, 0x7a, 0x7a, + 0x7b, 0x7b, 0x7c, 0x7c, 0x7d, 0x7d, 0x7e, 0x7e, 0xff, 0xfe, 0xfe, 0xfd, + 0xfd, 0xfc, 0xfc, 0xfb, 0xfb, 0xfa, 0xfa, 0xf9, 0xf9, 0xf8, 0xf8, 0xf7, + 0xf7, 0xf6, 0xf6, 0xf5, 0xf5, 0xf4, 0xf4, 0xf3, 0xf3, 0xf2, 0xf2, 0xf1, + 0xf1, 0xf0, 0xf0, 0xef, 0xef, 0xef, 0xef, 0xee, 0xee, 0xee, 0xee, 0xed, + 0xed, 0xed, 0xed, 0xec, 0xec, 0xec, 0xec, 0xeb, 0xeb, 0xeb, 0xeb, 0xea, + 0xea, 0xea, 0xea, 0xe9, 0xe9, 0xe9, 0xe9, 0xe8, 0xe8, 0xe8, 0xe8, 0xe7, + 0xe7, 0xe7, 0xe7, 0xe6, 0xe6, 0xe6, 0xe6, 0xe5, 0xe5, 0xe5, 0xe5, 0xe4, + 0xe4, 0xe4, 0xe4, 0xe3, 0xe3, 0xe3, 0xe3, 0xe2, 0xe2, 0xe2, 0xe2, 0xe1, + 0xe1, 0xe1, 0xe1, 0xe0, 0xe0, 0xe0, 0xe0, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, + 0xdf, 0xdf, 0xdf, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xde, 0xdd, + 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdc, 0xdc, 0xdc, 0xdc, 0xdc, + 0xdc, 0xdc, 0xdc, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xdb, 0xda, + 0xda, 0xda, 0xda, 0xda, 0xda, 0xda, 0xda, 0xd9, 0xd9, 0xd9, 0xd9, 0xd9, + 0xd9, 0xd9, 0xd9, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd8, 0xd7, + 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd7, 0xd6, 0xd6, 0xd6, 0xd6, 0xd6, + 0xd6, 0xd6, 0xd6, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd4, + 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd4, 0xd3, 0xd3, 0xd3, 0xd3, 0xd3, + 0xd3, 0xd3, 0xd3, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd2, 0xd1, + 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd1, 0xd0, 0xd0, 0xd0, 0xd0, 0xd0, + 0xd0, 0xd0, 0xd0, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, + 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xce, 0xce, 0xce, 0xce, 0xce, + 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xcd, + 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, + 0xcd, 0xcd, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, + 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xca, + 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, 0xca, + 0xca, 0xca, 0xca, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, + 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc9, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, + 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc8, 0xc7, + 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, + 0xc7, 0xc7, 0xc7, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, + 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, + 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc5, 0xc4, + 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, + 0xc4, 0xc4, 0xc4, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, + 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc3, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, + 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xc1, + 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, 0xc1, + 0xc1, 0xc1, 0xc1, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, + 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, 0xbf, + 0xbf, 0xbf, 0xbf, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, + 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, + 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbd, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, 0xbc, + 0xbc, 0xbc, 0xbc, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, + 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xba, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, 0xb9, + 0xb9, 0xb9, 0xb9, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, + 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb8, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, + 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb7, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, 0xb6, + 0xb6, 0xb6, 0xb6, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, + 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb5, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, + 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb4, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, 0xb3, + 0xb3, 0xb3, 0xb3, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, + 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb2, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, + 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb1, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, + 0xb0, 0xb0, 0xb0, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, + 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xaf, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, + 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xae, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, 0xad, + 0xad, 0xad, 0xad, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, + 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xac, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, + 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xab, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, + 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa9, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, + 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa8, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, 0xa7, + 0xa7, 0xa7, 0xa7, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, + 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, + 0xa4, 0xa4, 0xa4, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, + 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa3, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, + 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa2, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1, + 0xa1, 0xa1, 0xa1, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, + 0x9f, 0x9f, 0x9f, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, + 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9e, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, + 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9d, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, 0x9c, + 0x9c, 0x9c, 0x9c, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, + 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, + 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, + 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x98, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, + 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, 0x96, + 0x96, 0x96, 0x96, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, + 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x95, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, + 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, + 0x93, 0x93, 0x93, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, + 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, + 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x91, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, + 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8f, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, + 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8e, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, + 0x8d, 0x8d, 0x8d, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, + 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8c, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, + 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8b, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, 0x8a, + 0x8a, 0x8a, 0x8a, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, + 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x89, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, 0x87, + 0x87, 0x87, 0x87, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, + 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, + 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, + 0x84, 0x84, 0x84, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, + 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, + 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, + 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80 }; + return ulaw_encode[(F32_TO_S16(val)>>2) + 0x2000]; +} diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build new file mode 100644 index 0000000..f08527a --- /dev/null +++ b/spa/plugins/audioconvert/meson.build @@ -0,0 +1,206 @@ +audioconvert_sources = [ + 'audioadapter.c', + 'audioconvert.c', + 'plugin.c' +] + +simd_cargs = [] +simd_dependencies = [] + +audioconvert_c = static_library('audioconvert_c', + [ 'channelmix-ops-c.c', + 'biquad.c', + 'crossover.c', + 'volume-ops-c.c', + 'peaks-ops-c.c', + 'resample-native-c.c', + 'fmt-ops-c.c' ], + c_args : ['-Ofast', '-ffast-math'], + dependencies : [ spa_dep ], + install : false + ) +simd_dependencies += audioconvert_c + +if have_sse + audioconvert_sse = static_library('audioconvert_sse', + ['resample-native-sse.c', + 'volume-ops-sse.c', + 'peaks-ops-sse.c', + 'channelmix-ops-sse.c' ], + c_args : [sse_args, '-Ofast', '-DHAVE_SSE'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE'] + simd_dependencies += audioconvert_sse +endif +if have_sse2 + audioconvert_sse2 = static_library('audioconvert_sse2', + ['fmt-ops-sse2.c' ], + c_args : [sse2_args, '-O3', '-DHAVE_SSE2'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE2'] + simd_dependencies += audioconvert_sse2 +endif +if have_ssse3 + audioconvert_ssse3 = static_library('audioconvert_ssse3', + ['fmt-ops-ssse3.c', + 'resample-native-ssse3.c' ], + c_args : [ssse3_args, '-O3', '-DHAVE_SSSE3'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSSE3'] + simd_dependencies += audioconvert_ssse3 +endif +if have_sse41 + audioconvert_sse41 = static_library('audioconvert_sse41', + ['fmt-ops-sse41.c'], + c_args : [sse41_args, '-O3', '-DHAVE_SSE41'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE41'] + simd_dependencies += audioconvert_sse41 +endif +if have_avx and have_fma + audioconvert_avx = static_library('audioconvert_avx', + ['resample-native-avx.c'], + c_args : [avx_args, fma_args, '-O3', '-DHAVE_AVX', '-DHAVE_FMA'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_AVX', '-DHAVE_FMA'] + simd_dependencies += audioconvert_avx +endif +if have_avx2 + audioconvert_avx2 = static_library('audioconvert_avx2', + ['fmt-ops-avx2.c'], + c_args : [avx2_args, '-O3', '-DHAVE_AVX2'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_AVX2'] + simd_dependencies += audioconvert_avx2 +endif + +if have_neon + audioconvert_neon = static_library('audioconvert_neon', + ['resample-native-neon.c', + 'fmt-ops-neon.c' ], + c_args : [neon_args, '-O3', '-DHAVE_NEON'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_NEON'] + simd_dependencies += audioconvert_neon +endif + +audioconvert_lib = static_library('audioconvert', + ['fmt-ops.c', + 'channelmix-ops.c', + 'peaks-ops.c', + 'resample-native.c', + 'resample-peaks.c', + 'volume-ops.c' ], + c_args : [ simd_cargs, '-O3'], + link_with : simd_dependencies, + include_directories : [configinc], + dependencies : [ spa_dep ], + install : false + ) +audioconvert_dep = declare_dependency(link_with: audioconvert_lib) + +spa_audioconvert_lib = shared_library('spa-audioconvert', + audioconvert_sources, + c_args : simd_cargs, + dependencies : [ spa_dep, mathlib, audioconvert_dep ], + install : true, + install_dir : spa_plugindir / 'audioconvert') +spa_audioconvert_dep = declare_dependency(link_with: spa_audioconvert_lib) + +test_lib = static_library('test_lib', + ['test-source.c' ], + c_args : ['-O3'], + dependencies : [ spa_dep ], + install : false + ) + +test_apps = [ + 'test-audioadapter', + 'test-audioconvert', + 'test-channelmix', + 'test-fmt-ops', + 'test-peaks', + 'test-resample', + ] + +foreach a : test_apps + test(a, + executable(a, a + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audioconvert_dep, spa_audioconvert_dep ], + include_directories : [ configinc ], + link_with : [ test_lib ], + install_rpath : spa_plugindir / 'audioconvert', + c_args : [ simd_cargs ], + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'audioconvert'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'audioconvert' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'audioconvert', + configuration: test_conf + ) + endif +endforeach + +benchmark_apps = [ + 'benchmark-fmt-ops', + 'benchmark-resample', + ] + +foreach a : benchmark_apps + benchmark(a, + executable(a, a + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audioconvert_dep, spa_audioconvert_dep ], + include_directories : [ configinc ], + c_args : [ simd_cargs ], + install_rpath : spa_plugindir / 'audioconvert', + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'audioconvert'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'audioconvert' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'audioconvert', + configuration: test_conf + ) + endif +endforeach + +if sndfile_dep.found() + sparesample_sources = [ + 'spa-resample.c', + ] + executable('spa-resample', + sparesample_sources, + link_with : [ test_lib ], + dependencies : [ spa_dep, sndfile_dep, mathlib, audioconvert_dep ], + install : true, + ) +endif diff --git a/spa/plugins/audioconvert/peaks-ops-c.c b/spa/plugins/audioconvert/peaks-ops-c.c new file mode 100644 index 0000000..45ab1dc --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops-c.c @@ -0,0 +1,50 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include "peaks-ops.h" + +void peaks_min_max_c(struct peaks *peaks, const float * SPA_RESTRICT src, + uint32_t n_samples, float *min, float *max) +{ + uint32_t n; + float t, mi = *min, ma = *max; + for (n = 0; n < n_samples; n++) { + t = src[n]; + mi = fminf(mi, t); + ma = fmaxf(ma, t); + } + *min = mi; + *max = ma; +} + +float peaks_abs_max_c(struct peaks *peaks, const float * SPA_RESTRICT src, + uint32_t n_samples, float max) +{ + uint32_t n; + for (n = 0; n < n_samples; n++) + max = fmaxf(fabsf(src[n]), max); + return max; +} diff --git a/spa/plugins/audioconvert/peaks-ops-sse.c b/spa/plugins/audioconvert/peaks-ops-sse.c new file mode 100644 index 0000000..7ceb2a8 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops-sse.c @@ -0,0 +1,122 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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..b5cb252 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops.c @@ -0,0 +1,89 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..29da794 --- /dev/null +++ b/spa/plugins/audioconvert/peaks-ops.h @@ -0,0 +1,72 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..03c206f --- /dev/null +++ b/spa/plugins/audioconvert/plugin.c @@ -0,0 +1,50 @@ +/* Spa Audioconvert plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_audioconvert_factory; +extern const struct spa_handle_factory spa_audioadapter_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_audioconvert_factory; + break; + case 1: + *factory = &spa_audioadapter_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/audioconvert/resample-native-avx.c b/spa/plugins/audioconvert/resample-native-avx.c new file mode 100644 index 0000000..136d6cb --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-avx.c @@ -0,0 +1,94 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "resample-native-impl.h" + +#include +#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 = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 0)); + sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(taps + i + 0), sy[0]); + ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 8)); + sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(taps + i + 8), sy[1]); + } + sy[0] = _mm256_add_ps(sy[1], sy[0]); + sx[1] = _mm256_extractf128_ps(sy[0], 1); + sx[0] = _mm256_extractf128_ps(sy[0], 0); + for (; i < n_taps; i += 8) { + tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 0)); + sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(taps + i + 0), sx[0]); + tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 4)); + sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(taps + i + 4), sx[1]); + } + sx[0] = _mm_add_ps(sx[0], sx[1]); + sx[0] = _mm_hadd_ps(sx[0], sx[0]); + sx[0] = _mm_hadd_ps(sx[0], sx[0]); + _mm_store_ss(d, sx[0]); +} + +static inline void inner_product_ip_avx(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x, + uint32_t n_taps) +{ + __m256 sy[2] = { _mm256_setzero_ps(), _mm256_setzero_ps() }, ty; + __m128 sx[2], tx; + uint32_t i, n_taps4 = n_taps & ~0xf; + + for (i = 0; i < n_taps4; i += 16) { + ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 0)); + sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(t0 + i + 0), sy[0]); + sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(t1 + i + 0), sy[1]); + ty = (__m256)_mm256_lddqu_si256((__m256i*)(s + i + 8)); + sy[0] = _mm256_fmadd_ps(ty, _mm256_load_ps(t0 + i + 8), sy[0]); + sy[1] = _mm256_fmadd_ps(ty, _mm256_load_ps(t1 + i + 8), sy[1]); + } + sx[0] = _mm_add_ps(_mm256_extractf128_ps(sy[0], 0), _mm256_extractf128_ps(sy[0], 1)); + sx[1] = _mm_add_ps(_mm256_extractf128_ps(sy[1], 0), _mm256_extractf128_ps(sy[1], 1)); + + for (; i < n_taps; i += 8) { + tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 0)); + sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(t0 + i + 0), sx[0]); + sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(t1 + i + 0), sx[1]); + tx = (__m128)_mm_lddqu_si128((__m128i*)(s + i + 4)); + sx[0] = _mm_fmadd_ps(tx, _mm_load_ps(t0 + i + 4), sx[0]); + sx[1] = _mm_fmadd_ps(tx, _mm_load_ps(t1 + i + 4), sx[1]); + } + sx[1] = _mm_mul_ps(_mm_sub_ps(sx[1], sx[0]), _mm_load1_ps(&x)); + sx[0] = _mm_add_ps(sx[0], sx[1]); + sx[0] = _mm_hadd_ps(sx[0], sx[0]); + sx[0] = _mm_hadd_ps(sx[0], sx[0]); + _mm_store_ss(d, sx[0]); +} + +MAKE_RESAMPLER_FULL(avx); +MAKE_RESAMPLER_INTER(avx); diff --git a/spa/plugins/audioconvert/resample-native-c.c b/spa/plugins/audioconvert/resample-native-c.c new file mode 100644 index 0000000..ce6c57d --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-c.c @@ -0,0 +1,65 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "resample-native-impl.h" + +static inline void inner_product_c(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT taps, uint32_t n_taps) +{ + float sum = 0.0f; +#if 1 + uint32_t i, j, nt2 = n_taps/2; + for (i = 0, j = n_taps-1; i < nt2; i++, j--) + sum += s[i] * taps[i] + s[j] * taps[j]; +#else + uint32_t i; + for (i = 0; i < n_taps; i++) + sum += s[i] * taps[i]; +#endif + *d = sum; +} + +static inline void inner_product_ip_c(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x, + uint32_t n_taps) +{ + float sum[2] = { 0.0f, 0.0f }; + uint32_t i; +#if 1 + uint32_t j, nt2 = n_taps/2; + for (i = 0, j = n_taps-1; i < nt2; i++, j--) { + sum[0] += s[i] * t0[i] + s[j] * t0[j]; + sum[1] += s[i] * t1[i] + s[j] * t1[j]; + } +#else + for (i = 0; i < n_taps; i++) { + sum[0] += s[i] * t0[i]; + sum[1] += s[i] * t1[i]; + } +#endif + *d = (sum[1] - sum[0]) * x + sum[0]; +} + +MAKE_RESAMPLER_FULL(c); +MAKE_RESAMPLER_INTER(c); diff --git a/spa/plugins/audioconvert/resample-native-impl.h b/spa/plugins/audioconvert/resample-native-impl.h new file mode 100644 index 0000000..5dfc40e --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-impl.h @@ -0,0 +1,191 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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; + uint32_t phase; + uint32_t inc; + uint32_t frac; + uint32_t filter_stride; + uint32_t filter_stride_os; + uint32_t hist; + float **history; + resample_func_t func; + float *filter; + float *hist_mem; + const struct resample_info *info; +}; + +#define DEFINE_RESAMPLER(type,arch) \ +void do_resample_##type##_##arch(struct resample *r, \ + const void * SPA_RESTRICT src[], uint32_t ioffs, uint32_t *in_len, \ + void * SPA_RESTRICT dst[], uint32_t ooffs, uint32_t *out_len) + +#define MAKE_RESAMPLER_COPY(arch) \ +DEFINE_RESAMPLER(copy,arch) \ +{ \ + struct native_data *data = r->data; \ + uint32_t index, n_taps = data->n_taps, n_taps2 = n_taps/2; \ + uint32_t c, olen = *out_len, ilen = *in_len; \ + \ + if (r->channels == 0) \ + return; \ + \ + index = ioffs; \ + if (ooffs < olen && index + n_taps <= ilen) { \ + uint32_t to_copy = SPA_MIN(olen - ooffs, \ + ilen - (index + n_taps) + 1); \ + for (c = 0; c < r->channels; c++) { \ + const float *s = src[c]; \ + float *d = dst[c]; \ + spa_memcpy(&d[ooffs], &s[index + n_taps2], \ + to_copy * sizeof(float)); \ + } \ + index += to_copy; \ + ooffs += to_copy; \ + } \ + *in_len = index; \ + *out_len = ooffs; \ +} + +#define INC(index,phase,n_phases) \ + index += inc; \ + phase += frac; \ + if (phase >= n_phases) { \ + phase -= n_phases; \ + index += 1; \ + } + +#define MAKE_RESAMPLER_FULL(arch) \ +DEFINE_RESAMPLER(full,arch) \ +{ \ + struct native_data *data = r->data; \ + uint32_t n_taps = data->n_taps, stride = data->filter_stride_os; \ + uint32_t index, phase, n_phases = data->out_rate; \ + uint32_t c, o, olen = *out_len, ilen = *in_len; \ + uint32_t inc = data->inc, frac = data->frac; \ + \ + if (r->channels == 0) \ + return; \ + \ + for (c = 0; c < r->channels; c++) { \ + const float *s = src[c]; \ + float *d = dst[c]; \ + \ + index = ioffs; \ + phase = data->phase; \ + \ + for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ + inner_product_##arch(&d[o], &s[index], \ + &data->filter[phase * stride], \ + n_taps); \ + INC(index, phase, n_phases); \ + } \ + } \ + *in_len = index; \ + *out_len = o; \ + data->phase = phase; \ +} + +#define MAKE_RESAMPLER_INTER(arch) \ +DEFINE_RESAMPLER(inter,arch) \ +{ \ + struct native_data *data = r->data; \ + uint32_t index, phase, stride = data->filter_stride; \ + uint32_t n_phases = data->n_phases, out_rate = data->out_rate; \ + uint32_t n_taps = data->n_taps; \ + uint32_t c, o, olen = *out_len, ilen = *in_len; \ + uint32_t inc = data->inc, frac = data->frac; \ + \ + if (r->channels == 0) \ + return; \ + \ + for (c = 0; c < r->channels; c++) { \ + const float *s = src[c]; \ + float *d = dst[c]; \ + \ + index = ioffs; \ + phase = data->phase; \ + \ + for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ + float ph = (float)phase * n_phases / out_rate; \ + uint32_t offset = floorf(ph); \ + inner_product_ip_##arch(&d[o], &s[index], \ + &data->filter[(offset + 0) * stride], \ + &data->filter[(offset + 1) * stride], \ + ph - offset, n_taps); \ + INC(index, phase, out_rate); \ + } \ + } \ + *in_len = index; \ + *out_len = o; \ + data->phase = phase; \ +} + + +DEFINE_RESAMPLER(copy,c); +DEFINE_RESAMPLER(full,c); +DEFINE_RESAMPLER(inter,c); + +#if defined (HAVE_NEON) +DEFINE_RESAMPLER(full,neon); +DEFINE_RESAMPLER(inter,neon); +#endif +#if defined (HAVE_SSE) +DEFINE_RESAMPLER(full,sse); +DEFINE_RESAMPLER(inter,sse); +#endif +#if defined (HAVE_SSSE3) +DEFINE_RESAMPLER(full,ssse3); +DEFINE_RESAMPLER(inter,ssse3); +#endif +#if defined (HAVE_AVX) && defined(HAVE_FMA) +DEFINE_RESAMPLER(full,avx); +DEFINE_RESAMPLER(inter,avx); +#endif diff --git a/spa/plugins/audioconvert/resample-native-neon.c b/spa/plugins/audioconvert/resample-native-neon.c new file mode 100644 index 0000000..079152a --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-neon.c @@ -0,0 +1,218 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "resample-native-impl.h" + +#include + +static inline void inner_product_neon(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT taps, uint32_t n_taps) +{ + unsigned int remainder = n_taps % 16; + n_taps = n_taps - remainder; + +#ifdef __aarch64__ + asm volatile( + " cmp %[n_taps], #0\n" + " bne 1f\n" + " ld1 {v4.4s}, [%[taps]], #16\n" + " ld1 {v8.4s}, [%[s]], #16\n" + " subs %[remainder], %[remainder], #4\n" + " fmul v0.4s, v4.4s, v8.4s\n" + " bne 4f\n" + " b 5f\n" + "1:" + " ld1 {v4.4s, v5.4s, v6.4s, v7.4s}, [%[taps]], #64\n" + " ld1 {v8.4s, v9.4s, v10.4s, v11.4s}, [%[s]], #64\n" + " subs %[n_taps], %[n_taps], #16\n" + " fmul v0.4s, v4.4s, v8.4s\n" + " fmul v1.4s, v5.4s, v9.4s\n" + " fmul v2.4s, v6.4s, v10.4s\n" + " fmul v3.4s, v7.4s, v11.4s\n" + " beq 3f\n" + "2:" + " ld1 {v4.4s, v5.4s, v6.4s, v7.4s}, [%[taps]], #64\n" + " ld1 {v8.4s, v9.4s, v10.4s, v11.4s}, [%[s]], #64\n" + " subs %[n_taps], %[n_taps], #16\n" + " fmla v0.4s, v4.4s, v8.4s\n" + " fmla v1.4s, v5.4s, v9.4s\n" + " fmla v2.4s, v6.4s, v10.4s\n" + " fmla v3.4s, v7.4s, v11.4s\n" + " bne 2b\n" + "3:" + " fadd v4.4s, v0.4s, v1.4s\n" + " fadd v5.4s, v2.4s, v3.4s\n" + " cmp %[remainder], #0\n" + " fadd v0.4s, v4.4s, v5.4s\n" + " beq 5f\n" + "4:" + " ld1 {v6.4s}, [%[taps]], #16\n" + " ld1 {v10.4s}, [%[s]], #16\n" + " subs %[remainder], %[remainder], #4\n" + " fmla v0.4s, v6.4s, v10.4s\n" + " bne 4b\n" + "5:" + " faddp v0.4s, v0.4s, v0.4s\n" + " faddp v0.2s, v0.2s, v0.2s\n" + " str s0, [%[d]]\n" + : [d] "+r" (d), [s] "+r" (s), [taps] "+r" (taps), + [n_taps] "+r" (n_taps), [remainder] "+r" (remainder) + : + : "cc", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", + "v9", "v10", "v11"); +#else + asm volatile ( + " cmp %[n_taps], #0\n" + " bne 1f\n" + " vld1.32 {q4}, [%[taps] :128]!\n" + " vld1.32 {q8}, [%[s]]!\n" + " subs %[remainder], %[remainder], #4\n" + " vmul.f32 q0, q4, q8\n" + " bne 4f\n" + " b 5f\n" + "1:" + " vld1.32 {q4, q5}, [%[taps] :128]!\n" + " vld1.32 {q8, q9}, [%[s]]!\n" + " vld1.32 {q6, q7}, [%[taps] :128]!\n" + " vld1.32 {q10, q11}, [%[s]]!\n" + " subs %[n_taps], %[n_taps], #16\n" + " vmul.f32 q0, q4, q8\n" + " vmul.f32 q1, q5, q9\n" + " vmul.f32 q2, q6, q10\n" + " vmul.f32 q3, q7, q11\n" + " beq 3f\n" + "2:" + " vld1.32 {q4, q5}, [%[taps] :128]!\n" + " vld1.32 {q8, q9}, [%[s]]!\n" + " vld1.32 {q6, q7}, [%[taps] :128]!\n" + " vld1.32 {q10, q11}, [%[s]]!\n" + " subs %[n_taps], %[n_taps], #16\n" + " vmla.f32 q0, q4, q8\n" + " vmla.f32 q1, q5, q9\n" + " vmla.f32 q2, q6, q10\n" + " vmla.f32 q3, q7, q11\n" + " bne 2b\n" + "3:" + " vadd.f32 q4, q0, q1\n" + " vadd.f32 q5, q2, q3\n" + " cmp %[remainder], #0\n" + " vadd.f32 q0, q4, q5\n" + " beq 5f\n" + "4:" + " vld1.32 {q6}, [%[taps] :128]!\n" + " vld1.32 {q10}, [%[s]]!\n" + " subs %[remainder], %[remainder], #4\n" + " vmla.f32 q0, q6, q10\n" + " bne 4b\n" + "5:" + " vadd.f32 d0, d0, d1\n" + " vpadd.f32 d0, d0, d0\n" + " vstr d0, [%[d]]\n" + : [d] "+r" (d), [s] "+r" (s), [taps] "+r" (taps), + [n_taps] "+l" (n_taps), [remainder] "+l" (remainder) + : + : "cc", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", + "q9", "q10", "q11"); +#endif +} + +static inline void inner_product_ip_neon(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x, + uint32_t n_taps) +{ +#ifdef __aarch64__ + asm volatile( + " dup v10.4s, %w[x]\n" + " ld1 {v4.4s, v5.4s}, [%[t0]], #32\n" + " ld1 {v8.4s, v9.4s}, [%[s]], #32\n" + " ld1 {v6.4s, v7.4s}, [%[t1]], #32\n" + " subs %[n_taps], %[n_taps], #8\n" + " fmul v0.4s, v4.4s, v8.4s\n" + " fmul v1.4s, v5.4s, v9.4s\n" + " fmul v2.4s, v6.4s, v8.4s\n" + " fmul v3.4s, v7.4s, v9.4s\n" + " beq 3f\n" + "2:" + " ld1 {v4.4s, v5.4s}, [%[t0]], #32\n" + " ld1 {v8.4s, v9.4s}, [%[s]], #32\n" + " ld1 {v6.4s, v7.4s}, [%[t1]], #32\n" + " subs %[n_taps], %[n_taps], #8\n" + " fmla v0.4s, v4.4s, v8.4s\n" + " fmla v1.4s, v5.4s, v9.4s\n" + " fmla v2.4s, v6.4s, v8.4s\n" + " fmla v3.4s, v7.4s, v9.4s\n" + " bne 2b\n" + "3:" + " fadd v0.4s, v0.4s, v1.4s\n" /* sum[0] */ + " fadd v2.4s, v2.4s, v3.4s\n" /* sum[1] */ + " fsub v2.4s, v2.4s, v0.4s\n" /* sum[1] -= sum[0] */ + " fmla v0.4s, v2.4s, v10.4s\n" /* sum[0] += sum[1] * x */ + " faddp v0.4s, v0.4s, v0.4s\n" + " faddp v0.2s, v0.2s, v0.2s\n" + " str s0, [%[d]]\n" + : [d] "+r" (d), [s] "+r" (s), [t0] "+r" (t0), [t1] "+r" (t1), + [n_taps] "+r" (n_taps), [x] "+r" (x) + : + : "cc", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", + "v9", "v10"); +#else + asm volatile( + " vdup.32 q10, %[x]\n" + " vld1.32 {q4, q5}, [%[t0] :128]!\n" + " vld1.32 {q8, q9}, [%[s]]!\n" + " vld1.32 {q6, q7}, [%[t1] :128]!\n" + " subs %[n_taps], %[n_taps], #8\n" + " vmul.f32 q0, q4, q8\n" + " vmul.f32 q1, q5, q9\n" + " vmul.f32 q2, q6, q8\n" + " vmul.f32 q3, q7, q9\n" + " beq 3f\n" + "2:" + " vld1.32 {q4, q5}, [%[t0] :128]!\n" + " vld1.32 {q8, q9}, [%[s]]!\n" + " vld1.32 {q6, q7}, [%[t1] :128]!\n" + " subs %[n_taps], %[n_taps], #8\n" + " vmla.f32 q0, q4, q8\n" + " vmla.f32 q1, q5, q9\n" + " vmla.f32 q2, q6, q8\n" + " vmla.f32 q3, q7, q9\n" + " bne 2b\n" + "3:" + " vadd.f32 q0, q0, q1\n" /* sum[0] */ + " vadd.f32 q2, q2, q3\n" /* sum[1] */ + " vsub.f32 q2, q2, q0\n" /* sum[1] -= sum[0] */ + " vmla.f32 q0, q2, q10\n" /* sum[0] += sum[1] * x */ + " vadd.f32 d0, d0, d1\n" + " vpadd.f32 d0, d0, d0\n" + " vstr d0, [%[d]]\n" + : [d] "+r" (d), [s] "+r" (s), [t0] "+r" (t0), [t1] "+r" (t1), + [n_taps] "+l" (n_taps), [x] "+l" (x) + : + : "cc", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", + "q9", "q10"); +#endif +} + +MAKE_RESAMPLER_FULL(neon); +MAKE_RESAMPLER_INTER(neon); diff --git a/spa/plugins/audioconvert/resample-native-sse.c b/spa/plugins/audioconvert/resample-native-sse.c new file mode 100644 index 0000000..fcdb32c --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-sse.c @@ -0,0 +1,94 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "resample-native-impl.h" + +#include + +static inline void inner_product_sse(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT taps, uint32_t n_taps) +{ + __m128 sum = _mm_setzero_ps(); + uint32_t i = 0; +#if 0 + uint32_t unrolled = n_taps & ~15; + + for (i = 0; i < unrolled; i += 16) { + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 0), + _mm_load_ps(taps + i + 0))); + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 4), + _mm_load_ps(taps + i + 4))); + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 8), + _mm_load_ps(taps + i + 8))); + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 12), + _mm_load_ps(taps + i + 12))); + } +#endif + for (; i < n_taps; i += 8) { + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 0), + _mm_load_ps(taps + i + 0))); + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_loadu_ps(s + i + 4), + _mm_load_ps(taps + i + 4))); + } + sum = _mm_add_ps(sum, _mm_movehl_ps(sum, sum)); + sum = _mm_add_ss(sum, _mm_shuffle_ps(sum, sum, 0x55)); + _mm_store_ss(d, sum); +} + +static inline void inner_product_ip_sse(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x, + uint32_t n_taps) +{ + __m128 sum[2] = { _mm_setzero_ps (), _mm_setzero_ps () }, t; + uint32_t i; + + for (i = 0; i < n_taps; i += 8) { + t = _mm_loadu_ps(s + i + 0); + sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(t, _mm_load_ps(t0 + i + 0))); + sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(t, _mm_load_ps(t1 + i + 0))); + t = _mm_loadu_ps(s + i + 4); + sum[0] = _mm_add_ps(sum[0], _mm_mul_ps(t, _mm_load_ps(t0 + i + 4))); + sum[1] = _mm_add_ps(sum[1], _mm_mul_ps(t, _mm_load_ps(t1 + i + 4))); + } + sum[1] = _mm_mul_ps(_mm_sub_ps(sum[1], sum[0]), _mm_load1_ps(&x)); + sum[0] = _mm_add_ps(sum[0], sum[1]); + sum[0] = _mm_add_ps(sum[0], _mm_movehl_ps(sum[0], sum[0])); + sum[0] = _mm_add_ss(sum[0], _mm_shuffle_ps(sum[0], sum[0], 0x55)); + _mm_store_ss(d, sum[0]); +} + +MAKE_RESAMPLER_FULL(sse); +MAKE_RESAMPLER_INTER(sse); diff --git a/spa/plugins/audioconvert/resample-native-ssse3.c b/spa/plugins/audioconvert/resample-native-ssse3.c new file mode 100644 index 0000000..ac3675f --- /dev/null +++ b/spa/plugins/audioconvert/resample-native-ssse3.c @@ -0,0 +1,115 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "resample-native-impl.h" + +#include + +static inline void inner_product_ssse3(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT taps, uint32_t n_taps) +{ + __m128 sum = _mm_setzero_ps(); + __m128 t0, t1; + uint32_t i; + + switch (SPA_PTR_ALIGNMENT(s, 16)) { + case 0: + for (i = 0; i < n_taps; i += 8) { + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_load_ps(s + i + 0), + _mm_load_ps(taps + i + 0))); + sum = _mm_add_ps(sum, + _mm_mul_ps( + _mm_load_ps(s + i + 4), + _mm_load_ps(taps + i + 4))); + } + break; + case 4: + t0 = _mm_load_ps(s - 1); + for (i = 0; i < n_taps; i += 8) { + t1 = _mm_load_ps(s + i + 3); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 4); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 0))); + t0 = t1; + t1 = _mm_load_ps(s + i + 7); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 4); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 4))); + t0 = t1; + } + break; + case 8: + t0 = _mm_load_ps(s - 2); + for (i = 0; i < n_taps; i += 8) { + t1 = _mm_load_ps(s + i + 2); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 8); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 0))); + t0 = t1; + t1 = _mm_load_ps(s + i + 6); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 8); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 4))); + t0 = t1; + } + break; + case 12: + t0 = _mm_load_ps(s - 3); + for (i = 0; i < n_taps; i += 8) { + t1 = _mm_load_ps(s + i + 1); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 12); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 0))); + t0 = t1; + t1 = _mm_load_ps(s + i + 5); + t0 = (__m128)_mm_alignr_epi8((__m128i)t1, (__m128i)t0, 12); + sum = _mm_add_ps(sum, + _mm_mul_ps(t0, _mm_load_ps(taps + i + 4))); + t0 = t1; + } + break; + } + sum = _mm_add_ps(sum, _mm_movehdup_ps(sum)); + sum = _mm_add_ss(sum, _mm_movehl_ps(sum, sum)); + _mm_store_ss(d, sum); +} + +static inline void inner_product_ip_ssse3(float *d, const float * SPA_RESTRICT s, + const float * SPA_RESTRICT t0, const float * SPA_RESTRICT t1, float x, + uint32_t n_taps) +{ + float sum[2] = { 0.0f, 0.0f }; + uint32_t i; + + for (i = 0; i < n_taps; i++) { + sum[0] += s[i] * t0[i]; + sum[1] += s[i] * t1[i]; + } + *d = (sum[1] - sum[0]) * x + sum[0]; +} + +MAKE_RESAMPLER_FULL(ssse3); +MAKE_RESAMPLER_INTER(ssse3); diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c new file mode 100644 index 0000000..05ce54b --- /dev/null +++ b/spa/plugins/audioconvert/resample-native.c @@ -0,0 +1,400 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +#include "resample-native-impl.h" + +struct quality { + uint32_t n_taps; + double cutoff; +}; + +static const struct quality window_qualities[] = { + { 8, 0.53, }, + { 16, 0.67, }, + { 24, 0.75, }, + { 32, 0.80, }, + { 48, 0.85, }, /* default */ + { 64, 0.88, }, + { 80, 0.895, }, + { 96, 0.910, }, + { 128, 0.936, }, + { 144, 0.945, }, + { 160, 0.950, }, + { 192, 0.960, }, + { 256, 0.970, }, + { 896, 0.990, }, + { 1024, 0.995, }, +}; + +static inline double sinc(double x) +{ + if (x < 1e-6) return 1.0; + x *= M_PI; + return sin(x) / x; +} + +static inline double window_blackman(double x, double n_taps) +{ + double alpha = 0.232, r; + x = 2.0 * M_PI * x / n_taps; + r = (1.0 - alpha) / 2.0 + (1.0 / 2.0) * cos(x) + + (alpha / 2.0) * cos(2.0 * x); + return r; +} + +static inline double window_cosh(double x, double n_taps) +{ + double r; + double A = 16.97789; + double x2; + x = 2.0 * x / n_taps; + x2 = x * x; + if (x2 >= 1.0) + return 0.0; + /* doi:10.1109/RME.2008.4595727 with tweak */ + r = (exp(A * sqrt(1 - x2)) - 1) / (exp(A) - 1); + return r; +} + +#define window (1 ? window_cosh : window_blackman) + +static int build_filter(float *taps, uint32_t stride, uint32_t n_taps, uint32_t n_phases, double cutoff) +{ + uint32_t i, j, n_taps12 = n_taps/2; + + for (i = 0; i <= n_phases; i++) { + double t = (double) i / (double) n_phases; + for (j = 0; j < n_taps12; j++, t += 1.0) { + /* exploit symmetry in filter taps */ + taps[(n_phases - i) * stride + n_taps12 + j] = + taps[i * stride + (n_taps12 - j - 1)] = + cutoff * sinc(t * cutoff) * window(t, n_taps); + } + } + return 0; +} + +MAKE_RESAMPLER_COPY(c); + +#define MAKE(fmt,copy,full,inter,...) \ + { SPA_AUDIO_FORMAT_ ##fmt, do_resample_ ##copy, #copy, \ + do_resample_ ##full, #full, do_resample_ ##inter, #inter, __VA_ARGS__ } + +static struct resample_info resample_table[] = +{ +#if defined (HAVE_NEON) + MAKE(F32, copy_c, full_neon, inter_neon, SPA_CPU_FLAG_NEON), +#endif +#if defined(HAVE_AVX) && defined(HAVE_FMA) + MAKE(F32, copy_c, full_avx, inter_avx, SPA_CPU_FLAG_AVX | SPA_CPU_FLAG_FMA3), +#endif +#if defined (HAVE_SSSE3) + MAKE(F32, copy_c, full_ssse3, inter_ssse3, SPA_CPU_FLAG_SSSE3 | SPA_CPU_FLAG_SLOW_UNALIGNED), +#endif +#if defined (HAVE_SSE) + MAKE(F32, copy_c, full_sse, inter_sse, SPA_CPU_FLAG_SSE), +#endif + MAKE(F32, copy_c, full_c, inter_c), +}; +#undef MAKE + +#define MATCH_CPU_FLAGS(a,b) ((a) == 0 || ((a) & (b)) == a) +static const struct resample_info *find_resample_info(uint32_t format, uint32_t cpu_flags) +{ + SPA_FOR_EACH_ELEMENT_VAR(resample_table, t) { + if (t->format == format && + MATCH_CPU_FLAGS(t->cpu_flags, cpu_flags)) + return t; + } + return NULL; +} + +static void impl_native_free(struct resample *r) +{ + spa_log_debug(r->log, "native %p: free", r); + free(r->data); + r->data = NULL; +} + +static inline uint32_t calc_gcd(uint32_t a, uint32_t b) +{ + while (b != 0) { + uint32_t temp = a; + a = b; + b = temp % b; + } + return a; +} + +static void impl_native_update_rate(struct resample *r, double rate) +{ + struct native_data *data = r->data; + uint32_t in_rate, out_rate, phase, gcd, old_out_rate; + + if (SPA_LIKELY(data->rate == rate)) + return; + + old_out_rate = data->out_rate; + in_rate = r->i_rate / rate; + out_rate = r->o_rate; + phase = data->phase; + + gcd = calc_gcd(in_rate, out_rate); + in_rate /= gcd; + out_rate /= gcd; + + data->rate = rate; + data->phase = phase * out_rate / old_out_rate; + data->in_rate = in_rate; + data->out_rate = out_rate; + + data->inc = data->in_rate / data->out_rate; + data->frac = data->in_rate % data->out_rate; + + if (data->in_rate == data->out_rate) { + data->func = data->info->process_copy; + r->func_name = data->info->copy_name; + } + else if (rate == 1.0) { + data->func = data->info->process_full; + r->func_name = data->info->full_name; + } + else { + data->func = data->info->process_inter; + r->func_name = data->info->inter_name; + } + + spa_log_trace_fp(r->log, "native %p: rate:%f in:%d out:%d phase:%d inc:%d frac:%d", r, + rate, data->in_rate, data->out_rate, data->phase, data->inc, data->frac); + +} + +static uint32_t impl_native_in_len(struct resample *r, uint32_t out_len) +{ + struct native_data *data = r->data; + uint32_t in_len; + + in_len = (data->phase + out_len * data->frac) / data->out_rate; + in_len += out_len * data->inc + (data->n_taps - data->hist); + + spa_log_trace_fp(r->log, "native %p: hist:%d %d->%d", r, data->hist, out_len, in_len); + + return in_len; +} + +static void impl_native_process(struct resample *r, + const void * SPA_RESTRICT src[], uint32_t *in_len, + void * SPA_RESTRICT dst[], uint32_t *out_len) +{ + struct native_data *data = r->data; + uint32_t n_taps = data->n_taps; + float **history = data->history; + const float **s = (const float **)src; + uint32_t c, refill, hist, in, out, remain; + + hist = data->hist; + refill = 0; + + if (SPA_LIKELY(hist)) { + /* first work on the history if any. */ + if (SPA_UNLIKELY(hist <= n_taps)) { + /* we need at least n_taps to completely process the + * history before we can work on the new input. When + * we have less, refill the history. */ + refill = SPA_MIN(*in_len, n_taps-1); + for (c = 0; c < r->channels; c++) + spa_memcpy(&history[c][hist], s[c], refill * sizeof(float)); + + if (SPA_UNLIKELY(hist + refill < n_taps)) { + /* not enough in the history, keep the input in + * the history and produce no output */ + data->hist = hist + refill; + *in_len = refill; + *out_len = 0; + return; + } + } + /* now we have at least n_taps of data in the history + * and we try to process it */ + in = hist + refill; + out = *out_len; + data->func(r, (const void**)history, 0, &in, dst, 0, &out); + spa_log_trace_fp(r->log, "native %p: in:%d/%d out %d/%d hist:%d", + r, hist + refill, in, *out_len, out, hist); + } else { + out = in = 0; + } + + if (SPA_LIKELY(in >= hist)) { + int skip = in - hist; + /* we are past the history and can now work on the new + * input data */ + in = *in_len; + data->func(r, src, skip, &in, dst, out, out_len); + + spa_log_trace_fp(r->log, "native %p: in:%d/%d out %d/%d skip:%d", + r, *in_len, in, *out_len, out, skip); + + remain = *in_len - in; + if (remain > 0 && remain <= n_taps) { + /* not enough input data remaining for more output, + * copy to history */ + for (c = 0; c < r->channels; c++) + spa_memcpy(history[c], &s[c][in], remain * sizeof(float)); + } else { + /* we have enough input data remaining to produce + * more output ask to resubmit. */ + remain = 0; + *in_len = in; + } + } else { + /* we are still working on the history */ + *out_len = out; + remain = hist - in; + if (*in_len < n_taps) { + /* not enough input data, add it to the history because + * resubmitting it is not going to make progress. + * We copied this into the history above. */ + remain += refill; + } else { + /* input has enough data to possibly produce more output + * from the history so ask to resubmit */ + *in_len = 0; + } + if (remain) { + /* move history */ + for (c = 0; c < r->channels; c++) + spa_memmove(history[c], &history[c][in], remain * sizeof(float)); + } + spa_log_trace_fp(r->log, "native %p: in:%d remain:%d", r, in, remain); + + } + data->hist = remain; + return; +} + +static void impl_native_reset (struct resample *r) +{ + struct native_data *d = r->data; + if (d == NULL) + return; + memset(d->hist_mem, 0, r->channels * sizeof(float) * d->n_taps * 2); + if (r->options & RESAMPLE_OPTION_PREFILL) + d->hist = d->n_taps - 1; + else + d->hist = (d->n_taps / 2) - 1; + d->phase = 0; +} + +static uint32_t impl_native_delay (struct resample *r) +{ + struct native_data *d = r->data; + return d->n_taps / 2; +} + +int resample_native_init(struct resample *r) +{ + struct native_data *d; + const struct quality *q; + double scale; + uint32_t c, n_taps, n_phases, filter_size, in_rate, out_rate, gcd, filter_stride; + uint32_t history_stride, history_size, oversample; + + r->quality = SPA_CLAMP(r->quality, 0, (int) SPA_N_ELEMENTS(window_qualities) - 1); + r->free = impl_native_free; + r->update_rate = impl_native_update_rate; + r->in_len = impl_native_in_len; + r->process = impl_native_process; + r->reset = impl_native_reset; + r->delay = impl_native_delay; + + q = &window_qualities[r->quality]; + + gcd = calc_gcd(r->i_rate, r->o_rate); + + in_rate = r->i_rate / gcd; + out_rate = r->o_rate / gcd; + + scale = SPA_MIN(q->cutoff * out_rate / in_rate, q->cutoff); + + /* multiple of 8 taps to ease simd optimizations */ + n_taps = SPA_ROUND_UP_N((uint32_t)ceil(q->n_taps / scale), 8); + n_taps = SPA_MIN(n_taps, 1u << 18); + + /* try to get at least 256 phases so that interpolation is + * accurate enough when activated */ + n_phases = out_rate; + oversample = (255 + n_phases) / n_phases; + n_phases *= oversample; + + filter_stride = SPA_ROUND_UP_N(n_taps * sizeof(float), 64); + filter_size = filter_stride * (n_phases + 1); + history_stride = SPA_ROUND_UP_N(2 * n_taps * sizeof(float), 64); + history_size = r->channels * history_stride; + + d = calloc(1, sizeof(struct native_data) + + filter_size + + history_size + + (r->channels * sizeof(float*)) + + 64); + + if (d == NULL) + return -errno; + + r->data = d; + d->n_taps = n_taps; + d->n_phases = n_phases; + d->in_rate = in_rate; + d->out_rate = out_rate; + d->filter = SPA_PTROFF_ALIGN(d, sizeof(struct native_data), 64, float); + d->hist_mem = SPA_PTROFF_ALIGN(d->filter, filter_size, 64, float); + d->history = SPA_PTROFF(d->hist_mem, history_size, float*); + d->filter_stride = filter_stride / sizeof(float); + d->filter_stride_os = d->filter_stride * oversample; + for (c = 0; c < r->channels; c++) + d->history[c] = SPA_PTROFF(d->hist_mem, c * history_stride, float); + + build_filter(d->filter, d->filter_stride, n_taps, n_phases, scale); + + d->info = find_resample_info(SPA_AUDIO_FORMAT_F32, r->cpu_flags); + if (SPA_UNLIKELY(d->info == NULL)) { + spa_log_error(r->log, "failed to find suitable resample format!"); + return -ENOTSUP; + } + + spa_log_debug(r->log, "native %p: q:%d in:%d out:%d n_taps:%d n_phases:%d features:%08x:%08x", + r, r->quality, in_rate, out_rate, n_taps, n_phases, + r->cpu_flags, d->info->cpu_flags); + + r->cpu_flags = d->info->cpu_flags; + + impl_native_reset(r); + impl_native_update_rate(r, 1.0); + + return 0; +} diff --git a/spa/plugins/audioconvert/resample-peaks.c b/spa/plugins/audioconvert/resample-peaks.c new file mode 100644 index 0000000..c151d60 --- /dev/null +++ b/spa/plugins/audioconvert/resample-peaks.c @@ -0,0 +1,148 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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); + + m = peaks_abs_max(&pd->peaks, &s[i], chunk - i, m); + + i += chunk; + + if (i == end) { + d[o++] = m; + m = 0.0f; + o_count++; + } + } + pd->max_f[c] = m; + } + *out_len = o; + *in_len = i; + pd->o_count = o_count; + pd->i_count = i_count + i; + + while (pd->i_count >= r->i_rate) { + pd->i_count -= r->i_rate; + pd->o_count -= r->o_rate; + } +} + +static void impl_peaks_free(struct resample *r) +{ + struct peaks_data *d = r->data; + if (d != NULL) { + peaks_free(&d->peaks); + free(d); + } + r->data = NULL; +} + +static void impl_peaks_update_rate(struct resample *r, double rate) +{ +} + +static uint32_t impl_peaks_delay (struct resample *r) +{ + return 0; +} + +static uint32_t impl_peaks_in_len(struct resample *r, uint32_t out_len) +{ + return out_len; +} + +static void impl_peaks_reset (struct resample *r) +{ + struct peaks_data *d = r->data; + d->i_count = d->o_count = 0; +} + +int resample_peaks_init(struct resample *r) +{ + struct peaks_data *d; + int res; + + r->free = impl_peaks_free; + r->update_rate = impl_peaks_update_rate; + + d = calloc(1, sizeof(struct peaks_data) + sizeof(float) * r->channels); + if (d == NULL) + return -errno; + + d->peaks.log = r->log; + d->peaks.cpu_flags = r->cpu_flags; + if ((res = peaks_init(&d->peaks)) < 0) { + free(d); + return res; + } + + r->data = d; + r->process = resample_peaks_process; + r->reset = impl_peaks_reset; + r->delay = impl_peaks_delay; + r->in_len = impl_peaks_in_len; + + spa_log_debug(r->log, "peaks %p: in:%d out:%d features:%08x:%08x", r, + r->i_rate, r->o_rate, r->cpu_flags, d->peaks.cpu_flags); + + r->cpu_flags = d->peaks.cpu_flags; + d->i_count = d->o_count = 0; + return 0; +} diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h new file mode 100644 index 0000000..b1c89d5 --- /dev/null +++ b/spa/plugins/audioconvert/resample.h @@ -0,0 +1,69 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef RESAMPLE_H +#define RESAMPLE_H + +#include +#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); + void *data; +}; + +#define resample_free(r) (r)->free(r) +#define resample_update_rate(r,...) (r)->update_rate(r,__VA_ARGS__) +#define resample_in_len(r,...) (r)->in_len(r,__VA_ARGS__) +#define resample_out_len(r,...) (r)->out_len(r,__VA_ARGS__) +#define resample_process(r,...) (r)->process(r,__VA_ARGS__) +#define resample_reset(r) (r)->reset(r) +#define resample_delay(r) (r)->delay(r) + +int resample_native_init(struct resample *r); +int resample_peaks_init(struct resample *r); + +#endif /* RESAMPLE_H */ diff --git a/spa/plugins/audioconvert/spa-resample.c b/spa/plugins/audioconvert/spa-resample.c new file mode 100644 index 0000000..4efb718 --- /dev/null +++ b/spa/plugins/audioconvert/spa-resample.c @@ -0,0 +1,341 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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) +{ + d->ifile = sf_open(d->iname, SFM_READ, &d->iinfo); + if (d->ifile == NULL) { + fprintf(stderr, "error: failed to open input file \"%s\": %s\n", + d->iname, sf_strerror(NULL)); + return -EIO; + } + + d->oinfo.channels = d->iinfo.channels; + d->oinfo.samplerate = d->rate > 0 ? d->rate : d->iinfo.samplerate; + d->oinfo.format = d->format > 0 ? d->format : d->iinfo.format; + d->oinfo.format |= SF_FORMAT_WAV; + + d->ofile = sf_open(d->oname, SFM_WRITE, &d->oinfo); + if (d->ofile == NULL) { + fprintf(stderr, "error: failed to open output file \"%s\": %s\n", + d->oname, sf_strerror(NULL)); + return -EIO; + } + if (d->verbose) { + fprintf(stdout, "input '%s': channels:%d rate:%d format:%s\n", + d->iname, d->iinfo.channels, d->iinfo.samplerate, + sf_fmt_to_str(d->iinfo.format)); + fprintf(stdout, "output '%s': channels:%d rate:%d format:%s\n", + d->oname, d->oinfo.channels, d->oinfo.samplerate, + sf_fmt_to_str(d->oinfo.format)); + } + return 0; +} + +static int close_files(struct data *d) +{ + if (d->ifile) + sf_close(d->ifile); + if (d->ofile) + sf_close(d->ofile); + return 0; +} + +static int do_conversion(struct data *d) +{ + struct resample r; + int channels = d->iinfo.channels; + float in[MAX_SAMPLES * channels]; + float out[MAX_SAMPLES * channels]; + float ibuf[MAX_SAMPLES * channels]; + float obuf[MAX_SAMPLES * channels]; + uint32_t in_len, out_len, queued; + uint32_t pin_len, pout_len; + size_t read, written; + const void *src[channels]; + void *dst[channels]; + uint32_t i; + int res, j, k; + uint32_t flushing = UINT32_MAX; + + spa_zero(r); + r.cpu_flags = d->cpu_flags; + r.log = &logger.log; + r.channels = channels; + r.i_rate = d->iinfo.samplerate; + r.o_rate = d->oinfo.samplerate; + r.quality = d->quality < 0 ? DEFAULT_QUALITY : d->quality; + if ((res = resample_native_init(&r)) < 0) { + fprintf(stderr, "can't init converter: %s\n", spa_strerror(res)); + return res; + } + + for (j = 0; j < channels; j++) + src[j] = &in[MAX_SAMPLES * j]; + for (j = 0; j < channels; j++) + dst[j] = &out[MAX_SAMPLES * j]; + + read = written = queued = 0; + while (true) { + pout_len = out_len = MAX_SAMPLES; + in_len = SPA_MIN(MAX_SAMPLES, resample_in_len(&r, out_len)); + in_len -= SPA_MIN(queued, in_len); + + if (in_len > 0) { + pin_len = in_len = sf_readf_float(d->ifile, &ibuf[queued * channels], in_len); + + read += pin_len; + + if (pin_len == 0) { + if (flushing == 0) + break; + if (flushing == UINT32_MAX) + flushing = resample_delay(&r); + + pin_len = in_len = SPA_MIN(MAX_SAMPLES, flushing); + flushing -= in_len; + + for (k = 0, i = 0; i < pin_len; i++) { + for (j = 0; j < channels; j++) + ibuf[k++] = 0.0; + } + } + } + in_len += queued; + pin_len = in_len; + + for (k = 0, i = 0; i < pin_len; i++) { + for (j = 0; j < channels; j++) { + in[MAX_SAMPLES * j + i] = ibuf[k++]; + } + } + resample_process(&r, src, &pin_len, dst, &pout_len); + + queued = in_len - pin_len; + if (queued) + memmove(ibuf, &ibuf[pin_len * channels], queued * channels * sizeof(float)); + + if (pout_len > 0) { + for (k = 0, i = 0; i < pout_len; i++) { + for (j = 0; j < channels; j++) { + obuf[k++] = out[MAX_SAMPLES * j + i]; + } + } + pout_len = sf_writef_float(d->ofile, obuf, pout_len); + + written += pout_len; + } + } + if (d->verbose) + fprintf(stdout, "read %zu samples, wrote %zu samples\n", read, written); + + return 0; +} + +int main(int argc, char *argv[]) +{ + int c; + int longopt_index = 0, ret; + struct data data; + + spa_zero(data); + + logger.log.level = SPA_LOG_LEVEL_DEBUG; + + data.quality = -1; + while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) { + switch (c) { + case 'h': + show_usage(argv[0], false); + return EXIT_SUCCESS; + case 'v': + data.verbose = true; + break; + case 'r': + ret = atoi(optarg); + if (ret <= 0) { + fprintf(stderr, "error: bad rate %s\n", optarg); + goto error_usage; + } + data.rate = ret; + break; + case 'f': + ret = sf_str_to_fmt(optarg); + if (ret < 0) { + fprintf(stderr, "error: bad format %s\n", optarg); + goto error_usage; + } + data.format = ret; + break; + case 'q': + ret = atoi(optarg); + if (ret < 0) { + fprintf(stderr, "error: bad quality %s\n", optarg); + goto error_usage; + } + data.quality = ret; + break; + case 'c': + data.cpu_flags = strtol(optarg, NULL, 0); + break; + default: + fprintf(stderr, "error: unknown option '%c'\n", c); + goto error_usage; + } + } + if (optind + 1 >= argc) { + fprintf(stderr, "error: filename arguments missing (%d %d)\n", optind, argc); + goto error_usage; + } + data.iname = argv[optind++]; + data.oname = argv[optind++]; + + if (open_files(&data) < 0) + return EXIT_FAILURE; + + do_conversion(&data); + + close_files(&data); + + return 0; + +error_usage: + show_usage(argv[0], true); + return EXIT_FAILURE; +} diff --git a/spa/plugins/audioconvert/test-audioadapter.c b/spa/plugins/audioconvert/test-audioadapter.c new file mode 100644 index 0000000..a51feee --- /dev/null +++ b/spa/plugins/audioconvert/test-audioadapter.c @@ -0,0 +1,302 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..99ba866 --- /dev/null +++ b/spa/plugins/audioconvert/test-audioconvert.c @@ -0,0 +1,1158 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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[2]; + const struct spa_handle_factory *factory; + void *iface; + + logger.log.level = SPA_LOG_LEVEL_TRACE; + support[0] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, &logger); + + /* make convert */ + factory = find_factory(SPA_NAME_AUDIO_CONVERT); + spa_assert_se(factory != NULL); + + size = spa_handle_factory_get_size(factory, NULL); + + ctx->convert_handle = calloc(1, size); + spa_assert_se(ctx->convert_handle != NULL); + + items[0] = SPA_DICT_ITEM_INIT("clock.quantum-limit", "8192"); + + res = spa_handle_factory_init(factory, + ctx->convert_handle, + &SPA_DICT_INIT(items, 1), + support, 1); + spa_assert_se(res >= 0); + + res = spa_handle_get_interface(ctx->convert_handle, + SPA_TYPE_INTERFACE_Node, &iface); + spa_assert_se(res >= 0); + ctx->convert_node = iface; + + return 0; +} + +static int clean_context(struct context *ctx) +{ + spa_handle_clear(ctx->convert_handle); + free(ctx->convert_handle); + return 0; +} + +static void node_info_check(void *data, const struct spa_node_info *info) +{ + struct context *ctx = data; + + fprintf(stderr, "input %d, output %d\n", + info->max_input_ports, + info->max_output_ports); + + spa_assert_se(info->max_input_ports == MAX_PORTS); + spa_assert_se(info->max_output_ports == MAX_PORTS); + + ctx->got_node_info = true; +} + +static void port_info_check(void *data, + enum spa_direction direction, uint32_t port, + const struct spa_port_info *info) +{ + struct context *ctx = data; + + fprintf(stderr, "port %d %d %p\n", direction, port, info); + + ctx->got_port_info[direction][port] = true; + ctx->n_port_info[direction]++; +} + +static int test_init_state(struct context *ctx) +{ + struct spa_hook listener; + static const struct spa_node_events init_events = { + SPA_VERSION_NODE_EVENTS, + .info = node_info_check, + .port_info = port_info_check, + }; + + spa_zero(ctx->got_node_info); + spa_zero(ctx->n_port_info); + spa_zero(ctx->got_port_info); + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &init_events, ctx); + spa_hook_remove(&listener); + + spa_assert_se(ctx->got_node_info); + spa_assert_se(ctx->n_port_info[0] == 1); + spa_assert_se(ctx->n_port_info[1] == 1); + spa_assert_se(ctx->got_port_info[0][0] == true); + spa_assert_se(ctx->got_port_info[1][0] == true); + + return 0; +} + +static int test_set_in_format(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + + /* other format */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + info = (struct spa_audio_info_raw) { + .format = SPA_AUDIO_FORMAT_S16, + .rate = 44100, + .channels = 2, + .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, } + }; + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_INPUT, 0, + SPA_PARAM_Format, 0, param); + spa_assert_se(res == 0); + + return 0; +} + +static int test_split_setup1(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, output as DSP */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.rate = 48000; + info.channels = 6; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + info.position[2] = SPA_AUDIO_CHANNEL_FC; + info.position[3] = SPA_AUDIO_CHANNEL_LFE; + info.position[4] = SPA_AUDIO_CHANNEL_SL; + info.position[5] = SPA_AUDIO_CHANNEL_SR; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_split_setup2(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, output as DSP */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.rate = 48000; + info.channels = 4; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + info.position[2] = SPA_AUDIO_CHANNEL_RL; + info.position[3] = SPA_AUDIO_CHANNEL_RR; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_convert_setup1(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, output convert */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_set_out_format(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + + /* out format */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + info = (struct spa_audio_info_raw) { + .format = SPA_AUDIO_FORMAT_S32P, + .rate = 96000, + .channels = 8, + .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, + SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, } + }; + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, param); + spa_assert_se(res == 0); + + return 0; +} + +static int test_merge_setup1(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, output as DSP */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.rate = 48000; + info.channels = 6; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + info.position[2] = SPA_AUDIO_CHANNEL_FC; + info.position[3] = SPA_AUDIO_CHANNEL_LFE; + info.position[4] = SPA_AUDIO_CHANNEL_RL; + info.position[5] = SPA_AUDIO_CHANNEL_RR; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_set_out_format2(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + + /* out format */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + info = (struct spa_audio_info_raw) { + .format = SPA_AUDIO_FORMAT_S16, + .rate = 32000, + .channels = 2, + .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, } + }; + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_OUTPUT, 0, + SPA_PARAM_Format, 0, param); + spa_assert_se(res == 0); + + return 0; +} + +static int test_merge_setup2(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, output as DSP */ + spa_zero(info); + info.format = SPA_AUDIO_FORMAT_F32P; + info.rate = 96000; + info.channels = 4; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + info.position[2] = SPA_AUDIO_CHANNEL_FC; + info.position[3] = SPA_AUDIO_CHANNEL_LFE; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_convert_setup2(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + int res; + struct spa_hook listener; + static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .port_info = port_info_check, + }; + + spa_zero(listener); + spa_node_add_listener(ctx->convert_node, + &listener, &node_events, ctx); + + /* port config, input convert */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert)); + + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + spa_hook_remove(&listener); + + return 0; +} + +static int test_set_in_format2(struct context *ctx) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_audio_info_raw info; + int res; + + /* other format */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + info = (struct spa_audio_info_raw) { + .format = SPA_AUDIO_FORMAT_S24, + .rate = 48000, + .channels = 3, + .position = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_LFE, } + }; + param = spa_format_audio_raw_build(&b, SPA_PARAM_Format, &info); + + res = spa_node_port_set_param(ctx->convert_node, SPA_DIRECTION_INPUT, 0, + SPA_PARAM_Format, 0, param); + spa_assert_se(res == 0); + + return 0; +} + +static int setup_direction(struct context *ctx, enum spa_direction direction, uint32_t mode, + struct spa_audio_info_raw *info) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param, *format; + int res; + uint32_t i; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + format = spa_format_audio_raw_build(&b, SPA_PARAM_Format, info); + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(format)); + break; + + case SPA_PARAM_PORT_CONFIG_MODE_convert: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode)); + break; + default: + return -EINVAL; + } + res = spa_node_set_param(ctx->convert_node, SPA_PARAM_PortConfig, 0, param); + spa_assert_se(res == 0); + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_convert: + res = spa_node_port_set_param(ctx->convert_node, direction, 0, + SPA_PARAM_Format, 0, format); + spa_assert_se(res == 0); + break; + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + format = spa_format_audio_dsp_build(&b, SPA_PARAM_Format, + &SPA_AUDIO_INFO_DSP_INIT( + .format = SPA_AUDIO_FORMAT_F32P)); + for (i = 0; i < info->channels; i++) { + res = spa_node_port_set_param(ctx->convert_node, direction, i, + SPA_PARAM_Format, 0, format); + spa_assert_se(res == 0); + } + break; + default: + return -EINVAL; + } + return 0; +} + +struct buffer { + struct spa_buffer buffer; + struct spa_data datas[MAX_PORTS]; + struct spa_chunk chunks[MAX_PORTS]; +}; + +struct data { + uint32_t mode; + struct spa_audio_info_raw info; + uint32_t ports; + uint32_t planes; + const void *data[MAX_PORTS]; + uint32_t size; +}; + +static int run_convert(struct context *ctx, struct data *in_data, + struct data *out_data) +{ + struct spa_command cmd; + int res; + uint32_t i, j, k; + struct buffer in_buffers[in_data->ports]; + struct buffer out_buffers[out_data->ports]; + struct spa_io_buffers in_io[in_data->ports]; + struct spa_io_buffers out_io[out_data->ports]; + + setup_direction(ctx, SPA_DIRECTION_INPUT, in_data->mode, &in_data->info); + setup_direction(ctx, SPA_DIRECTION_OUTPUT, out_data->mode, &out_data->info); + + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Start); + res = spa_node_send_command(ctx->convert_node, &cmd); + spa_assert_se(res == 0); + + for (i = 0, k = 0; i < in_data->ports; i++) { + struct buffer *b = &in_buffers[i]; + struct spa_buffer *buffers[1]; + spa_zero(*b); + b->buffer.datas = b->datas; + b->buffer.n_datas = in_data->planes; + + for (j = 0; j < in_data->planes; j++, k++) { + b->datas[j].type = SPA_DATA_MemPtr; + b->datas[j].flags = 0; + b->datas[j].fd = -1; + b->datas[j].mapoffset = 0; + b->datas[j].maxsize = in_data->size; + b->datas[j].data = (void *)in_data->data[k]; + b->datas[j].chunk = &b->chunks[j]; + b->datas[j].chunk->offset = 0; + b->datas[j].chunk->size = in_data->size; + b->datas[j].chunk->stride = 0; + } + buffers[0] = &b->buffer; + res = spa_node_port_use_buffers(ctx->convert_node, SPA_DIRECTION_INPUT, i, + 0, buffers, 1); + spa_assert_se(res == 0); + + in_io[i].status = SPA_STATUS_HAVE_DATA; + in_io[i].buffer_id = 0; + + res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_INPUT, i, + SPA_IO_Buffers, &in_io[i], sizeof(in_io[i])); + spa_assert_se(res == 0); + } + for (i = 0; i < out_data->ports; i++) { + struct buffer *b = &out_buffers[i]; + struct spa_buffer *buffers[1]; + spa_zero(*b); + b->buffer.datas = b->datas; + b->buffer.n_datas = out_data->planes; + + for (j = 0; j < out_data->planes; j++) { + b->datas[j].type = SPA_DATA_MemPtr; + b->datas[j].flags = 0; + b->datas[j].fd = -1; + b->datas[j].mapoffset = 0; + b->datas[j].maxsize = out_data->size; + b->datas[j].data = calloc(1, out_data->size); + b->datas[j].chunk = &b->chunks[j]; + b->datas[j].chunk->offset = 0; + b->datas[j].chunk->size = 0; + b->datas[j].chunk->stride = 0; + } + buffers[0] = &b->buffer; + res = spa_node_port_use_buffers(ctx->convert_node, + SPA_DIRECTION_OUTPUT, i, 0, buffers, 1); + spa_assert_se(res == 0); + + out_io[i].status = SPA_STATUS_NEED_DATA; + out_io[i].buffer_id = -1; + + res = spa_node_port_set_io(ctx->convert_node, SPA_DIRECTION_OUTPUT, i, + SPA_IO_Buffers, &out_io[i], sizeof(out_io[i])); + spa_assert_se(res == 0); + } + + res = spa_node_process(ctx->convert_node); + spa_assert_se(res == (SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA)); + + for (i = 0, k = 0; i < out_data->ports; i++) { + struct buffer *b = &out_buffers[i]; + + spa_assert_se(out_io[i].status == SPA_STATUS_HAVE_DATA); + spa_assert_se(out_io[i].buffer_id == 0); + + for (j = 0; j < out_data->planes; j++, k++) { + spa_assert_se(b->datas[j].chunk->offset == 0); + spa_assert_se(b->datas[j].chunk->size == out_data->size); + + res = memcmp(b->datas[j].data, out_data->data[k], out_data->size); + if (res != 0) { + fprintf(stderr, "error port %d plane %d\n", i, j); + spa_debug_mem(0, b->datas[j].data, out_data->size); + spa_debug_mem(0, out_data->data[k], out_data->size); + } + spa_assert_se(res == 0); + + free(b->datas[j].data); + } + } + cmd = SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_Suspend); + res = spa_node_send_command(ctx->convert_node, &cmd); + spa_assert_se(res == 0); + + return 0; +} + +static const float data_f32p_1[] = { 0.1f, 0.1f, 0.1f, 0.1f }; +static const float data_f32p_2[] = { 0.2f, 0.2f, 0.2f, 0.2f }; +static const float data_f32p_3[] = { 0.3f, 0.3f, 0.3f, 0.3f }; +static const float data_f32p_4[] = { 0.4f, 0.4f, 0.4f, 0.4f }; +static const float data_f32p_5[] = { 0.5f, 0.5f, 0.5f, 0.5f }; +static const float data_f32p_5_6p1[] = { 0.953553438f, 0.953553438f, 0.953553438f, 0.953553438f }; +static const float data_f32p_6[] = { 0.6f, 0.6f, 0.6f, 0.6f }; +static const float data_f32p_6_6p1[] = { 1.053553343f, 1.053553343f, 1.053553343f, 1.053553343f }; +static const float data_f32p_7[] = { 0.7f, 0.7f, 0.7f, 0.7f }; +static const float data_f32p_8[] = { 0.8f, 0.8f, 0.8f, 0.8f }; + +static const float data_f32_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f }; +static const float data_f32_6p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f }; +static const float data_f32_6p1_from_5p1[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f, + 0.1f, 0.2f, 0.3f, 0.4f, 0.55f, 0.5f, 0.6f }; + +static const float data_f32_7p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 0.8f, 0.3f, 0.4f }; +static const float data_f32_5p1_remapped[] = { 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f, + 0.1f, 0.2f, 0.5f, 0.6f, 0.3f, 0.4f }; + +struct data dsp_5p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 6, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, }, + .size = sizeof(float) * 4 +}; + +struct data dsp_5p1_from_6p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 6, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5_6p1, data_f32p_6_6p1, }, + .size = sizeof(float) * 4 +}; + + +struct data dsp_5p1_remapped = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + }), + .ports = 6, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, }, + .size = sizeof(float) * 4 +}; + +struct data dsp_5p1_remapped_from_6p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + }), + .ports = 6, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_5_6p1, data_f32p_6_6p1, data_f32p_3, data_f32p_4, }, + .size = sizeof(float) * 4 +}; + +struct data dsp_6p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 7, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 }, + .size = sizeof(float) * 4 +}; + +struct data dsp_6p1_side = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_SL, + SPA_AUDIO_CHANNEL_SR, + }), + .ports = 7, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 }, + .size = sizeof(float) * 4 +}; + +struct data dsp_7p1_remapped = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 8, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_SL, + SPA_AUDIO_CHANNEL_SR, + }), + .ports = 8, + .planes = 1, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_7, data_f32p_8, data_f32p_5, data_f32p_6 }, + .size = sizeof(data_f32p_1) +}; + +struct data dsp_5p1_remapped_2 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_dsp, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FL, + }), + .ports = 6, + .planes = 1, + .data = { data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_2, data_f32p_1, }, + .size = sizeof(float) * 4 +}; + +struct data conv_f32_48000_5p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_5p1 }, + .size = sizeof(data_f32_5p1) +}; + +struct data conv_f32_48000_5p1_remapped = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_5p1_remapped }, + .size = sizeof(data_f32_5p1_remapped) +}; + +struct data conv_f32p_48000_5p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32P, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 1, + .planes = 6, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, }, + .size = sizeof(float) * 4 +}; + +struct data conv_f32_48000_6p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_6p1 }, + .size = sizeof(data_f32_6p1) +}; + +struct data conv_f32_48000_6p1_from_5p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_6p1_from_5p1 }, + .size = sizeof(data_f32_6p1_from_5p1) +}; + +struct data conv_f32_48000_6p1_side = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_SL, + SPA_AUDIO_CHANNEL_SR, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_6p1 }, + .size = sizeof(data_f32_6p1) +}; + +struct data conv_f32p_48000_6p1 = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32P, + .rate = 48000, + .channels = 7, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + SPA_AUDIO_CHANNEL_RC, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + }), + .ports = 1, + .planes = 7, + .data = { data_f32p_1, data_f32p_2, data_f32p_3, data_f32p_4, data_f32p_5, data_f32p_6, data_f32p_7 }, + .size = sizeof(float) * 4 +}; + +struct data conv_f32p_48000_5p1_remapped = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32P, + .rate = 48000, + .channels = 6, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + }), + .ports = 1, + .planes = 6, + .data = { data_f32p_1, data_f32p_2, data_f32p_5, data_f32p_6, data_f32p_3, data_f32p_4, }, + .size = sizeof(float) * 4 +}; + +struct data conv_f32_48000_7p1_remapped = { + .mode = SPA_PARAM_PORT_CONFIG_MODE_convert, + .info = SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32, + .rate = 48000, + .channels = 8, + .position = { + SPA_AUDIO_CHANNEL_FL, + SPA_AUDIO_CHANNEL_FR, + SPA_AUDIO_CHANNEL_SL, + SPA_AUDIO_CHANNEL_SR, + SPA_AUDIO_CHANNEL_RL, + SPA_AUDIO_CHANNEL_RR, + SPA_AUDIO_CHANNEL_FC, + SPA_AUDIO_CHANNEL_LFE, + }), + .ports = 1, + .planes = 1, + .data = { data_f32_7p1_remapped, }, + .size = sizeof(data_f32_7p1_remapped) +}; + +static int test_convert_remap_dsp(struct context *ctx) +{ + run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1); + run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1); + run_convert(ctx, &dsp_5p1, &conv_f32_48000_5p1_remapped); + run_convert(ctx, &dsp_5p1, &conv_f32p_48000_5p1_remapped); + run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1); + run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1); + run_convert(ctx, &dsp_5p1_remapped, &conv_f32_48000_5p1_remapped); + run_convert(ctx, &dsp_5p1_remapped, &conv_f32p_48000_5p1_remapped); + run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1); + run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1); + run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32_48000_5p1_remapped); + run_convert(ctx, &dsp_5p1_remapped_2, &conv_f32p_48000_5p1_remapped); + run_convert(ctx, &dsp_6p1, &conv_f32p_48000_6p1); + run_convert(ctx, &dsp_6p1, &conv_f32_48000_6p1); + run_convert(ctx, &dsp_6p1_side, &conv_f32_48000_6p1_side); + + run_convert(ctx, &dsp_5p1, &conv_f32_48000_6p1_from_5p1); + return 0; +} + +static int test_convert_remap_conv(struct context *ctx) +{ + run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1); + run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped); + run_convert(ctx, &conv_f32_48000_5p1, &dsp_5p1_remapped_2); + run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1); + run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped); + run_convert(ctx, &conv_f32p_48000_5p1, &dsp_5p1_remapped_2); + run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1); + run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped); + run_convert(ctx, &conv_f32_48000_5p1_remapped, &dsp_5p1_remapped_2); + run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1); + run_convert(ctx, &conv_f32p_48000_6p1, &dsp_6p1); + run_convert(ctx, &conv_f32_48000_6p1, &dsp_6p1); + run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_6p1_side); + run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped); + run_convert(ctx, &conv_f32_48000_7p1_remapped, &dsp_7p1_remapped); + run_convert(ctx, &conv_f32p_48000_5p1_remapped, &dsp_5p1_remapped_2); + + run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_from_6p1); + run_convert(ctx, &conv_f32_48000_6p1_side, &dsp_5p1_from_6p1); + run_convert(ctx, &conv_f32_48000_6p1, &dsp_5p1_remapped_from_6p1); + return 0; +} + +int main(int argc, char *argv[]) +{ + struct context ctx; + + spa_zero(ctx); + + setup_context(&ctx); + + test_init_state(&ctx); + test_set_in_format(&ctx); + test_split_setup1(&ctx); + test_split_setup2(&ctx); + test_convert_setup1(&ctx); + test_set_out_format(&ctx); + test_merge_setup1(&ctx); + test_set_out_format2(&ctx); + test_merge_setup2(&ctx); + test_convert_setup2(&ctx); + test_set_in_format2(&ctx); + test_set_out_format(&ctx); + + test_convert_remap_dsp(&ctx); + test_convert_remap_conv(&ctx); + + clean_context(&ctx); + + return 0; +} diff --git a/spa/plugins/audioconvert/test-channelmix.c b/spa/plugins/audioconvert/test-channelmix.c new file mode 100644 index 0000000..9e052e2 --- /dev/null +++ b/spa/plugins/audioconvert/test-channelmix.c @@ -0,0 +1,374 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +static uint32_t cpu_flags; + +SPA_LOG_IMPL(logger); + +#define MATRIX(...) (float[]) { __VA_ARGS__ } + +#include "test-helper.h" +#include "channelmix-ops.c" + +#define CLOSE_ENOUGH(a,b) (fabs((a)-(b)) < 0.000001f) + +static void dump_matrix(struct channelmix *mix, float *coeff) +{ + uint32_t i, j; + + for (i = 0; i < mix->dst_chan; i++) { + for (j = 0; j < mix->src_chan; j++) { + float v = mix->matrix[i][j]; + spa_log_debug(mix->log, "%d %d: %f <-> %f", i, j, v, *coeff); + spa_assert_se(CLOSE_ENOUGH(v, *coeff)); + coeff++; + } + } +} + +static void test_mix(uint32_t src_chan, uint32_t src_mask, uint32_t dst_chan, uint32_t dst_mask, uint32_t options, float *coeff) +{ + struct channelmix mix; + + spa_log_debug(&logger.log, "start %d->%d (%08x -> %08x)", src_chan, dst_chan, src_mask, dst_mask); + + spa_zero(mix); + mix.options = options; + mix.src_chan = src_chan; + mix.dst_chan = dst_chan; + mix.src_mask = src_mask; + mix.dst_mask = dst_mask; + mix.log = &logger.log; + + spa_assert_se(channelmix_init(&mix) == 0); + channelmix_set_volume(&mix, 1.0f, false, 0, NULL); + dump_matrix(&mix, coeff); +} + +static void test_1_N_MONO(void) +{ + test_mix(1, _M(MONO), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 1.0)); + test_mix(1, _M(MONO), 3, _M(FL)|_M(FR)|_M(LFE), 0, + MATRIX(1.0, 1.0, 1.0)); + test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(1.0, 1.0, 1.0, 1.0)); + test_mix(1, _M(MONO), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 1.0, 1.0, 1.0)); + test_mix(1, _M(MONO), 12, 0, 0, + MATRIX(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)); +} + +static void test_1_N_FC(void) +{ + test_mix(1, _M(FC), 2, _M(FL)|_M(FR), 0, + MATRIX(0.707107, 0.707107)); + test_mix(1, _M(FC), 3, _M(FL)|_M(FR)|_M(LFE), 0, + MATRIX(0.707107, 0.707107, 0.0)); + test_mix(1, _M(FC), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(0.0, 0.0, 1.0, 0.0)); + test_mix(1, _M(FC), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0, + MATRIX(0.707107, 0.707107, 0.0, 0.0)); + test_mix(1, _M(FC), 12, 0, 0, + MATRIX(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)); +} + +static void test_N_1(void) +{ + test_mix(1, _M(MONO), 1, _M(MONO), 0, + MATRIX(1.0)); + test_mix(1, _M(MONO), 1, _M(FC), 0, + MATRIX(1.0)); + test_mix(1, _M(FC), 1, _M(MONO), 0, + MATRIX(1.0)); + test_mix(1, _M(FC), 1, _M(FC), 0, + MATRIX(1.0)); + test_mix(2, _M(FL)|_M(FR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107)); + test_mix(12, 0, 1, _M(MONO), 0, + MATRIX(0.083333, 0.083333, 0.083333, 0.083333, 0.083333, 0.083333, + 0.083333, 0.083333, 0.083333, 0.083333, 0.083333, 0.0833333)); +} + +static void test_3p1_N(void) +{ + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 1.0, 0.0)); + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0 )); + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 3, _M(FL)|_M(FR)|_M(LFE), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, + 0.0, 0.0, 0.0, 1.0 )); + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0,)); + test_mix(4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0,)); +} + +static void test_4_N(void) +{ + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 0.5, 0.5)); + test_mix(4, _M(FL)|_M(FR)|_M(SL)|_M(SR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 0.5, 0.5)); + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.707107)); + test_mix(4, _M(FL)|_M(FR)|_M(SL)|_M(SR), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.707107)); + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 3, _M(FL)|_M(FR)|_M(LFE), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 0.0)); + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0)); + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0)); + test_mix(4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), CHANNELMIX_OPTION_UPMIX, + MATRIX(1.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.707107, + 0.707107, 0.707107, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0)); +} + +static void test_5p1_N(void) +{ + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 3, _M(FL)|_M(FR)|_M(LFE), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 4, _M(FL)|_M(FR)|_M(LFE)|_M(FC), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.707107, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 4, _M(FL)|_M(FR)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.707107, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 5, _M(FL)|_M(FR)|_M(FC)|_M(SL)|_M(SR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)); + test_mix(6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)); +} + +static void test_6p1_N(void) +{ + test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(SL)|_M(SR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5, 0.5)); + test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC), + 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107)); + test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC), + 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.707107)); + test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RC)|_M(RL)|_M(RR), + 6, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.707107, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.707107, 0.0, 1.0)); + test_mix(7, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RC), + 8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 0, + MATRIX(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.707107)); +} + +static void test_7p1_N(void) +{ + test_mix(8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 1, _M(MONO), 0, + MATRIX(0.707107, 0.707107, 1.0, 0.0, 0.5, 0.5, 0.5, 0.5)); + test_mix(8, _M(FL)|_M(FR)|_M(LFE)|_M(FC)|_M(SL)|_M(SR)|_M(RL)|_M(RR), 2, _M(FL)|_M(FR), 0, + MATRIX(1.0, 0.0, 0.707107, 0.0, 0.707107, 0.0, 0.707107, 0.0, + 0.0, 1.0, 0.707107, 0.0, 0.0, 0.707107, 0.0, 0.707107)); +} + +static void check_samples(float **s1, float **s2, uint32_t n_s, uint32_t n_samples) +{ + uint32_t i, j; + for (i = 0; i < n_s; i++) { + for (j = 0; j < n_samples; j++) { + spa_assert_se(CLOSE_ENOUGH(s1[i][j], s2[i][j])); + } + } +} + +static void run_n_m_impl(struct channelmix *mix, const void **src, uint32_t n_samples) +{ + uint32_t dst_chan = mix->dst_chan, i; + float dst_c_data[dst_chan][n_samples]; + float dst_x_data[dst_chan][n_samples]; + void *dst_c[dst_chan], *dst_x[dst_chan]; + + for (i = 0; i < dst_chan; i++) { + dst_c[i] = dst_c_data[i]; + dst_x[i] = dst_x_data[i]; + } + + channelmix_f32_n_m_c(mix, dst_c, src, n_samples); + + channelmix_f32_n_m_c(mix, dst_x, src, n_samples); + check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples); + +#if defined(HAVE_SSE) + if (cpu_flags & SPA_CPU_FLAG_SSE) { + channelmix_f32_n_m_sse(mix, dst_x, src, n_samples); + check_samples((float**)dst_c, (float**)dst_x, dst_chan, n_samples); + } +#endif +} + +static void test_n_m_impl(void) +{ + struct channelmix mix; + unsigned int i, j; +#define N_SAMPLES 251 + float src_data[16][N_SAMPLES], *src[16]; + + spa_log_debug(&logger.log, "start"); + + for (i = 0; i < 16; i++) { + for (j = 0; j < N_SAMPLES; j++) + src_data[i][j] = (drand48() - 0.5f) * 2.5f; + src[i] = src_data[i]; + } + + spa_zero(mix); + mix.src_chan = 16; + mix.dst_chan = 12; + mix.log = &logger.log; + mix.cpu_flags = cpu_flags; + spa_assert_se(channelmix_init(&mix) == 0); + channelmix_set_volume(&mix, 1.0f, false, 0, NULL); + + /* identity matrix */ + run_n_m_impl(&mix, (const void**)src, N_SAMPLES); + + /* some zero destination */ + mix.matrix_orig[2][2] = 0.0f; + mix.matrix_orig[7][7] = 0.0f; + channelmix_set_volume(&mix, 1.0f, false, 0, NULL); + run_n_m_impl(&mix, (const void**)src, N_SAMPLES); + + /* random matrix */ + for (i = 0; i < mix.dst_chan; i++) { + for (j = 0; j < mix.src_chan; j++) { + mix.matrix_orig[i][j] = drand48() - 0.5f; + } + } + channelmix_set_volume(&mix, 1.0f, false, 0, NULL); + + run_n_m_impl(&mix, (const void**)src, N_SAMPLES); +} + +int main(int argc, char *argv[]) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + srand48(SPA_TIMESPEC_TO_NSEC(&ts)); + + logger.log.level = SPA_LOG_LEVEL_TRACE; + + cpu_flags = get_cpu_flags(); + printf("got CPU flags %d\n", cpu_flags); + + test_1_N_MONO(); + test_1_N_FC(); + test_N_1(); + test_3p1_N(); + test_4_N(); + test_5p1_N(); + test_6p1_N(); + test_7p1_N(); + + test_n_m_impl(); + + return 0; +} diff --git a/spa/plugins/audioconvert/test-fmt-ops.c b/spa/plugins/audioconvert/test-fmt-ops.c new file mode 100644 index 0000000..8c8d4cd --- /dev/null +++ b/spa/plugins/audioconvert/test-fmt-ops.c @@ -0,0 +1,798 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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 +} + +static void test_s16_f32(void) +{ + static const int16_t in[] = { 0, 32767, -32768, 16384, -16384, }; + static const float out[] = { 0.0f, 0.999969482422f, -1.0f, 0.5f, -0.5f }; + + run_test("test_s16_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s16_to_f32d_c); + run_test("test_s16d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_s16d_to_f32_c); + run_test("test_s16_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_s16_to_f32_c); + run_test("test_s16d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_s16d_to_f32d_c); +#if defined(HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s16_f32d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s16_to_f32d_sse2); + } +#endif +#if defined(HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s16_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s16_to_f32d_avx2); + } +#endif +#if defined(HAVE_NEON) + if (cpu_flags & SPA_CPU_FLAG_NEON) { + run_test("test_s16_f32d_neon", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s16_to_f32d_neon); + } +#endif +} + +static void test_f32_u32(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const uint32_t out[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000, + 0xffffff00, 0x0, + 0x80000100, 0x80000000, 0x7fffff00, 0x80000000 }; + + run_test("test_f32_u32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_u32_c); + run_test("test_f32d_u32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_u32_c); +} + +static void test_u32_f32(void) +{ + static const uint32_t in[] = { 0x80000000, 0xffffff00, 0x0, 0xc0000000, 0x40000000 }; + static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, }; + + run_test("test_u32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_u32_to_f32d_c); + run_test("test_u32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_u32_to_f32_c); +} + +static void test_f32_s32(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const int32_t out[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000, + 0x7fffff00, 0x80000000, + 0x00000100, 0x00000000, 0xffffff00, 0x00000000 }; + + run_test("test_f32_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_s32_c); + run_test("test_f32d_s32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s32_c); + run_test("test_f32_s32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_f32_to_s32d_c); + run_test("test_f32d_s32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_f32d_to_s32d_c); +#if defined(HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_f32d_s32_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s32_sse2); + } +#endif +#if defined(HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_f32d_s32_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s32_avx2); + } +#endif +} + +static void test_s32_f32(void) +{ + static const int32_t in[] = { 0, 0x7fffff00, 0x80000000, 0x40000000, 0xc0000000 }; + static const float out[] = { 0.0f, 0.999999880791, -1.0f, 0.5, -0.5, }; + + run_test("test_s32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s32_to_f32d_c); + run_test("test_s32d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_s32d_to_f32_c); + run_test("test_s32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_s32_to_f32_c); + run_test("test_s32d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_s32d_to_f32d_c); +#if defined(HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s32_f32d_sse2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s32_to_f32d_sse2); + } +#endif +#if defined(HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s32_f32d_avx2", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s32_to_f32d_avx2); + } +#endif +} + +static void test_f32_u24(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const uint24_t out[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff), + U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000), + U32_TO_U24(0xffffff), U32_TO_U24(0x000000), + U32_TO_U24(0x800001), U32_TO_U24(0x800000), U32_TO_U24(0x7fffff), + U32_TO_U24(0x800000) }; + + run_test("test_f32_u24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + true, true, conv_f32_to_u24_c); + run_test("test_f32d_u24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + false, true, conv_f32d_to_u24_c); +} + +static void test_u24_f32(void) +{ + static const uint24_t in[] = { U32_TO_U24(0x00800000), U32_TO_U24(0xffffff), + U32_TO_U24(0x000000), U32_TO_U24(0xc00000), U32_TO_U24(0x400000) }; + static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5, -0.5, }; + + run_test("test_u24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_u24_to_f32d_c); + run_test("test_u24_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_u24_to_f32_c); +} + +static void test_f32_s24(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const int24_t out[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff), + S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000), + S32_TO_S24(0x7fffff), S32_TO_S24(0xff800000), + S32_TO_S24(0x000001), S32_TO_S24(0x000000), S32_TO_S24(0xffffffff), + S32_TO_S24(0x000000) }; + + run_test("test_f32_s24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + true, true, conv_f32_to_s24_c); + run_test("test_f32d_s24", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + false, true, conv_f32d_to_s24_c); + run_test("test_f32_s24d", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + true, false, conv_f32_to_s24d_c); + run_test("test_f32d_s24d", in, sizeof(in[0]), out, 3, SPA_N_ELEMENTS(in), + false, false, conv_f32d_to_s24d_c); +} + +static void test_s24_f32(void) +{ + static const int24_t in[] = { S32_TO_S24(0), S32_TO_S24(0x7fffff), + S32_TO_S24(0xff800000), S32_TO_S24(0x400000), S32_TO_S24(0xc00000) }; + static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, }; + + run_test("test_s24_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_to_f32d_c); + run_test("test_s24d_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_s24d_to_f32_c); + run_test("test_s24_f32", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_s24_to_f32_c); + run_test("test_s24d_f32d", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_s24d_to_f32d_c); +#if defined(HAVE_SSE2) + if (cpu_flags & SPA_CPU_FLAG_SSE2) { + run_test("test_s24_f32d_sse2", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_to_f32d_sse2); + } +#endif +#if defined(HAVE_SSSE3) + if (cpu_flags & SPA_CPU_FLAG_SSSE3) { + run_test("test_s24_f32d_ssse3", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_to_f32d_ssse3); + } +#endif +#if defined(HAVE_SSE41) + if (cpu_flags & SPA_CPU_FLAG_SSE41) { + run_test("test_s24_f32d_sse41", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_to_f32d_sse41); + } +#endif +#if defined(HAVE_AVX2) + if (cpu_flags & SPA_CPU_FLAG_AVX2) { + run_test("test_s24_f32d_avx2", in, 3, out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_to_f32d_avx2); + } +#endif +} + +static void test_f32_u24_32(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const uint32_t out[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000, + 0xffffff, 0x000000, + 0x800001, 0x800000, 0x7fffff, 0x800000 }; + + run_test("test_f32_u24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_u24_32_c); + run_test("test_f32d_u24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_u24_32_c); +} + +static void test_u24_32_f32(void) +{ + static const uint32_t in[] = { 0x800000, 0xffffff, 0x0, 0xc00000, 0x400000, 0x11000000 }; + static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f }; + + run_test("test_u24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_u24_32_to_f32d_c); + run_test("test_u24_32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_u24_32_to_f32_c); +} + +static void test_f32_s24_32(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f, + 1.0f/0xa00000, 1.0f/0x1000000, -1.0f/0xa00000, -1.0f/0x1000000 }; + static const int32_t out[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000, + 0x7fffff, 0xff800000, + 0x000001, 0x000000, 0xffffffff, 0x000000 }; + + run_test("test_f32_s24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_s24_32_c); + run_test("test_f32d_s24_32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_s24_32_c); + run_test("test_f32_s24_32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_f32_to_s24_32d_c); + run_test("test_f32d_s24_32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_f32d_to_s24_32d_c); +} + +static void test_s24_32_f32(void) +{ + static const int32_t in[] = { 0, 0x7fffff, 0xff800000, 0x400000, 0xffc00000, 0x66800000 }; + static const float out[] = { 0.0f, 0.999999880791f, -1.0f, 0.5f, -0.5f, -1.0f }; + + run_test("test_s24_32_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_s24_32_to_f32d_c); + run_test("test_s24_32d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_s24_32d_to_f32_c); + run_test("test_s24_32_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_s24_32_to_f32_c); + run_test("test_s24_32d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_s24_32d_to_f32d_c); +} + +static void test_f64_f32(void) +{ + static const double in[] = { 0.0, 1.0, -1.0, 0.5, -0.5, }; + static const float out[] = { 0.0, 1.0, -1.0, 0.5, -0.5, }; + + run_test("test_f64_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_f64_to_f32d_c); + run_test("test_f64d_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f64d_to_f32_c); + run_test("test_f64_f32", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f64_to_f32_c); + run_test("test_f64d_f32d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_f64d_to_f32d_c); +} + +static void test_f32_f64(void) +{ + static const float in[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f }; + static const double out[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 1.1f, -1.1f }; + + run_test("test_f32_f64", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, true, conv_f32_to_f64_c); + run_test("test_f32d_f64", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, true, conv_f32d_to_f64_c); + run_test("test_f32_f64d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + true, false, conv_f32_to_f64d_c); + run_test("test_f32d_f64d", in, sizeof(in[0]), out, sizeof(out[0]), SPA_N_ELEMENTS(out), + false, false, conv_f32d_to_f64d_c); +} + +static void test_lossless_s8(void) +{ + int8_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S8_MIN; i < S8_MAX; i+=1) { + float v = S8_TO_F32(i); + int8_t t = F32_TO_S8(v); + spa_assert_se(i == t); + } +} + +static void test_lossless_u8(void) +{ + uint8_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = U8_MIN; i < U8_MAX; i+=1) { + float v = U8_TO_F32(i); + uint8_t t = F32_TO_U8(v); + spa_assert_se(i == t); + } +} +static void test_lossless_s16(void) +{ + int16_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S16_MIN; i < S16_MAX; i+=3) { + float v = S16_TO_F32(i); + int16_t t = F32_TO_S16(v); + spa_assert_se(i == t); + } +} + +static void test_lossless_u16(void) +{ + uint32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = U16_MIN; i < U16_MAX; i+=3) { + float v = U16_TO_F32(i); + uint16_t t = F32_TO_U16(v); + spa_assert_se(i == t); + } +} + +static void test_lossless_s24(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S24_MIN; i < S24_MAX; i+=13) { + float v = S24_TO_F32(s32_to_s24(i)); + int32_t t = s24_to_s32(F32_TO_S24(v)); + spa_assert_se(i == t); + } +} + +static void test_lossless_u24(void) +{ + uint32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = U24_MIN; i < U24_MAX; i+=11) { + float v = U24_TO_F32(u32_to_u24(i)); + uint32_t t = u24_to_u32(F32_TO_U24(v)); + spa_assert_se(i == t); + } +} + +static void test_lossless_s32(void) +{ + int32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = S32_MIN; i < S32_MAX; i+=255) { + float v = S32_TO_F32(i); + int32_t t = F32_TO_S32(v); + spa_assert_se(SPA_ABS(i - t) <= 256); + } +} + +static void test_lossless_u32(void) +{ + uint32_t i; + + fprintf(stderr, "test %s:\n", __func__); + for (i = U32_MIN; i < U32_MAX; i+=255) { + float v = U32_TO_F32(i); + uint32_t t = F32_TO_U32(v); + spa_assert_se(i > t ? (i - t) <= 256 : (t - i) <= 256); + } +} + +static void test_swaps(void) +{ + { + uint24_t v = U32_TO_U24(0x123456); + uint24_t t = U32_TO_U24(0x563412); + uint24_t s = bswap_u24(v); + spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0); + } + { + int24_t v = S32_TO_S24(0xfffe1dc0); + int24_t t = S32_TO_S24(0xffc01dfe); + int24_t s = bswap_s24(v); + spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0); + } + { + int24_t v = S32_TO_S24(0x123456); + int24_t t = S32_TO_S24(0x563412); + int24_t s = bswap_s24(v); + spa_assert_se(memcmp(&s, &t, sizeof(t)) == 0); + } +} + +static void run_test_noise(uint32_t fmt, uint32_t noise, uint32_t flags) +{ + struct convert conv; + const void *ip[N_CHANNELS]; + void *op[N_CHANNELS]; + uint32_t i, range; + bool all_zero; + + spa_zero(conv); + + conv.noise_bits = noise; + conv.src_fmt = SPA_AUDIO_FORMAT_F32P; + conv.dst_fmt = fmt; + conv.n_channels = 2; + conv.rate = 44100; + conv.cpu_flags = flags; + spa_assert_se(convert_init(&conv) == 0); + fprintf(stderr, "test noise %s:\n", conv.func_name); + + memset(samp_in, 0, sizeof(samp_in)); + for (i = 0; i < conv.n_channels; i++) { + ip[i] = samp_in; + op[i] = samp_out; + } + convert_process(&conv, op, ip, N_SAMPLES); + + range = 1 << conv.noise_bits; + + all_zero = true; + for (i = 0; i < conv.n_channels * N_SAMPLES; i++) { + switch (fmt) { + case SPA_AUDIO_FORMAT_S8: + { + int8_t *d = (int8_t *)samp_out; + if (d[i] != 0) + all_zero = false; + spa_assert_se(SPA_ABS(d[i] - 0) <= (int8_t)range); + break; + } + case SPA_AUDIO_FORMAT_U8: + { + uint8_t *d = (uint8_t *)samp_out; + if (d[i] != 0x80) + all_zero = false; + spa_assert_se((int8_t)SPA_ABS(d[i] - 0x80) <= (int8_t)(range<<1)); + break; + } + case SPA_AUDIO_FORMAT_S16: + { + int16_t *d = (int16_t *)samp_out; + if (d[i] != 0) + all_zero = false; + spa_assert_se(SPA_ABS(d[i] - 0) <= (int16_t)range); + break; + } + case SPA_AUDIO_FORMAT_S24: + { + int24_t *d = (int24_t *)samp_out; + int32_t t = s24_to_s32(d[i]); + if (t != 0) + all_zero = false; + spa_assert_se(SPA_ABS(t - 0) <= (int32_t)range); + break; + } + case SPA_AUDIO_FORMAT_S32: + { + int32_t *d = (int32_t *)samp_out; + if (d[i] != 0) + all_zero = false; + spa_assert_se(SPA_ABS(d[i] - 0) <= (int32_t)(range << 8)); + break; + } + default: + spa_assert_not_reached(); + break; + } + } + spa_assert_se(all_zero == false); + convert_free(&conv); +} + +static void test_noise(void) +{ + run_test_noise(SPA_AUDIO_FORMAT_S8, 1, 0); + run_test_noise(SPA_AUDIO_FORMAT_S8, 2, 0); + run_test_noise(SPA_AUDIO_FORMAT_U8, 1, 0); + run_test_noise(SPA_AUDIO_FORMAT_U8, 2, 0); + run_test_noise(SPA_AUDIO_FORMAT_S16, 1, 0); + run_test_noise(SPA_AUDIO_FORMAT_S16, 2, 0); + run_test_noise(SPA_AUDIO_FORMAT_S24, 1, 0); + run_test_noise(SPA_AUDIO_FORMAT_S24, 2, 0); + run_test_noise(SPA_AUDIO_FORMAT_S32, 1, 0); + run_test_noise(SPA_AUDIO_FORMAT_S32, 2, 0); +} + +int main(int argc, char *argv[]) +{ + cpu_flags = get_cpu_flags(); + printf("got CPU flags %d\n", cpu_flags); + + test_f32_s8(); + test_s8_f32(); + test_f32_u8(); + test_u8_f32(); + test_f32_u16(); + test_u16_f32(); + test_f32_s16(); + test_s16_f32(); + test_f32_u32(); + test_u32_f32(); + test_f32_s32(); + test_s32_f32(); + test_f32_u24(); + test_u24_f32(); + test_f32_s24(); + test_s24_f32(); + test_f32_u24_32(); + test_u24_32_f32(); + test_f32_s24_32(); + test_s24_32_f32(); + test_f32_f64(); + test_f64_f32(); + + test_lossless_s8(); + test_lossless_u8(); + test_lossless_s16(); + test_lossless_u16(); + test_lossless_s24(); + test_lossless_u24(); + test_lossless_s32(); + test_lossless_u32(); + + test_swaps(); + + test_noise(); + + return 0; +} diff --git a/spa/plugins/audioconvert/test-helper.h b/spa/plugins/audioconvert/test-helper.h new file mode 100644 index 0000000..8c789bd --- /dev/null +++ b/spa/plugins/audioconvert/test-helper.h @@ -0,0 +1,97 @@ +#include + +#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/audioconvert/test-peaks.c b/spa/plugins/audioconvert/test-peaks.c new file mode 100644 index 0000000..3f7d093 --- /dev/null +++ b/spa/plugins/audioconvert/test-peaks.c @@ -0,0 +1,128 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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] = (drand48() - 0.5f) * 2.5f; + + peaks_min_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[0], &max[0]); + printf("c peaks min:%f max:%f\n", min[0], max[0]); + + absmax[0] = peaks_abs_max_c(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f); + printf("c peaks abs-max:%f\n", absmax[0]); + +#if defined(HAVE_SSE) + if (cpu_flags & SPA_CPU_FLAG_SSE) { + peaks_min_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, &min[1], &max[1]); + printf("sse peaks min:%f max:%f\n", min[1], max[1]); + + absmax[1] = peaks_abs_max_sse(&peaks, &vals[1], SPA_N_ELEMENTS(vals) - 1, 0.0f); + printf("sse peaks abs-max:%f\n", absmax[1]); + + spa_assert(min[0] == min[1]); + spa_assert(max[0] == max[1]); + spa_assert(absmax[0] == absmax[1]); + } +#endif + +} + +static void test_min_max(void) +{ + struct peaks peaks; + const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f }; + float min = 0.0f, max = 0.0f; + + spa_zero(peaks); + peaks.log = &logger.log; + peaks.cpu_flags = cpu_flags; + peaks_init(&peaks); + + peaks_min_max(&peaks, vals, SPA_N_ELEMENTS(vals), &min, &max); + + spa_assert(min == -0.8f); + spa_assert(max == 0.6f); +} + +static void test_abs_max(void) +{ + struct peaks peaks; + const float vals[] = { 0.0f, 0.5f, -0.5f, 0.0f, 0.6f, -0.8f, -0.5f, 0.0f }; + float max = 0.0f; + + spa_zero(peaks); + peaks.log = &logger.log; + peaks.cpu_flags = cpu_flags; + peaks_init(&peaks); + + max = peaks_abs_max(&peaks, vals, SPA_N_ELEMENTS(vals), max); + + spa_assert(max == 0.8f); +} + +int main(int argc, char *argv[]) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + srand48(SPA_TIMESPEC_TO_NSEC(&ts)); + + logger.log.level = SPA_LOG_LEVEL_TRACE; + + cpu_flags = get_cpu_flags(); + printf("got CPU flags %d\n", cpu_flags); + + test_impl(); + + test_min_max(); + test_abs_max(); + + return 0; +} diff --git a/spa/plugins/audioconvert/test-resample.c b/spa/plugins/audioconvert/test-resample.c new file mode 100644 index 0000000..4f11f53 --- /dev/null +++ b/spa/plugins/audioconvert/test-resample.c @@ -0,0 +1,177 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..b352323 --- /dev/null +++ b/spa/plugins/audioconvert/test-source.c @@ -0,0 +1,931 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "test-source" + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 + +#define MAX_BUFFERS 32 + +struct impl; + +#define DEFAULT_MUTE false +#define DEFAULT_VOLUME 1.0f + +struct props { + float volume; + bool mute; +}; + +static void props_reset(struct props *props) +{ + props->mute = DEFAULT_MUTE; + props->volume = DEFAULT_VOLUME; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1 << 0) + uint32_t flags; + struct spa_list link; + struct spa_buffer *outbuf; + struct spa_meta_header *h; +}; + +struct port { + uint32_t direction; + uint32_t id; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + struct spa_io_buffers *io; + + struct spa_audio_info format; + uint32_t stride; + uint32_t blocks; + uint32_t size; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list queue; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + uint32_t quantum_limit; + + struct spa_hook_list hooks; + + uint64_t info_all; + struct spa_node_info info; + struct props props; + struct spa_param_info params[8]; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + struct port out_port; + + unsigned int started:1; +}; + +#define CHECK_PORT(this,d,id) (d == SPA_DIRECTION_OUTPUT && id == 0) +#define GET_OUT_PORT(this,id) (&this->out_port) +#define GET_PORT(this,d,id) (d == SPA_DIRECTION_OUTPUT ? GET_OUT_PORT(this,id) : NULL) + +static void emit_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, NAME" %p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), + SPA_PROP_INFO_description, SPA_POD_String("Volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute), + SPA_PROP_INFO_description, SPA_POD_String("Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mute)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_volume, SPA_POD_Float(p->volume), + SPA_PROP_mute, SPA_POD_Bool(p->mute)); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + struct props *p = &this->props; + int changed = 0; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + if (spa_pod_get_float(&prop->value, &p->volume) == 0) + changed++; + break; + case SPA_PROP_mute: + if (spa_pod_get_bool(&prop->value, &p->mute) == 0) + changed++; + break; + default: + break; + } + } + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + if (param == NULL) { + props_reset(&this->props); + return 0; + } + if (apply_props(this, param) > 0) { + this->info.change_mask = SPA_NODE_CHANGE_MASK_PARAMS; + this->params[1].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(this, false); + } + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int impl_node_add_port(void *object, + enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, + enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX)); + break; + case 1: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S32), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, ¶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, NAME " %p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int calc_width(struct spa_audio_info *info) +{ + switch (info->info.raw.format) { + case SPA_AUDIO_FORMAT_U8P: + case SPA_AUDIO_FORMAT_U8: + return 1; + case SPA_AUDIO_FORMAT_S16P: + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + return 2; + case SPA_AUDIO_FORMAT_S24P: + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + return 3; + case SPA_AUDIO_FORMAT_S32P: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + return 4; + default: + return 0; + } +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + if (format == NULL) { + if (port->have_format) { + port->have_format = false; + clear_buffers(this, port); + } + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if ((res = spa_format_audio_raw_parse(format, &info.info.raw)) < 0) + return res; + + port->stride = calc_width(&info); + if (port->stride == 0) + return -EINVAL; + if (info.info.raw.rate == 0 || + info.info.raw.channels == 0) + return -EINVAL; + + if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) { + port->blocks = info.info.raw.channels; + } + else { + port->stride *= info.info.raw.channels; + port->blocks = 1; + } + port->have_format = true; + port->format = info; + + spa_log_debug(this->log, NAME " %p: set format on port %d %d", this, port_id, res); + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(object != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL); + + spa_log_debug(this->log, NAME" %p: set param %d", this, id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(object, direction, port_id, flags, param); + break; + default: + res = -ENOENT; + } + return res; +} + +static void recycle_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_list_append(&port->queue, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + spa_log_trace_fp(this->log, NAME " %p: recycle buffer %d", this, b->id); + } +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i, j, size = SPA_ID_INVALID; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_return_val_if_fail(port->have_format, -EIO); + + spa_log_debug(this->log, NAME " %p: use buffers %d on port %d", this, n_buffers, port_id); + + clear_buffers(this, port); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + uint32_t n_datas = buffers[i]->n_datas; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + b->outbuf = buffers[i]; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + for (j = 0; j < n_datas; j++) { + if (size == SPA_ID_INVALID) + size = d[j].maxsize; + else if (size != d[j].maxsize) + return -EINVAL; + + if (d[j].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + if (!SPA_IS_ALIGNED(d[j].data, 16)) { + spa_log_warn(this->log, NAME " %p: memory %d on buffer %d not aligned", + this, j, i); + } + } + recycle_buffer(this, port, b); + } + port->n_buffers = n_buffers; + port->size = size; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static struct buffer *dequeue_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + return b; +} + + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id); + if (buffer_id < port->n_buffers) + recycle_buffer(this, port, &port->buffers[buffer_id]); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + struct buffer *buf; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + if ((io = port->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, NAME " %p: status %d", this, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) + goto done; + + /* recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, &port->buffers[io->buffer_id]); + io->buffer_id = SPA_ID_INVALID; + } + + if ((buf = dequeue_buffer(this, port)) == NULL) + return io->status = -EPIPE; + + io->status = SPA_STATUS_HAVE_DATA; + io->buffer_id = buf->id; + + done: + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + } + + spa_log_debug(this->log, NAME " %p: init", this); + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + props_reset(&this->props); + + port = GET_OUT_PORT(this, 0); + port->direction = SPA_DIRECTION_OUTPUT; + port->id = 0; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->queue); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory test_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_AUDIO_PROCESS_CHANNELMIX, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/audioconvert/volume-ops-c.c b/spa/plugins/audioconvert/volume-ops-c.c new file mode 100644 index 0000000..0cc6f5f --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops-c.c @@ -0,0 +1,45 @@ +/* Spa + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "volume-ops.h" + +void +volume_f32_c(struct volume *vol, void * SPA_RESTRICT dst, + const void * SPA_RESTRICT src, float volume, uint32_t n_samples) +{ + uint32_t n; + float *d = (float*)dst; + const float *s = (const float*)src; + + if (volume == VOLUME_MIN) { + memset(d, 0, n_samples * sizeof(float)); + } + else if (volume == VOLUME_NORM) { + spa_memcpy(d, s, n_samples * sizeof(float)); + } + else { + for (n = 0; n < n_samples; n++) + d[n] = s[n] * volume; + } +} diff --git a/spa/plugins/audioconvert/volume-ops-sse.c b/spa/plugins/audioconvert/volume-ops-sse.c new file mode 100644 index 0000000..cd1f3cc --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops-sse.c @@ -0,0 +1,66 @@ +/* Spa + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "volume-ops.h" + +#include + +void +volume_f32_sse(struct volume *vol, void * SPA_RESTRICT dst, + const void * SPA_RESTRICT src, float volume, uint32_t n_samples) +{ + uint32_t n, unrolled; + float *d = (float*)dst; + const float *s = (const float*)src; + + if (volume == VOLUME_MIN) { + memset(d, 0, n_samples * sizeof(float)); + } + else if (volume == VOLUME_NORM) { + spa_memcpy(d, s, n_samples * sizeof(float)); + } + else { + __m128 t[4]; + const __m128 vol = _mm_set1_ps(volume); + + if (SPA_IS_ALIGNED(d, 16) && + SPA_IS_ALIGNED(s, 16)) + unrolled = n_samples & ~15; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 16) { + t[0] = _mm_load_ps(&s[n]); + t[1] = _mm_load_ps(&s[n+4]); + t[2] = _mm_load_ps(&s[n+8]); + t[3] = _mm_load_ps(&s[n+12]); + _mm_store_ps(&d[n], _mm_mul_ps(t[0], vol)); + _mm_store_ps(&d[n+4], _mm_mul_ps(t[1], vol)); + _mm_store_ps(&d[n+8], _mm_mul_ps(t[2], vol)); + _mm_store_ps(&d[n+12], _mm_mul_ps(t[3], vol)); + } + for(; n < n_samples; n++) + _mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), vol)); + } +} diff --git a/spa/plugins/audioconvert/volume-ops.c b/spa/plugins/audioconvert/volume-ops.c new file mode 100644 index 0000000..6890cfa --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops.c @@ -0,0 +1,84 @@ +/* Spa + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..0825712 --- /dev/null +++ b/spa/plugins/audioconvert/volume-ops.h @@ -0,0 +1,68 @@ +/* Spa + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c new file mode 100644 index 0000000..25da978 --- /dev/null +++ b/spa/plugins/audiomixer/audiomixer.c @@ -0,0 +1,990 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 +static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.audiomixer"); + +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 + +#define MAX_BUFFERS 64 +#define MAX_PORTS 128 +#define MAX_CHANNELS 64 +#define MAX_ALIGN MIX_OPS_MAX_ALIGN + +#define PORT_DEFAULT_VOLUME 1.0 +#define PORT_DEFAULT_MUTE false + +struct port_props { + double volume; + int32_t mute; +}; + +static void port_props_reset(struct port_props *props) +{ + props->volume = PORT_DEFAULT_VOLUME; + props->mute = PORT_DEFAULT_MUTE; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1 << 0) + uint32_t flags; + + struct spa_list link; + struct spa_buffer *buffer; + struct spa_meta_header *h; + struct spa_buffer buf; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct port_props props; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + unsigned int valid:1; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list queue; + size_t queued_bytes; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + uint32_t cpu_flags; + uint32_t max_align; + uint32_t quantum_limit; + + struct mix_ops ops; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[8]; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + int n_formats; + struct spa_audio_info format; + + unsigned int have_format:1; + unsigned int started:1; + uint32_t stride; + uint32_t blocks; +}; + +#define PORT_VALID(p) ((p) != NULL && (p)->valid) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) +#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) +#define GET_IN_PORT(this,p) (this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + for (i = 0; i < this->last_port; i++) { + if (PORT_VALID(this->in_ports[i])) + emit_port_info(this, GET_IN_PORT(this, i), true); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT(this, port_id); + if (port == NULL) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + this->in_ports[port_id] = port; + } + port->direction = SPA_DIRECTION_INPUT; + port->id = port_id; + + port_props_reset(&port->props); + + spa_list_init(&port->queue); + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_DYNAMIC_DATA | + SPA_PORT_FLAG_REMOVABLE | + SPA_PORT_FLAG_OPTIONAL; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + this->port_count++; + if (this->last_port <= port_id) + this->last_port = port_id + 1; + port->valid = true; + + spa_log_debug(this->log, "%p: add port %d:%d %d", this, + direction, port_id, this->last_port); + emit_port_info(this, port, true); + + return 0; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT(this, port_id); + + port->valid = false; + this->port_count--; + if (port->have_format && this->have_format) { + if (--this->n_formats == 0) + this->have_format = false; + } + spa_memzero(port, sizeof(struct port)); + + if (port_id + 1 == this->last_port) { + int i; + + for (i = this->last_port - 1; i >= 0; i--) + if (PORT_VALID(GET_IN_PORT(this, i))) + break; + + this->last_port = i + 1; + } + spa_log_debug(this->log, "%p: remove port %d:%d %d", this, + direction, port_id, this->last_port); + + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + + return 0; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + + switch (index) { + case 0: + if (this->have_format) { + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(this->format.info.raw.format), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(this->format.info.raw.rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(this->format.info.raw.channels)); + } else { + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Int(12, + SPA_AUDIO_FORMAT_S8, + SPA_AUDIO_FORMAT_U8, + SPA_AUDIO_FORMAT_S16, + SPA_AUDIO_FORMAT_U16, + SPA_AUDIO_FORMAT_S24, + SPA_AUDIO_FORMAT_U24, + SPA_AUDIO_FORMAT_S32, + SPA_AUDIO_FORMAT_U32, + SPA_AUDIO_FORMAT_S24_32, + SPA_AUDIO_FORMAT_U24_32, + SPA_AUDIO_FORMAT_F32, + SPA_AUDIO_FORMAT_F64), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_RATE, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_CHANNELS, 1, INT32_MAX)); + } + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, result.index, ¶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->format.info.raw); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->stride, + 16 * this->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride)); + break; + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, "%p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int queue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return -EINVAL; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, "%p: queue buffer %d", this, b->id); + return 0; +} + +static struct buffer *dequeue_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id); + return b; +} + +static int calc_width(struct spa_audio_info *info) +{ + switch (info->info.raw.format) { + case SPA_AUDIO_FORMAT_U8P: + case SPA_AUDIO_FORMAT_U8: + case SPA_AUDIO_FORMAT_S8P: + case SPA_AUDIO_FORMAT_S8: + case SPA_AUDIO_FORMAT_ALAW: + case SPA_AUDIO_FORMAT_ULAW: + return 1; + case SPA_AUDIO_FORMAT_S16P: + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + return 2; + case SPA_AUDIO_FORMAT_S24P: + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + return 3; + case SPA_AUDIO_FORMAT_F64P: + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + return 8; + default: + return 4; + } +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + if (format == NULL) { + if (port->have_format) { + port->have_format = false; + if (--this->n_formats == 0) + this->have_format = false; + clear_buffers(this, port); + } + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (this->have_format) { + if (memcmp(&info, &this->format, sizeof(struct spa_audio_info))) + return -EINVAL; + } else { + if (info.info.raw.format == 0 || + info.info.raw.channels == 0) + return -EINVAL; + + this->ops.fmt = info.info.raw.format; + this->ops.n_channels = info.info.raw.channels; + this->ops.cpu_flags = this->cpu_flags; + + if ((res = mix_ops_init(&this->ops)) < 0) + return res; + + this->stride = calc_width(&info); + + if (SPA_AUDIO_FORMAT_IS_PLANAR(info.info.raw.format)) { + this->blocks = info.info.raw.channels; + } else { + this->stride *= info.info.raw.channels; + this->blocks = 1; + } + + this->have_format = true; + this->format = info; + } + if (!port->have_format) { + this->n_formats++; + port->have_format = true; + spa_log_debug(this->log, "%p: set format on port %d", + this, port_id); + } + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + if (id == SPA_PARAM_Format) { + return port_set_format(this, direction, port_id, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers on port %d:%d", + this, n_buffers, direction, port_id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->buffer = buffers[i]; + b->flags = 0; + b->id = i; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + b->buf = *buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + if (!SPA_IS_ALIGNED(d[0].data, this->max_align)) { + spa_log_warn(this->log, "%p: memory on buffer %d not aligned", this, i); + } + if (direction == SPA_DIRECTION_OUTPUT) + queue_buffer(this, port, b); + + spa_log_debug(this->log, "%p: port %d:%d buffer:%d n_data:%d data:%p maxsize:%d", + this, direction, port_id, i, + buffers[i]->n_datas, d[0].data, d[0].maxsize); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: port %d:%d io %d %p/%zd", this, + direction, port_id, id, data, size); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + port = GET_OUT_PORT(this, 0); + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + return queue_buffer(this, port, &port->buffers[buffer_id]); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *outport; + struct spa_io_buffers *outio; + uint32_t n_buffers, i, maxsize; + struct buffer **buffers; + struct buffer *outb; + const void **datas; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: status %p %d %d", + this, outio, outio->status, outio->buffer_id); + + if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA)) + return outio->status; + + /* recycle */ + if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) { + queue_buffer(this, outport, &outport->buffers[outio->buffer_id]); + outio->buffer_id = SPA_ID_INVALID; + } + + buffers = alloca(MAX_PORTS * sizeof(struct buffer *)); + datas = alloca(MAX_PORTS * sizeof(void *)); + n_buffers = 0; + + maxsize = UINT32_MAX; + + for (i = 0; i < this->last_port; i++) { + struct port *inport = GET_IN_PORT(this, i); + struct spa_io_buffers *inio = NULL; + struct buffer *inb; + struct spa_data *bd; + uint32_t size, offs; + + if (SPA_UNLIKELY(!PORT_VALID(inport) || + (inio = inport->io) == NULL || + inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, PORT_VALID(inport), inio, + inio ? inio->status : -1, + inio ? inio->buffer_id : SPA_ID_INVALID, + inport->n_buffers); + continue; + } + + inb = &inport->buffers[inio->buffer_id]; + bd = &inb->buffer->datas[0]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + maxsize = SPA_MIN(size, maxsize); + + spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d", this, + i, inio, outio, inio->status, inio->buffer_id, + offs, size); + + if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) { + datas[n_buffers] = SPA_PTROFF(bd->data, offs, void); + buffers[n_buffers++] = inb; + } + inio->status = SPA_STATUS_NEED_DATA; + } + + outb = dequeue_buffer(this, outport); + if (SPA_UNLIKELY(outb == NULL)) { + spa_log_trace(this->log, "%p: out of buffers", this); + return -EPIPE; + } + + if (n_buffers == 1) { + *outb->buffer = *buffers[0]->buffer; + } else { + struct spa_data *d = outb->buf.datas; + + *outb->buffer = outb->buf; + + maxsize = SPA_MIN(maxsize, d[0].maxsize); + + d[0].chunk->offset = 0; + d[0].chunk->size = maxsize; + d[0].chunk->stride = this->stride; + SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0); + + mix_ops_process(&this->ops, d[0].data, + datas, n_buffers, maxsize / this->stride); + } + + outio->buffer_id = outb->id; + outio->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + for (i = 0; i < MAX_PORTS; i++) + free(this->in_ports[i]); + mix_ops_free(&this->ops); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + if (this->cpu) { + this->cpu_flags = spa_cpu_get_flags(this->cpu); + this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); + } + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = 1; + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; + + port = GET_OUT_PORT(this, 0); + port->valid = true; + port->direction = SPA_DIRECTION_OUTPUT; + port->id = 0; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + spa_list_init(&port->queue); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_audiomixer_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_AUDIO_MIXER, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/audiomixer/benchmark-mix-ops.c b/spa/plugins/audiomixer/benchmark-mix-ops.c new file mode 100644 index 0000000..e698417 --- /dev/null +++ b/spa/plugins/audiomixer/benchmark-mix-ops.c @@ -0,0 +1,222 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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..5a4a1fb --- /dev/null +++ b/spa/plugins/audiomixer/meson.build @@ -0,0 +1,126 @@ +audiomixer_sources = [ + 'audiomixer.c', + 'mixer-dsp.c', + 'plugin.c' +] + +simd_cargs = [] +simd_dependencies = [] + +audiomixer_c = static_library('audiomixer_c', + ['mix-ops-c.c' ], + c_args : ['-O3'], + dependencies : [ spa_dep ], + install : false +) +simd_dependencies += audiomixer_c + +if have_sse + audiomixer_sse = static_library('audiomixer_sse', + ['mix-ops-sse.c' ], + c_args : [sse_args, '-O3', '-DHAVE_SSE'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE'] + simd_dependencies += audiomixer_sse +endif +if have_sse2 + audiomixer_sse2 = static_library('audiomixer_sse2', + ['mix-ops-sse2.c' ], + c_args : [sse2_args, '-O3', '-DHAVE_SSE2'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_SSE2'] + simd_dependencies += audiomixer_sse2 +endif +if have_avx and have_fma + audiomixer_avx = static_library('audiomixer_avx', + ['mix-ops-avx.c'], + c_args : [avx_args, fma_args, '-O3', '-DHAVE_AVX', '-DHAVE_FMA'], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_AVX', '-DHAVE_FMA'] + simd_dependencies += audiomixer_avx +endif + +audiomixer_lib = static_library('audiomixer', + ['mix-ops.c' ], + c_args : [ simd_cargs, '-O3'], + link_with : simd_dependencies, + include_directories : [configinc], + dependencies : [ spa_dep ], + install : false + ) +audiomixer_dep = declare_dependency(link_with: audiomixer_lib) + +spa_audiomixer_lib = shared_library('spa-audiomixer', + audiomixer_sources, + c_args : simd_cargs, + link_with : simd_dependencies, + dependencies : [ spa_dep, mathlib, audiomixer_dep ], + install : true, + install_dir : spa_plugindir / 'audiomixer' +) +spa_audiomixer_dep = declare_dependency(link_with: spa_audiomixer_lib) + +test_apps = [ + 'test-mix-ops', + ] + +foreach a : test_apps + test(a, + executable(a, a + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ], + include_directories : [ configinc ], + link_with : [ test_lib ], + install_rpath : spa_plugindir / 'audiomixer', + c_args : [ simd_cargs ], + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'audiomixer'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'audiomixer', + configuration: test_conf + ) + endif +endforeach + +benchmark_apps = [ + 'benchmark-mix-ops', + ] + +foreach a : benchmark_apps + benchmark(a, + executable(a, a + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, audiomixer_dep ], + include_directories : [ configinc ], + c_args : [ simd_cargs ], + install_rpath : spa_plugindir / 'audiomixer', + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'audiomixer'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'audiomixer' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'audiomixer', + configuration: test_conf + ) + endif +endforeach diff --git a/spa/plugins/audiomixer/mix-ops-avx.c b/spa/plugins/audiomixer/mix-ops-avx.c new file mode 100644 index 0000000..3c5aa6b --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-avx.c @@ -0,0 +1,88 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..2f79cd8 --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-c.c @@ -0,0 +1,68 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..bae619b --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-sse.c @@ -0,0 +1,87 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..e2f632d --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops-sse2.c @@ -0,0 +1,87 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..b459926 --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops.c @@ -0,0 +1,136 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..11e88dc --- /dev/null +++ b/spa/plugins/audiomixer/mix-ops.h @@ -0,0 +1,169 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +typedef struct { +#if __BYTE_ORDER == __LITTLE_ENDIAN + uint8_t v3; + uint8_t v2; + uint8_t v1; +#else + uint8_t v1; + uint8_t v2; + uint8_t v3; +#endif +} __attribute__ ((packed)) uint24_t; + +typedef struct { +#if __BYTE_ORDER == __LITTLE_ENDIAN + uint8_t v3; + uint8_t v2; + int8_t v1; +#else + int8_t v1; + uint8_t v2; + uint8_t v3; +#endif +} __attribute__ ((packed)) int24_t; + +static inline uint32_t u24_to_u32(uint24_t src) +{ + return ((uint32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3; +} + +#define U32_TO_U24(s) (uint24_t) { .v1 = (uint8_t)(((uint32_t)s) >> 16), \ + .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) } + +static inline uint24_t u32_to_u24(uint32_t src) +{ + return U32_TO_U24(src); +} + +static inline int32_t s24_to_s32(int24_t src) +{ + return ((int32_t)src.v1 << 16) | ((uint32_t)src.v2 << 8) | (uint32_t)src.v3; +} + +#define S32_TO_S24(s) (int24_t) { .v1 = (int8_t)(((int32_t)s) >> 16), \ + .v2 = (uint8_t)(((uint32_t)s) >> 8), .v3 = (uint8_t)((uint32_t)s) } + +static inline int24_t s32_to_s24(int32_t src) +{ + return S32_TO_S24(src); +} + + +#define S8_MIN -128 +#define S8_MAX 127 +#define S8_ACCUM(a,b) ((a) + (int16_t)(b)) +#define S8_CLAMP(a) (int8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX)) +#define U8_OFFS 128 +#define U8_ACCUM(a,b) ((a) + ((int16_t)(b) - U8_OFFS)) +#define U8_CLAMP(a) (uint8_t)(SPA_CLAMP((a), S8_MIN, S8_MAX) + U8_OFFS) + +#define S16_MIN -32768 +#define S16_MAX 32767 +#define S16_ACCUM(a,b) ((a) + (int32_t)(b)) +#define S16_CLAMP(a) (int16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX)) +#define U16_OFFS 32768 +#define U16_ACCUM(a,b) ((a) + ((int32_t)(b) - U16_OFFS)) +#define U16_CLAMP(a) (uint16_t)(SPA_CLAMP((a), S16_MIN, S16_MAX) + U16_OFFS) + +#define S24_32_MIN -8388608 +#define S24_32_MAX 8388607 +#define S24_32_ACCUM(a,b) ((a) + (int32_t)(b)) +#define S24_32_CLAMP(a) (int32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX)) +#define U24_32_OFFS 8388608 +#define U24_32_ACCUM(a,b) ((a) + ((int32_t)(b) - U24_32_OFFS)) +#define U24_32_CLAMP(a) (uint32_t)(SPA_CLAMP((a), S24_32_MIN, S24_32_MAX) + U24_32_OFFS) + +#define S24_ACCUM(a,b) S24_32_ACCUM(a, s24_to_s32(b)) +#define S24_CLAMP(a) s32_to_s24(S24_32_CLAMP(a)) +#define U24_ACCUM(a,b) U24_32_ACCUM(a, u24_to_u32(b)) +#define U24_CLAMP(a) u32_to_u24(U24_32_CLAMP(a)) + +#define S32_MIN -2147483648 +#define S32_MAX 2147483647 +#define S32_ACCUM(a,b) ((a) + (int64_t)(b)) +#define S32_CLAMP(a) (int32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX)) +#define U32_OFFS 2147483648 +#define U32_ACCUM(a,b) ((a) + ((int64_t)(b) - U32_OFFS)) +#define U32_CLAMP(a) (uint32_t)(SPA_CLAMP((a), S32_MIN, S32_MAX) + U32_OFFS) + +#define F32_ACCUM(a,b) ((a) + (b)) +#define F32_CLAMP(a) (a) +#define F64_ACCUM(a,b) ((a) + (b)) +#define F64_CLAMP(a) (a) + +struct mix_ops { + uint32_t fmt; + uint32_t n_channels; + uint32_t cpu_flags; + + void (*clear) (struct mix_ops *ops, void * SPA_RESTRICT dst, uint32_t n_samples); + void (*process) (struct mix_ops *ops, + void * SPA_RESTRICT dst, + const void * SPA_RESTRICT src[], uint32_t n_src, + uint32_t n_samples); + void (*free) (struct mix_ops *ops); + + const void *priv; +}; + +int mix_ops_init(struct mix_ops *ops); + +#define mix_ops_clear(ops,...) (ops)->clear(ops, __VA_ARGS__) +#define mix_ops_process(ops,...) (ops)->process(ops, __VA_ARGS__) +#define mix_ops_free(ops) (ops)->free(ops) + +#define DEFINE_FUNCTION(name,arch) \ +void mix_##name##_##arch(struct mix_ops *ops, void * SPA_RESTRICT dst, \ + const void * SPA_RESTRICT src[], uint32_t n_src, \ + uint32_t n_samples) \ + +#define MIX_OPS_MAX_ALIGN 32 + +DEFINE_FUNCTION(s8, c); +DEFINE_FUNCTION(u8, c); +DEFINE_FUNCTION(s16, c); +DEFINE_FUNCTION(u16, c); +DEFINE_FUNCTION(s24, c); +DEFINE_FUNCTION(u24, c); +DEFINE_FUNCTION(s32, c); +DEFINE_FUNCTION(u32, c); +DEFINE_FUNCTION(s24_32, c); +DEFINE_FUNCTION(u24_32, c); +DEFINE_FUNCTION(f32, c); +DEFINE_FUNCTION(f64, c); + +#if defined(HAVE_SSE) +DEFINE_FUNCTION(f32, sse); +#endif +#if defined(HAVE_SSE2) +DEFINE_FUNCTION(f64, sse2); +#endif +#if defined(HAVE_AVX) +DEFINE_FUNCTION(f32, avx); +#endif diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c new file mode 100644 index 0000000..534cfab --- /dev/null +++ b/spa/plugins/audiomixer/mixer-dsp.c @@ -0,0 +1,927 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 +static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.mixer-dsp"); + +#define MAX_BUFFERS 64 +#define MAX_PORTS 128 +#define MAX_ALIGN MIX_OPS_MAX_ALIGN + +#define PORT_DEFAULT_VOLUME 1.0 +#define PORT_DEFAULT_MUTE false + +struct port_props { + double volume; + int32_t mute; +}; + +static void port_props_reset(struct port_props *props) +{ + props->volume = PORT_DEFAULT_VOLUME; + props->mute = PORT_DEFAULT_MUTE; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1 << 0) + uint32_t flags; + + struct spa_list link; + struct spa_buffer *buffer; + struct spa_meta_header *h; + struct spa_buffer buf; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct port_props props; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + unsigned int valid:1; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list queue; + size_t queued_bytes; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + uint32_t cpu_flags; + uint32_t max_align; + + uint32_t quantum_limit; + + struct mix_ops ops; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[8]; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + int n_formats; + struct spa_audio_info format; + uint32_t stride; + + unsigned int have_format:1; + unsigned int started:1; +}; + +#define PORT_VALID(p) ((p) != NULL && (p)->valid) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) +#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) +#define GET_IN_PORT(this,p) (this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + for (i = 0; i < this->last_port; i++) { + if (PORT_VALID(this->in_ports[i])) + emit_port_info(this, GET_IN_PORT(this, i), true); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT (this, port_id); + if (port == NULL) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + this->in_ports[port_id] = port; + } + + port->direction = direction; + port->id = port_id; + + port_props_reset(&port->props); + + spa_list_init(&port->queue); + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_DYNAMIC_DATA | + SPA_PORT_FLAG_REMOVABLE | + SPA_PORT_FLAG_OPTIONAL; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + this->port_count++; + if (this->last_port <= port_id) + this->last_port = port_id + 1; + port->valid = true; + + spa_log_debug(this->log, "%p: add port %d:%d %d", this, + direction, port_id, this->last_port); + emit_port_info(this, port, true); + + return 0; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT (this, port_id); + + port->valid = false; + this->port_count--; + if (port->have_format && this->have_format) { + if (--this->n_formats == 0) + this->have_format = false; + } + spa_memzero(port, sizeof(struct port)); + + if (port_id + 1 == this->last_port) { + int i; + + for (i = this->last_port - 1; i >= 0; i--) + if (PORT_VALID(GET_IN_PORT(this, i))) + break; + + this->last_port = i + 1; + } + spa_log_debug(this->log, "%p: remove port %d:%d %d", this, + direction, port_id, this->last_port); + + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + + return 0; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + + switch (index) { + case 0: + if (this->have_format) { + *param = spa_format_audio_dsp_build(builder, SPA_PARAM_EnumFormat, + &this->format.info.dsp); + } else { + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32)); + } + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, result.index, ¶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, &this->format.info.dsp); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->stride, + 16 * this->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->stride)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, "%p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int queue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return -EINVAL; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, "%p: queue buffer %d", this, b->id); + return 0; +} + +static struct buffer *dequeue_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id); + return b; +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + if (format == NULL) { + if (port->have_format) { + port->have_format = false; + if (--this->n_formats == 0) + this->have_format = false; + clear_buffers(this, port); + } + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) + return -EINVAL; + + if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0) + return -EINVAL; + + if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) + return -EINVAL; + + if (!this->have_format) { + this->ops.fmt = info.info.dsp.format; + this->ops.n_channels = 1; + this->ops.cpu_flags = this->cpu_flags; + + if ((res = mix_ops_init(&this->ops)) < 0) + return res; + + this->stride = sizeof(float); + this->have_format = true; + this->format = info; + } + if (!port->have_format) { + this->n_formats++; + port->have_format = true; + spa_log_debug(this->log, "%p: set format on port %d:%d", + this, direction, port_id); + } + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + if (id == SPA_PARAM_Format) { + return port_set_format(this, direction, port_id, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers on port %d:%d", + this, n_buffers, direction, port_id); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->buffer = buffers[i]; + b->flags = 0; + b->id = i; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + b->buf = *buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: invalid memory on buffer %d", this, i); + return -EINVAL; + } + if (!SPA_IS_ALIGNED(d[0].data, this->max_align)) { + spa_log_warn(this->log, "%p: memory on buffer %d not aligned", this, i); + } + if (direction == SPA_DIRECTION_OUTPUT) + queue_buffer(this, port, b); + + spa_log_debug(this->log, "%p: port %d:%d buffer:%d n_data:%d data:%p maxsize:%d", + this, direction, port_id, i, + buffers[i]->n_datas, d[0].data, d[0].maxsize); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: port %d:%d io %d %p/%zd", this, + direction, port_id, id, data, size); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + port = GET_OUT_PORT(this, 0); + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + return queue_buffer(this, port, &port->buffers[buffer_id]); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *outport; + struct spa_io_buffers *outio; + uint32_t n_buffers, i, maxsize; + struct buffer **buffers; + struct buffer *outb; + const void **datas; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: status %p %d %d", + this, outio, outio->status, outio->buffer_id); + + if (SPA_UNLIKELY(outio->status == SPA_STATUS_HAVE_DATA)) + return outio->status; + + /* recycle */ + if (SPA_LIKELY(outio->buffer_id < outport->n_buffers)) { + queue_buffer(this, outport, &outport->buffers[outio->buffer_id]); + outio->buffer_id = SPA_ID_INVALID; + } + + buffers = alloca(MAX_PORTS * sizeof(struct buffer *)); + datas = alloca(MAX_PORTS * sizeof(void *)); + n_buffers = 0; + + maxsize = UINT32_MAX; + + for (i = 0; i < this->last_port; i++) { + struct port *inport = GET_IN_PORT(this, i); + struct spa_io_buffers *inio = NULL; + struct buffer *inb; + struct spa_data *bd; + uint32_t size, offs; + + if (SPA_UNLIKELY(!PORT_VALID(inport) || + (inio = inport->io) == NULL || + inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA)) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, PORT_VALID(inport), inio, + inio ? inio->status : -1, + inio ? inio->buffer_id : SPA_ID_INVALID, + inport->n_buffers); + continue; + } + + inb = &inport->buffers[inio->buffer_id]; + bd = &inb->buffer->datas[0]; + + offs = SPA_MIN(bd->chunk->offset, bd->maxsize); + size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); + maxsize = SPA_MIN(maxsize, size); + + spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d", this, + i, inio, outio, inio->status, inio->buffer_id, + offs, size); + + if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) { + datas[n_buffers] = SPA_PTROFF(bd->data, offs, void); + buffers[n_buffers++] = inb; + } + inio->status = SPA_STATUS_NEED_DATA; + } + + outb = dequeue_buffer(this, outport); + if (SPA_UNLIKELY(outb == NULL)) { + spa_log_trace(this->log, "%p: out of buffers", this); + return -EPIPE; + } + + if (n_buffers == 1) { + *outb->buffer = *buffers[0]->buffer; + } else { + struct spa_data *d = outb->buf.datas; + + *outb->buffer = outb->buf; + + maxsize = SPA_MIN(maxsize, d[0].maxsize); + + d[0].chunk->offset = 0; + d[0].chunk->size = maxsize; + d[0].chunk->stride = sizeof(float); + SPA_FLAG_UPDATE(d[0].chunk->flags, SPA_CHUNK_FLAG_EMPTY, n_buffers == 0); + + spa_log_trace_fp(this->log, "%p: %d mix %d", this, n_buffers, maxsize); + + mix_ops_process(&this->ops, d[0].data, + datas, n_buffers, maxsize / sizeof(float)); + } + + outio->buffer_id = outb->id; + outio->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + for (i = 0; i < MAX_PORTS; i++) + free(this->in_ports[i]); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + if (this->cpu) { + this->cpu_flags = spa_cpu_get_flags(this->cpu); + this->max_align = SPA_MIN(MAX_ALIGN, spa_cpu_get_max_align(this->cpu)); + } + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = 1; + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; + + port = GET_OUT_PORT(this, 0); + port->valid = true; + port->direction = SPA_DIRECTION_OUTPUT; + port->id = 0; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + spa_list_init(&port->queue); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_mixer_dsp_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_AUDIO_MIXER_DSP, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/audiomixer/plugin.c b/spa/plugins/audiomixer/plugin.c new file mode 100644 index 0000000..3915425 --- /dev/null +++ b/spa/plugins/audiomixer/plugin.c @@ -0,0 +1,50 @@ +/* Spa Audiomixer plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_audiomixer_factory; +extern const struct spa_handle_factory spa_mixer_dsp_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_audiomixer_factory; + break; + case 1: + *factory = &spa_mixer_dsp_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/audiomixer/test-helper.h b/spa/plugins/audiomixer/test-helper.h new file mode 100644 index 0000000..8c789bd --- /dev/null +++ b/spa/plugins/audiomixer/test-helper.h @@ -0,0 +1,97 @@ +#include + +#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/audiomixer/test-mix-ops.c b/spa/plugins/audiomixer/test-mix-ops.c new file mode 100644 index 0000000..a20f7a4 --- /dev/null +++ b/spa/plugins/audiomixer/test-mix-ops.c @@ -0,0 +1,293 @@ +/* Spa + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#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..1501e1d --- /dev/null +++ b/spa/plugins/audiotestsrc/audiotestsrc.c @@ -0,0 +1,1159 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "audiotestsrc" + +#define SAMPLES_TO_TIME(this,s) ((s) * SPA_NSEC_PER_SEC / (port)->current_format.info.raw.rate) +#define BYTES_TO_SAMPLES(this,b) ((b)/(port)->bpf) +#define BYTES_TO_TIME(this,b) SAMPLES_TO_TIME(this, BYTES_TO_SAMPLES (this, b)) + +enum wave_type { + WAVE_SINE, + WAVE_SQUARE, +}; + +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 + +#define DEFAULT_LIVE true +#define DEFAULT_WAVE WAVE_SINE +#define DEFAULT_FREQ 440.0 +#define DEFAULT_VOLUME 1.0 + +struct props { + bool live; + uint32_t wave; + float freq; + float volume; +}; + +static void reset_props(struct props *props) +{ + props->live = DEFAULT_LIVE; + props->wave = DEFAULT_WAVE; + props->freq = DEFAULT_FREQ; + props->volume = DEFAULT_VOLUME; +} + +#define MAX_BUFFERS 16 +#define MAX_PORTS 1 + +struct buffer { + uint32_t id; + struct spa_buffer *outbuf; + bool outstanding; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct impl; + +typedef void (*render_func_t) (struct impl *this, void *samples, size_t n_samples); + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + struct spa_io_sequence *io_control; + + bool have_format; + struct spa_audio_info current_format; + size_t bpf; + render_func_t render_func; + float accumulator; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint32_t quantum_limit; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + struct props props; + struct spa_io_clock *clock; + struct spa_io_position *position; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + bool async; + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + uint64_t start_time; + uint64_t elapsed_time; + + uint64_t sample_count; + + struct port port; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live), + SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), + SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); + break; + case 1: + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_waveType), + SPA_PROP_INFO_description, SPA_POD_String("Select the waveform"), + SPA_PROP_INFO_type, SPA_POD_Int(p->wave), + 0); + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(&b, &f[1]); + spa_pod_builder_int(&b, WAVE_SINE); + spa_pod_builder_string(&b, "Sine wave"); + spa_pod_builder_int(&b, WAVE_SQUARE); + spa_pod_builder_string(&b, "Square wave"); + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_frequency), + SPA_PROP_INFO_description, SPA_POD_String("Select the frequency"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->freq, 0.0, 50000000.0)); + break; + case 3: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), + SPA_PROP_INFO_description, SPA_POD_String("Select the volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_live, SPA_POD_Bool(p->live), + SPA_PROP_waveType, SPA_POD_Int(p->wave), + SPA_PROP_frequency, SPA_POD_Float(p->freq), + SPA_PROP_volume, SPA_POD_Float(p->volume)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_IO: + { + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (id == SPA_PARAM_Props) { + struct props *p = &this->props; + struct port *port = &this->port; + + if (param == NULL) { + reset_props(p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_live, SPA_POD_OPT_Bool(&p->live), + SPA_PROP_waveType, SPA_POD_OPT_Int(&p->wave), + SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq), + SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume)); + + if (p->live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + else + port->info.flags &= ~SPA_PORT_FLAG_LIVE; + } + else + return -ENOENT; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + if (size > 0 && size < sizeof(struct spa_io_clock)) + return -EINVAL; + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +#include "render.c" + +static void set_timer(struct impl *this, bool enabled) +{ + if (this->async || this->props.live) { + if (enabled) { + if (this->props.live) { + uint64_t next_time = this->start_time + this->elapsed_time; + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 1; + } + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + } + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + } +} + +static int read_timer(struct impl *this) +{ + uint64_t expirations; + int res = 0; + + if (this->async || this->props.live) { + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return 0; +} + +static int make_buffer(struct impl *this) +{ + struct buffer *b; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + uint32_t n_bytes, n_samples, maxsize; + void *data; + struct spa_data *d; + uint32_t filled, avail; + uint32_t index, offset, l0, l1; + + if (read_timer(this) < 0) + return 0; + + if (spa_list_is_empty(&port->empty)) { + set_timer(this, false); + spa_log_error(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + b->outstanding = true; + + d = b->outbuf->datas; + maxsize = d[0].maxsize; + data = d[0].data; + + n_bytes = maxsize; + + spa_log_trace(this->log, NAME " %p: dequeue buffer %d %d %d", this, b->id, + maxsize, n_bytes); + + filled = 0; + index = 0; + avail = maxsize - filled; + + offset = index % maxsize; + + if (this->position && this->position->clock.duration) { + n_bytes = SPA_MIN(avail, n_bytes); + n_samples = this->position->clock.duration; + if (n_samples * port->bpf < n_bytes) + n_bytes = n_samples * port->bpf; + } else { + n_bytes = SPA_MIN(avail, n_bytes); + n_samples = n_bytes / port->bpf; + } + l0 = SPA_MIN(n_bytes, maxsize - offset) / port->bpf; + l1 = n_samples - l0; + + port->render_func(this, SPA_PTROFF(data, offset, void), l0); + if (l1 > 0) + port->render_func(this, data, l1); + + d[0].chunk->offset = index; + d[0].chunk->size = n_bytes; + d[0].chunk->stride = port->bpf; + + if (b->h) { + b->h->seq = this->sample_count; + b->h->pts = this->start_time + this->elapsed_time; + b->h->dts_offset = 0; + } + + this->sample_count += n_samples; + this->elapsed_time = SAMPLES_TO_TIME(this, this->sample_count); + set_timer(this, true); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return io->status; +} + +static void on_output(struct spa_source *source) +{ + struct impl *this = source->data; + int res; + + res = make_buffer(this); + + if (res == SPA_STATUS_HAVE_DATA) + spa_node_call_ready(&this->callbacks, res); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct timespec now; + + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (this->started) + return 0; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (this->props.live) + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); + else + this->start_time = 0; + this->sample_count = 0; + this->elapsed_time = 0; + + this->started = true; + set_timer(this, true); + break; + } + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + this->started = false; + set_timer(this, false); + break; + + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_MEDIA_CLASS, "Audio/Source" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +port_enum_formats(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(5, + SPA_AUDIO_FORMAT_S16, + SPA_AUDIO_FORMAT_S16, + SPA_AUDIO_FORMAT_S32, + SPA_AUDIO_FORMAT_F32, + SPA_AUDIO_FORMAT_F64), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_RATE, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_CHANNELS, 1, INT32_MAX)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->empty); + this->started = false; + set_timer(this, false); + } + return 0; +} + +static int +port_set_format(struct impl *this, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + struct port *port = &this->port; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + int idx; + const size_t sizes[4] = { 2, 4, 4, 8 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.rate == 0 || + info.info.raw.channels == 0) + return -EINVAL; + + switch (info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16: + idx = 0; + break; + case SPA_AUDIO_FORMAT_S32: + idx = 1; + break; + case SPA_AUDIO_FORMAT_F32: + idx = 2; + break; + case SPA_AUDIO_FORMAT_F64: + idx = 3; + break; + default: + return -EINVAL; + } + + port->bpf = sizes[idx] * info.info.raw.channels; + port->current_format = info; + port->have_format = true; + port->render_func = sine_funcs[idx]; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + if (id == SPA_PARAM_Format) + return port_set_format(this, direction, port_id, flags, param); + + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->outstanding = false; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + spa_list_append(&port->empty, &b->link); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_Control: + port->io_control = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + spa_return_if_fail(b->outstanding); + + spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id); + + b->outstanding = false; + spa_list_append(&port->empty, &b->link); + + if (!this->props.live) + set_timer(this, true); +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int process_control(struct impl *this, struct spa_pod_sequence *sequence) +{ + struct spa_pod_control *c; + + SPA_POD_SEQUENCE_FOREACH(sequence, c) { + switch (c->type) { + case SPA_CONTROL_Properties: + { + struct props *p = &this->props; + spa_pod_parse_object(&c->value, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq), + SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume)); + break; + } + default: + break; + } + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + + io = port->io; + if ((io = port->io) == NULL) + return -EIO; + + if (port->io_control) + process_control(this, &port->io_control->sequence); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (!this->props.live) + return make_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (this->data_loop) + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + } + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + reset_props(&this->props); + + this->timer_source.func = on_output; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + if (this->data_loop) + spa_loop_add_source(this->data_loop, &this->timer_source); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + + spa_log_info(this->log, NAME " %p: initialized", this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Generate an audio test pattern" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_audiotestsrc_factory = { + SPA_VERSION_HANDLE_FACTORY, + NAME, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/audiotestsrc/meson.build b/spa/plugins/audiotestsrc/meson.build new file mode 100644 index 0000000..d1b2242 --- /dev/null +++ b/spa/plugins/audiotestsrc/meson.build @@ -0,0 +1,7 @@ +audiotestsrc_sources = ['audiotestsrc.c', 'plugin.c'] + +audiotestsrclib = shared_library('spa-audiotestsrc', + audiotestsrc_sources, + dependencies : [ spa_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'audiotestsrc') diff --git a/spa/plugins/audiotestsrc/plugin.c b/spa/plugins/audiotestsrc/plugin.c new file mode 100644 index 0000000..4c51895 --- /dev/null +++ b/spa/plugins/audiotestsrc/plugin.c @@ -0,0 +1,46 @@ +/* Spa Volume plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_audiotestsrc_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_audiotestsrc_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/audiotestsrc/render.c b/spa/plugins/audiotestsrc/render.c new file mode 100644 index 0000000..4133fd0 --- /dev/null +++ b/spa/plugins/audiotestsrc/render.c @@ -0,0 +1,64 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#define M_PI_M2 ( M_PI + M_PI ) + +#define DEFINE_SINE(type,scale) \ +static void \ +audio_test_src_create_sine_##type (struct impl *this, type *samples, size_t n_samples) \ +{ \ + size_t i; \ + uint32_t c, channels; \ + float step, amp; \ + float freq = this->props.freq; \ + float volume = this->props.volume; \ + \ + channels = this->port.current_format.info.raw.channels; \ + step = M_PI_M2 * freq / this->port.current_format.info.raw.rate; \ + amp = volume * scale; \ + \ + for (i = 0; i < n_samples; i++) { \ + type val; \ + this->port.accumulator += step; \ + if (this->port.accumulator >= M_PI_M2) \ + this->port.accumulator -= M_PI_M2; \ + val = (type) (sin (this->port.accumulator) * amp); \ + for (c = 0; c < channels; ++c) \ + *samples++ = val; \ + } \ +} + +DEFINE_SINE(int16_t, 32767.0); +DEFINE_SINE(int32_t, 2147483647.0); +DEFINE_SINE(float, 1.0); +DEFINE_SINE(double, 1.0); + +static const render_func_t sine_funcs[] = { + (render_func_t) audio_test_src_create_sine_int16_t, + (render_func_t) audio_test_src_create_sine_int32_t, + (render_func_t) audio_test_src_create_sine_float, + (render_func_t) audio_test_src_create_sine_double +}; diff --git a/spa/plugins/avb/avb-pcm-sink.c b/spa/plugins/avb/avb-pcm-sink.c new file mode 100644 index 0000000..f453494 --- /dev/null +++ b/spa/plugins/avb/avb-pcm-sink.c @@ -0,0 +1,910 @@ +/* Spa AVB PCM Sink + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_Latency].user++; + emit_port_info(this, port, false); + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (port->n_buffers > 0) { + spa_avb_pause(this); + clear_buffers(this, port); + } + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + port->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = GET_PORT(this, SPA_DIRECTION_INPUT, 0); + if ((io = port->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status, + io->buffer_id, port->n_buffers); + + if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + if (io->status == SPA_STATUS_HAVE_DATA && + io->buffer_id < port->n_buffers) { + struct buffer *b = &port->buffers[io->buffer_id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_warn(this->log, "%p: buffer %u in use", + this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id); + spa_list_append(&port->ready, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + io->buffer_id = SPA_ID_INVALID; + + spa_avb_write(this); + + io->status = SPA_STATUS_OK; + } + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_avb_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) +{ + struct state *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + avb_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + spa_hook_list_init(&this->hooks); + + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + reset_props(&this->props); + + port = GET_PORT(this, SPA_DIRECTION_INPUT, 0); + port->direction = SPA_DIRECTION_INPUT; + + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + spa_list_init(&port->ready); + + this->latency[port->direction] = SPA_LATENCY_INFO( + port->direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + return spa_avb_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" }, + { SPA_KEY_FACTORY_USAGE, "[]" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_avb_sink_factory = { + SPA_VERSION_HANDLE_FACTORY, + "avb.pcm.sink", + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/avb/avb-pcm-source.c b/spa/plugins/avb/avb-pcm-source.c new file mode 100644 index 0000000..9ff9b21 --- /dev/null +++ b/spa/plugins/avb/avb-pcm-source.c @@ -0,0 +1,910 @@ +/* Spa AVB PCM Source + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_Latency].user++; + emit_port_info(this, port, false); + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (port->n_buffers > 0) { + spa_avb_pause(this); + clear_buffers(this, port); + } + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + port->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct port *port; + struct spa_io_buffers *io; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0); + if ((io = port->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: process %d %d/%d %d", this, io->status, + io->buffer_id, port->n_buffers, this->following); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + spa_avb_recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&port->ready) && this->following) { + spa_avb_read(this); + } + if (spa_list_is_empty(&port->ready) || !this->following) + return SPA_STATUS_OK; + + b = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_avb_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) +{ + struct state *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + avb_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + reset_props(&this->props); + + port = GET_PORT(this, SPA_DIRECTION_OUTPUT, 0); + port->direction = SPA_DIRECTION_OUTPUT; + + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + spa_list_init(&port->ready); + + this->latency[port->direction] = SPA_LATENCY_INFO( + port->direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + + return spa_avb_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with AVB" }, + { SPA_KEY_FACTORY_USAGE, "[]" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_avb_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + "avb.pcm.source", + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c new file mode 100644 index 0000000..484adc6 --- /dev/null +++ b/spa/plugins/avb/avb-pcm.c @@ -0,0 +1,1227 @@ +/* Spa AVB PCM + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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_avb_format_from_name(s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + spa_avb_parse_position(&state->default_pos, s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) { + state->n_allowed_rates = spa_avb_parse_rates(state->allowed_rates, + MAX_RATES, s, strlen(s)); + fmt_change++; + } else if (spa_streq(k, "avb.ifname")) { + snprintf(p->ifname, sizeof(p->ifname), "%s", s); + } else if (spa_streq(k, "avb.macaddr")) { + parse_addr(p->addr, s); + } else if (spa_streq(k, "avb.prio")) { + p->prio = atoi(s); + } else if (spa_streq(k, "avb.streamid")) { + parse_streamid(&p->streamid, s); + } else if (spa_streq(k, "avb.mtt")) { + p->mtt = atoi(s); + } else if (spa_streq(k, "avb.time-uncertainty")) { + p->t_uncertainty = atoi(s); + } else if (spa_streq(k, "avb.frames-per-pdu")) { + p->frames_per_pdu = atoi(s); + } else if (spa_streq(k, "avb.ptime-tolerance")) { + p->ptime_tolerance = atoi(s); + } else if (spa_streq(k, "latency.internal.rate")) { + state->process_latency.rate = atoi(s); + } else if (spa_streq(k, "latency.internal.ns")) { + state->process_latency.ns = atoi(s); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(state->clock_name, + sizeof(state->clock_name), "%s", s); + } else + return 0; + + if (fmt_change > 0) { + struct port *port = &state->ports[0]; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_EnumFormat].user++; + } + return 1; +} + +static int position_to_string(struct channel_map *map, char *val, size_t len) +{ + uint32_t i, o = 0; + int r; + o += snprintf(val, len, "[ "); + for (i = 0; i < map->channels; i++) { + r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ", + spa_debug_type_find_short_name(spa_type_audio_channel, + map->pos[i])); + if (r < 0 || o + r >= len) + return -ENOSPC; + o += r; + } + if (len > o) + o += snprintf(val+o, len-o, " ]"); + return 0; +} + +static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len) +{ + uint32_t i, o = 0; + int r; + o += snprintf(val, len, "[ "); + for (i = 0; i < n_vals; i++) { + r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]); + if (r < 0 || o + r >= len) + return -ENOSPC; + o += r; + } + if (len > o) + o += snprintf(val+o, len-o, " ]"); + return 0; +} + +struct spa_pod *spa_avb_enum_propinfo(struct state *state, + uint32_t idx, struct spa_pod_builder *b) +{ + struct spa_pod *param; + struct props *p = &state->props; + char tmp[128]; + + switch (idx) { + case 0: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS), + SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"), + SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 1: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE), + SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"), + SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 2: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT), + SPA_PROP_INFO_description, SPA_POD_String("Audio Format"), + SPA_PROP_INFO_type, SPA_POD_String( + spa_debug_type_find_short_name(spa_type_audio_format, + state->default_format)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 3: + { + char buf[1024]; + position_to_string(&state->default_pos, buf, sizeof(buf)); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION), + SPA_PROP_INFO_description, SPA_POD_String("Audio Position"), + SPA_PROP_INFO_type, SPA_POD_String(buf), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + } + case 4: + { + char buf[1024]; + uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf)); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES), + SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"), + SPA_PROP_INFO_type, SPA_POD_String(buf), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + } + case 5: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.ifname"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB interface name"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->ifname, sizeof(p->ifname)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 6: + format_addr(tmp, sizeof(tmp), p->addr); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.macaddr"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB MAC address"), + SPA_PROP_INFO_type, SPA_POD_String(tmp), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 7: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.prio"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB stream priority"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->prio, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 8: + format_streamid(tmp, sizeof(tmp), p->streamid); + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.streamid"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB stream id"), + SPA_PROP_INFO_type, SPA_POD_String(tmp), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 9: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.mtt"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB mtt"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->mtt, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 10: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.time-uncertainty"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB time uncertainty"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->t_uncertainty, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 11: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.frames-per-pdu"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB frames per packet"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->frames_per_pdu, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 12: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("avb.ptime-tolerance"), + SPA_PROP_INFO_description, SPA_POD_String("The AVB packet tolerance"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->ptime_tolerance, 0, INT32_MAX), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 13: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"), + SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate, + 0, 65536), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 14: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"), + SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns, + 0, 2 * SPA_NSEC_PER_SEC), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 15: + param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_name, SPA_POD_String("clock.name"), + SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"), + SPA_PROP_INFO_type, SPA_POD_String(state->clock_name), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + default: + return NULL; + } + return param; +} + +int spa_avb_add_prop_params(struct state *state, struct spa_pod_builder *b) +{ + struct props *p = &state->props; + struct spa_pod_frame f[1]; + char buf[1024]; + + spa_pod_builder_prop(b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(b, &f[0]); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS); + spa_pod_builder_int(b, state->default_channels); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE); + spa_pod_builder_int(b, state->default_rate); + + spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT); + spa_pod_builder_string(b, + spa_debug_type_find_short_name(spa_type_audio_format, + state->default_format)); + + position_to_string(&state->default_pos, buf, sizeof(buf)); + spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION); + spa_pod_builder_string(b, buf); + + uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, + buf, sizeof(buf)); + spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES); + spa_pod_builder_string(b, buf); + + spa_pod_builder_string(b, "avb.ifname"); + spa_pod_builder_string(b, p->ifname); + + format_addr(buf, sizeof(buf), p->addr); + spa_pod_builder_string(b, "avb.macadr"); + spa_pod_builder_string(b, buf); + + spa_pod_builder_string(b, "avb.prio"); + spa_pod_builder_int(b, p->prio); + + format_streamid(buf, sizeof(buf), p->streamid); + spa_pod_builder_string(b, "avb.streamid"); + spa_pod_builder_string(b, buf); + spa_pod_builder_string(b, "avb.mtt"); + spa_pod_builder_int(b, p->mtt); + spa_pod_builder_string(b, "avb.time-uncertainty"); + spa_pod_builder_int(b, p->t_uncertainty); + spa_pod_builder_string(b, "avb.frames-per-pdu"); + spa_pod_builder_int(b, p->frames_per_pdu); + spa_pod_builder_string(b, "avb.ptime-tolerance"); + spa_pod_builder_int(b, p->ptime_tolerance); + + spa_pod_builder_string(b, "latency.internal.rate"); + spa_pod_builder_int(b, state->process_latency.rate); + + spa_pod_builder_string(b, "latency.internal.ns"); + spa_pod_builder_long(b, state->process_latency.ns); + + spa_pod_builder_string(b, "clock.name"); + spa_pod_builder_string(b, state->clock_name); + + spa_pod_builder_pop(b, &f[0]); + return 0; +} + +int spa_avb_parse_prop_params(struct state *state, struct spa_pod *params) +{ + struct spa_pod_parser prs; + struct spa_pod_frame f; + int changed = 0; + + if (params == NULL) + return 0; + + spa_pod_parser_pod(&prs, params); + if (spa_pod_parser_push_struct(&prs, &f) < 0) + return 0; + + while (true) { + const char *name; + struct spa_pod *pod; + char value[512]; + + if (spa_pod_parser_get_string(&prs, &name) < 0) + break; + + if (spa_pod_parser_get_pod(&prs, &pod) < 0) + break; + if (spa_pod_is_string(pod)) { + spa_pod_copy_string(pod, sizeof(value), value); + } else if (spa_pod_is_int(pod)) { + snprintf(value, sizeof(value), "%d", + SPA_POD_VALUE(struct spa_pod_int, pod)); + } else if (spa_pod_is_long(pod)) { + snprintf(value, sizeof(value), "%"PRIi64, + SPA_POD_VALUE(struct spa_pod_long, pod)); + } else if (spa_pod_is_bool(pod)) { + snprintf(value, sizeof(value), "%s", + SPA_POD_VALUE(struct spa_pod_bool, pod) ? + "true" : "false"); + } else + continue; + + spa_log_info(state->log, "key:'%s' val:'%s'", name, value); + avb_set_param(state, name, value); + changed++; + } + if (changed > 0) { + state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + state->params[NODE_Props].user++; + } + return changed; +} + +int spa_avb_init(struct state *state, const struct spa_dict *info) +{ + uint32_t i; + + state->quantum_limit = 8192; + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &state->quantum_limit, 0); + } else { + avb_set_param(state, k, s); + } + } + + state->ringbuffer_size = state->quantum_limit * 64; + state->ringbuffer_data = calloc(1, state->ringbuffer_size * 4); + spa_ringbuffer_init(&state->ring); + return 0; +} + +int spa_avb_clear(struct state *state) +{ + return 0; +} + +static int spa_format_to_aaf(uint32_t format) +{ + switch(format) { + case SPA_AUDIO_FORMAT_F32_BE: return SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT; + case SPA_AUDIO_FORMAT_S32_BE: return SPA_AVBTP_AAF_FORMAT_INT_32BIT; + case SPA_AUDIO_FORMAT_S24_BE: return SPA_AVBTP_AAF_FORMAT_INT_24BIT; + case SPA_AUDIO_FORMAT_S16_BE: return SPA_AVBTP_AAF_FORMAT_INT_16BIT; + default: return SPA_AVBTP_AAF_FORMAT_USER; + } +} + +static int calc_frame_size(uint32_t format) +{ + switch(format) { + case SPA_AUDIO_FORMAT_F32_BE: + case SPA_AUDIO_FORMAT_S32_BE: return 4; + case SPA_AUDIO_FORMAT_S24_BE: return 3; + case SPA_AUDIO_FORMAT_S16_BE: return 2; + default: return 0; + } +} + +static int spa_rate_to_aaf(uint32_t rate) +{ + switch(rate) { + case 8000: return SPA_AVBTP_AAF_PCM_NSR_8KHZ; + case 16000: return SPA_AVBTP_AAF_PCM_NSR_16KHZ; + case 24000: return SPA_AVBTP_AAF_PCM_NSR_24KHZ; + case 32000: return SPA_AVBTP_AAF_PCM_NSR_32KHZ; + case 44100: return SPA_AVBTP_AAF_PCM_NSR_44_1KHZ; + case 48000: return SPA_AVBTP_AAF_PCM_NSR_48KHZ; + case 88200: return SPA_AVBTP_AAF_PCM_NSR_88_2KHZ; + case 96000: return SPA_AVBTP_AAF_PCM_NSR_96KHZ; + case 176400: return SPA_AVBTP_AAF_PCM_NSR_176_4KHZ; + case 192000: return SPA_AVBTP_AAF_PCM_NSR_192KHZ; + default: return SPA_AVBTP_AAF_PCM_NSR_USER; + } +} + +int +spa_avb_enum_format(struct state *state, int seq, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_pod *fmt; + int res = 0; + struct spa_result_node_params result; + uint32_t count = 0; + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + +next: + result.index = result.next++; + + if (result.index > 0) + return 0; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + 0); + + spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_format, 0); + if (state->default_format != 0) { + spa_pod_builder_id(&b, state->default_format); + } else { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0); + spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE); + spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_F32_BE); + spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S32_BE); + spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S24_BE); + spa_pod_builder_id(&b, SPA_AUDIO_FORMAT_S16_BE); + spa_pod_builder_pop(&b, &f[1]); + } + spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_rate, 0); + if (state->default_rate != 0) { + spa_pod_builder_int(&b, state->default_rate); + } else { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Enum, 0); + spa_pod_builder_int(&b, 48000); + spa_pod_builder_int(&b, 8000); + spa_pod_builder_int(&b, 16000); + spa_pod_builder_int(&b, 24000); + spa_pod_builder_int(&b, 32000); + spa_pod_builder_int(&b, 44100); + spa_pod_builder_int(&b, 48000); + spa_pod_builder_int(&b, 88200); + spa_pod_builder_int(&b, 96000); + spa_pod_builder_int(&b, 176400); + spa_pod_builder_int(&b, 192000); + spa_pod_builder_pop(&b, &f[1]); + } + spa_pod_builder_prop(&b, SPA_FORMAT_AUDIO_channels, 0); + if (state->default_channels != 0) { + spa_pod_builder_int(&b, state->default_channels); + } else { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Range, 0); + spa_pod_builder_int(&b, 8); + spa_pod_builder_int(&b, 2); + spa_pod_builder_int(&b, 32); + spa_pod_builder_pop(&b, &f[1]); + } + fmt = spa_pod_builder_pop(&b, &f[0]); + + if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) + goto next; + + spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return res; +} + +static int setup_socket(struct state *state) +{ + int fd, res; + struct ifreq req; + struct props *p = &state->props; + + fd = socket(AF_PACKET, SOCK_DGRAM|SOCK_NONBLOCK, htons(ETH_P_TSN)); + if (fd < 0) { + spa_log_error(state->log, "socket() failed: %m"); + return -errno; + } + + snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", p->ifname); + res = ioctl(fd, SIOCGIFINDEX, &req); + if (res < 0) { + spa_log_error(state->log, "SIOCGIFINDEX %s failed: %m", p->ifname); + res = -errno; + goto error_close; + } + + state->sock_addr.sll_family = AF_PACKET; + state->sock_addr.sll_protocol = htons(ETH_P_TSN); + state->sock_addr.sll_halen = ETH_ALEN; + state->sock_addr.sll_ifindex = req.ifr_ifindex; + memcpy(&state->sock_addr.sll_addr, p->addr, ETH_ALEN); + + if (state->ports[0].direction == SPA_DIRECTION_INPUT) { + struct sock_txtime txtime_cfg; + + res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &p->prio, + sizeof(p->prio)); + if (res < 0) { + spa_log_error(state->log, "setsockopt(SO_PRIORITY %d) failed: %m", p->prio); + res = -errno; + goto error_close; + } + + txtime_cfg.clockid = CLOCK_TAI; + txtime_cfg.flags = 0; + res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg, + sizeof(txtime_cfg)); + if (res < 0) { + spa_log_error(state->log, "setsockopt(SO_TXTIME) failed: %m"); + res = -errno; + goto error_close; + } + } else { + struct packet_mreq mreq = { 0 }; + + res = bind(fd, (struct sockaddr *) &state->sock_addr, + sizeof(state->sock_addr)); + if (res < 0) { + spa_log_error(state->log, "bind() failed: %m"); + res = -errno; + goto error_close; + } + + mreq.mr_ifindex = req.ifr_ifindex; + mreq.mr_type = PACKET_MR_MULTICAST; + mreq.mr_alen = ETH_ALEN; + memcpy(&mreq.mr_address, p->addr, ETH_ALEN); + res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, + &mreq, sizeof(struct packet_mreq)); + if (res < 0) { + spa_log_error(state->log, "setsockopt(ADD_MEMBERSHIP) failed: %m"); + res = -errno; + goto error_close; + } + } + state->sockfd = fd; + return 0; + +error_close: + close(fd); + return res; +} + +static int setup_packet(struct state *state, struct spa_audio_info *fmt) +{ + struct spa_avbtp_packet_aaf *pdu; + struct props *p = &state->props; + ssize_t payload_size, hdr_size, pdu_size; + + hdr_size = sizeof(*pdu); + payload_size = state->stride * p->frames_per_pdu; + pdu_size = hdr_size + payload_size; + if ((pdu = calloc(1, pdu_size)) == NULL) + return -errno; + + SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(pdu, SPA_AVBTP_SUBTYPE_AAF); + + if (state->ports[0].direction == SPA_DIRECTION_INPUT) { + SPA_AVBTP_PACKET_AAF_SET_SV(pdu, 1); + SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(pdu, p->streamid); + SPA_AVBTP_PACKET_AAF_SET_TV(pdu, 1); + SPA_AVBTP_PACKET_AAF_SET_FORMAT(pdu, spa_format_to_aaf(state->format)); + SPA_AVBTP_PACKET_AAF_SET_NSR(pdu, spa_rate_to_aaf(state->rate)); + SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(pdu, state->channels); + SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(pdu, calc_frame_size(state->format)*8); + SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(pdu, payload_size); + SPA_AVBTP_PACKET_AAF_SET_SP(pdu, SPA_AVBTP_AAF_PCM_SP_NORMAL); + } + state->pdu = pdu; + state->hdr_size = hdr_size; + state->payload_size = payload_size; + state->pdu_size = pdu_size; + return 0; +} + +static int setup_msg(struct state *state) +{ + state->iov[0].iov_base = state->pdu; + state->iov[0].iov_len = state->hdr_size; + state->iov[1].iov_base = state->pdu->payload; + state->iov[1].iov_len = state->payload_size; + state->iov[2].iov_base = state->pdu->payload; + state->iov[2].iov_len = 0; + state->msg.msg_name = &state->sock_addr; + state->msg.msg_namelen = sizeof(state->sock_addr); + state->msg.msg_iov = state->iov; + state->msg.msg_iovlen = 3; + state->msg.msg_control = state->control; + state->msg.msg_controllen = sizeof(state->control); + state->cmsg = CMSG_FIRSTHDR(&state->msg); + state->cmsg->cmsg_level = SOL_SOCKET; + state->cmsg->cmsg_type = SCM_TXTIME; + state->cmsg->cmsg_len = CMSG_LEN(sizeof(__u64)); + return 0; +} + +int spa_avb_clear_format(struct state *state) +{ + close(state->sockfd); + close(state->timerfd); + free(state->pdu); + + return 0; +} + +int spa_avb_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags) +{ + int res, frame_size; + struct props *p = &state->props; + + frame_size = calc_frame_size(fmt->info.raw.format); + if (frame_size == 0) + return -EINVAL; + + if (fmt->info.raw.rate == 0 || + fmt->info.raw.channels == 0) + return -EINVAL; + + state->format = fmt->info.raw.format; + state->rate = fmt->info.raw.rate; + state->channels = fmt->info.raw.channels; + state->blocks = 1; + state->stride = state->channels * frame_size; + + if ((res = setup_socket(state)) < 0) + return res; + + if ((res = spa_system_timerfd_create(state->data_system, + CLOCK_REALTIME, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_close_sockfd; + + state->timerfd = res; + + if ((res = setup_packet(state, fmt)) < 0) + return res; + + if ((res = setup_msg(state)) < 0) + return res; + + state->pdu_period = SPA_NSEC_PER_SEC * p->frames_per_pdu / + state->rate; + + return 0; + +error_close_sockfd: + close(state->sockfd); + return res; +} + +void spa_avb_recycle_buffer(struct state *this, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&port->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } +} + +static void reset_buffers(struct state *this, struct port *port) +{ + uint32_t i; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + if (port->direction == SPA_DIRECTION_INPUT) { + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } else { + spa_list_append(&port->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } + } +} + +static inline bool is_pdu_valid(struct state *state) +{ + uint8_t seq_num; + + seq_num = SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(state->pdu); + + if (state->prev_seq != 0 && (uint8_t)(state->prev_seq + 1) != seq_num) { + spa_log_warn(state->log, "dropped packets %d != %d", state->prev_seq + 1, seq_num); + } + state->prev_seq = seq_num; + return true; +} + +static inline void +set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size, + uint32_t offset, struct iovec *iov, uint32_t len) +{ + iov[0].iov_len = SPA_MIN(len, size - offset); + iov[0].iov_base = SPA_PTROFF(buffer, offset, void); + iov[1].iov_len = len - iov[0].iov_len; + iov[1].iov_base = buffer; +} + +static void avb_on_socket_event(struct spa_source *source) +{ + struct state *state = source->data; + ssize_t n; + int32_t filled; + uint32_t subtype, index; + struct spa_avbtp_packet_aaf *pdu = state->pdu; + bool overrun = false; + + filled = spa_ringbuffer_get_write_index(&state->ring, &index); + overrun = filled > (int32_t) state->ringbuffer_size; + if (overrun) { + state->iov[1].iov_base = state->pdu->payload; + state->iov[1].iov_len = state->payload_size; + state->iov[2].iov_len = 0; + } else { + set_iovec(&state->ring, + state->ringbuffer_data, + state->ringbuffer_size, + index % state->ringbuffer_size, + &state->iov[1], state->payload_size); + } + + n = recvmsg(state->sockfd, &state->msg, 0); + if (n < 0) { + spa_log_error(state->log, "recv() failed: %m"); + return; + } + if (n != (ssize_t)state->pdu_size) { + spa_log_error(state->log, "AVB packet dropped: Invalid size"); + return; + } + + subtype = SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(pdu); + if (subtype != SPA_AVBTP_SUBTYPE_AAF) { + spa_log_error(state->log, "non supported subtype %d", subtype); + return; + } + if (!is_pdu_valid(state)) { + spa_log_error(state->log, "AAF PDU invalid"); + return; + } + if (overrun) { + spa_log_warn(state->log, "overrun %d", filled); + return; + } + index += state->payload_size; + spa_ringbuffer_write_update(&state->ring, index); +} + +static void set_timeout(struct state *state, uint64_t next_time) +{ + struct itimerspec ts; + uint64_t time_utc; + + spa_log_trace(state->log, "set timeout %"PRIu64, next_time); + + time_utc = next_time > TAI_OFFSET ? TAI_TO_UTC(next_time) : 0; + ts.it_value.tv_sec = time_utc / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time_utc % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, + state->timer_source.fd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static int flush_write(struct state *state, uint64_t current_time) +{ + int32_t avail, wanted; + uint32_t index; + uint64_t ptime, txtime; + int pdu_count; + struct props *p = &state->props; + struct spa_avbtp_packet_aaf *pdu = state->pdu; + ssize_t n; + + avail = spa_ringbuffer_get_read_index(&state->ring, &index); + wanted = state->duration * state->stride; + if (avail < wanted) { + spa_log_warn(state->log, "underrun %d < %d", avail, wanted); + return -EPIPE; + } + + pdu_count = state->duration / p->frames_per_pdu; + + txtime = current_time + p->t_uncertainty; + ptime = txtime + p->mtt; + + while (pdu_count--) { + *(__u64 *)CMSG_DATA(state->cmsg) = txtime; + + set_iovec(&state->ring, + state->ringbuffer_data, + state->ringbuffer_size, + index % state->ringbuffer_size, + &state->iov[1], state->payload_size); + + SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(pdu, state->pdu_seq++); + SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(pdu, ptime); + + n = sendmsg(state->sockfd, &state->msg, MSG_NOSIGNAL); + if (n < 0 || n != (ssize_t)state->pdu_size) { + spa_log_error(state->log, "sendmdg() failed: %m"); + } + txtime += state->pdu_period; + ptime += state->pdu_period; + index += state->payload_size; + } + spa_ringbuffer_read_update(&state->ring, index); + return 0; +} + +int spa_avb_write(struct state *state) +{ + int32_t filled; + uint32_t index, to_write; + struct port *port = &state->ports[0]; + + filled = spa_ringbuffer_get_write_index(&state->ring, &index); + if (filled < 0) { + spa_log_warn(state->log, "underrun %d", filled); + } else if (filled > (int32_t)state->ringbuffer_size) { + spa_log_warn(state->log, "overrun %d", filled); + } + to_write = state->ringbuffer_size - filled; + + while (!spa_list_is_empty(&port->ready) && to_write > 0) { + size_t n_bytes; + struct buffer *b; + struct spa_data *d; + uint32_t offs, avail, size; + + b = spa_list_first(&port->ready, struct buffer, link); + d = b->buf->datas; + + offs = SPA_MIN(d[0].chunk->offset + port->ready_offset, d[0].maxsize); + size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs); + avail = size - offs; + + n_bytes = SPA_MIN(avail, to_write); + if (n_bytes == 0) + break; + + spa_ringbuffer_write_data(&state->ring, + state->ringbuffer_data, + state->ringbuffer_size, + index % state->ringbuffer_size, + SPA_PTROFF(d[0].data, offs, void), + n_bytes); + + port->ready_offset += n_bytes; + + if (port->ready_offset >= size || avail == 0) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + port->io->buffer_id = b->id; + spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id); + + spa_node_call_reuse_buffer(&state->callbacks, 0, b->id); + + port->ready_offset = 0; + } + to_write -= n_bytes; + index += n_bytes; + } + spa_ringbuffer_write_update(&state->ring, index); + + if (state->following) { + flush_write(state, state->position->clock.nsec); + } + return 0; +} + +static int handle_play(struct state *state, uint64_t current_time) +{ + flush_write(state, current_time); + spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA); + return 0; +} + +int spa_avb_read(struct state *state) +{ + int32_t avail, wanted; + uint32_t index; + struct port *port = &state->ports[0]; + struct buffer *b; + struct spa_data *d; + uint32_t n_bytes; + + if (state->position) + state->duration = state->position->clock.duration; + + avail = spa_ringbuffer_get_read_index(&state->ring, &index); + wanted = state->duration * state->stride; + + if (spa_list_is_empty(&port->free)) { + spa_log_warn(state->log, "out of buffers"); + return -EPIPE; + } + + b = spa_list_first(&port->free, struct buffer, link); + d = b->buf->datas; + + n_bytes = SPA_MIN(d[0].maxsize, (uint32_t)wanted); + + if (avail < wanted) { + spa_log_warn(state->log, "capture underrun %d < %d", avail, wanted); + memset(d[0].data, 0, n_bytes); + } else { + spa_ringbuffer_read_data(&state->ring, + state->ringbuffer_data, + state->ringbuffer_size, + index % state->ringbuffer_size, + d[0].data, n_bytes); + index += n_bytes; + spa_ringbuffer_read_update(&state->ring, index); + } + + d[0].chunk->offset = 0; + d[0].chunk->size = n_bytes; + d[0].chunk->stride = state->stride; + d[0].chunk->flags = 0; + + spa_list_remove(&b->link); + spa_list_append(&port->ready, &b->link); + + return 0; +} + +static int handle_capture(struct state *state, uint64_t current_time) +{ + struct port *port = &state->ports[0]; + struct spa_io_buffers *io; + struct buffer *b; + + spa_avb_read(state); + + if (spa_list_is_empty(&port->ready)) + return 0; + + io = port->io; + if (io != NULL && + (io->status != SPA_STATUS_HAVE_DATA || port->rate_match != NULL)) { + if (io->buffer_id < port->n_buffers) + spa_avb_recycle_buffer(state, port, io->buffer_id); + + b = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id); + } + spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA); + return 0; +} + +static void avb_on_timeout_event(struct spa_source *source) +{ + struct state *state = source->data; + uint64_t expirations, current_time, duration; + uint32_t rate; + int res; + + spa_log_trace(state->log, "timeout"); + + if ((res = spa_system_timerfd_read(state->data_system, + state->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(state->log, "read timerfd: %s", spa_strerror(res)); + return; + } + + current_time = state->next_time; + if (SPA_LIKELY(state->position)) { + duration = state->position->clock.duration; + rate = state->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + state->duration = duration; + + if (state->ports[0].direction == SPA_DIRECTION_INPUT) + handle_play(state, current_time); + else + handle_capture(state, current_time); + + state->next_time = current_time + duration * SPA_NSEC_PER_SEC / rate; + + if (SPA_LIKELY(state->clock)) { + state->clock->nsec = current_time; + state->clock->position += duration; + state->clock->duration = duration; + state->clock->delay = 0; + state->clock->rate_diff = 1.0; + state->clock->next_nsec = state->next_time; + } + + set_timeout(state, state->next_time); +} + +static int set_timers(struct state *state) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(state->data_system, CLOCK_TAI, &now)) < 0) + return res; + + state->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (state->following) { + set_timeout(state, 0); + } else { + set_timeout(state, state->next_time); + } + return 0; +} + +static inline bool is_following(struct state *state) +{ + return state->position && state->clock && state->position->clock.id != state->clock->id; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + spa_dll_init(&state->dll); + set_timers(state); + return 0; +} + +int spa_avb_reassign_follower(struct state *state) +{ + bool following, freewheel; + + if (!state->started) + return 0; + + following = is_following(state); + if (following != state->following) { + spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following); + state->following = following; + spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + } + + freewheel = state->position && + SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL); + + if (state->freewheel != freewheel) { + spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel); + state->freewheel = freewheel; + } + return 0; +} + +int spa_avb_start(struct state *state) +{ + if (state->started) + return 0; + + if (state->position) { + state->duration = state->position->clock.duration; + state->rate_denom = state->position->clock.rate.denom; + } else { + state->duration = 1024; + state->rate_denom = state->rate; + } + + spa_dll_init(&state->dll); + state->max_error = (256.0 * state->rate) / state->rate_denom; + + state->following = is_following(state); + + state->timer_source.func = avb_on_timeout_event; + state->timer_source.data = state; + state->timer_source.fd = state->timerfd; + state->timer_source.mask = SPA_IO_IN; + state->timer_source.rmask = 0; + spa_loop_add_source(state->data_loop, &state->timer_source); + + state->pdu_seq = 0; + + if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) { + state->sock_source.func = avb_on_socket_event; + state->sock_source.data = state; + state->sock_source.fd = state->sockfd; + state->sock_source.mask = SPA_IO_IN; + state->sock_source.rmask = 0; + spa_loop_add_source(state->data_loop, &state->sock_source); + } + + reset_buffers(state, &state->ports[0]); + + set_timers(state); + + state->started = true; + + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + + spa_loop_remove_source(state->data_loop, &state->timer_source); + + if (state->ports[0].direction == SPA_DIRECTION_OUTPUT) { + spa_loop_remove_source(state->data_loop, &state->sock_source); + } + return 0; +} + +int spa_avb_pause(struct state *state) +{ + if (!state->started) + return 0; + + spa_log_debug(state->log, "%p: pause", state); + + spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + + state->started = false; + set_timeout(state, 0); + + return 0; +} diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h new file mode 100644 index 0000000..bb3bce6 --- /dev/null +++ b/spa/plugins/avb/avb-pcm.h @@ -0,0 +1,343 @@ +/* Spa AVB PCM + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AVB_PCM_H +#define SPA_AVB_PCM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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_format_from_name(const char *name, size_t len) +{ + int i; + for (i = 0; spa_type_audio_format[i].name; i++) { + if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0) + return spa_type_audio_format[i].type; + } + return SPA_AUDIO_FORMAT_UNKNOWN; +} + +static inline uint32_t spa_avb_channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static inline void spa_avb_parse_position(struct channel_map *map, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + map->channels = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + map->channels < SPA_AUDIO_MAX_CHANNELS) { + map->pos[map->channels++] = spa_avb_channel_from_name(v); + } +} + +static inline uint32_t spa_avb_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + uint32_t count; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + count = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max) + rates[count++] = atoi(v); + return count; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AVB_PCM_H */ diff --git a/spa/plugins/avb/avb.c b/spa/plugins/avb/avb.c new file mode 100644 index 0000000..8f67310 --- /dev/null +++ b/spa/plugins/avb/avb.c @@ -0,0 +1,54 @@ +/* Spa AVB support + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_avb_sink_factory; +extern const struct spa_handle_factory spa_avb_source_factory; + +struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.avb"); +struct spa_log_topic *avb_log_topic = &log_topic; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_avb_sink_factory; + break; + case 1: + *factory = &spa_avb_source_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/avb/avb.h b/spa/plugins/avb/avb.h new file mode 100644 index 0000000..a99a0fe --- /dev/null +++ b/spa/plugins/avb/avb.h @@ -0,0 +1,39 @@ +/* Spa AVB + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AVB_H +#define SPA_AVB_H + +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT avb_log_topic +extern struct spa_log_topic *avb_log_topic; + +static inline void avb_log_topic_init(struct spa_log *log) +{ + spa_log_topic_init(log, avb_log_topic); +} + +#endif /* SPA_AVB_H */ diff --git a/spa/plugins/avb/avbtp/packets.h b/spa/plugins/avb/avbtp/packets.h new file mode 100644 index 0000000..3d4a652 --- /dev/null +++ b/spa/plugins/avb/avbtp/packets.h @@ -0,0 +1,220 @@ +/* Spa AVB support + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_AVB_PACKETS_H +#define SPA_AVB_PACKETS_H + +#define SPA_AVBTP_SUBTYPE_61883_IIDC 0x00 +#define SPA_AVBTP_SUBTYPE_MMA_STREAM 0x01 +#define SPA_AVBTP_SUBTYPE_AAF 0x02 +#define SPA_AVBTP_SUBTYPE_CVF 0x03 +#define SPA_AVBTP_SUBTYPE_CRF 0x04 +#define SPA_AVBTP_SUBTYPE_TSCF 0x05 +#define SPA_AVBTP_SUBTYPE_SVF 0x06 +#define SPA_AVBTP_SUBTYPE_RVF 0x07 +#define SPA_AVBTP_SUBTYPE_AEF_CONTINUOUS 0x6E +#define SPA_AVBTP_SUBTYPE_VSF_STREAM 0x6F +#define SPA_AVBTP_SUBTYPE_EF_STREAM 0x7F +#define SPA_AVBTP_SUBTYPE_NTSCF 0x82 +#define SPA_AVBTP_SUBTYPE_ESCF 0xEC +#define SPA_AVBTP_SUBTYPE_EECF 0xED +#define SPA_AVBTP_SUBTYPE_AEF_DISCRETE 0xEE +#define SPA_AVBTP_SUBTYPE_ADP 0xFA +#define SPA_AVBTP_SUBTYPE_AECP 0xFB +#define SPA_AVBTP_SUBTYPE_ACMP 0xFC +#define SPA_AVBTP_SUBTYPE_MAAP 0xFE +#define SPA_AVBTP_SUBTYPE_EF_CONTROL 0xFF + +struct spa_avbtp_packet_common { + uint8_t subtype; +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned sv:1; /* stream_id valid */ + unsigned version:3; + unsigned subtype_data1:4; +#elif __BYTE_ORDER == __LITTLE_ENDIAN + unsigned subtype_data1:4; + unsigned version:3; + unsigned sv:1; +#elif +#error "Unknown byte order" +#endif + uint16_t subtype_data2; + uint64_t stream_id; + uint8_t payload[0]; +} __attribute__ ((__packed__)); + +#define SPA_AVBTP_PACKET_SET_SUBTYPE(p,v) ((p)->subtype = (v)) +#define SPA_AVBTP_PACKET_SET_SV(p,v) ((p)->sv = (v)) +#define SPA_AVBTP_PACKET_SET_VERSION(p,v) ((p)->version = (v)) +#define SPA_AVBTP_PACKET_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v)) + +#define SPA_AVBTP_PACKET_GET_SUBTYPE(p) ((p)->subtype) +#define SPA_AVBTP_PACKET_GET_SV(p) ((p)->sv) +#define SPA_AVBTP_PACKET_GET_VERSION(p) ((p)->version) +#define SPA_AVBTP_PACKET_GET_STREAM_ID(p) be64toh((p)->stream_id) + +struct spa_avbtp_packet_cc { + uint8_t subtype; +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned sv:1; + unsigned version:3; + unsigned control_data1:4; +#elif __BYTE_ORDER == __LITTLE_ENDIAN + unsigned control_data1:4; + unsigned version:3; + unsigned sv:1; +#endif + uint8_t status; + uint16_t control_frame_length; + uint64_t stream_id; + uint8_t payload[0]; +} __attribute__ ((__packed__)); + +#define SPA_AVBTP_PACKET_CC_SET_SUBTYPE(p,v) ((p)->subtype = (v)) +#define SPA_AVBTP_PACKET_CC_SET_SV(p,v) ((p)->sv = (v)) +#define SPA_AVBTP_PACKET_CC_SET_VERSION(p,v) ((p)->version = (v)) +#define SPA_AVBTP_PACKET_CC_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v)) +#define SPA_AVBTP_PACKET_CC_SET_STATUS(p,v) ((p)->status = (v)) +#define SPA_AVBTP_PACKET_CC_SET_LENGTH(p,v) ((p)->control_frame_length = htons(v)) + +#define SPA_AVBTP_PACKET_CC_GET_SUBTYPE(p) ((p)->subtype) +#define SPA_AVBTP_PACKET_CC_GET_SV(p) ((p)->sv) +#define SPA_AVBTP_PACKET_CC_GET_VERSION(p) ((p)->version) +#define SPA_AVBTP_PACKET_CC_GET_STREAM_ID(p) be64toh((p)->stream_id) +#define SPA_AVBTP_PACKET_CC_GET_STATUS(p) ((p)->status) +#define SPA_AVBTP_PACKET_CC_GET_LENGTH(p) ntohs((p)->control_frame_length) + +/* AAF */ +struct spa_avbtp_packet_aaf { + uint8_t subtype; +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned sv:1; + unsigned version:3; + unsigned mr:1; + unsigned _r1:1; + unsigned gv:1; + unsigned tv:1; + + uint8_t seq_num; + + unsigned _r2:7; + unsigned tu:1; +#elif __BYTE_ORDER == __LITTLE_ENDIAN + unsigned tv:1; + unsigned gv:1; + unsigned _r1:1; + unsigned mr:1; + unsigned version:3; + unsigned sv:1; + + uint8_t seq_num; + + unsigned tu:1; + unsigned _r2:7; +#endif + uint64_t stream_id; + uint32_t timestamp; +#define SPA_AVBTP_AAF_FORMAT_USER 0x00 +#define SPA_AVBTP_AAF_FORMAT_FLOAT_32BIT 0x01 +#define SPA_AVBTP_AAF_FORMAT_INT_32BIT 0x02 +#define SPA_AVBTP_AAF_FORMAT_INT_24BIT 0x03 +#define SPA_AVBTP_AAF_FORMAT_INT_16BIT 0x04 +#define SPA_AVBTP_AAF_FORMAT_AES3_32BIT 0x05 + uint8_t format; + +#define SPA_AVBTP_AAF_PCM_NSR_USER 0x00 +#define SPA_AVBTP_AAF_PCM_NSR_8KHZ 0x01 +#define SPA_AVBTP_AAF_PCM_NSR_16KHZ 0x02 +#define SPA_AVBTP_AAF_PCM_NSR_32KHZ 0x03 +#define SPA_AVBTP_AAF_PCM_NSR_44_1KHZ 0x04 +#define SPA_AVBTP_AAF_PCM_NSR_48KHZ 0x05 +#define SPA_AVBTP_AAF_PCM_NSR_88_2KHZ 0x06 +#define SPA_AVBTP_AAF_PCM_NSR_96KHZ 0x07 +#define SPA_AVBTP_AAF_PCM_NSR_176_4KHZ 0x08 +#define SPA_AVBTP_AAF_PCM_NSR_192KHZ 0x09 +#define SPA_AVBTP_AAF_PCM_NSR_24KHZ 0x0A +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned nsr:4; + unsigned _r3:4; +#elif __BYTE_ORDER == __LITTLE_ENDIAN + unsigned _r3:4; + unsigned nsr:4; +#endif + uint8_t chan_per_frame; + uint8_t bit_depth; + uint16_t data_len; + +#define SPA_AVBTP_AAF_PCM_SP_NORMAL 0x00 +#define SPA_AVBTP_AAF_PCM_SP_SPARSE 0x01 +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned _r4:3; + unsigned sp:1; + unsigned event:4; +#elif __BYTE_ORDER == __LITTLE_ENDIAN + unsigned event:4; + unsigned sp:1; + unsigned _r4:3; +#endif + uint8_t _r5; + uint8_t payload[0]; +} __attribute__ ((__packed__)); + +#define SPA_AVBTP_PACKET_AAF_SET_SUBTYPE(p,v) ((p)->subtype = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_SV(p,v) ((p)->sv = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_VERSION(p,v) ((p)->version = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_MR(p,v) ((p)->mr = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_GV(p,v) ((p)->gv = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_TV(p,v) ((p)->tv = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_SEQ_NUM(p,v) ((p)->seq_num = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_TU(p,v) ((p)->tu = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_STREAM_ID(p,v) ((p)->stream_id = htobe64(v)) +#define SPA_AVBTP_PACKET_AAF_SET_TIMESTAMP(p,v) ((p)->timestamp = htonl(v)) +#define SPA_AVBTP_PACKET_AAF_SET_DATA_LEN(p,v) ((p)->data_len = htons(v)) +#define SPA_AVBTP_PACKET_AAF_SET_FORMAT(p,v) ((p)->format = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_NSR(p,v) ((p)->nsr = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_CHAN_PER_FRAME(p,v) ((p)->chan_per_frame = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_BIT_DEPTH(p,v) ((p)->bit_depth = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_SP(p,v) ((p)->sp = (v)) +#define SPA_AVBTP_PACKET_AAF_SET_EVENT(p,v) ((p)->event = (v)) + +#define SPA_AVBTP_PACKET_AAF_GET_SUBTYPE(p) ((p)->subtype) +#define SPA_AVBTP_PACKET_AAF_GET_SV(p) ((p)->sv) +#define SPA_AVBTP_PACKET_AAF_GET_VERSION(p) ((p)->version) +#define SPA_AVBTP_PACKET_AAF_GET_MR(p) ((p)->mr) +#define SPA_AVBTP_PACKET_AAF_GET_GV(p) ((p)->gv) +#define SPA_AVBTP_PACKET_AAF_GET_TV(p) ((p)->tv) +#define SPA_AVBTP_PACKET_AAF_GET_SEQ_NUM(p) ((p)->seq_num) +#define SPA_AVBTP_PACKET_AAF_GET_TU(p) ((p)->tu) +#define SPA_AVBTP_PACKET_AAF_GET_STREAM_ID(p) be64toh((p)->stream_id) +#define SPA_AVBTP_PACKET_AAF_GET_TIMESTAMP(p) ntohl((p)->timestamp) +#define SPA_AVBTP_PACKET_AAF_GET_DATA_LEN(p) ntohs((p)->data_len) +#define SPA_AVBTP_PACKET_AAF_GET_FORMAT(p) ((p)->format) +#define SPA_AVBTP_PACKET_AAF_GET_NSR(p) ((p)->nsr) +#define SPA_AVBTP_PACKET_AAF_GET_CHAN_PER_FRAME(p) ((p)->chan_per_frame) +#define SPA_AVBTP_PACKET_AAF_GET_BIT_DEPTH(p) ((p)->bit_depth) +#define SPA_AVBTP_PACKET_AAF_GET_SP(p) ((p)->sp) +#define SPA_AVBTP_PACKET_AAF_GET_EVENT(p) ((p)->event) + + +#endif /* SPA_AVB_PACKETS_H */ diff --git a/spa/plugins/avb/meson.build b/spa/plugins/avb/meson.build new file mode 100644 index 0000000..2d9759e --- /dev/null +++ b/spa/plugins/avb/meson.build @@ -0,0 +1,14 @@ +spa_avb_sources = ['avb.c', + 'avb.h', + 'avb-pcm-sink.c', + 'avb-pcm-source.c', + 'avb-pcm.c' ] + +spa_avb = shared_library( + 'spa-avb', + [ spa_avb_sources ], + include_directories : [configinc], + dependencies : [ spa_dep, mathlib, epoll_shim_dep ], + install : true, + install_dir : spa_plugindir / 'avb' +) diff --git a/spa/plugins/bluez5/README-MIDI.md b/spa/plugins/bluez5/README-MIDI.md new file mode 100644 index 0000000..45d092c --- /dev/null +++ b/spa/plugins/bluez5/README-MIDI.md @@ -0,0 +1,24 @@ +## BLE MIDI & SELinux + +The SELinux configuration on Fedora 37 (as of 2022-11-10) does not +permit access to the bluetoothd APIs needed for BLE MIDI. + +As a workaround, hopefully to be not necessary in future, you can +permit such access by creating a file `blemidi.te` with contents: + + policy_module(blemidi, 1.0); + + require { + type system_dbusd_t; + type unconfined_t; + type bluetooth_t; + } + + allow bluetooth_t unconfined_t:unix_stream_socket { read write }; + allow system_dbusd_t bluetooth_t:unix_stream_socket { read write }; + +Then having package `selinux-policy-devel` installed, running +`make -f /usr/share/selinux/devel/Makefile blemidi.pp`, and finally +to insert the rules via `sudo semodule -i blemidi.pp`. + +The policy change can be removed by `sudo semodule -r blemidi`. diff --git a/spa/plugins/bluez5/README-OPUS-A2DP.md b/spa/plugins/bluez5/README-OPUS-A2DP.md new file mode 100644 index 0000000..a7aefc1 --- /dev/null +++ b/spa/plugins/bluez5/README-OPUS-A2DP.md @@ -0,0 +1,335 @@ +--- +title: OPUS-A2DP-0.5 specification +author: Pauli Virtanen +date: Jun 4, 2022 +--- + +# OPUS-A2DP-0.5 specification + +In this file, a way to use Opus as an A2DP vendor codec is specified. + +We will call this "OPUS-A2DP-0.5". There is no previous public +specification for using Opus as an A2DP vendor codec (to my +knowledge), which is why we need this one. + +[[_TOC_]] + +# Media Codec Capabilities + +The Media Codec Specific Information Elements ([AVDTP v1.3], §8.21.5) +capability and configuration structure is as follows: + +| Octet | Bits | Meaning | +|-------|------|-----------------------------------------------| +| 0-5 | 0-7 | Vendor ID Part | +| 6-7 | 0-7 | Channel Configuration | +| 8-11 | 0-7 | Audio Location Configuration | +| 12-14 | 0-7 | Limits Configuration | +| 15-16 | 0-7 | Return Direction Channel Configuration | +| 17-20 | 0-7 | Return Direction Audio Location Configuration | +| 21-23 | 0-7 | Return Direction Limits Configuration | + +All integer fields and multi-byte bitfields are laid out in **little +endian** order. All integer fields are unsigned. + +Each entry may have different meaning when present as a capability. +Below, we indicate this by abbreviations CAP for capability and SEL +for the value selected by SRC. + +Bits in fields marked RFA (Reserved For Additions) shall be set to +zero. + +> **Note** +> +> See `a2dp-codec-caps.h` for definition as C structs. + +## Vendor ID Part + +The fixed value + +| Octet | Bits | Meaning | +|-------|------|-------------------------------| +| 0-3 | 0-7 | A2DP Vendor ID (0x05F1) | +| 4-5 | 0-7 | A2DP Vendor Codec ID (0x1005) | + +> **Note** +> +> The Vendor ID is that of the Linux Foundation, and we are using it +> here unofficially. + +## Channel Configuration + +The channel configuration consists of the channel count, and the count +of coupled streams. The latter indicates which channels are encoded as +left/right pairs, as defined in Sec. 5.1.1 of Opus Ogg Encapsulation [RFC7845]. + +| Octet | Bits | Meaning | +|-------|------|------------------------------------------------------------| +| 6 | 0-7 | Channel Count. CAP: maximum number supported. SEL: actual. | +| 7 | 0-7 | Coupled Stream Count. CAP: 0. SEL: actual. | + +The Channel Count indicates the number of logical channels encoded in +the data stream. + +The Coupled Stream Count indicates the number of streams that encode a +coupled (left & right) channel pair. The count shall satisfy +`(Channel Count) >= 2*(Coupled Stream Count)`. +The Stream Count is `(Channel Count) - (Coupled Stream Count)`. + +The logical Channels are identified by a Channel Index *j* such that `0 <= j +< (Channel Count)`. The channels `0 <= j < 2*(Coupled Stream Count)` +are encoded in the *k*-th stream of the payload, where `k = floor(j/2)` and +`j mod 2` determines which of the two channels of the stream the logical +channel is. The channels `2*(Coupled Stream Count) <= j < (Channel Count)` +are encoded in the *k*-th stream of the payload, where `k = j - (Coupled Stream Count)`. + +> **Note** +> +> The prescription here is identical to [RFC7845] with channel mapping +> `mapping[j] = j`. We do not want to include the mapping table in the +> A2DP capabilities, so it is assumed to be trivial. + +## Audio Location Configuration + +The semantic meaning for each channel is determined by their Audio +Location bitfield. + +| Octet | Bits | Meaning | +|-------|------|------------------------------------------------------| +| 8-11 | 0-7 | Audio Location bitfield. CAP: available. SEL: actual | + +The values specified in CAP are informative, and SEL may contain bits +that were not set in CAP. SNK shall handle unsupported audio +locations. It may do this for example by ignoring unsupported channels +or via suitable up/downmixing. Hence, SRC may transmit channels with +audio locations that are not marked supported by SNK. + +The audio location bit values are: + +| Channel Order | Bitmask | Audio Location | +|---------------|------------|-------------------------| +| 0 | 0x00000001 | Front Left | +| 1 | 0x00000002 | Front Right | +| 2 | 0x00000400 | Side Left | +| 3 | 0x00000800 | Side Right | +| 4 | 0x00000010 | Back Left | +| 5 | 0x00000020 | Back Right | +| 6 | 0x00000040 | Front Left of Center | +| 7 | 0x00000080 | Front Right of Center | +| 8 | 0x00001000 | Top Front Left | +| 9 | 0x00002000 | Top Front Right | +| 10 | 0x00040000 | Top Side Left | +| 11 | 0x00080000 | Top Side Right | +| 12 | 0x00010000 | Top Back Left | +| 13 | 0x00020000 | Top Back Right | +| 14 | 0x00400000 | Bottom Front Left | +| 15 | 0x00800000 | Bottom Front Right | +| 16 | 0x01000000 | Front Left Wide | +| 17 | 0x02000000 | Front Right Wide | +| 18 | 0x04000000 | Left Surround | +| 19 | 0x08000000 | Right Surround | +| 20 | 0x00000004 | Front Center | +| 21 | 0x00000100 | Back Center | +| 22 | 0x00004000 | Top Front Center | +| 23 | 0x00008000 | Top Center | +| 24 | 0x00100000 | Top Back Center | +| 25 | 0x00200000 | Bottom Front Center | +| 26 | 0x00000008 | Low Frequency Effects 1 | +| 27 | 0x00000200 | Low Frequency Effects 2 | +| 28 | 0x10000000 | RFA | +| 29 | 0x20000000 | RFA | +| 30 | 0x40000000 | RFA | +| 31 | 0x80000000 | RFA | + +Each bit value is associated with a Channel Order. The bits set in +the bitfield define audio locations for the streams present in the +payload. The set bit with the smallest Channel Order value defines the +audio location for the Channel Index *j=0*, the bit with the next +lowest Channel Order value defines the audio location for the Channel +Index *j=1*, and so forth. + +When the Channel Count is larger than the number of bits set in the +Audio Location bitfield, the audio locations of the remaining channels +are unspecified. Implementations may handle them as appropriate for +their use case, considering them as AUX0–AUXN, or in the case of +Channel Count = 1, as the single mono audio channel. + +When the Channel Count is smaller than the number of bits set in the +Audio Location bitfield, the audio locations for the channels are +assigned as above, and remaining excess bits shall be ignored. + +> **Note** +> +> The channel audio location specification is similar to the location +> bitfield of the `Audio_Channel_Allocation` LTV structure in Bluetooth +> SIG [Assigned Numbers, Generic Audio] used in the LE Audio, and the +> bitmasks defined above are the same. +> +> The channel ordering differs from LE Audio, and is defined here to be +> compatible with the internal stream ordering in the reference Opus +> Multistream surround encoder Mapping Family 0 and 1 output. This +> allows making use of its surround masking and LFE handling +> capabilities. The stream ordering of the reference Opus surround +> encoder, although being unchanged since its addition in 2013, is an +> internal detail of the encoder. Implementations using the surround +> encoder need to check that the mapping table used by the encoder +> corresponds to the above channel ordering. +> +> For reference, we list the Audio Location bitfield values +> corresponding to the different channel counts in Opus Mapping Family 0 +> and 1 surround encoder output, and the expected mapping table: +> +> | Mapping Family | Channel Count | Audio Location Value | Stream Ordering | Mapping Table | +> |----------------|---------------|----------------------|---------------------------------|--------------------------| +> | 0 | 1 | 0x00000000 | mono | {0} | +> | 0 | 2 | 0x00000003 | FL, FR | {0, 1} | +> | 1 | 1 | 0x00000000 | mono | {0} | +> | 1 | 2 | 0x00000003 | FL, FR | {0, 1} | +> | 1 | 3 | 0x00000007 | FL, FR, FC | {0, 2, 1} | +> | 1 | 4 | 0x00000033 | FL, FR, BL, BR | {0, 1, 2, 3} | +> | 1 | 5 | 0x00000037 | FL, FR, BL, BR, FC | {0, 4, 1, 2, 3} | +> | 1 | 6 | 0x0000003f | FL, FR, BL, BR, FC, LFE | {0, 4, 1, 2, 3, 5} | +> | 1 | 7 | 0x00000d0f | FL, FR, SL, SR, FC, BC, LFE | {0, 4, 1, 2, 3, 5, 6} | +> | 1 | 8 | 0x00000c3f | FL, FR, SL, SR, BL, BR, FC, LFE | {0, 6, 1, 2, 3, 4, 5, 7} | +> +> The Mapping Table in the table indicates the mapping table selected by +> `opus_multistream_surround_encoder_create` (Opus 1.3.1). If the +> encoder outputs a different mapping table in a future Opus encoder +> release, the channel ordering will be incorrect, and the surround +> encoder can not be used. We expect that the probability of the Opus +> encoder authors making such changes is negligible. + +## Limits Configuration + +The limits for allowed frame durations and maximum bitrate can also be +configured. + +| Octet | Bits | Meaning | +|-------|------|-----------------------------------------------------| +| 16 | 0 | Frame duration 2.5ms. CAP: supported, SEL: selected | +| 16 | 1 | Frame duration 5ms. CAP: supported, SEL: selected | +| 16 | 2 | Frame duration 10ms. CAP: supported, SEL: selected | +| 16 | 3 | Frame duration 20ms. CAP: supported, SEL: selected | +| 16 | 4 | Frame duration 40ms. CAP: supported, SEL: selected | +| 16 | 5-7 | RFA | + +| Octet | Bits | Meaning | +|-------|------|------------------------------------------------| +| 17-18 | 0-7 | Maximum bitrate. CAP: supported, SEL: selected | + +The maximum bitrate is given in units of 1024 bits per second. + +The maximum bitrate field in CAP may contain value 0 to indicate +everything is supported. + +## Bidirectional Audio Configuration + +Bidirectional audio may be supported. Its Channel Configuration, Audio +Location Configuration, and Limits Configuration have identical form +to the forward direction, and represented by exactly similar +structures. + +Namely: + +| Octet | Bits | Meaning | +|-------|------|----------------------------------------------------| +| 19-20 | 0-7 | Channel Configuration fields, for return direction | +| 21-28 | 0-7 | Audio Location fields, for return direction | +| 29-31 | 0-7 | Limits Configuration fields, for return direction | + +If no return channel is supported or selected, the number of channels +is set to 0 in CAP or SEL. + +> **Note** +> +> This is a nonstandard extension to A2DP. The return direction audio +> data is simply sent back via the underlying L2CAP connection, which +> is bidirectional, in the same format as the forward direction audio. +> This is similar to what aptX-LL and FastStream do. + +# Packet Structure + +Each packet consists of an RTP header, an RTP payload header, and a +payload containing Opus Multistream data. + +| Octet | Bits | Meaning | +|-------|------|--------------------------| +| 0-11 | 0-7 | RTP header | +| 12 | 0-7 | RTP payload header | +| 13-N | 0-7 | Opus Multistream payload | + +For each Bluetooth packet, the payload shall contain exactly one Opus +Multistream packet, or a fragment of one. The Opus Multistream packet +may be fragmented to several consecutive Bluetooth packets. + +The format of the Multistream data is the same as in the audio packets +of [RFC7845], or, as produced/consumed by the Opus Multistream API. + +> **Note** +> +> We DO NOT follow [RFC7587], as we want fragmentation and multichannel support. + +## RTP Header + +See [RFC3550]. + +The RTP payload type is pt=96 (dynamic). + +## RTP Payload Header + +The RTP payload header is used to indicate if and how the Opus +Multistream packet is fragmented across several consecutive Bluetooth +packets. + +| Octet | Bits | Meaning +|--------|------|-------------------------------------------------------- +| 0 | 0-3 | Frame Count +| 4 | 4 | RFA +| 4 | 5 | Is Last Fragment +| 4 | 6 | Is First Fragment +| 4 | 7 | Is Fragmented + +In each packet, Frame Count indicates how many Bluetooth packets are +still to be received (including the present packet) before the Opus +Multistream packet is complete. + +The Is Fragment flag indicates whether the present packet contains +fragmented payload. + +The Is Last Fragment flag indicates whether the present packet is the +last part of fragmented payload. + +The Is First Fragment flag indicates whether the present packet is the +first part of fragmented payload. + +In non-fragmented packets, Frame Count shall be (1), and the other bits +in the header zero. + +## Opus Payload + +The Opus payload is a single Opus Multistream packet, or its fragment. + +In case of fragmentation, as indicated by the RTP payload header, +concatenating the payloads of the fragment Bluetooth packets shall +yield the total Opus Multistream packet. + +The SRC should choose encoder parameters such that Bluetooth bandwidth +limitations are not exceeded. + +The SRC may include FEC data. The SNK may enable forward error +correction instead of PLC. + + +# References + +1. Bluetooth [AVDTP v1.3] +2. IETF [RFC3550] +3. IETF [RFC7587] +4. IETF [RFC7845] +5. Bluetooth [Assigned Numbers, Generic Audio] + +[AVDTP v1.3]: https://www.bluetooth.com/specifications/specs/a-v-distribution-transport-protocol-1-3/ +[RFC3550]: https://datatracker.ietf.org/doc/html/rfc3550 +[RFC7587]: https://datatracker.ietf.org/doc/html/rfc7587 +[RFC7845]: https://datatracker.ietf.org/doc/html/rfc7845 +[Assigned Numbers, Generic Audio]: https://www.bluetooth.com/specifications/assigned-numbers/ diff --git a/spa/plugins/bluez5/README-SBC-XQ.md b/spa/plugins/bluez5/README-SBC-XQ.md new file mode 100644 index 0000000..3b393aa --- /dev/null +++ b/spa/plugins/bluez5/README-SBC-XQ.md @@ -0,0 +1,54 @@ +## SBC XQ + +SBC XQ is standard SBC codec operating at high bitrates and thus reaching the +transparent audio transport quality of AptX (HD) or other proprietary codecs. + +A2DP specification (A2DP SPEC) defines SBC parameters. These parameters are +negotiated between the source (SRC) and the receiver (SNK) at connection time : + +- Audio channel mode : Joint Stereo, Stereo, Dual Channel, Mono : all modes + are MANDATORY for the SNK according to A2DP specification +- Number of subbands: 4 or 8 - both MANDATORY for the SNK implementation +- Blocks Length: 4, 8, 12, 16 - all MANDATORY for the SNK implementation +- Allocation Method: Loudness, SNR - both MANDATORY for the SNK implementation +- Maximum and minimum bit pool : between 2 to 250, expressed in 8 bit uint + (Unsigned integer, Most significant bit first) : + - A2DP spec v1.2 states that requires all SNK implementation shall handle + bitrates of up to 512 kbps (which correspond to bitpool = 76). + - A2DP spec v1.3 doesn't specify any bitrate limit, and some high-end SNK + devices announce bitpool between 62 and 94 (bitpool 94 = 551kbps bitrate). + +Bluetooth standard radio capabilities are as follow : + +| Bluetooth speed EDR | EDR 2Mbps | | EDR 3Mbps | +|-------------------------|-----------|-------|-----------| +| Speed (b/s) | 2097152 | | 3145728 | +| Radio slot length (s) | 0.000625 | | 0.000625 | +| Radio slots / s | 1600 | | 1600 | +| Slot size (B) | 163.84 | | 245.76 | +| Max payload/5 slots (B) | 676.2 | | 1085.8 | +| max bitrate (Kb/s) | 1408.75 | | 2262.08 | + +The A2DP specification V1.3 provides RECOMMENDATIONS for bitpool implementation +for the encoder of the SRC : it is required to support AT LEAST the following +settings : + +- STEREO MODE : 53 +- MONO MODE : 31 +- DUAL CHANNEL : unspecified, so let's assume that the MONO value can be used : 3 + +According to http://soundexpert.org/articles/-/blogs/audio-quality-of-sbc-xq-bluetooth-audio-codec , +AptX quality can be reached either : + +- in STEREO MODE, with bitpool ~ 76 +- in DUAL CHANNEL MODE, with bitpool ~ 38 per channel + +| sampling Freq (Hz) | 44100 | 48000 | +|-------------------------|-----------|-------| +| bitpool / channel | 38 | 35 | +| Frame length DUAL (B) | 164 | 152 | +| Frame length JST (B) | 165 | 153 | +| Frame length ST (B) | 164 | 152 | +| bitrate DUAL CH (kb/s) | 452 | 456 | +| bitrate JOINT ST (kb/s) | 454 | 459 | +| bitrate STEREO (kb/s) | 452 | 456 | diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c new file mode 100644 index 0000000..46a8740 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -0,0 +1,661 @@ +/* Spa A2DP AAC codec + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "rtp.h" +#include "media-codecs.h" + +static struct spa_log *log; +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.aac"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define DEFAULT_AAC_BITRATE 320000 +#define MIN_AAC_BITRATE 64000 + +struct props { + int bitratemode; +}; + +struct impl { + HANDLE_AACENCODER aacenc; + HANDLE_AACDECODER aacdec; + + struct rtp_header *header; + + size_t mtu; + int codesize; + + int max_bitrate; + int cur_bitrate; + + uint32_t rate; + uint32_t channels; + int samplesize; +}; + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + static const a2dp_aac_t a2dp_aac = { + .object_type = + /* NOTE: AAC Long Term Prediction and AAC Scalable are + * not supported by the FDK-AAC library. */ + AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC, + AAC_INIT_FREQUENCY( + AAC_SAMPLING_FREQ_8000 | + AAC_SAMPLING_FREQ_11025 | + AAC_SAMPLING_FREQ_12000 | + AAC_SAMPLING_FREQ_16000 | + AAC_SAMPLING_FREQ_22050 | + AAC_SAMPLING_FREQ_24000 | + AAC_SAMPLING_FREQ_32000 | + AAC_SAMPLING_FREQ_44100 | + AAC_SAMPLING_FREQ_48000 | + AAC_SAMPLING_FREQ_64000 | + AAC_SAMPLING_FREQ_88200 | + AAC_SAMPLING_FREQ_96000) + .channels = + AAC_CHANNELS_1 | + AAC_CHANNELS_2, + .vbr = 1, + AAC_INIT_BITRATE(DEFAULT_AAC_BITRATE) + }; + + memcpy(caps, &a2dp_aac, sizeof(a2dp_aac)); + return sizeof(a2dp_aac); +} + +static const struct media_codec_config +aac_frequencies[] = { + { AAC_SAMPLING_FREQ_48000, 48000, 11 }, + { AAC_SAMPLING_FREQ_44100, 44100, 10 }, + { AAC_SAMPLING_FREQ_96000, 96000, 9 }, + { AAC_SAMPLING_FREQ_88200, 88200, 8 }, + { AAC_SAMPLING_FREQ_64000, 64000, 7 }, + { AAC_SAMPLING_FREQ_32000, 32000, 6 }, + { AAC_SAMPLING_FREQ_24000, 24000, 5 }, + { AAC_SAMPLING_FREQ_22050, 22050, 4 }, + { AAC_SAMPLING_FREQ_16000, 16000, 3 }, + { AAC_SAMPLING_FREQ_12000, 12000, 2 }, + { AAC_SAMPLING_FREQ_11025, 11025, 1 }, + { AAC_SAMPLING_FREQ_8000, 8000, 0 }, +}; + +static const struct media_codec_config +aac_channel_modes[] = { + { AAC_CHANNELS_2, 2, 1 }, + { AAC_CHANNELS_1, 1, 0 }, +}; + +static int get_valid_aac_bitrate(a2dp_aac_t *conf) +{ + if (AAC_GET_BITRATE(*conf) < MIN_AAC_BITRATE) { + /* Unknown (0) or bogus bitrate */ + return DEFAULT_AAC_BITRATE; + } else { + return SPA_MIN(AAC_GET_BITRATE(*conf), DEFAULT_AAC_BITRATE); + } +} + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_aac_t conf; + int i; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + conf = *(a2dp_aac_t*)caps; + + if (conf.object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC) + conf.object_type = AAC_OBJECT_TYPE_MPEG2_AAC_LC; + else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) + conf.object_type = AAC_OBJECT_TYPE_MPEG4_AAC_LC; + else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LTP) + return -ENOTSUP; /* Not supported by FDK-AAC */ + else if (conf.object_type & AAC_OBJECT_TYPE_MPEG4_AAC_SCA) + return -ENOTSUP; /* Not supported by FDK-AAC */ + else + return -ENOTSUP; + + if ((i = media_codec_select_config(aac_frequencies, + SPA_N_ELEMENTS(aac_frequencies), + AAC_GET_FREQUENCY(conf), + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + AAC_SET_FREQUENCY(conf, aac_frequencies[i].config); + + if ((i = media_codec_select_config(aac_channel_modes, + SPA_N_ELEMENTS(aac_channel_modes), + conf.channels, + info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS + )) < 0) + return -ENOTSUP; + conf.channels = aac_channel_modes[i].config; + + AAC_SET_BITRATE(conf, get_valid_aac_bitrate(&conf)); + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_aac_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) { + if (AAC_GET_FREQUENCY(conf) & f->config) { + if (i++ == 0) + spa_pod_builder_int(b, f->value); + spa_pod_builder_int(b, f->value); + } + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + + if (SPA_FLAG_IS_SET(conf.channels, AAC_CHANNELS_1 | AAC_CHANNELS_2)) { + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), + 0); + } else if (conf.channels & AAC_CHANNELS_1) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + } else if (conf.channels & AAC_CHANNELS_2) { + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + } else + return -EINVAL; + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + a2dp_aac_t conf; + size_t j; + + if (caps == NULL || caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16; + + /* + * A2DP v1.3.2, 4.5.2: only one bit shall be set in bitfields. + * However, there is a report (#1342) of device setting multiple + * bits for AAC object type. It's not clear if this was due to + * a BlueZ bug, but we can be lax here and below in codec_init. + */ + if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC))) + return -EINVAL; + j = 0; + SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) { + if (AAC_GET_FREQUENCY(conf) & f->config) { + info->info.raw.rate = f->value; + j++; + break; + } + } + if (j == 0) + return -EINVAL; + + if (conf.channels & AAC_CHANNELS_2) { + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + } else if (conf.channels & AAC_CHANNELS_1) { + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + } else { + return -EINVAL; + } + + return 0; +} + +static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings) +{ + struct props *p = calloc(1, sizeof(struct props)); + const char *str; + + if (p == NULL) + return NULL; + + if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.aac.bitratemode")) == NULL) + str = "0"; + + p->bitratemode = SPA_CLAMP(atoi(str), 0, 5); + return p; +} + +static void codec_clear_props(void *props) +{ + free(props); +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this; + a2dp_aac_t *conf = config; + struct props *p = props; + UINT bitratemode; + int res; + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) { + res = -errno; + goto error; + } + this->mtu = mtu; + this->rate = info->info.raw.rate; + this->channels = info->info.raw.channels; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16) { + res = -EINVAL; + goto error; + } + this->samplesize = 2; + + bitratemode = p ? p->bitratemode : 0; + + res = aacEncOpen(&this->aacenc, 0, this->channels); + if (res != AACENC_OK) + goto error; + + if (!(conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | + AAC_OBJECT_TYPE_MPEG4_AAC_LC))) { + res = -EINVAL; + goto error; + } + + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_SAMPLERATE, this->rate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_CHANNELMODE, this->channels); + if (res != AACENC_OK) + goto error; + + if (conf->vbr) { + res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATEMODE, + bitratemode); + if (res != AACENC_OK) + goto error; + } + + res = aacEncoder_SetParam(this->aacenc, AACENC_AUDIOMUXVER, 2); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_SIGNALING_MODE, 1); + if (res != AACENC_OK) + goto error; + + // Fragmentation is not implemented yet, + // so make sure every encoded AAC frame fits in (mtu - header) + this->max_bitrate = ((this->mtu - sizeof(struct rtp_header)) * 8 * this->rate) / 1024; + this->max_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf)); + this->cur_bitrate = this->max_bitrate; + + res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_PEAK_BITRATE, this->max_bitrate); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_TRANSMUX, TT_MP4_LATM_MCP1); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_HEADER_PERIOD, 1); + if (res != AACENC_OK) + goto error; + + res = aacEncoder_SetParam(this->aacenc, AACENC_AFTERBURNER, 1); + if (res != AACENC_OK) + goto error; + + res = aacEncEncode(this->aacenc, NULL, NULL, NULL, NULL); + if (res != AACENC_OK) + goto error; + + AACENC_InfoStruct enc_info = {}; + res = aacEncInfo(this->aacenc, &enc_info); + if (res != AACENC_OK) + goto error; + + this->codesize = enc_info.frameLength * this->channels * this->samplesize; + + this->aacdec = aacDecoder_Open(TT_MP4_LATM_MCP1, 1); + if (!this->aacdec) { + res = -EINVAL; + goto error; + } + +#ifdef AACDECODER_LIB_VL0 + res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MIN_OUTPUT_CHANNELS, this->channels); + if (res != AAC_DEC_OK) { + spa_log_debug(log, "Couldn't set min output channels: 0x%04X", res); + goto error; + } + + res = aacDecoder_SetParam(this->aacdec, AAC_PCM_MAX_OUTPUT_CHANNELS, this->channels); + if (res != AAC_DEC_OK) { + spa_log_debug(log, "Couldn't set max output channels: 0x%04X", res); + goto error; + } +#else + res = aacDecoder_SetParam(this->aacdec, AAC_PCM_OUTPUT_CHANNELS, this->channels); + if (res != AAC_DEC_OK) { + spa_log_debug(log, "Couldn't set output channels: 0x%04X", res); + goto error; + } +#endif + + return this; + +error: + if (this && this->aacenc) + aacEncClose(&this->aacenc); + if (this && this->aacdec) + aacDecoder_Close(this->aacdec); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + if (this->aacenc) + aacEncClose(&this->aacenc); + if (this->aacdec) + aacDecoder_Close(this->aacdec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->header = (struct rtp_header *)dst; + memset(this->header, 0, sizeof(struct rtp_header)); + + this->header->v = 2; + this->header->pt = 96; + this->header->sequence_number = htons(seqnum); + this->header->timestamp = htonl(timestamp); + this->header->ssrc = htonl(1); + return sizeof(struct rtp_header); +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res; + + void *in_bufs[] = {(void *) src}; + int in_buf_ids[] = {IN_AUDIO_DATA}; + int in_buf_sizes[] = {src_size}; + int in_buf_el_sizes[] = {this->samplesize}; + AACENC_BufDesc in_buf_desc = { + .numBufs = 1, + .bufs = in_bufs, + .bufferIdentifiers = in_buf_ids, + .bufSizes = in_buf_sizes, + .bufElSizes = in_buf_el_sizes, + }; + AACENC_InArgs in_args = { + .numInSamples = src_size / this->samplesize, + }; + + void *out_bufs[] = {dst}; + int out_buf_ids[] = {OUT_BITSTREAM_DATA}; + int out_buf_sizes[] = {dst_size}; + int out_buf_el_sizes[] = {this->samplesize}; + AACENC_BufDesc out_buf_desc = { + .numBufs = 1, + .bufs = out_bufs, + .bufferIdentifiers = out_buf_ids, + .bufSizes = out_buf_sizes, + .bufElSizes = out_buf_el_sizes, + }; + AACENC_OutArgs out_args = {}; + + res = aacEncEncode(this->aacenc, &in_buf_desc, &out_buf_desc, &in_args, &out_args); + if (res != AACENC_OK) + return -EINVAL; + + *dst_out = out_args.numOutBytes; + *need_flush = NEED_FLUSH_ALL; + + /* RFC6416: It is set to 1 to indicate that the RTP packet contains a complete + * audioMuxElement or the last fragment of an audioMuxElement */ + this->header->m = 1; + + return out_args.numInSamples * this->samplesize; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + const struct rtp_header *header = src; + size_t header_size = sizeof(struct rtp_header); + + spa_return_val_if_fail (src_size > header_size, -EINVAL); + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + + return header_size; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + uint data_size = (uint)src_size; + uint bytes_valid = data_size; + CStreamInfo *aacinf; + int res; + + res = aacDecoder_Fill(this->aacdec, (UCHAR **)&src, &data_size, &bytes_valid); + if (res != AAC_DEC_OK) { + spa_log_debug(log, "AAC buffer fill error: 0x%04X", res); + return -EINVAL; + } + + res = aacDecoder_DecodeFrame(this->aacdec, dst, dst_size, 0); + if (res != AAC_DEC_OK) { + spa_log_debug(log, "AAC decode frame error: 0x%04X", res); + return -EINVAL; + } + + aacinf = aacDecoder_GetStreamInfo(this->aacdec); + if (!aacinf) { + spa_log_debug(log, "AAC get stream info failed"); + return -EINVAL; + } + *dst_out = aacinf->frameSize * aacinf->numChannels * this->samplesize; + + return src_size - bytes_valid; +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_change_bitrate(struct impl *this, int new_bitrate) +{ + int res; + + new_bitrate = SPA_MIN(new_bitrate, this->max_bitrate); + new_bitrate = SPA_MAX(new_bitrate, 64000); + + if (new_bitrate == this->cur_bitrate) + return 0; + + this->cur_bitrate = new_bitrate; + + res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate); + if (res != AACENC_OK) + return -EINVAL; + + return this->cur_bitrate; +} + +static int codec_reduce_bitpool(void *data) +{ + struct impl *this = data; + return codec_change_bitrate(this, (this->cur_bitrate * 2) / 3); +} + +static int codec_increase_bitpool(void *data) +{ + struct impl *this = data; + return codec_change_bitrate(this, (this->cur_bitrate * 4) / 3); +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &log_topic); +} + +const struct media_codec a2dp_codec_aac = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC, + .codec_id = A2DP_CODEC_MPEG24, + .name = "aac", + .description = "AAC", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init_props = codec_init_props, + .clear_props = codec_clear_props, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .abr_process = codec_abr_process, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, + .set_log = codec_set_log, +}; + +MEDIA_CODEC_EXPORT_DEF( + "aac", + &a2dp_codec_aac +); diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c new file mode 100644 index 0000000..6938e47 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-aptx.c @@ -0,0 +1,748 @@ +/* Spa A2DP aptX codec + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + size_t actual_conf_size = codec_get_caps_size(codec); + const a2dp_aptx_t a2dp_aptx = { + .info = codec->vendor, + .frequency = + APTX_SAMPLING_FREQ_16000 | + APTX_SAMPLING_FREQ_32000 | + APTX_SAMPLING_FREQ_44100 | + APTX_SAMPLING_FREQ_48000, + .channel_mode = + APTX_CHANNEL_MODE_STEREO, + }; + const a2dp_aptx_ll_t a2dp_aptx_ll = { + .aptx = a2dp_aptx, + .bidirect_link = codec->duplex_codec ? true : false, + .has_new_caps = false, + }; + if (codec_is_ll(codec)) + memcpy(caps, &a2dp_aptx_ll, sizeof(a2dp_aptx_ll)); + else + memcpy(caps, &a2dp_aptx, sizeof(a2dp_aptx)); + return actual_conf_size; +} + +static const struct media_codec_config +aptx_frequencies[] = { + { APTX_SAMPLING_FREQ_48000, 48000, 3 }, + { APTX_SAMPLING_FREQ_44100, 44100, 2 }, + { APTX_SAMPLING_FREQ_32000, 32000, 1 }, + { APTX_SAMPLING_FREQ_16000, 16000, 0 }, +}; + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_aptx_t conf; + int i; + size_t actual_conf_size = codec_get_caps_size(codec); + + if (caps_size < sizeof(conf) || actual_conf_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + if ((i = media_codec_select_config(aptx_frequencies, + SPA_N_ELEMENTS(aptx_frequencies), + conf.frequency, + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + conf.frequency = aptx_frequencies[i].config; + + if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO) + conf.channel_mode = APTX_CHANNEL_MODE_STEREO; + else + return -ENOTSUP; + + memcpy(config, &conf, sizeof(conf)); + + return actual_conf_size; +} + +static int codec_select_config_ll(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_aptx_ll_ext_t conf = { 0 }; + size_t actual_conf_size; + int res; + + /* caps may contain only conf.base, or also the extended attributes */ + + if (caps_size < sizeof(conf.base)) + return -EINVAL; + + memcpy(&conf, caps, SPA_MIN(caps_size, sizeof(conf))); + + actual_conf_size = conf.base.has_new_caps ? sizeof(conf) : sizeof(conf.base); + if (caps_size < actual_conf_size) + return -EINVAL; + + if (codec->duplex_codec && !conf.base.bidirect_link) + return -ENOTSUP; + + if ((res = codec_select_config(codec, flags, caps, caps_size, info, settings, config)) < 0) + return res; + + memcpy(&conf.base.aptx, config, sizeof(conf.base.aptx)); + + if (conf.base.has_new_caps) { + int target_level = APTX_LL_LEVEL(conf.target_level1, conf.target_level2); + int initial_level = APTX_LL_LEVEL(conf.initial_level1, conf.initial_level2); + int good_working_level = APTX_LL_LEVEL(conf.good_working_level1, conf.good_working_level2); + + target_level = SPA_MAX(target_level, APTX_LL_TARGET_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT); + initial_level = SPA_MAX(initial_level, APTX_LL_INITIAL_CODEC_LEVEL * LL_LEVEL_ADJUSTMENT); + good_working_level = SPA_MAX(good_working_level, APTX_LL_GOOD_WORKING_LEVEL * LL_LEVEL_ADJUSTMENT); + + conf.target_level1 = APTX_LL_LEVEL1(target_level); + conf.target_level2 = APTX_LL_LEVEL2(target_level); + conf.initial_level1 = APTX_LL_LEVEL1(initial_level); + conf.initial_level2 = APTX_LL_LEVEL2(initial_level); + conf.good_working_level1 = APTX_LL_LEVEL1(good_working_level); + conf.good_working_level2 = APTX_LL_LEVEL2(good_working_level); + + if (conf.sra_max_rate == 0) + conf.sra_max_rate = APTX_LL_SRA_MAX_RATE; + if (conf.sra_avg_time == 0) + conf.sra_avg_time = APTX_LL_SRA_AVG_TIME; + } + + memcpy(config, &conf, actual_conf_size); + + return actual_conf_size; +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_aptx_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.frequency & APTX_SAMPLING_FREQ_48000) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.frequency & APTX_SAMPLING_FREQ_44100) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } + if (conf.frequency & APTX_SAMPLING_FREQ_32000) { + if (i++ == 0) + spa_pod_builder_int(b, 32000); + spa_pod_builder_int(b, 32000); + } + if (conf.frequency & APTX_SAMPLING_FREQ_16000) { + if (i++ == 0) + spa_pod_builder_int(b, 16000); + spa_pod_builder_int(b, 16000); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (SPA_FLAG_IS_SET(conf.channel_mode, APTX_CHANNEL_MODE_MONO | APTX_CHANNEL_MODE_STEREO)) { + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), + 0); + } else if (conf.channel_mode & APTX_CHANNEL_MODE_MONO) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + } else if (conf.channel_mode & APTX_CHANNEL_MODE_STEREO) { + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + } else + return -EINVAL; + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this; + int res; + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + this->hd = codec_is_hd(codec); + + if ((this->aptx = aptx_init(this->hd)) == NULL) + goto error_errno; + + this->mtu = mtu; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S24) { + res = -EINVAL; + goto error; + } + this->frame_length = this->hd ? 6 : 4; + this->codesize = 4 * 3 * 2; + + if (this->hd) + this->max_frames = (this->mtu - sizeof(struct rtp_header)) / this->frame_length; + else if (codec_is_ll(codec)) + this->max_frames = SPA_MIN(256u, this->mtu) / this->frame_length; + else + this->max_frames = this->mtu / this->frame_length; + + return this; + +error_errno: + res = -errno; + goto error; +error: + if (this->aptx) + aptx_finish(this->aptx); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + aptx_finish(this->aptx); + free(this); +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->frame_count = 0; + + if (!this->hd) + return 0; + + this->header = (struct rtp_header *)dst; + memset(this->header, 0, sizeof(struct rtp_header)); + + this->header->v = 2; + this->header->pt = 96; + this->header->sequence_number = htons(seqnum); + this->header->timestamp = htonl(timestamp); + return sizeof(struct rtp_header); +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + size_t avail_dst_size; + int res; + + avail_dst_size = (this->max_frames - this->frame_count) * this->frame_length; + if (SPA_UNLIKELY(dst_size < avail_dst_size)) { + *need_flush = NEED_FLUSH_ALL; + return 0; + } + + res = aptx_encode(this->aptx, src, src_size, + dst, avail_dst_size, dst_out); + if(SPA_UNLIKELY(res < 0)) + return -EINVAL; + + this->frame_count += *dst_out / this->frame_length; + *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO; + return res; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + + if (!this->hd) + return 0; + + const struct rtp_header *header = src; + size_t header_size = sizeof(struct rtp_header); + + spa_return_val_if_fail(src_size > header_size, -EINVAL); + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + return header_size; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int res; + + res = aptx_decode(this->aptx, src, src_size, + dst, dst_size, dst_out); + + return res; +} + +/* + * mSBC duplex codec + * + * When connected as SRC to SNK, aptX-LL sink may send back mSBC data. + */ + +static int msbc_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_audio_info_raw info = { 0, }; + + if (caps_size < sizeof(a2dp_aptx_ll_t)) + return -EINVAL; + + if (idx > 0) + return 0; + + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.channels = 1; + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + info.rate = 16000; + + *param = spa_format_audio_raw_build(b, id, &info); + return *param == NULL ? -EIO : 1; +} + +static int msbc_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + info->info.raw.rate = 16000; + return 0; +} + +static int msbc_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int msbc_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int msbc_get_block_size(void *data) +{ + return MSBC_DECODED_SIZE; +} + +static void *msbc_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct msbc_impl *this = NULL; + int res; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct msbc_impl))) == NULL) + goto error_errno; + + if ((res = sbc_init_msbc(&this->msbc, 0)) < 0) + goto error; + + this->msbc.endian = SBC_LE; + + return this; + +error_errno: + res = -errno; + goto error; +error: + free(this); + errno = -res; + return NULL; +} + +static void msbc_deinit(void *data) +{ + struct msbc_impl *this = data; + sbc_finish(&this->msbc); + free(this); +} + +static int msbc_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int msbc_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + return -ENOTSUP; +} + +static int msbc_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + return -ENOTSUP; +} + +static int msbc_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +static int msbc_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct msbc_impl *this = data; + const uint8_t sync[3] = { 0xAD, 0x00, 0x00 }; + size_t processed = 0; + int res; + + spa_assert(sizeof(sync) <= MSBC_PAYLOAD_SIZE); + + *dst_out = 0; + + /* Scan for msbc sync sequence. + * We could probably assume fixed (<57-byte payload><1-byte pad>)+ format + * which devices seem to be sending. Don't know if there are variations, + * so we make weaker assumption here. + */ + while (src_size >= MSBC_PAYLOAD_SIZE) { + if (memcmp(src, sync, sizeof(sync)) == 0) + break; + src = (uint8_t*)src + 1; + --src_size; + ++processed; + } + + res = sbc_decode(&this->msbc, src, src_size, + dst, dst_size, dst_out); + if (res <= 0) + res = SPA_MIN((size_t)MSBC_PAYLOAD_SIZE, src_size); /* skip bad payload */ + + processed += res; + return processed; +} + + +const struct media_codec a2dp_codec_aptx = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX, + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = APTX_VENDOR_ID, + .codec_id = APTX_CODEC_ID }, + .name = "aptx", + .description = "aptX", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, +}; + + +const struct media_codec a2dp_codec_aptx_hd = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = APTX_HD_VENDOR_ID, + .codec_id = APTX_HD_CODEC_ID }, + .name = "aptx_hd", + .description = "aptX HD", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, +}; + +#define APTX_LL_COMMON_DEFS \ + .codec_id = A2DP_CODEC_VENDOR, \ + .description = "aptX-LL", \ + .fill_caps = codec_fill_caps, \ + .select_config = codec_select_config_ll, \ + .enum_config = codec_enum_config, \ + .init = codec_init, \ + .deinit = codec_deinit, \ + .get_block_size = codec_get_block_size, \ + .abr_process = codec_abr_process, \ + .start_encode = codec_start_encode, \ + .encode = codec_encode, \ + .reduce_bitpool = codec_reduce_bitpool, \ + .increase_bitpool = codec_increase_bitpool + + +const struct media_codec a2dp_codec_aptx_ll_0 = { + APTX_LL_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, + .vendor = { .vendor_id = APTX_LL_VENDOR_ID, + .codec_id = APTX_LL_CODEC_ID }, + .name = "aptx_ll", + .endpoint_name = "aptx_ll_0", +}; + +const struct media_codec a2dp_codec_aptx_ll_1 = { + APTX_LL_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, + .vendor = { .vendor_id = APTX_LL_VENDOR_ID2, + .codec_id = APTX_LL_CODEC_ID }, + .name = "aptx_ll", + .endpoint_name = "aptx_ll_1", +}; + +/* Voice channel mSBC, not a real A2DP codec */ +static const struct media_codec aptx_ll_msbc = { + .codec_id = A2DP_CODEC_VENDOR, + .name = "aptx_ll_msbc", + .description = "aptX-LL mSBC", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config_ll, + .enum_config = msbc_enum_config, + .validate_config = msbc_validate_config, + .init = msbc_init, + .deinit = msbc_deinit, + .get_block_size = msbc_get_block_size, + .abr_process = msbc_abr_process, + .start_encode = msbc_start_encode, + .encode = msbc_encode, + .start_decode = msbc_start_decode, + .decode = msbc_decode, + .reduce_bitpool = msbc_reduce_bitpool, + .increase_bitpool = msbc_increase_bitpool, +}; + +static const struct spa_dict_item duplex_info_items[] = { + { "duplex.boost", "true" }, +}; +static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items); + +const struct media_codec a2dp_codec_aptx_ll_duplex_0 = { + APTX_LL_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, + .vendor = { .vendor_id = APTX_LL_VENDOR_ID, + .codec_id = APTX_LL_CODEC_ID }, + .name = "aptx_ll_duplex", + .endpoint_name = "aptx_ll_duplex_0", + .duplex_codec = &aptx_ll_msbc, + .info = &duplex_info, +}; + +const struct media_codec a2dp_codec_aptx_ll_duplex_1 = { + APTX_LL_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, + .vendor = { .vendor_id = APTX_LL_VENDOR_ID2, + .codec_id = APTX_LL_CODEC_ID }, + .name = "aptx_ll_duplex", + .endpoint_name = "aptx_ll_duplex_1", + .duplex_codec = &aptx_ll_msbc, + .info = &duplex_info, +}; + +MEDIA_CODEC_EXPORT_DEF( + "aptx", + &a2dp_codec_aptx_hd, + &a2dp_codec_aptx, + &a2dp_codec_aptx_ll_0, + &a2dp_codec_aptx_ll_1, + &a2dp_codec_aptx_ll_duplex_0, + &a2dp_codec_aptx_ll_duplex_1 +); diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h new file mode 100644 index 0000000..9f72592 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-caps.h @@ -0,0 +1,460 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * 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_SAMPLING_FREQ_8000 0x0800 +#define AAC_SAMPLING_FREQ_11025 0x0400 +#define AAC_SAMPLING_FREQ_12000 0x0200 +#define AAC_SAMPLING_FREQ_16000 0x0100 +#define AAC_SAMPLING_FREQ_22050 0x0080 +#define AAC_SAMPLING_FREQ_24000 0x0040 +#define AAC_SAMPLING_FREQ_32000 0x0020 +#define AAC_SAMPLING_FREQ_44100 0x0010 +#define AAC_SAMPLING_FREQ_48000 0x0008 +#define AAC_SAMPLING_FREQ_64000 0x0004 +#define AAC_SAMPLING_FREQ_88200 0x0002 +#define AAC_SAMPLING_FREQ_96000 0x0001 + +#define AAC_CHANNELS_1 0x02 +#define AAC_CHANNELS_2 0x01 + +#define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \ + (a).bitrate2 << 8 | (a).bitrate3) +#define AAC_GET_FREQUENCY(a) ((a).frequency1 << 4 | (a).frequency2) + +#define AAC_SET_BITRATE(a, b) \ + do { \ + (a).bitrate1 = ((b) >> 16) & 0x7f; \ + (a).bitrate2 = ((b) >> 8) & 0xff; \ + (a).bitrate3 = (b) & 0xff; \ + } while (0) +#define AAC_SET_FREQUENCY(a, f) \ + do { \ + (a).frequency1 = ((f) >> 4) & 0xff; \ + (a).frequency2 = (f) & 0x0f; \ + } while (0) + +#define AAC_INIT_BITRATE(b) \ + .bitrate1 = ((b) >> 16) & 0x7f, \ + .bitrate2 = ((b) >> 8) & 0xff, \ + .bitrate3 = (b) & 0xff, +#define AAC_INIT_FREQUENCY(f) \ + .frequency1 = ((f) >> 4) & 0xff, \ + .frequency2 = (f) & 0x0f, + +#define APTX_VENDOR_ID 0x0000004f +#define APTX_CODEC_ID 0x0001 + +#define APTX_CHANNEL_MODE_MONO 0x01 +#define APTX_CHANNEL_MODE_STEREO 0x02 + +#define APTX_SAMPLING_FREQ_16000 0x08 +#define APTX_SAMPLING_FREQ_32000 0x04 +#define APTX_SAMPLING_FREQ_44100 0x02 +#define APTX_SAMPLING_FREQ_48000 0x01 + +#define APTX_HD_VENDOR_ID 0x000000D7 +#define APTX_HD_CODEC_ID 0x0024 + +#define APTX_HD_CHANNEL_MODE_MONO 0x1 +#define APTX_HD_CHANNEL_MODE_STEREO 0x2 + +#define APTX_HD_SAMPLING_FREQ_16000 0x8 +#define APTX_HD_SAMPLING_FREQ_32000 0x4 +#define APTX_HD_SAMPLING_FREQ_44100 0x2 +#define APTX_HD_SAMPLING_FREQ_48000 0x1 + +#define APTX_LL_VENDOR_ID 0x0000000a +#define APTX_LL_VENDOR_ID2 0x000000d7 +#define APTX_LL_CODEC_ID 0x0002 + +/** + * Default parameters for aptX LL (Sprint) encoder + */ +#define APTX_LL_TARGET_CODEC_LEVEL 180 /* target codec buffer level */ +#define APTX_LL_INITIAL_CODEC_LEVEL 360 /* initial codec buffer level */ +#define APTX_LL_SRA_MAX_RATE 50 /* x/10000 = 0.005 SRA rate */ +#define APTX_LL_SRA_AVG_TIME 1 /* SRA averaging time = 1s */ +#define APTX_LL_GOOD_WORKING_LEVEL 180 /* good working buffer level */ + +#define LDAC_VENDOR_ID 0x0000012d +#define LDAC_CODEC_ID 0x00aa + +#define LDAC_CHANNEL_MODE_MONO 0x04 +#define LDAC_CHANNEL_MODE_DUAL_CHANNEL 0x02 +#define LDAC_CHANNEL_MODE_STEREO 0x01 + +#define LDAC_SAMPLING_FREQ_44100 0x20 +#define LDAC_SAMPLING_FREQ_48000 0x10 +#define LDAC_SAMPLING_FREQ_88200 0x08 +#define LDAC_SAMPLING_FREQ_96000 0x04 +#define LDAC_SAMPLING_FREQ_176400 0x02 +#define LDAC_SAMPLING_FREQ_192000 0x01 + +#define FASTSTREAM_VENDOR_ID 0x0000000a +#define FASTSTREAM_CODEC_ID 0x0001 + +#define FASTSTREAM_DIRECTION_SINK 0x1 +#define FASTSTREAM_DIRECTION_SOURCE 0x2 + +#define FASTSTREAM_SINK_SAMPLING_FREQ_44100 0x2 +#define FASTSTREAM_SINK_SAMPLING_FREQ_48000 0x1 + +#define FASTSTREAM_SOURCE_SAMPLING_FREQ_16000 0x2 + +#define LC3PLUS_HR_GET_FRAME_DURATION(a) ((a).frame_duration & 0xf0) +#define LC3PLUS_HR_INIT_FRAME_DURATION(v) \ + .frame_duration = ((v) & 0xf0), +#define LC3PLUS_HR_SET_FRAME_DURATION(a, v) \ + do { \ + (a).frame_duration = ((v) & 0xf0); \ + } while (0) + +#define LC3PLUS_HR_GET_FREQUENCY(a) (((a).frequency1 << 8) | (a).frequency2) +#define LC3PLUS_HR_INIT_FREQUENCY(v) \ + .frequency1 = (((v) >> 8) & 0xff), \ + .frequency2 = ((v) & 0xff), +#define LC3PLUS_HR_SET_FREQUENCY(a, v) \ + do { \ + (a).frequency1 = ((v) >> 8) & 0xff; \ + (a).frequency2 = (v) & 0xff; \ + } while (0) + +#define LC3PLUS_HR_VENDOR_ID 0x000008a9 +#define LC3PLUS_HR_CODEC_ID 0x0001 + +#define LC3PLUS_HR_FRAME_DURATION_10MS (1 << 6) +#define LC3PLUS_HR_FRAME_DURATION_5MS (1 << 5) +#define LC3PLUS_HR_FRAME_DURATION_2_5MS (1 << 4) + +#define LC3PLUS_HR_CHANNELS_1 (1 << 7) +#define LC3PLUS_HR_CHANNELS_2 (1 << 6) + +#define LC3PLUS_HR_SAMPLING_FREQ_48000 (1 << 8) +#define LC3PLUS_HR_SAMPLING_FREQ_96000 (1 << 7) + +#define OPUS_05_VENDOR_ID 0x000005f1 +#define OPUS_05_CODEC_ID 0x1005 + +#define OPUS_05_MAPPING_FAMILY_0 (1 << 0) +#define OPUS_05_MAPPING_FAMILY_1 (1 << 1) +#define OPUS_05_MAPPING_FAMILY_255 (1 << 2) + +#define OPUS_05_FRAME_DURATION_25 (1 << 0) +#define OPUS_05_FRAME_DURATION_50 (1 << 1) +#define OPUS_05_FRAME_DURATION_100 (1 << 2) +#define OPUS_05_FRAME_DURATION_200 (1 << 3) +#define OPUS_05_FRAME_DURATION_400 (1 << 4) + +#define OPUS_05_GET_UINT16(a, field) \ + (((a).field ## 2 << 8) | (a).field ## 1) +#define OPUS_05_INIT_UINT16(field, v) \ + .field ## 1 = ((v) & 0xff), \ + .field ## 2 = (((v) >> 8) & 0xff), +#define OPUS_05_SET_UINT16(a, field, v) \ + do { \ + (a).field ## 1 = ((v) & 0xff); \ + (a).field ## 2 = (((v) >> 8) & 0xff); \ + } while (0) +#define OPUS_05_GET_UINT32(a, field) \ + (((a).field ## 4 << 24) | ((a).field ## 3 << 16) | \ + ((a).field ## 2 << 8) | (a).field ## 1) +#define OPUS_05_INIT_UINT32(field, v) \ + .field ## 1 = ((v) & 0xff), \ + .field ## 2 = (((v) >> 8) & 0xff), \ + .field ## 3 = (((v) >> 16) & 0xff), \ + .field ## 4 = (((v) >> 24) & 0xff), +#define OPUS_05_SET_UINT32(a, field, v) \ + do { \ + (a).field ## 1 = ((v) & 0xff); \ + (a).field ## 2 = (((v) >> 8) & 0xff); \ + (a).field ## 3 = (((v) >> 16) & 0xff); \ + (a).field ## 4 = (((v) >> 24) & 0xff); \ + } while (0) + +#define OPUS_05_GET_LOCATION(a) OPUS_05_GET_UINT32(a, location) +#define OPUS_05_INIT_LOCATION(v) OPUS_05_INIT_UINT32(location, v) +#define OPUS_05_SET_LOCATION(a, v) OPUS_05_SET_UINT32(a, location, v) + +#define OPUS_05_GET_BITRATE(a) OPUS_05_GET_UINT16(a, bitrate) +#define OPUS_05_INIT_BITRATE(v) OPUS_05_INIT_UINT16(bitrate, v) +#define OPUS_05_SET_BITRATE(a, v) OPUS_05_SET_UINT16(a, bitrate, v) + + +typedef struct { + uint32_t vendor_id; + uint16_t codec_id; +} __attribute__ ((packed)) a2dp_vendor_codec_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency; + uint8_t channel_mode; +} __attribute__ ((packed)) a2dp_ldac_t; + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +typedef struct { + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t allocation_method:2; + uint8_t subbands:2; + uint8_t block_length:4; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t channel_mode:4; + uint8_t crc:1; + uint8_t layer:3; + uint8_t frequency:6; + uint8_t mpf:1; + uint8_t rfa:1; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +typedef struct { + uint8_t object_type; + uint8_t frequency1; + uint8_t rfa:2; + uint8_t channels:2; + uint8_t frequency2:4; + uint8_t bitrate1:7; + uint8_t vbr:1; + uint8_t bitrate2; + uint8_t bitrate3; +} __attribute__ ((packed)) a2dp_aac_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t channel_mode:4; + uint8_t frequency:4; + uint32_t rfa; +} __attribute__ ((packed)) a2dp_aptx_hd_t; + +typedef struct { + a2dp_aptx_t aptx; + uint8_t bidirect_link:1; + uint8_t has_new_caps:1; + uint8_t reserved:6; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t sink_frequency:4; + uint8_t source_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +#elif __BYTE_ORDER == __BIG_ENDIAN + +typedef struct { + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t block_length:4; + uint8_t subbands:2; + uint8_t allocation_method:2; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t layer:3; + uint8_t crc:1; + uint8_t channel_mode:4; + uint8_t rfa:1; + uint8_t mpf:1; + uint8_t frequency:6; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +typedef struct { + uint8_t object_type; + uint8_t frequency1; + uint8_t frequency2:4; + uint8_t channels:2; + uint8_t rfa:2; + uint8_t vbr:1; + uint8_t bitrate1:7; + uint8_t bitrate2; + uint8_t bitrate3; +} __attribute__ ((packed)) a2dp_aac_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; +} __attribute__ ((packed)) a2dp_aptx_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frequency:4; + uint8_t channel_mode:4; + uint32_t rfa; +} __attribute__ ((packed)) a2dp_aptx_hd_t; + +typedef struct { + a2dp_aptx_t aptx; + uint8_t reserved:6; + uint8_t has_new_caps:1; + uint8_t bidirect_link:1; +} __attribute__ ((packed)) a2dp_aptx_ll_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t direction; + uint8_t source_frequency:4; + uint8_t sink_frequency:4; +} __attribute__ ((packed)) a2dp_faststream_t; + +#else +#error "Unknown byte order" +#endif + +typedef struct { + a2dp_aptx_ll_t base; + uint8_t reserved; + uint8_t target_level2; + uint8_t target_level1; + uint8_t initial_level2; + uint8_t initial_level1; + uint8_t sra_max_rate; + uint8_t sra_avg_time; + uint8_t good_working_level2; + uint8_t good_working_level1; +} __attribute__ ((packed)) a2dp_aptx_ll_ext_t; + +typedef struct { + a2dp_vendor_codec_t info; + uint8_t frame_duration; + uint8_t channels; + uint8_t frequency1; + uint8_t frequency2; +} __attribute__ ((packed)) a2dp_lc3plus_hr_t; + +typedef struct { + uint8_t channels; + uint8_t coupled_streams; + uint8_t location1; + uint8_t location2; + uint8_t location3; + uint8_t location4; + uint8_t frame_duration; + uint8_t bitrate1; + uint8_t bitrate2; +} __attribute__ ((packed)) a2dp_opus_05_direction_t; + +typedef struct { + a2dp_vendor_codec_t info; + a2dp_opus_05_direction_t main; + a2dp_opus_05_direction_t bidi; +} __attribute__ ((packed)) a2dp_opus_05_t; + +#endif diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c new file mode 100644 index 0000000..a579ead --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-faststream.c @@ -0,0 +1,640 @@ +/* Spa A2DP FastStream codec + * + * Copyright © 2020 Wim Taymans + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#if __BYTE_ORDER != __LITTLE_ENDIAN +#include +#endif + +#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, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + const a2dp_faststream_t a2dp_faststream = { + .info = codec->vendor, + .direction = FASTSTREAM_DIRECTION_SINK | + (codec->duplex_codec ? FASTSTREAM_DIRECTION_SOURCE : 0), + .sink_frequency = + FASTSTREAM_SINK_SAMPLING_FREQ_44100 | + FASTSTREAM_SINK_SAMPLING_FREQ_48000, + .source_frequency = + FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, + }; + + memcpy(caps, &a2dp_faststream, sizeof(a2dp_faststream)); + return sizeof(a2dp_faststream); +} + +static const struct media_codec_config +frequencies[] = { + { FASTSTREAM_SINK_SAMPLING_FREQ_48000, 48000, 1 }, + { FASTSTREAM_SINK_SAMPLING_FREQ_44100, 44100, 0 }, +}; + +static const struct media_codec_config +duplex_frequencies[] = { + { FASTSTREAM_SOURCE_SAMPLING_FREQ_16000, 16000, 0 }, +}; + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_faststream_t conf; + int i; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + if (codec->duplex_codec && !(conf.direction & FASTSTREAM_DIRECTION_SOURCE)) + return -ENOTSUP; + + if (!(conf.direction & FASTSTREAM_DIRECTION_SINK)) + return -ENOTSUP; + + conf.direction = FASTSTREAM_DIRECTION_SINK; + + if (codec->duplex_codec) + conf.direction |= FASTSTREAM_DIRECTION_SOURCE; + + if ((i = media_codec_select_config(frequencies, + SPA_N_ELEMENTS(frequencies), + conf.sink_frequency, + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + conf.sink_frequency = frequencies[i].config; + + if ((i = media_codec_select_config(duplex_frequencies, + SPA_N_ELEMENTS(duplex_frequencies), + conf.source_frequency, + 16000 + )) < 0) + return -ENOTSUP; + conf.source_frequency = duplex_frequencies[i].config; + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_faststream_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_48000) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.sink_frequency & FASTSTREAM_SINK_SAMPLING_FREQ_44100) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static size_t ceil2(size_t v) +{ + if (v % 2 != 0 && v < SIZE_MAX) + v += 1; + return v; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + a2dp_faststream_t *conf = config; + struct impl *this; + bool sbc_initialized = false; + int res; + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + if ((res = sbc_init(&this->sbc, 0)) < 0) + goto error; + + sbc_initialized = true; + this->sbc.endian = SBC_LE; + this->mtu = mtu; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16) { + res = -EINVAL; + goto error; + } + + switch (conf->sink_frequency) { + case FASTSTREAM_SINK_SAMPLING_FREQ_44100: + this->sbc.frequency = SBC_FREQ_44100; + break; + case FASTSTREAM_SINK_SAMPLING_FREQ_48000: + this->sbc.frequency = SBC_FREQ_48000; + break; + default: + res = -EINVAL; + goto error; + } + + this->sbc.mode = SBC_MODE_JOINT_STEREO; + this->sbc.subbands = SBC_SB_8; + this->sbc.allocation = SBC_AM_LOUDNESS; + this->sbc.blocks = SBC_BLK_16; + this->sbc.bitpool = 29; + + this->codesize = sbc_get_codesize(&this->sbc); + + this->max_frames = 3; + if (this->mtu < this->max_frames * ceil2(sbc_get_frame_length(&this->sbc))) { + res = -EINVAL; + goto error; + } + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (sbc_initialized) + sbc_finish(&this->sbc); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + sbc_finish(&this->sbc); + free(this); +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + this->frame_count = 0; + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res; + + res = sbc_encode(&this->sbc, src, src_size, + dst, dst_size, (ssize_t*)dst_out); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + spa_assert(res == this->codesize); + + if (*dst_out % 2 != 0 && *dst_out < dst_size) { + /* Pad similarly as in input stream */ + *((uint8_t *)dst + *dst_out) = 0; + ++*dst_out; + } + + this->frame_count += res / this->codesize; + *need_flush = (this->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO; + return res; +} + +static SPA_UNUSED int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +static int do_decode(sbc_t *sbc, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + size_t processed = 0; + int res; + + *dst_out = 0; + + /* Scan for SBC syncword. + * We could probably assume 1-byte paddings instead, + * which devices seem to be sending. + */ + while (src_size >= 1) { + if (*(uint8_t*)src == 0x9C) + break; + src = (uint8_t*)src + 1; + --src_size; + ++processed; + } + + res = sbc_decode(sbc, src, src_size, + dst, dst_size, dst_out); + if (res <= 0) + res = SPA_MIN((size_t)1, src_size); /* skip bad payload */ + + processed += res; + return processed; +} + +static SPA_UNUSED int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + return do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out); +} + +/* + * Duplex codec + * + * When connected as SRC to SNK, FastStream sink may send back SBC data. + */ + +static int duplex_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_faststream_t conf; + struct spa_audio_info_raw info = { 0, }; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + switch (conf.source_frequency) { + case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000: + info.rate = 16000; + break; + default: + return -EINVAL; + } + + /* + * Some headsets send mono stream, others stereo. This information + * is contained in the SBC headers, and becomes known only when + * stream arrives. To be able to work in both cases, we will + * produce 2-channel output, and will double the channels + * in the decoding step if mono stream was received. + */ + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.channels = 2; + info.position[0] = SPA_AUDIO_CHANNEL_FL; + info.position[1] = SPA_AUDIO_CHANNEL_FR; + + *param = spa_format_audio_raw_build(b, id, &info); + return *param == NULL ? -EIO : 1; +} + +static int duplex_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + info->info.raw.rate = 16000; + return 0; +} + +static int duplex_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int duplex_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int duplex_get_block_size(void *data) +{ + return 0; +} + +static void *duplex_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + a2dp_faststream_t *conf = config; + struct duplex_impl *this = NULL; + int res; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16_LE) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct duplex_impl))) == NULL) + goto error_errno; + + if ((res = sbc_init(&this->sbc, 0)) < 0) + goto error; + + switch (conf->source_frequency) { + case FASTSTREAM_SOURCE_SAMPLING_FREQ_16000: + this->sbc.frequency = SBC_FREQ_16000; + break; + default: + res = -EINVAL; + goto error; + } + + this->sbc.endian = SBC_LE; + this->sbc.mode = SBC_MODE_MONO; + this->sbc.subbands = SBC_SB_8; + this->sbc.allocation = SBC_AM_LOUDNESS; + this->sbc.blocks = SBC_BLK_16; + this->sbc.bitpool = 32; + + return this; + +error_errno: + res = -errno; + goto error; +error: + free(this); + errno = -res; + return NULL; +} + +static void duplex_deinit(void *data) +{ + struct duplex_impl *this = data; + sbc_finish(&this->sbc); + free(this); +} + +static int duplex_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int duplex_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + return -ENOTSUP; +} + +static int duplex_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + return -ENOTSUP; +} + +static int duplex_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +/** Convert S16LE stereo -> S16LE mono, in-place (only for testing purposes) */ +static SPA_UNUSED size_t convert_s16le_c2_to_c1(int16_t *data, size_t size, size_t max_size) +{ + size_t i; + for (i = 0; i < size / 2; ++i) +#if __BYTE_ORDER == __LITTLE_ENDIAN + data[i] = data[2*i]/2 + data[2*i+1]/2; +#else + data[i] = bswap_16(bswap_16(data[2*i])/2 + bswap_16(data[2*i+1])/2); +#endif + return size / 2; +} + +/** Convert S16LE mono -> S16LE stereo, in-place */ +static size_t convert_s16le_c1_to_c2(uint8_t *data, size_t size, size_t max_size) +{ + size_t pos; + + pos = 2 * SPA_MIN(size / 2, max_size / 4); + size = 2 * pos; + + /* We'll trust the compiler to optimize this */ + while (pos >= 2) { + pos -= 2; + data[2*pos+3] = data[pos+1]; + data[2*pos+2] = data[pos]; + data[2*pos+1] = data[pos+1]; + data[2*pos] = data[pos]; + } + + return size; +} + +static int duplex_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct duplex_impl *this = data; + int res; + + *dst_out = 0; + res = do_decode(&this->sbc, src, src_size, dst, dst_size, dst_out); + + /* + * Depending on headers of first frame, libsbc may output either + * 1 or 2 channels. This function should always produce 2 channels, + * so we'll just double the channels here. + */ + if (this->sbc.mode == SBC_MODE_MONO) + *dst_out = convert_s16le_c1_to_c2(dst, *dst_out, dst_size); + + return res; +} + +/* Voice channel SBC, not a real A2DP codec */ +static const struct media_codec duplex_codec = { + .codec_id = A2DP_CODEC_VENDOR, + .name = "faststream_sbc", + .description = "FastStream duplex SBC", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = duplex_enum_config, + .validate_config = duplex_validate_config, + .init = duplex_init, + .deinit = duplex_deinit, + .get_block_size = duplex_get_block_size, + .abr_process = duplex_abr_process, + .start_encode = duplex_start_encode, + .encode = duplex_encode, + .start_decode = duplex_start_decode, + .decode = duplex_decode, + .reduce_bitpool = duplex_reduce_bitpool, + .increase_bitpool = duplex_increase_bitpool, +}; + +#define FASTSTREAM_COMMON_DEFS \ + .codec_id = A2DP_CODEC_VENDOR, \ + .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \ + .codec_id = FASTSTREAM_CODEC_ID }, \ + .description = "FastStream", \ + .fill_caps = codec_fill_caps, \ + .select_config = codec_select_config, \ + .enum_config = codec_enum_config, \ + .init = codec_init, \ + .deinit = codec_deinit, \ + .get_block_size = codec_get_block_size, \ + .abr_process = codec_abr_process, \ + .start_encode = codec_start_encode, \ + .encode = codec_encode, \ + .reduce_bitpool = codec_reduce_bitpool, \ + .increase_bitpool = codec_increase_bitpool + +const struct media_codec a2dp_codec_faststream = { + FASTSTREAM_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, + .name = "faststream", +}; + +static const struct spa_dict_item duplex_info_items[] = { + { "duplex.boost", "true" }, +}; +static const struct spa_dict duplex_info = SPA_DICT_INIT_ARRAY(duplex_info_items); + +const struct media_codec a2dp_codec_faststream_duplex = { + FASTSTREAM_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, + .name = "faststream_duplex", + .duplex_codec = &duplex_codec, + .info = &duplex_info, +}; + +MEDIA_CODEC_EXPORT_DEF( + "faststream", + &a2dp_codec_faststream, + &a2dp_codec_faststream_duplex +); diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c new file mode 100644 index 0000000..2896624 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c @@ -0,0 +1,790 @@ +/* Spa A2DP LC3plus HR codec + * + * Copyright © 2020 Wim Taymans + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#if __BYTE_ORDER != __LITTLE_ENDIAN +#include +#endif + +#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, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + const a2dp_lc3plus_hr_t a2dp_lc3plus_hr = { + .info = codec->vendor, + LC3PLUS_HR_INIT_FRAME_DURATION(LC3PLUS_HR_FRAME_DURATION_10MS + | LC3PLUS_HR_FRAME_DURATION_5MS + | LC3PLUS_HR_FRAME_DURATION_2_5MS) + .channels = LC3PLUS_HR_CHANNELS_1 | LC3PLUS_HR_CHANNELS_2, + LC3PLUS_HR_INIT_FREQUENCY(LC3PLUS_HR_SAMPLING_FREQ_48000 + | (lc3plus_samplerate_supported(96000) ? LC3PLUS_HR_SAMPLING_FREQ_96000 : 0)) + }; + memcpy(caps, &a2dp_lc3plus_hr, sizeof(a2dp_lc3plus_hr)); + return sizeof(a2dp_lc3plus_hr); +} + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_lc3plus_hr_t conf; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000) + && lc3plus_samplerate_supported(48000)) + LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_48000); + else if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000) + && lc3plus_samplerate_supported(96000)) + LC3PLUS_HR_SET_FREQUENCY(conf, LC3PLUS_HR_SAMPLING_FREQ_96000); + else + return -ENOTSUP; + + if ((conf.channels & LC3PLUS_HR_CHANNELS_2) && + lc3plus_channels_supported(2)) + conf.channels = LC3PLUS_HR_CHANNELS_2; + else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) && + lc3plus_channels_supported(1)) + conf.channels = LC3PLUS_HR_CHANNELS_1; + else + return -ENOTSUP; + + if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_10MS) + LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_10MS); + else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_5MS) + LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_5MS); + else if (LC3PLUS_HR_GET_FRAME_DURATION(conf) & LC3PLUS_HR_FRAME_DURATION_2_5MS) + LC3PLUS_HR_SET_FRAME_DURATION(conf, LC3PLUS_HR_FRAME_DURATION_2_5MS); + else + return -ENOTSUP; + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size, + const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings) +{ + a2dp_lc3plus_hr_t conf1, conf2; + a2dp_lc3plus_hr_t *conf; + int res1, res2; + int a, b; + + /* Order selected configurations by preference */ + res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1); + res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2); + +#define PREFER_EXPR(expr) \ + do { \ + conf = &conf1; \ + a = (expr); \ + conf = &conf2; \ + b = (expr); \ + if (a != b) \ + return b - a; \ + } while (0) + +#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0) + + /* Prefer valid */ + a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0; + b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_lc3plus_hr_t)) ? 1 : 0; + if (!a || !b) + return b - a; + + PREFER_BOOL(conf->channels & LC3PLUS_HR_CHANNELS_2); + PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & (LC3PLUS_HR_SAMPLING_FREQ_48000 | LC3PLUS_HR_SAMPLING_FREQ_96000)); + PREFER_BOOL(LC3PLUS_HR_GET_FREQUENCY(*conf) & LC3PLUS_HR_SAMPLING_FREQ_48000); + + return 0; + +#undef PREFER_EXPR +#undef PREFER_BOOL +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_lc3plus_hr_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_96000) && + lc3plus_samplerate_supported(96000)) { + if (i++ == 0) + spa_pod_builder_int(b, 96000); + spa_pod_builder_int(b, 96000); + } + if ((LC3PLUS_HR_GET_FREQUENCY(conf) & LC3PLUS_HR_SAMPLING_FREQ_48000) && + lc3plus_samplerate_supported(48000)) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if ((conf.channels & (LC3PLUS_HR_CHANNELS_2 | LC3PLUS_HR_CHANNELS_1)) && + lc3plus_channels_supported(2) && lc3plus_channels_supported(1)) { + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), + 0); + } else if ((conf.channels & LC3PLUS_HR_CHANNELS_2) && lc3plus_channels_supported(2)) { + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + } else if ((conf.channels & LC3PLUS_HR_CHANNELS_1) && lc3plus_channels_supported(1)) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + } + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + const a2dp_lc3plus_hr_t *conf; + + if (caps == NULL || caps_size < sizeof(*conf)) + return -EINVAL; + + conf = caps; + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S24_32; + + switch (LC3PLUS_HR_GET_FREQUENCY(*conf)) { + case LC3PLUS_HR_SAMPLING_FREQ_96000: + if (!lc3plus_samplerate_supported(96000)) + return -EINVAL; + info->info.raw.rate = 96000; + break; + case LC3PLUS_HR_SAMPLING_FREQ_48000: + if (!lc3plus_samplerate_supported(48000)) + return -EINVAL; + info->info.raw.rate = 48000; + break; + default: + return -EINVAL; + } + + switch (conf->channels) { + case LC3PLUS_HR_CHANNELS_2: + if (!lc3plus_channels_supported(2)) + return -EINVAL; + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + break; + case LC3PLUS_HR_CHANNELS_1: + if (!lc3plus_channels_supported(1)) + return -EINVAL; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + default: + return -EINVAL; + } + + switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) { + case LC3PLUS_HR_FRAME_DURATION_10MS: + case LC3PLUS_HR_FRAME_DURATION_5MS: + case LC3PLUS_HR_FRAME_DURATION_2_5MS: + break; + default: + return -EINVAL; + } + + return 0; +} + +static size_t ceildiv(size_t v, size_t divisor) +{ + if (v % divisor == 0) + return v / divisor; + else + return v / divisor + 1; +} + +static bool check_mtu_vs_frame_dms(struct impl *this) +{ + /* Only 10ms frames can be fragmented (max 0xf fragments); + * others must fit in single MTU */ + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + size_t max_fragments = (this->frame_dms == 100) ? 0xf : 1; + size_t payload_size = lc3plus_enc_get_num_bytes(this->enc); + return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments); +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + a2dp_lc3plus_hr_t *conf = config; + struct impl *this = NULL; + struct spa_audio_info config_info; + int size; + int res; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0) + goto error; + + this->mtu = mtu; + this->samplerate = config_info.info.raw.rate; + this->channels = config_info.info.raw.channels; + this->bitrate = BITRATE_DEFAULT * this->channels; + + switch (LC3PLUS_HR_GET_FRAME_DURATION(*conf)) { + case LC3PLUS_HR_FRAME_DURATION_10MS: + this->frame_dms = 100; + break; + case LC3PLUS_HR_FRAME_DURATION_5MS: + this->frame_dms = 50; + break; + case LC3PLUS_HR_FRAME_DURATION_2_5MS: + this->frame_dms = 25; + break; + default: + res = -EINVAL; + goto error; + } + + if ((size = lc3plus_enc_get_size(this->samplerate, this->channels)) == 0) { + res = -EIO; + goto error; + } + if ((this->enc = calloc(1, size)) == NULL) + goto error_errno; + if (lc3plus_enc_init(this->enc, this->samplerate, this->channels) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + if (lc3plus_enc_set_frame_ms(this->enc, this->frame_dms/10.0f) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + if (lc3plus_enc_set_hrmode(this->enc, 1) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + while (1) { + /* Find a valid bitrate */ + if (lc3plus_enc_set_bitrate(this->enc, this->bitrate) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + if (check_mtu_vs_frame_dms(this)) + break; + this->bitrate = this->bitrate * 3/4; + } + + if ((size = lc3plus_dec_get_size(this->samplerate, this->channels)) == 0) { + res = -EINVAL; + goto error; + } + if ((this->dec = calloc(1, size)) == NULL) + goto error_errno; + if (lc3plus_dec_init(this->dec, this->samplerate, this->channels, LC3PLUS_PLC_ADVANCED) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + if (lc3plus_dec_set_frame_ms(this->dec, this->frame_dms/10.0f) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + if (lc3plus_dec_set_hrmode(this->dec, 1) != LC3PLUS_OK) { + res = -EINVAL; + goto error; + } + + this->e.samples = lc3plus_enc_get_input_samples(this->enc); + this->e.codesize = this->e.samples * this->channels * sizeof(int32_t); + + spa_assert(this->e.samples <= LC3PLUS_MAX_SAMPLES); + + this->e.bitrate = this->bitrate; + this->e.next_bitrate = this->bitrate; + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (this && this->enc) + lc3plus_enc_free_memory(this->enc); + if (this && this->dec) + lc3plus_dec_free_memory(this->dec); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + lc3plus_enc_free_memory(this->enc); + lc3plus_dec_free_memory(this->dec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->e.codesize; +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_update_bitrate(struct impl *this) +{ + this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate, + BITRATE_MIN * this->channels, BITRATE_MAX * this->channels); + + if (this->e.next_bitrate == this->e.bitrate) + return 0; + + this->e.bitrate = this->e.next_bitrate; + + if (lc3plus_enc_set_bitrate(this->enc, this->e.bitrate) != LC3PLUS_OK || + !check_mtu_vs_frame_dms(this)) { + lc3plus_enc_set_bitrate(this->enc, this->bitrate); + return -EINVAL; + } + + this->bitrate = this->e.bitrate; + + return 0; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + if (dst_size <= header_size) + return -EINVAL; + + codec_update_bitrate(this); + + this->e.header = (struct rtp_header *)dst; + this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload); + memset(dst, 0, header_size); + + this->e.payload->frame_count = 0; + this->e.header->v = 2; + this->e.header->pt = 96; + this->e.header->sequence_number = htons(seqnum); + this->e.header->timestamp = htonl(timestamp); + this->e.header->ssrc = htonl(1); + + this->e.packet_size = header_size; + return this->e.packet_size; +} + +static void deinterleave_32_c2(int32_t * SPA_RESTRICT * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT src, size_t n_samples) +{ + /* We'll trust the compiler to optimize this */ + const size_t n_channels = 2; + size_t i, j; + for (j = 0; j < n_samples; ++j) + for (i = 0; i < n_channels; ++i) + dst[i][j] = *src++; +} + +static void interleave_32_c2(int32_t * SPA_RESTRICT dst, const int32_t * SPA_RESTRICT * SPA_RESTRICT src, size_t n_samples) +{ + const size_t n_channels = 2; + size_t i, j; + for (j = 0; j < n_samples; ++j) + for (i = 0; i < n_channels; ++i) + *dst++ = src[i][j]; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int frame_bytes; + LC3PLUS_Error res; + int size, processed; + int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + int32_t *inputs[2]; + + if (src == NULL) { + /* Produce fragment packets. + * + * We assume the caller gives the same buffer here as in the previous + * calls to encode(), without changes in the buffer content. + */ + if (this->e.fragment == NULL || + this->e.fragment_count <= 1 || + this->e.fragment < dst || + SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) { + this->e.fragment = NULL; + return -EINVAL; + } + + size = SPA_MIN(this->mtu - header_size, this->e.fragment_size); + memmove(dst, this->e.fragment, size); + *dst_out = size; + + this->e.payload->is_fragmented = 1; + this->e.payload->frame_count = --this->e.fragment_count; + this->e.payload->is_last_fragment = (this->e.fragment_count == 1); + + if (this->e.fragment_size > size && this->e.fragment_count > 1) { + this->e.fragment = SPA_PTROFF(this->e.fragment, size, void); + this->e.fragment_size -= size; + *need_flush = NEED_FLUSH_FRAGMENT; + } else { + this->e.fragment = NULL; + *need_flush = NEED_FLUSH_ALL; + } + return 0; + } + + frame_bytes = lc3plus_enc_get_num_bytes(this->enc); + processed = 0; + + if (src_size < (size_t)this->e.codesize) + goto done; + if (dst_size < (size_t)frame_bytes) + goto done; + if (this->e.payload->frame_count > 0 && + this->e.packet_size + frame_bytes > this->mtu) + goto done; + + if (this->channels == 1) { + inputs[0] = (int32_t *)src; + res = lc3plus_enc24(this->enc, inputs, dst, &size); + } else { + inputs[0] = this->buf[0]; + inputs[1] = this->buf[1]; + deinterleave_32_c2(inputs, src, this->e.samples); + res = lc3plus_enc24(this->enc, inputs, dst, &size); + } + if (SPA_UNLIKELY(res != LC3PLUS_OK)) + return -EINVAL; + *dst_out = size; + + processed += this->e.codesize; + this->e.packet_size += size; + this->e.payload->frame_count++; + +done: + if (this->e.payload->frame_count == 0) + return processed; + if (this->e.payload->frame_count < 0xf && + this->frame_dms * (this->e.payload->frame_count + 1) < 200 && + this->e.packet_size + frame_bytes <= this->mtu) + return processed; /* add another frame */ + + if (this->e.packet_size > this->mtu) { + /* Fragment packet */ + spa_assert(this->e.payload->frame_count == 1); + spa_assert(this->frame_dms == 100); + + this->e.fragment_count = ceildiv(this->e.packet_size - header_size, + this->mtu - header_size); + + this->e.payload->is_fragmented = 1; + this->e.payload->is_first_fragment = 1; + this->e.payload->frame_count = this->e.fragment_count; + + this->e.fragment_size = this->e.packet_size - this->mtu; + this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void); + *need_flush = NEED_FLUSH_FRAGMENT; + + /* + * We keep the rest of the encoded frame in the same buffer, and rely + * that the caller won't overwrite it before the next call to encode() + */ + *dst_out = SPA_PTRDIFF(this->e.fragment, dst); + } else { + *need_flush = NEED_FLUSH_ALL; + } + + return processed; +} + +static SPA_UNUSED int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + const struct rtp_header *header = src; + const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void); + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + spa_return_val_if_fail (src_size > header_size, -EINVAL); + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + + if (payload->is_fragmented) { + if (payload->is_first_fragment) { + this->d.fragment_size = 0; + } else if (payload->frame_count + 1 != this->d.fragment_count || + (payload->frame_count == 1 && !payload->is_last_fragment)){ + /* Fragments not in right order: drop packet */ + return -EINVAL; + } + this->d.fragment_count = payload->frame_count; + this->d.frame_size = src_size - header_size; + } else { + if (payload->frame_count <= 0) + return -EINVAL; + this->d.fragment_count = 0; + this->d.frame_size = (src_size - header_size) / payload->frame_count; + if (this->d.frame_size <= 0) + return -EINVAL; + } + + return header_size; +} + +static SPA_UNUSED int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + LC3PLUS_Error res; + int32_t *outputs[2]; + int consumed; + int samples; + + if (this->d.fragment_count > 0) { + /* Fragmented frame */ + size_t avail; + avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size); + memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail); + + this->d.fragment_size += avail; + consumed = src_size; + + if (this->d.fragment_count > 1) { + /* More fragments to come */ + *dst_out = 0; + return consumed; + } + + src = this->d.fragment; + src_size = this->d.fragment_size; + + this->d.fragment_count = 0; + this->d.fragment_size = 0; + } else { + src_size = SPA_MIN((size_t)this->d.frame_size, src_size); + consumed = src_size; + } + + samples = lc3plus_dec_get_output_samples(this->dec); + *dst_out = samples * this->channels * sizeof(int32_t); + if (dst_size < *dst_out) + return -EINVAL; + + if (this->channels == 1) { + outputs[0] = (int32_t *)dst; + res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0); + } else { + outputs[0] = this->buf[0]; + outputs[1] = this->buf[1]; + res = lc3plus_dec24(this->dec, (void *)src, src_size, outputs, 0); + interleave_32_c2(dst, (const int32_t**)outputs, samples); + } + if (SPA_UNLIKELY(res != LC3PLUS_OK && res != LC3PLUS_DECODE_ERROR)) + return -EINVAL; + + return consumed; +} + +static int codec_reduce_bitpool(void *data) +{ + struct impl *this = data; + this->e.next_bitrate = SPA_CLAMP(this->bitrate * 3 / 4, + BITRATE_MIN * this->channels, BITRATE_MAX * this->channels); + return this->e.next_bitrate; +} + +static int codec_increase_bitpool(void *data) +{ + struct impl *this = data; + this->e.next_bitrate = SPA_CLAMP(this->bitrate * 5 / 4, + BITRATE_MIN * this->channels, BITRATE_MAX * this->channels); + return this->e.next_bitrate; +} + +const struct media_codec a2dp_codec_lc3plus_hr = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, + .name = "lc3plus_hr", + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = LC3PLUS_HR_VENDOR_ID, + .codec_id = LC3PLUS_HR_CODEC_ID }, + .description = "LC3plus HR", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .caps_preference_cmp = codec_caps_preference_cmp, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool +}; + +MEDIA_CODEC_EXPORT_DEF( + "lc3plus", + &a2dp_codec_lc3plus_hr +); diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c new file mode 100644 index 0000000..624dd4a --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-ldac.c @@ -0,0 +1,604 @@ +/* Spa A2DP LDAC codec + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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, uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + static const a2dp_ldac_t a2dp_ldac = { + .info.vendor_id = LDAC_VENDOR_ID, + .info.codec_id = LDAC_CODEC_ID, + .frequency = LDACBT_SAMPLING_FREQ_044100 | + LDACBT_SAMPLING_FREQ_048000 | + LDACBT_SAMPLING_FREQ_088200 | + LDACBT_SAMPLING_FREQ_096000, + .channel_mode = LDACBT_CHANNEL_MODE_MONO | + LDACBT_CHANNEL_MODE_DUAL_CHANNEL | + LDACBT_CHANNEL_MODE_STEREO, + }; + + memcpy(caps, &a2dp_ldac, sizeof(a2dp_ldac)); + return sizeof(a2dp_ldac); +} + +static const struct media_codec_config +ldac_frequencies[] = { + { LDACBT_SAMPLING_FREQ_044100, 44100, 3 }, + { LDACBT_SAMPLING_FREQ_048000, 48000, 2 }, + { LDACBT_SAMPLING_FREQ_088200, 88200, 1 }, + { LDACBT_SAMPLING_FREQ_096000, 96000, 0 }, +}; + +static const struct media_codec_config +ldac_channel_modes[] = { + { LDACBT_CHANNEL_MODE_STEREO, 2, 2 }, + { LDACBT_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 }, + { LDACBT_CHANNEL_MODE_MONO, 1, 0 }, +}; + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_ldac_t conf; + int i; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + if ((i = media_codec_select_config(ldac_frequencies, + SPA_N_ELEMENTS(ldac_frequencies), + conf.frequency, + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + conf.frequency = ldac_frequencies[i].config; + + if ((i = media_codec_select_config(ldac_channel_modes, + SPA_N_ELEMENTS(ldac_channel_modes), + conf.channel_mode, + info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS + )) < 0) + return -ENOTSUP; + conf.channel_mode = ldac_channel_modes[i].config; + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_ldac_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t i = 0; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(5, + SPA_AUDIO_FORMAT_F32, + SPA_AUDIO_FORMAT_F32, + SPA_AUDIO_FORMAT_S32, + SPA_AUDIO_FORMAT_S24, + SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.frequency & LDACBT_SAMPLING_FREQ_048000) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.frequency & LDACBT_SAMPLING_FREQ_044100) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } + if (conf.frequency & LDACBT_SAMPLING_FREQ_088200) { + if (i++ == 0) + spa_pod_builder_int(b, 88200); + spa_pod_builder_int(b, 88200); + } + if (conf.frequency & LDACBT_SAMPLING_FREQ_096000) { + if (i++ == 0) + spa_pod_builder_int(b, 96000); + spa_pod_builder_int(b, 96000); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO && + conf.channel_mode & (LDACBT_CHANNEL_MODE_STEREO | + LDACBT_CHANNEL_MODE_DUAL_CHANNEL)) { + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), + 0); + } else if (conf.channel_mode & LDACBT_CHANNEL_MODE_MONO) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + } else { + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + } + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_reduce_bitpool(void *data) +{ +#ifdef ENABLE_LDAC_ABR + return -ENOTSUP; +#else + struct impl *this = data; + int res; + if (this->eqmid == LDACBT_EQMID_MQ || !this->enable_abr) + return this->eqmid; + res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_CONNECTION); + return res; +#endif +} + +static int codec_increase_bitpool(void *data) +{ +#ifdef ENABLE_LDAC_ABR + return -ENOTSUP; +#else + struct impl *this = data; + int res; + if (!this->enable_abr) + return this->eqmid; + res = ldacBT_alter_eqmid_priority(this->ldac, LDACBT_EQMID_INC_QUALITY); + return res; +#endif +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static int string_to_eqmid(const char * eqmid) +{ + if (spa_streq("auto", eqmid)) + return LDACBT_EQMID_AUTO; + else if (spa_streq("hq", eqmid)) + return LDACBT_EQMID_HQ; + else if (spa_streq("sq", eqmid)) + return LDACBT_EQMID_SQ; + else if (spa_streq("mq", eqmid)) + return LDACBT_EQMID_MQ; + else + return LDACBT_EQMID_AUTO; +} + +static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings) +{ + struct props *p = calloc(1, sizeof(struct props)); + const char *str; + + if (p == NULL) + return NULL; + + if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.ldac.quality")) == NULL) + str = "auto"; + + p->eqmid = string_to_eqmid(str); + return p; +} + +static void codec_clear_props(void *props) +{ + free(props); +} + +static int codec_enum_props(void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct props *p = props; + struct spa_pod_frame f[2]; + switch (id) { + case SPA_PARAM_PropInfo: + { + switch (idx) { + case 0: + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0); + spa_pod_builder_id(b, SPA_PROP_quality); + spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0); + spa_pod_builder_string(b, "LDAC quality"); + + spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0); + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + spa_pod_builder_int(b, p->eqmid); + spa_pod_builder_int(b, LDACBT_EQMID_AUTO); + spa_pod_builder_int(b, LDACBT_EQMID_HQ); + spa_pod_builder_int(b, LDACBT_EQMID_SQ); + spa_pod_builder_int(b, LDACBT_EQMID_MQ); + spa_pod_builder_pop(b, &f[1]); + + spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(b, &f[1]); + spa_pod_builder_int(b, LDACBT_EQMID_AUTO); + spa_pod_builder_string(b, "auto"); + spa_pod_builder_int(b, LDACBT_EQMID_HQ); + spa_pod_builder_string(b, "hq"); + spa_pod_builder_int(b, LDACBT_EQMID_SQ); + spa_pod_builder_string(b, "sq"); + spa_pod_builder_int(b, LDACBT_EQMID_MQ); + spa_pod_builder_string(b, "mq"); + spa_pod_builder_pop(b, &f[1]); + + *param = spa_pod_builder_pop(b, &f[0]); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + switch (idx) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_quality, SPA_POD_Int(p->eqmid)); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + return 1; +} + +static int codec_set_props(void *props, const struct spa_pod *param) +{ + struct props *p = props; + const int prev_eqmid = p->eqmid; + if (param == NULL) { + p->eqmid = LDACBT_EQMID_AUTO; + } else { + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_quality, SPA_POD_OPT_Int(&p->eqmid)); + if (p->eqmid != LDACBT_EQMID_AUTO && + (p->eqmid < LDACBT_EQMID_HQ || p->eqmid > LDACBT_EQMID_MQ)) + p->eqmid = prev_eqmid; + } + + return prev_eqmid != p->eqmid; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this; + a2dp_ldac_t *conf = config; + int res; + struct props *p = props; + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) + goto error_errno; + + this->ldac = ldacBT_get_handle(); + if (this->ldac == NULL) + goto error_errno; + +#ifdef ENABLE_LDAC_ABR + this->ldac_abr = ldac_ABR_get_handle(); + if (this->ldac_abr == NULL) + goto error_errno; +#endif + + if (p == NULL || p->eqmid == LDACBT_EQMID_AUTO) { + this->eqmid = LDACBT_EQMID_SQ; + this->enable_abr = true; + } else { + this->eqmid = p->eqmid; + this->enable_abr = false; + } + + this->mtu = mtu; + this->frequency = info->info.raw.rate; + this->codesize = info->info.raw.channels * LDACBT_ENC_LSU; + + switch (info->info.raw.format) { + case SPA_AUDIO_FORMAT_F32: + this->fmt = LDACBT_SMPL_FMT_F32; + this->codesize *= 4; + break; + case SPA_AUDIO_FORMAT_S32: + this->fmt = LDACBT_SMPL_FMT_S32; + this->codesize *= 4; + break; + case SPA_AUDIO_FORMAT_S24: + this->fmt = LDACBT_SMPL_FMT_S24; + this->codesize *= 3; + break; + case SPA_AUDIO_FORMAT_S16: + this->fmt = LDACBT_SMPL_FMT_S16; + this->codesize *= 2; + break; + default: + res = -EINVAL; + goto error; + } + + res = ldacBT_init_handle_encode(this->ldac, + this->mtu, + this->eqmid, + conf->channel_mode, + this->fmt, + this->frequency); + if (res < 0) + goto error; + +#ifdef ENABLE_LDAC_ABR + res = ldac_ABR_Init(this->ldac_abr, LDAC_ABR_INTERVAL_MS); + if (res < 0) + goto error; + + res = ldac_ABR_set_thresholds(this->ldac_abr, + LDAC_ABR_THRESHOLD_CRITICAL, + LDAC_ABR_THRESHOLD_DANGEROUSTREND, + LDAC_ABR_THRESHOLD_SAFETY_FOR_HQSQ); + if (res < 0) + goto error; +#endif + + return this; + +error_errno: + res = -errno; +error: + if (this && this->ldac) + ldacBT_free_handle(this->ldac); +#ifdef ENABLE_LDAC_ABR + if (this && this->ldac_abr) + ldac_ABR_free_handle(this->ldac_abr); +#endif + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + if (this->ldac) + ldacBT_free_handle(this->ldac); +#ifdef ENABLE_LDAC_ABR + if (this->ldac_abr) + ldac_ABR_free_handle(this->ldac_abr); +#endif + free(this); +} + +static int codec_update_props(void *data, void *props) +{ + struct impl *this = data; + struct props *p = props; + int res; + + if (p == NULL) + return 0; + + if (p->eqmid == LDACBT_EQMID_AUTO) { + this->eqmid = LDACBT_EQMID_SQ; + this->enable_abr = true; + } else { + this->eqmid = p->eqmid; + this->enable_abr = false; + } + + if ((res = ldacBT_set_eqmid(this->ldac, this->eqmid)) < 0) + goto error; + return 0; +error: + return res; +} + +static int codec_abr_process(void *data, size_t unsent) +{ +#ifdef ENABLE_LDAC_ABR + struct impl *this = data; + int res; + res = ldac_ABR_Proc(this->ldac, this->ldac_abr, + unsent / LDAC_ABR_MAX_PACKET_NBYTES, this->enable_abr); + return res; +#else + return -ENOTSUP; +#endif +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->header = (struct rtp_header *)dst; + this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload); + memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload)); + + this->payload->frame_count = 0; + this->header->v = 2; + this->header->pt = 96; + this->header->sequence_number = htons(seqnum); + this->header->timestamp = htonl(timestamp); + this->header->ssrc = htonl(1); + return sizeof(struct rtp_header) + sizeof(struct rtp_payload); +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res, src_used, dst_used, frame_num = 0; + + src_used = src_size; + dst_used = dst_size; + + res = ldacBT_encode(this->ldac, (void*)src, &src_used, dst, &dst_used, &frame_num); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + + *dst_out = dst_used; + + this->payload->frame_count += frame_num; + *need_flush = (this->payload->frame_count > 0) ? NEED_FLUSH_ALL : NEED_FLUSH_NO; + + return src_used; +} + +const struct media_codec a2dp_codec_ldac = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + .codec_id = A2DP_CODEC_VENDOR, + .vendor = { .vendor_id = LDAC_VENDOR_ID, + .codec_id = LDAC_CODEC_ID }, + .name = "ldac", + .description = "LDAC", +#ifdef ENABLE_LDAC_ABR + .send_buf_size = LDAC_ABR_SOCK_BUFFER_SIZE, +#endif + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .init_props = codec_init_props, + .enum_props = codec_enum_props, + .set_props = codec_set_props, + .clear_props = codec_clear_props, + .init = codec_init, + .deinit = codec_deinit, + .update_props = codec_update_props, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, +}; + +MEDIA_CODEC_EXPORT_DEF( + "ldac", + &a2dp_codec_ldac +); diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c new file mode 100644 index 0000000..32ae290 --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-opus.c @@ -0,0 +1,1444 @@ +/* Spa A2DP Opus Codec + * + * Copyright © 2020 Wim Taymans + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#if __BYTE_ORDER != __LITTLE_ENDIAN +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rtp.h" +#include "media-codecs.h" + +static struct spa_log *log; +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs.opus"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define BUFSIZE_FROM_BITRATE(frame_dms,bitrate) ((bitrate)/8 * (frame_dms) / 10000 * 5/4) /* estimate */ + +/* + * Opus CVBR target bitrate. When connecting, it is set to the INITIAL + * value, and after that adjusted according to link quality between the MIN and + * MAX values. The bitrate adjusts up to either MAX or the value at + * which the socket buffer starts filling up, whichever is lower. + * + * With perfect connection quality, the target bitrate converges to the MAX + * value. Under realistic conditions, the upper limit may often be as low as + * 300-500kbit/s, so the INITIAL values are not higher than this. + * + * The MAX is here set to 2-2.5x and INITIAL to 1.5x the upper Opus recommended + * values [1], to be safer quality-wise for CVBR, and MIN to the lower + * recommended value. + * + * [1] https://wiki.xiph.org/Opus_Recommended_Settings + */ +#define BITRATE_INITIAL 192000 +#define BITRATE_MAX 320000 +#define BITRATE_MIN 96000 + +#define BITRATE_INITIAL_51 384000 +#define BITRATE_MAX_51 600000 +#define BITRATE_MIN_51 128000 + +#define BITRATE_INITIAL_71 450000 +#define BITRATE_MAX_71 900000 +#define BITRATE_MIN_71 256000 + +#define BITRATE_DUPLEX_BIDI 160000 + +#define OPUS_05_MAX_BYTES (15 * 1024) + +struct props { + uint32_t channels; + uint32_t coupled_streams; + uint32_t location; + uint32_t max_bitrate; + uint8_t frame_duration; + int application; + + uint32_t bidi_channels; + uint32_t bidi_coupled_streams; + uint32_t bidi_location; + uint32_t bidi_max_bitrate; + uint32_t bidi_frame_duration; + int bidi_application; +}; + +struct dec_data { + int fragment_size; + int fragment_count; + uint8_t fragment[OPUS_05_MAX_BYTES]; +}; + +struct abr { + uint64_t now; + uint64_t last_update; + + uint32_t buffer_level; + uint32_t packet_size; + uint32_t total_size; + bool bad; + + uint64_t last_change; + uint64_t retry_interval; + + bool prev_bad; +}; + +struct enc_data { + struct rtp_header *header; + struct rtp_payload *payload; + + struct abr abr; + + int samples; + int codesize; + + int packet_size; + int fragment_size; + int fragment_count; + void *fragment; + + int bitrate_min; + int bitrate_max; + + int bitrate; + int next_bitrate; + + int frame_dms; + int application; +}; + +struct impl { + OpusMSEncoder *enc; + OpusMSDecoder *dec; + + int mtu; + int samplerate; + int application; + + uint8_t channels; + uint8_t streams; + uint8_t coupled_streams; + + bool is_bidi; + + struct dec_data d; + struct enc_data e; +}; + +struct audio_location { + uint32_t mask; + enum spa_audio_channel position; +}; + +struct surround_encoder_mapping { + uint8_t channels; + uint8_t coupled_streams; + uint32_t location; + uint8_t mapping[8]; /**< permutation streams -> vorbis order */ + uint8_t inv_mapping[8]; /**< permutation vorbis order -> streams */ +}; + +/* Bluetooth SIG, Assigned Numbers, Generic Audio, Audio Location Definitions */ +#define BT_AUDIO_LOCATION_FL 0x00000001 /* Front Left */ +#define BT_AUDIO_LOCATION_FR 0x00000002 /* Front Right */ +#define BT_AUDIO_LOCATION_FC 0x00000004 /* Front Center */ +#define BT_AUDIO_LOCATION_LFE 0x00000008 /* Low Frequency Effects 1 */ +#define BT_AUDIO_LOCATION_RL 0x00000010 /* Back Left */ +#define BT_AUDIO_LOCATION_RR 0x00000020 /* Back Right */ +#define BT_AUDIO_LOCATION_FLC 0x00000040 /* Front Left of Center */ +#define BT_AUDIO_LOCATION_FRC 0x00000080 /* Front Right of Center */ +#define BT_AUDIO_LOCATION_RC 0x00000100 /* Back Center */ +#define BT_AUDIO_LOCATION_LFE2 0x00000200 /* Low Frequency Effects 2 */ +#define BT_AUDIO_LOCATION_SL 0x00000400 /* Side Left */ +#define BT_AUDIO_LOCATION_SR 0x00000800 /* Side Right */ +#define BT_AUDIO_LOCATION_TFL 0x00001000 /* Top Front Left */ +#define BT_AUDIO_LOCATION_TFR 0x00002000 /* Top Front Right */ +#define BT_AUDIO_LOCATION_TFC 0x00004000 /* Top Front Center */ +#define BT_AUDIO_LOCATION_TC 0x00008000 /* Top Center */ +#define BT_AUDIO_LOCATION_TRL 0x00010000 /* Top Back Left */ +#define BT_AUDIO_LOCATION_TRR 0x00020000 /* Top Back Right */ +#define BT_AUDIO_LOCATION_TSL 0x00040000 /* Top Side Left */ +#define BT_AUDIO_LOCATION_TSR 0x00080000 /* Top Side Right */ +#define BT_AUDIO_LOCATION_TRC 0x00100000 /* Top Back Center */ +#define BT_AUDIO_LOCATION_BC 0x00200000 /* Bottom Front Center */ +#define BT_AUDIO_LOCATION_BLC 0x00400000 /* Bottom Front Left */ +#define BT_AUDIO_LOCATION_BRC 0x00800000 /* Bottom Front Right */ +#define BT_AUDIO_LOCATION_FLW 0x01000000 /* Fron Left Wide */ +#define BT_AUDIO_LOCATION_FRW 0x02000000 /* Front Right Wide */ +#define BT_AUDIO_LOCATION_SSL 0x04000000 /* Left Surround */ +#define BT_AUDIO_LOCATION_SSR 0x08000000 /* Right Surround */ + +#define BT_AUDIO_LOCATION_ANY 0x0fffffff + +static const struct audio_location audio_locations[] = { + { BT_AUDIO_LOCATION_FL, SPA_AUDIO_CHANNEL_FL }, + { BT_AUDIO_LOCATION_FR, SPA_AUDIO_CHANNEL_FR }, + { BT_AUDIO_LOCATION_SL, SPA_AUDIO_CHANNEL_SL }, + { BT_AUDIO_LOCATION_SR, SPA_AUDIO_CHANNEL_SR }, + { BT_AUDIO_LOCATION_RL, SPA_AUDIO_CHANNEL_RL }, + { BT_AUDIO_LOCATION_RR, SPA_AUDIO_CHANNEL_RR }, + { BT_AUDIO_LOCATION_FLC, SPA_AUDIO_CHANNEL_FLC }, + { BT_AUDIO_LOCATION_FRC, SPA_AUDIO_CHANNEL_FRC }, + { BT_AUDIO_LOCATION_TFL, SPA_AUDIO_CHANNEL_TFL }, + { BT_AUDIO_LOCATION_TFR, SPA_AUDIO_CHANNEL_TFR }, + { BT_AUDIO_LOCATION_TSL, SPA_AUDIO_CHANNEL_TSL }, + { BT_AUDIO_LOCATION_TSR, SPA_AUDIO_CHANNEL_TSR }, + { BT_AUDIO_LOCATION_TRL, SPA_AUDIO_CHANNEL_TRL }, + { BT_AUDIO_LOCATION_TRR, SPA_AUDIO_CHANNEL_TRR }, + { BT_AUDIO_LOCATION_BLC, SPA_AUDIO_CHANNEL_BLC }, + { BT_AUDIO_LOCATION_BRC, SPA_AUDIO_CHANNEL_BRC }, + { BT_AUDIO_LOCATION_FLW, SPA_AUDIO_CHANNEL_FLW }, + { BT_AUDIO_LOCATION_FRW, SPA_AUDIO_CHANNEL_FRW }, + { BT_AUDIO_LOCATION_SSL, SPA_AUDIO_CHANNEL_SL }, /* ~ Side Left */ + { BT_AUDIO_LOCATION_SSR, SPA_AUDIO_CHANNEL_SR }, /* ~ Side Right */ + { BT_AUDIO_LOCATION_FC, SPA_AUDIO_CHANNEL_FC }, + { BT_AUDIO_LOCATION_RC, SPA_AUDIO_CHANNEL_RC }, + { BT_AUDIO_LOCATION_TFC, SPA_AUDIO_CHANNEL_TFC }, + { BT_AUDIO_LOCATION_TC, SPA_AUDIO_CHANNEL_TC }, + { BT_AUDIO_LOCATION_TRC, SPA_AUDIO_CHANNEL_TRC }, + { BT_AUDIO_LOCATION_BC, SPA_AUDIO_CHANNEL_BC }, + { BT_AUDIO_LOCATION_LFE, SPA_AUDIO_CHANNEL_LFE }, + { BT_AUDIO_LOCATION_LFE2, SPA_AUDIO_CHANNEL_LFE2 }, +}; + +/* Opus surround encoder mapping tables for the supported channel configurations */ +static const struct surround_encoder_mapping surround_encoders[] = { + { 1, 0, (0x0), + { 0 }, { 0 } }, + { 2, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR), + { 0, 1 }, { 0, 1 } }, + { 3, 1, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_FC), + { 0, 2, 1 }, { 0, 2, 1 } }, + { 4, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL | + BT_AUDIO_LOCATION_RR), + { 0, 1, 2, 3 }, { 0, 1, 2, 3 } }, + { 5, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL | + BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC), + { 0, 4, 1, 2, 3 }, { 0, 2, 3, 4, 1 } }, + { 6, 2, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_RL | + BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC | + BT_AUDIO_LOCATION_LFE), + { 0, 4, 1, 2, 3, 5 }, { 0, 2, 3, 4, 1, 5 } }, + { 7, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL | + BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_FC | + BT_AUDIO_LOCATION_RC | BT_AUDIO_LOCATION_LFE), + { 0, 4, 1, 2, 3, 5, 6 }, { 0, 2, 3, 4, 1, 5, 6 } }, + { 8, 3, (BT_AUDIO_LOCATION_FL | BT_AUDIO_LOCATION_FR | BT_AUDIO_LOCATION_SL | + BT_AUDIO_LOCATION_SR | BT_AUDIO_LOCATION_RL | + BT_AUDIO_LOCATION_RR | BT_AUDIO_LOCATION_FC | + BT_AUDIO_LOCATION_LFE), + { 0, 6, 1, 2, 3, 4, 5, 7 }, { 0, 2, 3, 4, 5, 6, 1, 7 } }, +}; + +static uint32_t bt_channel_from_name(const char *name) +{ + size_t i; + enum spa_audio_channel position = SPA_AUDIO_CHANNEL_UNKNOWN; + + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) { + position = spa_type_audio_channel[i].type; + break; + } + } + for (i = 0; i < SPA_N_ELEMENTS(audio_locations); i++) { + if (position == audio_locations[i].position) + return audio_locations[i].mask; + } + return 0; +} + +static uint32_t parse_locations(const char *str) +{ + char *s, *p, *save = NULL; + uint32_t location = 0; + + if (!str) + return 0; + + s = strdup(str); + if (s == NULL) + return 0; + + for (p = s; (p = strtok_r(p, ", ", &save)) != NULL; p = NULL) { + if (*p == '\0') + continue; + location |= bt_channel_from_name(p); + } + free(s); + + return location; +} + +static void parse_settings(struct props *props, const struct spa_dict *settings) +{ + const char *str; + uint32_t v; + + /* Pro Audio settings */ + spa_zero(*props); + props->channels = 8; + props->coupled_streams = 0; + props->location = 0; + props->max_bitrate = BITRATE_MAX; + props->frame_duration = OPUS_05_FRAME_DURATION_100; + props->application = OPUS_APPLICATION_AUDIO; + + props->bidi_channels = 1; + props->bidi_coupled_streams = 0; + props->bidi_location = 0; + props->bidi_max_bitrate = BITRATE_DUPLEX_BIDI; + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400; + props->bidi_application = OPUS_APPLICATION_AUDIO; + + if (settings == NULL) + return; + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.channels"), &v, 0)) + props->channels = SPA_CLAMP(v, 1u, SPA_AUDIO_MAX_CHANNELS); + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.max-bitrate"), &v, 0)) + props->max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN); + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.coupled-streams"), &v, 0)) + props->coupled_streams = SPA_CLAMP(v, 0u, props->channels / 2); + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.channels"), &v, 0)) + props->bidi_channels = SPA_CLAMP(v, 0u, SPA_AUDIO_MAX_CHANNELS); + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.max-bitrate"), &v, 0)) + props->bidi_max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN); + if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.coupled-streams"), &v, 0)) + props->bidi_coupled_streams = SPA_CLAMP(v, 0u, props->bidi_channels / 2); + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.locations"); + props->location = parse_locations(str); + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.locations"); + props->bidi_location = parse_locations(str); + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.frame-dms"); + if (spa_streq(str, "25")) + props->frame_duration = OPUS_05_FRAME_DURATION_25; + else if (spa_streq(str, "50")) + props->frame_duration = OPUS_05_FRAME_DURATION_50; + else if (spa_streq(str, "100")) + props->frame_duration = OPUS_05_FRAME_DURATION_100; + else if (spa_streq(str, "200")) + props->frame_duration = OPUS_05_FRAME_DURATION_200; + else if (spa_streq(str, "400")) + props->frame_duration = OPUS_05_FRAME_DURATION_400; + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.frame-dms"); + if (spa_streq(str, "25")) + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_25; + else if (spa_streq(str, "50")) + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_50; + else if (spa_streq(str, "100")) + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_100; + else if (spa_streq(str, "200")) + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_200; + else if (spa_streq(str, "400")) + props->bidi_frame_duration = OPUS_05_FRAME_DURATION_400; + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.application"); + if (spa_streq(str, "audio")) + props->application = OPUS_APPLICATION_AUDIO; + else if (spa_streq(str, "voip")) + props->application = OPUS_APPLICATION_VOIP; + else if (spa_streq(str, "lowdelay")) + props->application = OPUS_APPLICATION_RESTRICTED_LOWDELAY; + + + str = spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.application"); + if (spa_streq(str, "audio")) + props->bidi_application = OPUS_APPLICATION_AUDIO; + else if (spa_streq(str, "voip")) + props->bidi_application = OPUS_APPLICATION_VOIP; + else if (spa_streq(str, "lowdelay")) + props->bidi_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY; +} + +static int set_channel_conf(const struct media_codec *codec, a2dp_opus_05_t *caps, const struct props *props) +{ + /* + * Predefined codec profiles + */ + if (caps->main.channels < 1) + return -EINVAL; + + caps->main.coupled_streams = 0; + OPUS_05_SET_LOCATION(caps->main, 0); + + caps->bidi.coupled_streams = 0; + OPUS_05_SET_LOCATION(caps->bidi, 0); + + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05: + caps->main.channels = SPA_MIN(2, caps->main.channels); + if (caps->main.channels == 2) { + caps->main.coupled_streams = surround_encoders[1].coupled_streams; + OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location); + } + caps->bidi.channels = 0; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51: + if (caps->main.channels < 6) + return -EINVAL; + caps->main.channels = surround_encoders[5].channels; + caps->main.coupled_streams = surround_encoders[5].coupled_streams; + OPUS_05_SET_LOCATION(caps->main, surround_encoders[5].location); + caps->bidi.channels = 0; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71: + if (caps->main.channels < 8) + return -EINVAL; + caps->main.channels = surround_encoders[7].channels; + caps->main.coupled_streams = surround_encoders[7].coupled_streams; + OPUS_05_SET_LOCATION(caps->main, surround_encoders[7].location); + caps->bidi.channels = 0; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX: + if (caps->bidi.channels < 1) + return -EINVAL; + caps->main.channels = SPA_MIN(2, caps->main.channels); + if (caps->main.channels == 2) { + caps->main.coupled_streams = surround_encoders[1].coupled_streams; + OPUS_05_SET_LOCATION(caps->main, surround_encoders[1].location); + } + caps->bidi.channels = SPA_MIN(2, caps->bidi.channels); + if (caps->bidi.channels == 2) { + caps->bidi.coupled_streams = surround_encoders[1].coupled_streams; + OPUS_05_SET_LOCATION(caps->bidi, surround_encoders[1].location); + } + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO: + if (caps->main.channels < props->channels) + return -EINVAL; + if (props->bidi_channels == 0 && caps->bidi.channels != 0) + return -EINVAL; + if (caps->bidi.channels < props->bidi_channels) + return -EINVAL; + caps->main.channels = props->channels; + caps->main.coupled_streams = props->coupled_streams; + OPUS_05_SET_LOCATION(caps->main, props->location); + caps->bidi.channels = props->bidi_channels; + caps->bidi.coupled_streams = props->bidi_coupled_streams; + OPUS_05_SET_LOCATION(caps->bidi, props->bidi_location); + break; + default: + spa_assert(false); + }; + + return 0; +} + +static void get_default_bitrates(const struct media_codec *codec, bool bidi, int *min, int *max, int *init) +{ + int tmp; + + if (min == NULL) + min = &tmp; + if (max == NULL) + max = &tmp; + if (init == NULL) + init = &tmp; + + if (bidi) { + *min = SPA_MIN(BITRATE_MIN, BITRATE_DUPLEX_BIDI); + *max = BITRATE_DUPLEX_BIDI; + *init = BITRATE_DUPLEX_BIDI; + return; + } + + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05: + *min = BITRATE_MIN; + *max = BITRATE_MAX; + *init = BITRATE_INITIAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51: + *min = BITRATE_MIN_51; + *max = BITRATE_MAX_51; + *init = BITRATE_INITIAL_51; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71: + *min = BITRATE_MIN_71; + *max = BITRATE_MAX_71; + *init = BITRATE_INITIAL_71; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX: + *min = BITRATE_MIN; + *max = BITRATE_MAX; + *init = BITRATE_INITIAL; + break; + case SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO: + default: + spa_assert_not_reached(); + }; +} + +static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direction_t *conf, + bool use_surround_encoder, uint8_t *streams_ret, uint8_t *coupled_streams_ret, + const uint8_t **surround_mapping, uint32_t *positions) +{ + const uint8_t channels = conf->channels; + const uint32_t location = OPUS_05_GET_LOCATION(*conf); + const uint8_t coupled_streams = conf->coupled_streams; + const uint8_t *permutation = NULL; + size_t i, j; + + if (channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + if (2 * coupled_streams > channels) + return -EINVAL; + + if (streams_ret) + *streams_ret = channels - coupled_streams; + if (coupled_streams_ret) + *coupled_streams_ret = coupled_streams; + + if (channels == 0) + return 0; + + if (use_surround_encoder) { + /* Opus surround encoder supports only some channel configurations, and + * needs a specific input channel ordering */ + for (i = 0; i < SPA_N_ELEMENTS(surround_encoders); ++i) { + const struct surround_encoder_mapping *m = &surround_encoders[i]; + + if (m->channels == channels && + m->coupled_streams == coupled_streams && + m->location == location) + { + spa_assert(channels <= SPA_N_ELEMENTS(m->inv_mapping)); + permutation = m->inv_mapping; + if (surround_mapping) + *surround_mapping = m->mapping; + break; + } + } + if (permutation == NULL && surround_mapping) + *surround_mapping = NULL; + } + + if (positions) { + for (i = 0, j = 0; i < SPA_N_ELEMENTS(audio_locations) && j < channels; ++i) { + const struct audio_location loc = audio_locations[i]; + + if (location & loc.mask) { + if (permutation) + positions[permutation[j++]] = loc.position; + else + positions[j++] = loc.position; + } + } + for (i = SPA_AUDIO_CHANNEL_START_Aux; j < channels; ++i, ++j) + positions[j] = i; + } + + return 0; +} + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_opus_05_t a2dp_opus_05 = { + .info = codec->vendor, + .main = { + .channels = SPA_AUDIO_MAX_CHANNELS, + .frame_duration = (OPUS_05_FRAME_DURATION_25 | + OPUS_05_FRAME_DURATION_50 | + OPUS_05_FRAME_DURATION_100 | + OPUS_05_FRAME_DURATION_200 | + OPUS_05_FRAME_DURATION_400), + OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY) + OPUS_05_INIT_BITRATE(0) + }, + .bidi = { + .channels = SPA_AUDIO_MAX_CHANNELS, + .frame_duration = (OPUS_05_FRAME_DURATION_25 | + OPUS_05_FRAME_DURATION_50 | + OPUS_05_FRAME_DURATION_100 | + OPUS_05_FRAME_DURATION_200 | + OPUS_05_FRAME_DURATION_400), + OPUS_05_INIT_LOCATION(BT_AUDIO_LOCATION_ANY) + OPUS_05_INIT_BITRATE(0) + } + }; + + /* Only duplex/pro codec has bidi, since bluez5-device has to know early + * whether to show nodes or not. */ + if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX && + codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) + spa_zero(a2dp_opus_05.bidi); + + memcpy(caps, &a2dp_opus_05, sizeof(a2dp_opus_05)); + return sizeof(a2dp_opus_05); +} + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + struct props props; + a2dp_opus_05_t conf; + int res; + int max; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (codec->vendor.vendor_id != conf.info.vendor_id || + codec->vendor.codec_id != conf.info.codec_id) + return -ENOTSUP; + + parse_settings(&props, global_settings); + + /* Channel Configuration & Audio Location */ + if ((res = set_channel_conf(codec, &conf, &props)) < 0) + return res; + + /* Limits */ + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) { + max = props.max_bitrate; + if (OPUS_05_GET_BITRATE(conf.main) != 0) + OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024)); + else + OPUS_05_SET_BITRATE(conf.main, max / 1024); + + max = props.bidi_max_bitrate; + if (OPUS_05_GET_BITRATE(conf.bidi) != 0) + OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024)); + else + OPUS_05_SET_BITRATE(conf.bidi, max / 1024); + + if (conf.main.frame_duration & props.frame_duration) + conf.main.frame_duration = props.frame_duration; + else + return -EINVAL; + + if (conf.bidi.channels == 0) + true; + else if (conf.bidi.frame_duration & props.bidi_frame_duration) + conf.bidi.frame_duration = props.bidi_frame_duration; + else + return -EINVAL; + } else { + if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_100) + conf.main.frame_duration = OPUS_05_FRAME_DURATION_100; + else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_200) + conf.main.frame_duration = OPUS_05_FRAME_DURATION_200; + else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_400) + conf.main.frame_duration = OPUS_05_FRAME_DURATION_400; + else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_50) + conf.main.frame_duration = OPUS_05_FRAME_DURATION_50; + else if (conf.main.frame_duration & OPUS_05_FRAME_DURATION_25) + conf.main.frame_duration = OPUS_05_FRAME_DURATION_25; + else + return -EINVAL; + + get_default_bitrates(codec, false, NULL, &max, NULL); + + if (OPUS_05_GET_BITRATE(conf.main) != 0) + OPUS_05_SET_BITRATE(conf.main, SPA_MIN(OPUS_05_GET_BITRATE(conf.main), max / 1024)); + else + OPUS_05_SET_BITRATE(conf.main, max / 1024); + + /* longer bidi frames appear to work better */ + if (conf.bidi.channels == 0) + true; + else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_200) + conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_200; + else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_100) + conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_100; + else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_400) + conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_400; + else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_50) + conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_50; + else if (conf.bidi.frame_duration & OPUS_05_FRAME_DURATION_25) + conf.bidi.frame_duration = OPUS_05_FRAME_DURATION_25; + else + return -EINVAL; + + get_default_bitrates(codec, true, NULL, &max, NULL); + + if (conf.bidi.channels == 0) + true; + else if (OPUS_05_GET_BITRATE(conf.bidi) != 0) + OPUS_05_SET_BITRATE(conf.bidi, SPA_MIN(OPUS_05_GET_BITRATE(conf.bidi), max / 1024)); + else + OPUS_05_SET_BITRATE(conf.bidi, max / 1024); + } + + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size, + const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, + const struct spa_dict *global_settings) +{ + a2dp_opus_05_t conf1, conf2, cap1, cap2; + a2dp_opus_05_t *conf; + int res1, res2; + int a, b; + + /* Order selected configurations by preference */ + res1 = codec->select_config(codec, flags, caps1, caps1_size, info, global_settings, (uint8_t *)&conf1); + res2 = codec->select_config(codec, flags, caps2, caps2_size, info, global_settings, (uint8_t *)&conf2); + +#define PREFER_EXPR(expr) \ + do { \ + conf = &conf1; \ + a = (expr); \ + conf = &conf2; \ + b = (expr); \ + if (a != b) \ + return b - a; \ + } while (0) + +#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0) + + /* Prefer valid */ + a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_opus_05_t)) ? 1 : 0; + b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_opus_05_t)) ? 1 : 0; + if (!a || !b) + return b - a; + + memcpy(&cap1, caps1, sizeof(cap1)); + memcpy(&cap2, caps2, sizeof(cap2)); + + if (conf1.bidi.channels == 0 && conf2.bidi.channels == 0) { + /* If no bidi, prefer the SEP that has none */ + a = (cap1.bidi.channels == 0); + b = (cap2.bidi.channels == 0); + if (a != b) + return b - a; + } + + PREFER_EXPR(conf->main.channels); + PREFER_EXPR(conf->bidi.channels); + PREFER_EXPR(OPUS_05_GET_BITRATE(conf->main)); + PREFER_EXPR(OPUS_05_GET_BITRATE(conf->bidi)); + + return 0; + +#undef PREFER_EXPR +#undef PREFER_BOOL +} + +static bool is_duplex_codec(const struct media_codec *codec) +{ + return codec->id == 0; +} + +static bool use_surround_encoder(const struct media_codec *codec, bool is_sink) +{ + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) + return false; + + if (is_duplex_codec(codec)) + return is_sink; + else + return !is_sink; +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK); + a2dp_opus_05_t conf; + a2dp_opus_05_direction_t *dir; + struct spa_pod_frame f[1]; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + dir = !is_duplex_codec(codec) ? &conf.main : &conf.bidi; + + if (get_mapping(codec, dir, surround_encoder, NULL, NULL, NULL, position) < 0) + return -EINVAL; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_F32), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(6, + 48000, 48000, 24000, 16000, 12000, 8000), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(dir->channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, dir->channels, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK); + const a2dp_opus_05_direction_t *dir1, *dir2; + const a2dp_opus_05_t *conf; + + if (caps == NULL || caps_size < sizeof(*conf)) + return -EINVAL; + + conf = caps; + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_F32; + info->info.raw.rate = 0; /* not specified by config */ + + if (2 * conf->main.coupled_streams > conf->main.channels) + return -EINVAL; + if (2 * conf->bidi.coupled_streams > conf->bidi.channels) + return -EINVAL; + + if (!is_duplex_codec(codec)) { + dir1 = &conf->main; + dir2 = &conf->bidi; + } else { + dir1 = &conf->bidi; + dir2 = &conf->main; + } + + info->info.raw.channels = dir1->channels; + if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0) + return -EINVAL; + if (get_mapping(codec, dir2, surround_encoder, NULL, NULL, NULL, NULL) < 0) + return -EINVAL; + + return 0; +} + +static size_t ceildiv(size_t v, size_t divisor) +{ + if (v % divisor == 0) + return v / divisor; + else + return v / divisor + 1; +} + +static bool check_bitrate_vs_frame_dms(struct impl *this, size_t bitrate) +{ + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + size_t max_fragments = 0xf; + size_t payload_size = BUFSIZE_FROM_BITRATE(bitrate, this->e.frame_dms); + return (size_t)this->mtu >= header_size + ceildiv(payload_size, max_fragments); +} + +static int parse_frame_dms(int bitfield) +{ + switch (bitfield) { + case OPUS_05_FRAME_DURATION_25: + return 25; + case OPUS_05_FRAME_DURATION_50: + return 50; + case OPUS_05_FRAME_DURATION_100: + return 100; + case OPUS_05_FRAME_DURATION_200: + return 200; + case OPUS_05_FRAME_DURATION_400: + return 400; + default: + return -EINVAL; + } +} + +static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings) +{ + struct props *p; + + if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) + return NULL; + + p = calloc(1, sizeof(struct props)); + if (p == NULL) + return NULL; + + parse_settings(p, settings); + + return p; +} + +static void codec_clear_props(void *props) +{ + free(props); +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + const bool surround_encoder = use_surround_encoder(codec, flags & MEDIA_CODEC_FLAG_SINK); + a2dp_opus_05_t *conf = config; + a2dp_opus_05_direction_t *dir; + struct impl *this = NULL; + struct spa_audio_info config_info; + const uint8_t *enc_mapping = NULL; + unsigned char mapping[256]; + size_t i; + int res; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_F32) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + this->is_bidi = is_duplex_codec(codec); + dir = !this->is_bidi ? &conf->main : &conf->bidi; + + if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0) + goto error; + if ((res = get_mapping(codec, dir, surround_encoder, &this->streams, &this->coupled_streams, + &enc_mapping, NULL)) < 0) + goto error; + if (config_info.info.raw.channels != info->info.raw.channels) { + res = -EINVAL; + goto error; + } + + this->mtu = mtu; + this->samplerate = info->info.raw.rate; + this->channels = config_info.info.raw.channels; + this->application = OPUS_APPLICATION_AUDIO; + + if (codec->id == SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO && props) { + struct props *p = props; + this->application = !this->is_bidi ? p->application : + p->bidi_application; + } + + /* + * Setup encoder + */ + if (enc_mapping) { + int streams, coupled_streams; + bool incompatible_opus_surround_encoder = false; + + this->enc = opus_multistream_surround_encoder_create( + this->samplerate, this->channels, 1, &streams, &coupled_streams, + mapping, this->application, &res); + + if (this->enc) { + /* Check surround encoder channel mapping is what we want */ + if (streams != this->streams || coupled_streams != this->coupled_streams) + incompatible_opus_surround_encoder = true; + for (i = 0; i < this->channels; ++i) + if (enc_mapping[i] != mapping[i]) + incompatible_opus_surround_encoder = true; + } + + /* Assert: this should never happen */ + spa_assert(!incompatible_opus_surround_encoder); + if (incompatible_opus_surround_encoder) { + res = -EINVAL; + goto error; + } + } else { + for (i = 0; i < this->channels; ++i) + mapping[i] = i; + this->enc = opus_multistream_encoder_create( + this->samplerate, this->channels, this->streams, this->coupled_streams, + mapping, this->application, &res); + } + if (this->enc == NULL) { + res = -EINVAL; + goto error; + } + + if ((this->e.frame_dms = parse_frame_dms(dir->frame_duration)) < 0) { + res = -EINVAL; + goto error; + } + + if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO) { + get_default_bitrates(codec, this->is_bidi, &this->e.bitrate_min, + &this->e.bitrate_max, &this->e.bitrate); + this->e.bitrate_max = SPA_MIN(this->e.bitrate_max, + OPUS_05_GET_BITRATE(*dir) * 1024); + } else { + this->e.bitrate_max = OPUS_05_GET_BITRATE(*dir) * 1024; + this->e.bitrate_min = BITRATE_MIN; + this->e.bitrate = BITRATE_INITIAL; + } + + this->e.bitrate_min = SPA_MIN(this->e.bitrate_min, this->e.bitrate_max); + this->e.bitrate = SPA_CLAMP(this->e.bitrate, this->e.bitrate_min, this->e.bitrate_max); + + this->e.next_bitrate = this->e.bitrate; + opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate)); + + this->e.samples = this->e.frame_dms * this->samplerate / 10000; + this->e.codesize = this->e.samples * (int)this->channels * sizeof(float); + + + /* + * Setup decoder + */ + for (i = 0; i < this->channels; ++i) + mapping[i] = i; + this->dec = opus_multistream_decoder_create( + this->samplerate, this->channels, + this->streams, this->coupled_streams, + mapping, &res); + if (this->dec == NULL) { + res = -EINVAL; + goto error; + } + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (this && this->enc) + opus_multistream_encoder_destroy(this->enc); + if (this && this->dec) + opus_multistream_decoder_destroy(this->dec); + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + opus_multistream_encoder_destroy(this->enc); + opus_multistream_decoder_destroy(this->dec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->e.codesize; +} + +static int codec_update_bitrate(struct impl *this) +{ + this->e.next_bitrate = SPA_CLAMP(this->e.next_bitrate, + this->e.bitrate_min, this->e.bitrate_max); + + if (!check_bitrate_vs_frame_dms(this, this->e.next_bitrate)) { + this->e.next_bitrate = this->e.bitrate; + return 0; + } + + this->e.bitrate = this->e.next_bitrate; + opus_multistream_encoder_ctl(this->enc, OPUS_SET_BITRATE(this->e.bitrate)); + return 0; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + if (dst_size <= header_size) + return -EINVAL; + + codec_update_bitrate(this); + + this->e.header = (struct rtp_header *)dst; + this->e.payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload); + memset(dst, 0, header_size); + + this->e.payload->frame_count = 0; + this->e.header->v = 2; + this->e.header->pt = 96; + this->e.header->sequence_number = htons(seqnum); + this->e.header->timestamp = htonl(timestamp); + this->e.header->ssrc = htonl(1); + + this->e.packet_size = header_size; + return this->e.packet_size; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + const int header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + int size; + int res; + + if (src == NULL) { + /* Produce fragment packets. + * + * We assume the caller gives the same buffer here as in the previous + * calls to encode(), without changes in the buffer content. + */ + if (this->e.fragment == NULL || + this->e.fragment_count <= 1 || + this->e.fragment < dst || + SPA_PTROFF(this->e.fragment, this->e.fragment_size, void) > SPA_PTROFF(dst, dst_size, void)) { + this->e.fragment = NULL; + return -EINVAL; + } + + size = SPA_MIN(this->mtu - header_size, this->e.fragment_size); + memmove(dst, this->e.fragment, size); + *dst_out = size; + + this->e.payload->is_fragmented = 1; + this->e.payload->frame_count = --this->e.fragment_count; + this->e.payload->is_last_fragment = (this->e.fragment_count == 1); + + if (this->e.fragment_size > size && this->e.fragment_count > 1) { + this->e.fragment = SPA_PTROFF(this->e.fragment, size, void); + this->e.fragment_size -= size; + *need_flush = NEED_FLUSH_FRAGMENT; + } else { + this->e.fragment = NULL; + *need_flush = NEED_FLUSH_ALL; + } + return 0; + } + + if (src_size < (size_t)this->e.codesize) { + *dst_out = 0; + return 0; + } + + res = opus_multistream_encode_float( + this->enc, src, this->e.samples, dst, dst_size); + if (res < 0) + return -EINVAL; + *dst_out = res; + + this->e.packet_size += res; + this->e.payload->frame_count++; + + if (this->e.packet_size > this->mtu) { + /* Fragment packet */ + this->e.fragment_count = ceildiv(this->e.packet_size - header_size, + this->mtu - header_size); + + this->e.payload->is_fragmented = 1; + this->e.payload->is_first_fragment = 1; + this->e.payload->frame_count = this->e.fragment_count; + + this->e.fragment_size = this->e.packet_size - this->mtu; + this->e.fragment = SPA_PTROFF(dst, *dst_out - this->e.fragment_size, void); + *need_flush = NEED_FLUSH_FRAGMENT; + + /* + * We keep the rest of the encoded frame in the same buffer, and rely + * that the caller won't overwrite it before the next call to encode() + */ + *dst_out = SPA_PTRDIFF(this->e.fragment, dst); + } else { + *need_flush = NEED_FLUSH_ALL; + } + + return this->e.codesize; +} + +static SPA_UNUSED int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + const struct rtp_header *header = src; + const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void); + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + spa_return_val_if_fail (src_size > header_size, -EINVAL); + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + + if (payload->is_fragmented) { + if (payload->is_first_fragment) { + this->d.fragment_size = 0; + } else if (payload->frame_count + 1 != this->d.fragment_count || + (payload->frame_count == 1 && !payload->is_last_fragment)){ + /* Fragments not in right order: drop packet */ + return -EINVAL; + } + this->d.fragment_count = payload->frame_count; + } else { + if (payload->frame_count != 1) + return -EINVAL; + this->d.fragment_count = 0; + } + + return header_size; +} + +static SPA_UNUSED int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int consumed = src_size; + int res; + int dst_samples; + + if (this->d.fragment_count > 0) { + /* Fragmented frame */ + size_t avail; + avail = SPA_MIN(sizeof(this->d.fragment) - this->d.fragment_size, src_size); + memcpy(SPA_PTROFF(this->d.fragment, this->d.fragment_size, void), src, avail); + + this->d.fragment_size += avail; + + if (this->d.fragment_count > 1) { + /* More fragments to come */ + *dst_out = 0; + return consumed; + } + + src = this->d.fragment; + src_size = this->d.fragment_size; + + this->d.fragment_count = 0; + this->d.fragment_size = 0; + } + + dst_samples = dst_size / (sizeof(float) * this->channels); + res = opus_multistream_decode_float(this->dec, src, src_size, dst, dst_samples, 0); + if (res < 0) + return -EINVAL; + *dst_out = (size_t)res * this->channels * sizeof(float); + + return consumed; +} + +static int codec_abr_process(void *data, size_t unsent) +{ + const uint64_t interval = SPA_NSEC_PER_SEC; + struct impl *this = data; + struct abr *abr = &this->e.abr; + bool level_bad, level_good; + uint32_t actual_bitrate; + + abr->total_size += this->e.packet_size; + + if (this->e.payload->is_fragmented && !this->e.payload->is_first_fragment) + return 0; + + abr->now += this->e.frame_dms * SPA_NSEC_PER_MSEC / 10; + + abr->buffer_level = SPA_MAX(abr->buffer_level, unsent); + abr->packet_size = SPA_MAX(abr->packet_size, (uint32_t)this->e.packet_size); + abr->packet_size = SPA_MAX(abr->packet_size, 128u); + + level_bad = abr->buffer_level > 2 * (uint32_t)this->mtu || abr->bad; + level_good = abr->buffer_level == 0; + + if (!(abr->last_update + interval <= abr->now || + (level_bad && abr->last_change + interval <= abr->now))) + return 0; + + actual_bitrate = (uint64_t)abr->total_size*8*SPA_NSEC_PER_SEC + / SPA_MAX(1u, abr->now - abr->last_update); + + spa_log_debug(log, "opus ABR bitrate:%d actual:%d level:%d (%s) bad:%d retry:%ds size:%d", + (int)this->e.bitrate, + (int)actual_bitrate, + (int)abr->buffer_level, + level_bad ? "bad" : (level_good ? "good" : "-"), + (int)abr->bad, + (int)(abr->retry_interval / SPA_NSEC_PER_SEC), + (int)abr->packet_size); + + if (level_bad) { + this->e.next_bitrate = this->e.bitrate * 11 / 12; + abr->last_change = abr->now; + abr->retry_interval = SPA_MIN(abr->retry_interval + 10*interval, + 30 * interval); + } else if (!level_good) { + abr->last_change = abr->now; + } else if (abr->now < abr->last_change + abr->retry_interval) { + /* noop */ + } else if (actual_bitrate*3/2 < (uint32_t)this->e.bitrate) { + /* actual bitrate is small compared to target; probably silence */ + } else { + this->e.next_bitrate = this->e.bitrate + + SPA_MAX(1, this->e.bitrate_max / 40); + abr->last_change = abr->now; + abr->retry_interval = SPA_MAX(abr->retry_interval, (5+4)*interval) + - 4*interval; + } + + abr->last_update = abr->now; + abr->buffer_level = 0; + abr->bad = false; + abr->packet_size = 0; + abr->total_size = 0; + + return 0; +} + +static int codec_reduce_bitpool(void *data) +{ + struct impl *this = data; + struct abr *abr = &this->e.abr; + abr->bad = true; + return 0; +} + +static int codec_increase_bitpool(void *data) +{ + return 0; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &log_topic); +} + +#define OPUS_05_COMMON_DEFS \ + .codec_id = A2DP_CODEC_VENDOR, \ + .vendor = { .vendor_id = OPUS_05_VENDOR_ID, \ + .codec_id = OPUS_05_CODEC_ID }, \ + .select_config = codec_select_config, \ + .enum_config = codec_enum_config, \ + .validate_config = codec_validate_config, \ + .caps_preference_cmp = codec_caps_preference_cmp, \ + .init = codec_init, \ + .deinit = codec_deinit, \ + .get_block_size = codec_get_block_size, \ + .abr_process = codec_abr_process, \ + .start_encode = codec_start_encode, \ + .encode = codec_encode, \ + .reduce_bitpool = codec_reduce_bitpool, \ + .increase_bitpool = codec_increase_bitpool, \ + .set_log = codec_set_log + +#define OPUS_05_COMMON_FULL_DEFS \ + OPUS_05_COMMON_DEFS, \ + .start_decode = codec_start_decode, \ + .decode = codec_decode + +const struct media_codec a2dp_codec_opus_05 = { + OPUS_05_COMMON_FULL_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, + .name = "opus_05", + .description = "Opus", + .fill_caps = codec_fill_caps, +}; + +const struct media_codec a2dp_codec_opus_05_51 = { + OPUS_05_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, + .name = "opus_05_51", + .description = "Opus 5.1 Surround", + .endpoint_name = "opus_05", + .fill_caps = NULL, +}; + +const struct media_codec a2dp_codec_opus_05_71 = { + OPUS_05_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, + .name = "opus_05_71", + .description = "Opus 7.1 Surround", + .endpoint_name = "opus_05", + .fill_caps = NULL, +}; + +/* Bidi return channel codec: doesn't have endpoints */ +const struct media_codec a2dp_codec_opus_05_return = { + OPUS_05_COMMON_FULL_DEFS, + .id = 0, + .name = "opus_05_duplex_bidi", + .description = "Opus Duplex Bidi channel", +}; + +const struct media_codec a2dp_codec_opus_05_duplex = { + OPUS_05_COMMON_FULL_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, + .name = "opus_05_duplex", + .description = "Opus Duplex", + .duplex_codec = &a2dp_codec_opus_05_return, + .fill_caps = codec_fill_caps, +}; + +const struct media_codec a2dp_codec_opus_05_pro = { + OPUS_05_COMMON_DEFS, + .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, + .name = "opus_05_pro", + .description = "Opus Pro Audio", + .init_props = codec_init_props, + .clear_props = codec_clear_props, + .duplex_codec = &a2dp_codec_opus_05_return, + .endpoint_name = "opus_05_duplex", + .fill_caps = NULL, +}; + +MEDIA_CODEC_EXPORT_DEF( + "opus", + &a2dp_codec_opus_05, + &a2dp_codec_opus_05_51, + &a2dp_codec_opus_05_71, + &a2dp_codec_opus_05_duplex, + &a2dp_codec_opus_05_pro +); diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c new file mode 100644 index 0000000..27a57bd --- /dev/null +++ b/spa/plugins/bluez5/a2dp-codec-sbc.c @@ -0,0 +1,689 @@ +/* Spa A2DP SBC codec + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; +}; + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + static const a2dp_sbc_t a2dp_sbc = { + .frequency = + SBC_SAMPLING_FREQ_16000 | + SBC_SAMPLING_FREQ_32000 | + SBC_SAMPLING_FREQ_44100 | + SBC_SAMPLING_FREQ_48000, + .channel_mode = + SBC_CHANNEL_MODE_MONO | + SBC_CHANNEL_MODE_DUAL_CHANNEL | + SBC_CHANNEL_MODE_STEREO | + SBC_CHANNEL_MODE_JOINT_STEREO, + .block_length = + SBC_BLOCK_LENGTH_4 | + SBC_BLOCK_LENGTH_8 | + SBC_BLOCK_LENGTH_12 | + SBC_BLOCK_LENGTH_16, + .subbands = + SBC_SUBBANDS_4 | + SBC_SUBBANDS_8, + .allocation_method = + SBC_ALLOCATION_SNR | + SBC_ALLOCATION_LOUDNESS, + .min_bitpool = SBC_MIN_BITPOOL, + .max_bitpool = SBC_MAX_BITPOOL, + }; + + memcpy(caps, &a2dp_sbc, sizeof(a2dp_sbc)); + return sizeof(a2dp_sbc); +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode, bool xq) +{ + /* A2DP spec v1.2 states that all SNK implementation shall handle bitrates + * of up to 512 kbps (~ bitpool = 86 stereo, or 2x43 dual channel at 44.1KHz + * or ~ bitpool = 78 stereo, or 2x39 dual channel at 48KHz). */ + switch (freq) { + case SBC_SAMPLING_FREQ_16000: + case SBC_SAMPLING_FREQ_32000: + return 64; + + case SBC_SAMPLING_FREQ_44100: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return xq ? 43 : 32; + + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return xq ? 86 : 64; + } + return xq ? 86 : 64; + case SBC_SAMPLING_FREQ_48000: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return xq ? 39 : 29; + + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return xq ? 78 : 58; + } + return xq ? 78 : 58; + } + return xq ? 86 : 64; +} + + +static const struct media_codec_config +sbc_frequencies[] = { + { SBC_SAMPLING_FREQ_48000, 48000, 3 }, + { SBC_SAMPLING_FREQ_44100, 44100, 2 }, + { SBC_SAMPLING_FREQ_32000, 32000, 1 }, + { SBC_SAMPLING_FREQ_16000, 16000, 0 }, +}; + +static const struct media_codec_config +sbc_xq_frequencies[] = { + { SBC_SAMPLING_FREQ_44100, 44100, 1 }, + { SBC_SAMPLING_FREQ_48000, 48000, 0 }, +}; + +static const struct media_codec_config +sbc_channel_modes[] = { + { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 3 }, + { SBC_CHANNEL_MODE_STEREO, 2, 2 }, + { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 1 }, + { SBC_CHANNEL_MODE_MONO, 1, 0 }, +}; + +static const struct media_codec_config +sbc_xq_channel_modes[] = { + { SBC_CHANNEL_MODE_DUAL_CHANNEL, 2, 2 }, + { SBC_CHANNEL_MODE_JOINT_STEREO, 2, 1 }, + { SBC_CHANNEL_MODE_STEREO, 2, 0 }, +}; + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + a2dp_sbc_t conf; + int bitpool, i; + size_t n; + const struct media_codec_config *configs; + bool xq = false; + + + if (caps_size < sizeof(conf)) + return -EINVAL; + + xq = (spa_streq(codec->name, "sbc_xq")); + + memcpy(&conf, caps, sizeof(conf)); + + if (xq) { + configs = sbc_xq_frequencies; + n = SPA_N_ELEMENTS(sbc_xq_frequencies); + } else { + configs = sbc_frequencies; + n = SPA_N_ELEMENTS(sbc_frequencies); + } + if ((i = media_codec_select_config(configs, n, conf.frequency, + info ? info->rate : A2DP_CODEC_DEFAULT_RATE + )) < 0) + return -ENOTSUP; + conf.frequency = configs[i].config; + + if (xq) { + configs = sbc_xq_channel_modes; + n = SPA_N_ELEMENTS(sbc_xq_channel_modes); + } else { + configs = sbc_channel_modes; + n = SPA_N_ELEMENTS(sbc_channel_modes); + } + if ((i = media_codec_select_config(configs, n, conf.channel_mode, + info ? info->channels : A2DP_CODEC_DEFAULT_CHANNELS + )) < 0) + return -ENOTSUP; + conf.channel_mode = configs[i].config; + + if (conf.block_length & SBC_BLOCK_LENGTH_16) + conf.block_length = SBC_BLOCK_LENGTH_16; + else if (conf.block_length & SBC_BLOCK_LENGTH_12) + conf.block_length = SBC_BLOCK_LENGTH_12; + else if (conf.block_length & SBC_BLOCK_LENGTH_8) + conf.block_length = SBC_BLOCK_LENGTH_8; + else if (conf.block_length & SBC_BLOCK_LENGTH_4) + conf.block_length = SBC_BLOCK_LENGTH_4; + else + return -ENOTSUP; + + if (conf.subbands & SBC_SUBBANDS_8) + conf.subbands = SBC_SUBBANDS_8; + else if (conf.subbands & SBC_SUBBANDS_4) + conf.subbands = SBC_SUBBANDS_4; + else + return -ENOTSUP; + + if (conf.allocation_method & SBC_ALLOCATION_LOUDNESS) + conf.allocation_method = SBC_ALLOCATION_LOUDNESS; + else if (conf.allocation_method & SBC_ALLOCATION_SNR) + conf.allocation_method = SBC_ALLOCATION_SNR; + else + return -ENOTSUP; + + bitpool = default_bitpool(conf.frequency, conf.channel_mode, xq); + + conf.min_bitpool = SPA_MAX(SBC_MIN_BITPOOL, conf.min_bitpool); + conf.max_bitpool = SPA_MIN(bitpool, conf.max_bitpool); + memcpy(config, &conf, sizeof(conf)); + + return sizeof(conf); +} + +static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size, + const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings) +{ + a2dp_sbc_t conf1, conf2; + a2dp_sbc_t *conf; + int res1, res2; + int a, b; + bool xq = (spa_streq(codec->name, "sbc_xq")); + + /* Order selected configurations by preference */ + res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1); + res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2); + +#define PREFER_EXPR(expr) \ + do { \ + conf = &conf1; \ + a = (expr); \ + conf = &conf2; \ + b = (expr); \ + if (a != b) \ + return b - a; \ + } while (0) + +#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0) + + /* Prefer valid */ + a = (res1 > 0 && (size_t)res1 == sizeof(a2dp_sbc_t)) ? 1 : 0; + b = (res2 > 0 && (size_t)res2 == sizeof(a2dp_sbc_t)) ? 1 : 0; + if (!a || !b) + return b - a; + + PREFER_BOOL(conf->frequency & (SBC_SAMPLING_FREQ_48000 | SBC_SAMPLING_FREQ_44100)); + + if (xq) + PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL); + else + PREFER_BOOL(conf->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO); + + PREFER_EXPR(conf->max_bitpool); + + return 0; + +#undef PREFER_EXPR +#undef PREFER_BOOL +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + const a2dp_sbc_t *conf; + + if (caps == NULL || caps_size < sizeof(*conf)) + return -EINVAL; + + conf = caps; + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16; + + switch (conf->frequency) { + case SBC_SAMPLING_FREQ_16000: + info->info.raw.rate = 16000; + break; + case SBC_SAMPLING_FREQ_32000: + info->info.raw.rate = 32000; + break; + case SBC_SAMPLING_FREQ_44100: + info->info.raw.rate = 44100; + break; + case SBC_SAMPLING_FREQ_48000: + info->info.raw.rate = 48000; + break; + default: + return -EINVAL; + } + + switch (conf->channel_mode) { + case SBC_CHANNEL_MODE_MONO: + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + break; + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + info->info.raw.channels = 2; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_FL; + info->info.raw.position[1] = SPA_AUDIO_CHANNEL_FR; + break; + default: + return -EINVAL; + } + + switch (conf->subbands) { + case SBC_SUBBANDS_4: + case SBC_SUBBANDS_8: + break; + default: + return -EINVAL; + } + + switch (conf->block_length) { + case SBC_BLOCK_LENGTH_4: + case SBC_BLOCK_LENGTH_8: + case SBC_BLOCK_LENGTH_12: + case SBC_BLOCK_LENGTH_16: + break; + default: + return -EINVAL; + } + return 0; +} + +static int codec_set_bitpool(struct impl *this, int bitpool) +{ + size_t rtp_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + this->sbc.bitpool = SPA_CLAMP(bitpool, this->min_bitpool, this->max_bitpool); + this->codesize = sbc_get_codesize(&this->sbc); + this->max_frames = (this->mtu - rtp_size) / sbc_get_frame_length(&this->sbc); + if (this->max_frames > 15) + this->max_frames = 15; + return this->sbc.bitpool; +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + a2dp_sbc_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t i = 0; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + + if (caps_size < sizeof(conf)) + return -EINVAL; + + memcpy(&conf, caps, sizeof(conf)); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.frequency & SBC_SAMPLING_FREQ_48000) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.frequency & SBC_SAMPLING_FREQ_44100) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } + if (conf.frequency & SBC_SAMPLING_FREQ_32000) { + if (i++ == 0) + spa_pod_builder_int(b, 32000); + spa_pod_builder_int(b, 32000); + } + if (conf.frequency & SBC_SAMPLING_FREQ_16000) { + if (i++ == 0) + spa_pod_builder_int(b, 16000); + spa_pod_builder_int(b, 16000); + } + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if (conf.channel_mode & SBC_CHANNEL_MODE_MONO && + conf.channel_mode & (SBC_CHANNEL_MODE_JOINT_STEREO | + SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_DUAL_CHANNEL)) { + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), + 0); + } else if (conf.channel_mode & SBC_CHANNEL_MODE_MONO) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 1, position), + 0); + } else { + position[0] = SPA_AUDIO_CHANNEL_FL; + position[1] = SPA_AUDIO_CHANNEL_FR; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, 2, position), + 0); + } + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_reduce_bitpool(void *data) +{ + struct impl *this = data; + return codec_set_bitpool(this, this->sbc.bitpool - 2); +} + +static int codec_increase_bitpool(void *data) +{ + struct impl *this = data; + return codec_set_bitpool(this, this->sbc.bitpool + 1); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this; + a2dp_sbc_t *conf = config; + int res; + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) { + res = -errno; + goto error; + } + + sbc_init(&this->sbc, 0); + this->sbc.endian = SBC_LE; + this->mtu = mtu; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S16) { + res = -EINVAL; + goto error; + } + + switch (conf->frequency) { + case SBC_SAMPLING_FREQ_16000: + this->sbc.frequency = SBC_FREQ_16000; + break; + case SBC_SAMPLING_FREQ_32000: + this->sbc.frequency = SBC_FREQ_32000; + break; + case SBC_SAMPLING_FREQ_44100: + this->sbc.frequency = SBC_FREQ_44100; + break; + case SBC_SAMPLING_FREQ_48000: + this->sbc.frequency = SBC_FREQ_48000; + break; + default: + res = -EINVAL; + goto error; + } + + switch (conf->channel_mode) { + case SBC_CHANNEL_MODE_MONO: + this->sbc.mode = SBC_MODE_MONO; + break; + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + this->sbc.mode = SBC_MODE_DUAL_CHANNEL; + break; + case SBC_CHANNEL_MODE_STEREO: + this->sbc.mode = SBC_MODE_STEREO; + break; + case SBC_CHANNEL_MODE_JOINT_STEREO: + this->sbc.mode = SBC_MODE_JOINT_STEREO; + break; + default: + res = -EINVAL; + goto error; + } + + switch (conf->subbands) { + case SBC_SUBBANDS_4: + this->sbc.subbands = SBC_SB_4; + break; + case SBC_SUBBANDS_8: + this->sbc.subbands = SBC_SB_8; + break; + default: + res = -EINVAL; + goto error; + } + + if (conf->allocation_method & SBC_ALLOCATION_LOUDNESS) + this->sbc.allocation = SBC_AM_LOUDNESS; + else + this->sbc.allocation = SBC_AM_SNR; + + switch (conf->block_length) { + case SBC_BLOCK_LENGTH_4: + this->sbc.blocks = SBC_BLK_4; + break; + case SBC_BLOCK_LENGTH_8: + this->sbc.blocks = SBC_BLK_8; + break; + case SBC_BLOCK_LENGTH_12: + this->sbc.blocks = SBC_BLK_12; + break; + case SBC_BLOCK_LENGTH_16: + this->sbc.blocks = SBC_BLK_16; + break; + default: + res = -EINVAL; + goto error; + } + + this->min_bitpool = SPA_MAX(conf->min_bitpool, 12); + this->max_bitpool = conf->max_bitpool; + + codec_set_bitpool(this, conf->max_bitpool); + + return this; +error: + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + sbc_finish(&this->sbc); + free(this); +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->header = (struct rtp_header *)dst; + this->payload = SPA_PTROFF(dst, sizeof(struct rtp_header), struct rtp_payload); + memset(this->header, 0, sizeof(struct rtp_header)+sizeof(struct rtp_payload)); + + this->payload->frame_count = 0; + this->header->v = 2; + this->header->pt = 96; + this->header->sequence_number = htons(seqnum); + this->header->timestamp = htonl(timestamp); + this->header->ssrc = htonl(1); + return sizeof(struct rtp_header) + sizeof(struct rtp_payload); +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res; + + res = sbc_encode(&this->sbc, src, src_size, + dst, dst_size, (ssize_t*)dst_out); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + spa_assert(res == this->codesize); + + this->payload->frame_count += res / this->codesize; + *need_flush = (this->payload->frame_count >= this->max_frames) ? NEED_FLUSH_ALL : NEED_FLUSH_NO; + return res; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + const struct rtp_header *header = src; + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + spa_return_val_if_fail (src_size > header_size, -EINVAL); + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + return header_size; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int res; + + res = sbc_decode(&this->sbc, src, src_size, + dst, dst_size, dst_out); + + return res; +} + +const struct media_codec a2dp_codec_sbc = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC, + .codec_id = A2DP_CODEC_SBC, + .name = "sbc", + .description = "SBC", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .caps_preference_cmp = codec_caps_preference_cmp, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, +}; + +const struct media_codec a2dp_codec_sbc_xq = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, + .codec_id = A2DP_CODEC_SBC, + .name = "sbc_xq", + .description = "SBC-XQ", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .caps_preference_cmp = codec_caps_preference_cmp, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool, +}; + +MEDIA_CODEC_EXPORT_DEF( + "sbc", + &a2dp_codec_sbc, + &a2dp_codec_sbc_xq +); diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c new file mode 100644 index 0000000..93f512d --- /dev/null +++ b/spa/plugins/bluez5/backend-hsphfpd.c @@ -0,0 +1,1588 @@ +/* Spa hsphfpd backend + * + * Based on previous work for pulseaudio by: Pali Rohár + * Copyright © 2020 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "defs.h" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.hsphfpd"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + struct spa_bt_backend this; + + struct spa_bt_monitor *monitor; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_dbus *dbus; + DBusConnection *conn; + + const struct spa_bt_quirks *quirks; + + struct spa_list endpoint_list; + bool endpoints_listed; + + char *hsphfpd_service_id; + + bool acquire_in_progress; + + unsigned int filters_added:1; + unsigned int msbc_supported:1; +}; + +enum hsphfpd_volume_control { + HSPHFPD_VOLUME_CONTROL_NONE = 1, + HSPHFPD_VOLUME_CONTROL_LOCAL, + HSPHFPD_VOLUME_CONTROL_REMOTE, +}; + +enum hsphfpd_profile { + HSPHFPD_PROFILE_HEADSET = 1, + HSPHFPD_PROFILE_HANDSFREE, +}; + +enum hsphfpd_role { + HSPHFPD_ROLE_CLIENT = 1, + HSPHFPD_ROLE_GATEWAY, +}; + +struct hsphfpd_transport_data { + char *transport_path; + bool rx_soft_volume; + bool tx_soft_volume; + int rx_volume_gain; + int tx_volume_gain; + int max_rx_volume_gain; + int max_tx_volume_gain; + enum hsphfpd_volume_control rx_volume_control; + enum hsphfpd_volume_control tx_volume_control; +}; + +struct hsphfpd_endpoint { + struct spa_list link; + char *path; + bool valid; + bool connected; + char *remote_address; + char *local_address; + enum hsphfpd_profile profile; + enum hsphfpd_role role; + int air_codecs; +}; + +#define DBUS_INTERFACE_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager" + +#define HSPHFPD_APPLICATION_MANAGER_INTERFACE HSPHFPD_SERVICE ".ApplicationManager" +#define HSPHFPD_ENDPOINT_INTERFACE HSPHFPD_SERVICE ".Endpoint" +#define HSPHFPD_AUDIO_AGENT_INTERFACE HSPHFPD_SERVICE ".AudioAgent" +#define HSPHFPD_AUDIO_TRANSPORT_INTERFACE HSPHFPD_SERVICE ".AudioTransport" + +#define APPLICATION_OBJECT_MANAGER_PATH "/Profile/hsphfpd/manager" +#define HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ "/Profile/hsphfpd/pcm_s16le_8khz_agent" +#define HSPHFP_AUDIO_CLIENT_MSBC "/Profile/hsphfpd/msbc_agent" + +#define HSPHFP_AIR_CODEC_CVSD "CVSD" +#define HSPHFP_AIR_CODEC_MSBC "mSBC" +#define HSPHFP_AGENT_CODEC_PCM "PCM_s16le_8kHz" +#define HSPHFP_AGENT_CODEC_MSBC "mSBC" + +#define APPLICATION_OBJECT_MANAGER_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "\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); + if (endpoint->local_address) + free(endpoint->local_address); + if (endpoint->remote_address) + free(endpoint->remote_address); +} + +static bool hsphfpd_cmp_transport_path(struct spa_bt_transport *t, const void *data) +{ + struct hsphfpd_transport_data *td = t->user_data; + if (spa_streq(td->transport_path, data)) + return true; + + return false; +} + +static inline bool check_signature(DBusMessage *m, const char sig[]) +{ + return spa_streq(dbus_message_get_signature(m), sig); +} + +static int set_dbus_property(struct impl *backend, + const char *service, + const char *path, + const char *interface, + const char *property, + int type, + void *value) +{ + DBusMessage *m, *r; + DBusMessageIter iter; + DBusError err; + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "Set"); + if (m == NULL) + return -ENOMEM; + dbus_message_append_args(m, DBUS_TYPE_STRING, &interface, DBUS_TYPE_STRING, &property, DBUS_TYPE_INVALID); + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_append_basic(&iter, type, value); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_error(backend->log, "Transport Set() failed for transport %s (%s)", path, err.message); + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Set() returned error: %s", dbus_message_get_error_name(r)); + return -EIO; + } + + dbus_message_unref(r); + return 0; +} + +static inline void set_rx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain) +{ + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + struct hsphfpd_transport_data *transport_data = transport->user_data; + + if (transport->fd < 0 || transport_data->rx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE) + return; + if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path, + HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "RxVolumeGain", + DBUS_TYPE_UINT16, &gain)) + spa_log_error(backend->log, "Changing rx volume gain to %u for transport %s failed", + (unsigned)gain, transport_data->transport_path); +} + +static inline void set_tx_volume_gain_property(const struct spa_bt_transport *transport, uint16_t gain) +{ + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + struct hsphfpd_transport_data *transport_data = transport->user_data; + + if (transport->fd < 0 || transport_data->tx_volume_control <= HSPHFPD_VOLUME_CONTROL_NONE) + return; + if (set_dbus_property(backend, HSPHFPD_SERVICE, transport_data->transport_path, + HSPHFPD_AUDIO_TRANSPORT_INTERFACE, "TxVolumeGain", + DBUS_TYPE_UINT16, &gain)) + spa_log_error(backend->log, "Changing tx volume gain to %u for transport %s failed", + (unsigned)gain, transport_data->transport_path); +} + +static void parse_transport_properties_values(struct impl *backend, + const char *transport_path, + DBusMessageIter *i, + const char **endpoint_path, + const char **air_codec, + enum hsphfpd_volume_control *rx_volume_control, + enum hsphfpd_volume_control *tx_volume_control, + uint16_t *rx_volume_gain, + uint16_t *tx_volume_gain, + uint16_t *mtu) +{ + DBusMessageIter element_i; + + spa_assert(i); + + dbus_message_iter_recurse(i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, variant_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + + if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_STRING) { + spa_log_error(backend->log, "Received invalid property for transport %s", transport_path); + return; + } + + dbus_message_iter_get_basic(&dict_i, &key); + + if (!dbus_message_iter_next(&dict_i)) { + spa_log_error(backend->log, "Received invalid property for transport %s", transport_path); + return; + } + + if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_VARIANT) { + spa_log_error(backend->log, "Received invalid property for transport %s", transport_path); + return; + } + + dbus_message_iter_recurse(&dict_i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + case DBUS_TYPE_STRING: + if (spa_streq(key, "RxVolumeControl") || spa_streq(key, "TxVolumeControl")) { + const char *value; + enum hsphfpd_volume_control volume_control; + + dbus_message_iter_get_basic(&variant_i, &value); + if (spa_streq(value, "none")) + volume_control = HSPHFPD_VOLUME_CONTROL_NONE; + else if (spa_streq(value, "local")) + volume_control = HSPHFPD_VOLUME_CONTROL_LOCAL; + else if (spa_streq(value, "remote")) + volume_control = HSPHFPD_VOLUME_CONTROL_REMOTE; + else + volume_control = 0; + + if (!volume_control) + spa_log_warn(backend->log, "Transport %s received invalid '%s' property value '%s', ignoring", transport_path, key, value); + else if (spa_streq(key, "RxVolumeControl")) + *rx_volume_control = volume_control; + else if (spa_streq(key, "TxVolumeControl")) + *tx_volume_control = volume_control; + } else if (spa_streq(key, "AirCodec")) + dbus_message_iter_get_basic(&variant_i, air_codec); + break; + + case DBUS_TYPE_UINT16: + if (spa_streq(key, "MTU")) + dbus_message_iter_get_basic(&variant_i, mtu); + else if (spa_streq(key, "RxVolumeGain")) + dbus_message_iter_get_basic(&variant_i, rx_volume_gain); + else if (spa_streq(key, "TxVolumeGain")) + dbus_message_iter_get_basic(&variant_i, tx_volume_gain); + break; + + case DBUS_TYPE_OBJECT_PATH: + if (spa_streq(key, "Endpoint")) + dbus_message_iter_get_basic(&variant_i, endpoint_path); + break; + } + + dbus_message_iter_next(&element_i); + } +} + +static void hsphfpd_parse_transport_properties(struct impl *backend, struct spa_bt_transport *transport, DBusMessageIter *i) +{ + struct hsphfpd_transport_data *transport_data = transport->user_data; + const char *endpoint_path = NULL; + const char *air_codec = NULL; + enum hsphfpd_volume_control rx_volume_control = 0; + enum hsphfpd_volume_control tx_volume_control = 0; + uint16_t rx_volume_gain = -1; + uint16_t tx_volume_gain = -1; + uint16_t mtu = 0; + bool rx_volume_gain_changed = false; + bool tx_volume_gain_changed = false; + bool rx_volume_control_changed = false; + bool tx_volume_control_changed = false; + bool rx_soft_volume_changed = false; + bool tx_soft_volume_changed = false; + + parse_transport_properties_values(backend, transport_data->transport_path, i, &endpoint_path, + &air_codec, &rx_volume_control, &tx_volume_control, + &rx_volume_gain, &tx_volume_gain, &mtu); + + if (endpoint_path) + spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring", + transport_data->transport_path, "Endpoint"); + + if (air_codec) + spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring", + transport_data->transport_path, "AirCodec"); + + if (mtu) + spa_log_warn(backend->log, "Transport %s received a duplicate '%s' property, ignoring", + transport_data->transport_path, "MTU"); + + if (rx_volume_control) { + if (!!transport_data->rx_soft_volume != !!(rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) { + spa_log_info(backend->log, "Transport %s changed rx soft volume from %d to %d", + transport_data->transport_path, transport_data->rx_soft_volume, + (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)); + transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE); + rx_soft_volume_changed = true; + } + if (transport_data->rx_volume_control != rx_volume_control) { + transport_data->rx_volume_control = rx_volume_control; + rx_volume_control_changed = true; + } + } + + if (tx_volume_control) { + if (!!transport_data->tx_soft_volume != !!(tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)) { + spa_log_info(backend->log, "Transport %s changed tx soft volume from %d to %d", + transport_data->transport_path, transport_data->rx_soft_volume, + (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE)); + transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE); + tx_soft_volume_changed = true; + } + if (transport_data->tx_volume_control != tx_volume_control) { + transport_data->tx_volume_control = tx_volume_control; + tx_volume_control_changed = true; + } + } + + if (rx_volume_gain != (uint16_t)-1) { + if (transport_data->rx_volume_gain != rx_volume_gain) { + spa_log_info(backend->log, "Transport %s changed rx volume gain from %u to %u", + transport_data->transport_path, (unsigned)transport_data->rx_volume_gain, (unsigned)rx_volume_gain); + transport_data->rx_volume_gain = rx_volume_gain; + rx_volume_gain_changed = true; + } + } + + if (tx_volume_gain != (uint16_t)-1) { + if (transport_data->tx_volume_gain != tx_volume_gain) { + spa_log_info(backend->log, "Transport %s changed tx volume gain from %u to %u", + transport_data->transport_path, (unsigned)transport_data->tx_volume_gain, (unsigned)tx_volume_gain); + transport_data->tx_volume_gain = tx_volume_gain; + tx_volume_gain_changed = true; + } + } + +#if 0 + if (rx_volume_gain_changed || rx_soft_volume_changed) + pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport); + + if (tx_volume_gain_changed || tx_soft_volume_changed) + pa_hook_fire(pa_bluetooth_discovery_hook(transport_data->hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport); +#else + spa_log_debug(backend->log, "RX volume gain changed: %d, soft volume changed: %d", rx_volume_gain_changed, rx_soft_volume_changed); + spa_log_debug(backend->log, "TX volume gain changed: %d, soft volume changed: %d", tx_volume_gain_changed, tx_soft_volume_changed); +#endif + + if (rx_volume_control_changed) + set_rx_volume_gain_property(transport, transport_data->rx_volume_gain); + + if (tx_volume_control_changed) + set_tx_volume_gain_property(transport, transport_data->tx_volume_gain); +} + +static DBusHandlerResult audio_agent_get_property(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata) +{ + const char *interface; + const char *property; + const char *agent_codec; + DBusMessage *r = NULL; + + if (!check_signature(m, "ss")) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call"); + goto fail; + } + + if (dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &interface, + DBUS_TYPE_STRING, &property, + DBUS_TYPE_INVALID) == FALSE) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call"); + goto fail; + } + + if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (!spa_streq(property, "AgentCodec")) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid property in method call"); + goto fail; + } + + if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ)) + agent_codec = HSPHFP_AGENT_CODEC_PCM; + else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC)) + agent_codec = HSPHFP_AGENT_CODEC_MSBC; + else { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call"); + goto fail; + } + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + +fail: + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult audio_agent_getall_properties(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata) +{ + const char *interface; + DBusMessageIter iter, array, dict, data; + const char *agent_codec_key = "AgentCodec"; + const char *agent_codec; + DBusMessage *r = NULL; + + if (!check_signature(m, "s")) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call"); + goto fail; + } + + if (dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &interface, + DBUS_TYPE_INVALID) == FALSE) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid arguments in method call"); + goto fail; + } + + if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ)) + agent_codec = HSPHFP_AGENT_CODEC_PCM; + else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC)) + agent_codec = HSPHFP_AGENT_CODEC_MSBC; + else { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid path in method call"); + goto fail; + } + + if (!spa_streq(interface, HSPHFPD_AUDIO_AGENT_INTERFACE)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array); + dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &dict); + dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &agent_codec_key); + dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "s", &data); + dbus_message_iter_append_basic(&data, DBUS_TYPE_BOOLEAN, &agent_codec); + dbus_message_iter_close_container(&dict, &data); + dbus_message_iter_close_container(&array, &dict); + dbus_message_iter_close_container(&iter, &array); + +fail: + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult hsphfpd_new_audio_connection(DBusConnection *conn, DBusMessage *m, const char *path, void *userdata) +{ + struct impl *backend = userdata; + DBusMessageIter arg_i; + const char *transport_path; + int fd; + const char *sender; + const char *endpoint_path = NULL; + const char *air_codec = NULL; + enum hsphfpd_volume_control rx_volume_control = 0; + enum hsphfpd_volume_control tx_volume_control = 0; + uint16_t rx_volume_gain = -1; + uint16_t tx_volume_gain = -1; + uint16_t mtu = 0; + unsigned int codec; + struct hsphfpd_endpoint *endpoint; + struct spa_bt_transport *transport; + struct hsphfpd_transport_data *transport_data; + DBusMessage *r = NULL; + + if (!check_signature(m, "oha{sv}")) { + r = dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, "Invalid signature in method call"); + goto fail; + } + + if (!dbus_message_iter_init(m, &arg_i)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_iter_get_basic(&arg_i, &transport_path); + dbus_message_iter_next(&arg_i); + dbus_message_iter_get_basic(&arg_i, &fd); + + spa_log_debug(backend->log, "NewConnection %s, fd %d", transport_path, fd); + + sender = dbus_message_get_sender(m); + if (!spa_streq(sender, backend->hsphfpd_service_id)) { + close(fd); + spa_log_error(backend->log, "Sender '%s' is not authorized", sender); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Sender '%s' is not authorized", sender); + goto fail; + } + + if (spa_streq(path, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ)) + codec = HFP_AUDIO_CODEC_CVSD; + else if (spa_streq(path, HSPHFP_AUDIO_CLIENT_MSBC)) + codec = HFP_AUDIO_CODEC_MSBC; + else { + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Invalid path"); + goto fail; + } + + dbus_message_iter_next(&arg_i); + parse_transport_properties_values(backend, transport_path, &arg_i, + &endpoint_path, &air_codec, + &rx_volume_control, &tx_volume_control, + &rx_volume_gain, &tx_volume_gain, + &mtu); + + if (!endpoint_path) { + close(fd); + spa_log_error(backend->log, "Endpoint property was not specified"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "Endpoint property was not specified"); + goto fail; + } + + if (!air_codec) { + close(fd); + spa_log_error(backend->log, "AirCodec property was not specified"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "AirCodec property was not specified"); + goto fail; + } + + if (!rx_volume_control) { + close(fd); + spa_log_error(backend->log, "RxVolumeControl property was not specified"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeControl property was not specified"); + goto fail; + } + + if (!tx_volume_control) { + close(fd); + spa_log_error(backend->log, "TxVolumeControl property was not specified"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeControl property was not specified"); + goto fail; + } + + if (rx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) { + if (rx_volume_gain == (uint16_t)-1) { + close(fd); + spa_log_error(backend->log, "RxVolumeGain property was not specified, but VolumeControl is not none"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "RxVolumeGain property was not specified, but VolumeControl is not none"); + goto fail; + } + } else { + rx_volume_gain = 15; /* No volume control, so set maximal value */ + } + + if (tx_volume_control != HSPHFPD_VOLUME_CONTROL_NONE) { + if (tx_volume_gain == (uint16_t)-1) { + close(fd); + spa_log_error(backend->log, "TxVolumeGain property was not specified, but VolumeControl is not none"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "TxVolumeGain property was not specified, but VolumeControl is not none"); + goto fail; + } + } else { + tx_volume_gain = 15; /* No volume control, so set maximal value */ + } + + if (!mtu) { + close(fd); + spa_log_error(backend->log, "MTU property was not specified"); + r = dbus_message_new_error(m, HSPHFPD_ERROR_REJECTED, "MTU property was not specified"); + goto fail; + } + + endpoint = endpoint_find(backend, endpoint_path); + if (!endpoint) { + close(fd); + spa_log_error(backend->log, "Endpoint %s does not exist", endpoint_path); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s does not exist", endpoint_path); + goto fail; + } + + if (!endpoint->valid) { + close(fd); + spa_log_error(backend->log, "Endpoint %s is not valid", endpoint_path); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not valid", endpoint_path); + goto fail; + } + + transport = spa_bt_transport_find(backend->monitor, endpoint_path); + if (!transport) { + close(fd); + spa_log_error(backend->log, "Endpoint %s is not connected", endpoint_path); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s is not connected", endpoint_path); + goto fail; + } + + if (transport->codec != codec) + spa_log_warn(backend->log, "Expecting codec to be %d, got %d", transport->codec, codec); + + if (transport->fd >= 0) { + close(fd); + spa_log_error(backend->log, "Endpoint %s has already active transport", endpoint_path); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s has already active transport", endpoint_path); + goto fail; + } + + transport_data = transport->user_data; + transport_data->transport_path = strdup(transport_path); + transport_data->rx_soft_volume = (rx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE); + transport_data->tx_soft_volume = (tx_volume_control != HSPHFPD_VOLUME_CONTROL_REMOTE); + transport_data->rx_volume_gain = rx_volume_gain; + transport_data->tx_volume_gain = tx_volume_gain; + transport_data->rx_volume_control = rx_volume_control; + transport_data->tx_volume_control = tx_volume_control; + +#if 0 + pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_RX_VOLUME_GAIN_CHANGED), transport); + pa_hook_fire(pa_bluetooth_discovery_hook(hsphfpd->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_TX_VOLUME_GAIN_CHANGED), transport); +#endif + + transport->read_mtu = mtu; + transport->write_mtu = mtu; + + transport->fd = fd; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + +fail: + if (r) { + DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED; + if (!dbus_connection_send(backend->conn, r, NULL)) + res = DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_unref(r); + return res; + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult audio_agent_endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + DBusMessage *r; + DBusHandlerResult res; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = AUDIO_AGENT_ENDPOINT_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) + res = audio_agent_get_property(c, m, path, userdata); + else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) + res = audio_agent_getall_properties(c, m, path, userdata); + else if (dbus_message_is_method_call(m, HSPHFPD_AUDIO_AGENT_INTERFACE, "NewConnection")) + res = hsphfpd_new_audio_connection(c, m, path, userdata); + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void append_audio_agent_object(DBusMessageIter *iter, const char *endpoint, const char *agent_codec) +{ + const char *interface_name = HSPHFPD_AUDIO_AGENT_INTERFACE; + DBusMessageIter object, array, entry, dict, codec, data; + char *str = "AgentCodec"; + + dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object); + dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint); + + dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array); + + dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name); + + dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict); + + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &codec); + dbus_message_iter_append_basic(&codec, DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&codec, DBUS_TYPE_VARIANT, "s", &data); + dbus_message_iter_append_basic(&data, DBUS_TYPE_STRING, &agent_codec); + dbus_message_iter_close_container(&codec, &data); + dbus_message_iter_close_container(&dict, &codec); + + dbus_message_iter_close_container(&entry, &dict); + dbus_message_iter_close_container(&array, &entry); + dbus_message_iter_close_container(&object, &array); + dbus_message_iter_close_container(iter, &object); +} + +static DBusHandlerResult application_object_manager_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + DBusMessage *r; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = APPLICATION_OBJECT_MANAGER_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects")) { + DBusMessageIter iter, array; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_iter_init_append(r, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array); + + append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ, HSPHFP_AGENT_CODEC_PCM); + if (backend->msbc_supported) + append_audio_agent_object(&array, HSPHFP_AUDIO_CLIENT_MSBC, HSPHFP_AGENT_CODEC_MSBC); + + dbus_message_iter_close_container(&iter, &array); + } else + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void hsphfpd_audio_acquire_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + const char *transport_path; + const char *service_id; + const char *agent_path; + DBusError error; + + dbus_error_init(&error); + + backend->acquire_in_progress = false; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterApplication() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) { + spa_log_error(backend->log, "Reply for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() from invalid sender"); + goto finish; + } + + if (!check_signature(r, "oso")) { + spa_log_error(backend->log, "Invalid reply signature for " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio()"); + goto finish; + } + + if (dbus_message_get_args(r, &error, + DBUS_TYPE_OBJECT_PATH, &transport_path, + DBUS_TYPE_STRING, &service_id, + DBUS_TYPE_OBJECT_PATH, &agent_path, + DBUS_TYPE_INVALID) == FALSE) { + spa_log_error(backend->log, "Failed to parse " HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() reply: %s", error.message); + goto finish; + } + + if (!spa_streq(service_id, dbus_bus_get_unique_name(backend->conn))) { + spa_log_warn(backend->log, HSPHFPD_ENDPOINT_INTERFACE ".ConnectAudio() failed: Other audio application took audio socket"); + goto finish; + } + + spa_log_debug(backend->log, "hsphfpd audio acquired"); + +finish: + dbus_message_unref(r); + dbus_pending_call_unref(pending); +} + +static int hsphfpd_audio_acquire(void *data, bool optional) +{ + struct spa_bt_transport *transport = data; + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + DBusMessage *m; + const char *air_codec = HSPHFP_AIR_CODEC_CVSD; + const char *agent_codec = HSPHFP_AGENT_CODEC_PCM; + DBusPendingCall *call; + DBusError err; + + spa_log_debug(backend->log, "transport %p: Acquire %s", + transport, transport->path); + + if (backend->acquire_in_progress) + return -EINPROGRESS; + + if (transport->codec == HFP_AUDIO_CODEC_MSBC) { + air_codec = HSPHFP_AIR_CODEC_MSBC; + agent_codec = HSPHFP_AGENT_CODEC_MSBC; + } + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, + transport->path, + HSPHFPD_ENDPOINT_INTERFACE, + "ConnectAudio"); + if (m == NULL) + return -ENOMEM; + dbus_message_append_args(m, DBUS_TYPE_STRING, &air_codec, DBUS_TYPE_STRING, &agent_codec, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + + dbus_connection_send_with_reply(backend->conn, m, &call, -1); + dbus_pending_call_set_notify(call, hsphfpd_audio_acquire_reply, backend, NULL); + dbus_message_unref(m); + + /* The ConnectAudio method triggers Introspect and NewConnection calls, + which will set the fd to use for the SCO data. + We need to run the DBus loop to be able to reply to those method calls */ + backend->acquire_in_progress = true; + while (backend->acquire_in_progress && dbus_connection_read_write_dispatch(backend->conn, -1)) + ; // empty loop body + + return 0; +} + +static int hsphfpd_audio_release(void *data) +{ + struct spa_bt_transport *transport = data; + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + struct hsphfpd_transport_data *transport_data = transport->user_data; + + spa_log_debug(backend->log, "transport %p: Release %s", + transport, transport->path); + + if (transport->sco_io) { + spa_bt_sco_io_destroy(transport->sco_io); + transport->sco_io = NULL; + } + + /* shutdown to make sure connection is dropped immediately */ + shutdown(transport->fd, SHUT_RDWR); + close(transport->fd); + if (transport_data->transport_path) { + free(transport_data->transport_path); + transport_data->transport_path = NULL; + } + transport->fd = -1; + + return 0; +} + +static int hsphfpd_audio_destroy(void *data) +{ + struct spa_bt_transport *transport = data; + struct hsphfpd_transport_data *transport_data = transport->user_data; + + if (transport_data->transport_path) { + free(transport_data->transport_path); + transport_data->transport_path = NULL; + } + + return 0; +} + +static const struct spa_bt_transport_implementation hsphfpd_transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = hsphfpd_audio_acquire, + .release = hsphfpd_audio_release, + .destroy = hsphfpd_audio_destroy, +}; + +static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, struct hsphfpd_endpoint *endpoint, DBusMessageIter *i) +{ + DBusMessageIter element_i; + struct spa_bt_device *d; + struct spa_bt_transport *t; + + dbus_message_iter_recurse(i, &element_i); + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i, value_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &dict_i); + dbus_message_iter_get_basic(&dict_i, &key); + dbus_message_iter_next(&dict_i); + dbus_message_iter_recurse(&dict_i, &value_i); + switch (dbus_message_iter_get_arg_type(&value_i)) { + case DBUS_TYPE_STRING: + { + const char *value; + dbus_message_iter_get_basic(&value_i, &value); + if (spa_streq(key, "RemoteAddress")) + endpoint->remote_address = strdup(value); + else if (spa_streq(key, "LocalAddress")) + endpoint->local_address = strdup(value); + else if (spa_streq(key, "Profile")) { + if (endpoint->profile) + spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key); + else if (spa_streq(value, "headset")) + endpoint->profile = HSPHFPD_PROFILE_HEADSET; + else if (spa_streq(value, "handsfree")) + endpoint->profile = HSPHFPD_PROFILE_HANDSFREE; + else + spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value); + } else if (spa_streq(key, "Role")) { + if (endpoint->role) + spa_log_warn(backend->log, "Endpoint %s received a duplicate '%s' property, ignoring", endpoint->path, key); + else if (spa_streq(value, "client")) + endpoint->role = HSPHFPD_ROLE_CLIENT; + else if (spa_streq(value, "gateway")) + endpoint->role = HSPHFPD_ROLE_GATEWAY; + else + spa_log_warn(backend->log, "Endpoint %s received invalid '%s' property value '%s', ignoring", endpoint->path, key, value); + } + spa_log_trace(backend->log, " %s: %s (%p)", key, value, endpoint); + } + break; + + case DBUS_TYPE_BOOLEAN: + { + bool value; + dbus_message_iter_get_basic(&value_i, &value); + if (spa_streq(key, "Connected")) + endpoint->connected = value; + spa_log_trace(backend->log, " %s: %d", key, value); + } + break; + + case DBUS_TYPE_ARRAY: + { + if (spa_streq(key, "AudioCodecs")) { + DBusMessageIter array_i; + const char *value; + + endpoint->air_codecs = 0; + dbus_message_iter_recurse(&value_i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + dbus_message_iter_get_basic(&array_i, &value); + if (spa_streq(value, HSPHFP_AIR_CODEC_CVSD)) + endpoint->air_codecs |= HFP_AUDIO_CODEC_CVSD; + if (spa_streq(value, HSPHFP_AIR_CODEC_MSBC)) + endpoint->air_codecs |= HFP_AUDIO_CODEC_MSBC; + dbus_message_iter_next(&array_i); + } + } + } + break; + } + + dbus_message_iter_next(&element_i); + } + + if (!endpoint->valid && endpoint->local_address && endpoint->remote_address && endpoint->profile && endpoint->role) + endpoint->valid = true; + + if (!endpoint->remote_address || !endpoint->local_address) { + spa_log_debug(backend->log, "Missing addresses for %s", endpoint->path); + return DBUS_HANDLER_RESULT_HANDLED; + } + + d = spa_bt_device_find_by_address(backend->monitor, endpoint->remote_address, endpoint->local_address); + if (!d || !d->adapter) { + spa_log_debug(backend->log, "No device for %s", endpoint->path); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if ((t = spa_bt_transport_find(backend->monitor, endpoint->path)) != NULL) { + /* Release transport on disconnection, or when mSBC is supported if there + is an update of the remote codecs */ + if (!endpoint->connected || (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC) && t->codec == HFP_AUDIO_CODEC_CVSD)) { + spa_bt_transport_free(t); + spa_bt_device_check_profiles(d, false); + spa_log_debug(backend->log, "Transport released for %s", endpoint->path); + } else { + spa_log_debug(backend->log, "Transport already configured for %s", endpoint->path); + return DBUS_HANDLER_RESULT_HANDLED; + } + } + + if (!endpoint->valid || !endpoint->connected) + return DBUS_HANDLER_RESULT_HANDLED; + + char *t_path = strdup(endpoint->path); + t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct hsphfpd_transport_data)); + if (t == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + free(t_path); + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + spa_bt_transport_set_implementation(t, &hsphfpd_transport_impl, t); + + t->device = d; + spa_list_append(&t->device->transport_list, &t->device_link); + t->backend = &backend->this; + t->profile = SPA_BT_PROFILE_NULL; + if (endpoint->profile == HSPHFPD_PROFILE_HEADSET) { + if (endpoint->role == HSPHFPD_ROLE_CLIENT) + t->profile = SPA_BT_PROFILE_HSP_HS; + else if (endpoint->role == HSPHFPD_ROLE_GATEWAY) + t->profile = SPA_BT_PROFILE_HSP_AG; + } else if (endpoint->profile == HSPHFPD_PROFILE_HANDSFREE) { + if (endpoint->role == HSPHFPD_ROLE_CLIENT) + t->profile = SPA_BT_PROFILE_HFP_HF; + else if (endpoint->role == HSPHFPD_ROLE_GATEWAY) + t->profile = SPA_BT_PROFILE_HFP_AG; + } + if (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC)) + t->codec = HFP_AUDIO_CODEC_MSBC; + else + t->codec = HFP_AUDIO_CODEC_CVSD; + + t->n_channels = 1; + t->channels[0] = SPA_AUDIO_CHANNEL_MONO; + + spa_bt_device_add_profile(d, t->profile); + spa_bt_device_connect_profile(t->device, t->profile); + + spa_log_debug(backend->log, "Transport %s available for hsphfpd", endpoint->path); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult hsphfpd_parse_interfaces(struct impl *backend, DBusMessageIter *dict_i) +{ + DBusMessageIter element_i; + const char *path; + + spa_assert(backend); + spa_assert(dict_i); + + dbus_message_iter_get_basic(dict_i, &path); + dbus_message_iter_next(dict_i); + dbus_message_iter_recurse(dict_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter iface_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &iface_i); + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + + if (spa_streq(interface, HSPHFPD_ENDPOINT_INTERFACE)) { + struct hsphfpd_endpoint *endpoint; + + endpoint = endpoint_find(backend, path); + if (!endpoint) { + endpoint = calloc(1, sizeof(struct hsphfpd_endpoint)); + endpoint->path = strdup(path); + spa_list_append(&backend->endpoint_list, &endpoint->link); + spa_log_debug(backend->log, "Found endpoint %s", path); + } + hsphfpd_parse_endpoint_properties(backend, endpoint, &iface_i); + } else + spa_log_debug(backend->log, "Unknown interface %s found, skipping", interface); + + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void hsphfpd_get_endpoints_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + DBusMessageIter i, array_i; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Failed to get a list of endpoints from hsphfpd: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!spa_streq(dbus_message_get_sender(r), backend->hsphfpd_service_id)) { + spa_log_error(backend->log, "Reply for GetManagedObjects() from invalid sender"); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !check_signature(r, "a{oa{sa{sv}}}")) { + spa_log_error(backend->log, "Invalid arguments in GetManagedObjects() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&array_i, &dict_i); + hsphfpd_parse_interfaces(backend, &dict_i); + dbus_message_iter_next(&array_i); + } + + backend->endpoints_listed = true; + +finish: + dbus_message_unref(r); + dbus_pending_call_unref(pending); +} + +static int backend_hsphfpd_register(void *data) +{ + struct impl *backend = data; + DBusMessage *m, *r; + const char *path = APPLICATION_OBJECT_MANAGER_PATH; + DBusPendingCall *call; + DBusError err; + int res; + + spa_log_debug(backend->log, "Registering to hsphfpd"); + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/", + HSPHFPD_APPLICATION_MANAGER_INTERFACE, "RegisterApplication"); + if (m == NULL) + return -ENOMEM; + + dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + + if (r == NULL) { + if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) { + spa_log_info(backend->log, "hsphfpd not available: %s", + err.message); + res = -ENOTSUP; + } else { + spa_log_warn(backend->log, "Registering application %s failed: %s (%s)", + path, err.message, err.name); + res = -EIO; + } + dbus_error_free(&err); + return res; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterApplication() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + dbus_message_unref(r); + + backend->hsphfpd_service_id = strdup(dbus_message_get_sender(r)); + + spa_log_debug(backend->log, "Registered to hsphfpd"); + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/", + DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects"); + if (m == NULL) + goto finish; + + dbus_connection_send_with_reply(backend->conn, m, &call, -1); + dbus_pending_call_set_notify(call, hsphfpd_get_endpoints_reply, backend, NULL); + dbus_message_unref(m); + + return 0; + +finish: + dbus_message_unref(r); + return -EIO; +} + +static int backend_hsphfpd_unregistered(void *data) +{ + struct impl *backend = data; + struct hsphfpd_endpoint *endpoint; + + if (backend->hsphfpd_service_id) { + free(backend->hsphfpd_service_id); + backend->hsphfpd_service_id = NULL; + } + backend->endpoints_listed = false; + spa_list_consume(endpoint, &backend->endpoint_list, link) + endpoint_free(endpoint); + + return 0; +} + +static DBusHandlerResult hsphfpd_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + const char *sender; + struct impl *backend = user_data; + DBusError err; + + dbus_error_init(&err); + + sender = dbus_message_get_sender(m); + + if (backend->hsphfpd_service_id && spa_streq(sender, backend->hsphfpd_service_id)) { + if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesAdded")) { + DBusMessageIter arg_i; + + spa_log_warn(backend->log, "sender: %s", dbus_message_get_sender(m)); + + if (!backend->endpoints_listed) + goto finish; + + if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oa{sa{sv}}")) { + spa_log_error(backend->log, "Invalid signature found in InterfacesAdded"); + goto finish; + } + + hsphfpd_parse_interfaces(backend, &arg_i); + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, "InterfacesRemoved")) { + const char *path; + DBusMessageIter arg_i, element_i; + + if (!backend->endpoints_listed) + goto finish; + + if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "oas")) { + spa_log_error(backend->log, "Invalid signature found in InterfacesRemoved"); + goto finish; + } + + dbus_message_iter_get_basic(&arg_i, &path); + dbus_message_iter_next(&arg_i); + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(&element_i, &iface); + + if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) { + struct hsphfpd_endpoint *endpoint; + struct spa_bt_transport *transport = spa_bt_transport_find(backend->monitor, path); + + if (transport) + spa_bt_transport_free(transport); + + spa_log_debug(backend->log, "Remove endpoint %s", path); + endpoint = endpoint_find(backend, path); + if (endpoint) + endpoint_free(endpoint); + } + + dbus_message_iter_next(&element_i); + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged")) { + DBusMessageIter arg_i; + const char *iface; + const char *path; + + if (!backend->endpoints_listed) + goto finish; + + if (!dbus_message_iter_init(m, &arg_i) || !check_signature(m, "sa{sv}as")) { + spa_log_error(backend->log, "Invalid signature found in PropertiesChanged"); + goto finish; + } + + dbus_message_iter_get_basic(&arg_i, &iface); + dbus_message_iter_next(&arg_i); + + path = dbus_message_get_path(m); + + if (spa_streq(iface, HSPHFPD_ENDPOINT_INTERFACE)) { + struct hsphfpd_endpoint *endpoint = endpoint_find(backend, path); + if (!endpoint) { + spa_log_warn(backend->log, "Properties changed on unknown endpoint %s", path); + goto finish; + } + spa_log_debug(backend->log, "Properties changed on endpoint %s", path); + hsphfpd_parse_endpoint_properties(backend, endpoint, &arg_i); + } else if (spa_streq(iface, HSPHFPD_AUDIO_TRANSPORT_INTERFACE)) { + struct spa_bt_transport *transport = spa_bt_transport_find_full(backend->monitor, + hsphfpd_cmp_transport_path, + (const void *)path); + if (!transport) { + spa_log_warn(backend->log, "Properties changed on unknown transport %s", path); + goto finish; + } + spa_log_debug(backend->log, "Properties changed on transport %s", path); + hsphfpd_parse_transport_properties(backend, transport, &arg_i); + } + } + } + +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int add_filters(void *data) +{ + struct impl *backend = data; + DBusError err; + + if (backend->filters_added) + return 0; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(backend->conn, hsphfpd_filter_cb, backend, NULL)) { + spa_log_error(backend->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(backend->conn, + "type='signal',sender='" HSPHFPD_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesAdded'", &err); + dbus_bus_add_match(backend->conn, + "type='signal',sender='" HSPHFPD_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='InterfacesRemoved'", &err); + dbus_bus_add_match(backend->conn, + "type='signal',sender='" HSPHFPD_SERVICE "'," + "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged'," + "arg0='" HSPHFPD_ENDPOINT_INTERFACE "'", &err); + dbus_bus_add_match(backend->conn, + "type='signal',sender='" HSPHFPD_SERVICE "'," + "interface='" DBUS_INTERFACE_PROPERTIES "',member='PropertiesChanged'," + "arg0='" HSPHFPD_AUDIO_TRANSPORT_INTERFACE "'", &err); + + backend->filters_added = true; + + return 0; + +fail: + dbus_error_free(&err); + return -EIO; +} + +static int backend_hsphfpd_free(void *data) +{ + struct impl *backend = data; + struct hsphfpd_endpoint *endpoint; + + if (backend->filters_added) { + dbus_connection_remove_filter(backend->conn, hsphfpd_filter_cb, backend); + backend->filters_added = false; + } + + if (backend->msbc_supported) + dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC); + dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ); + dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH); + + spa_list_consume(endpoint, &backend->endpoint_list, link) + endpoint_free(endpoint); + + free(backend); + + return 0; +} + +static const struct spa_bt_backend_implementation backend_impl = { + SPA_VERSION_BT_BACKEND_IMPLEMENTATION, + .free = backend_hsphfpd_free, + .register_profiles = backend_hsphfpd_register, + .unregister_profiles = backend_hsphfpd_unregistered, +}; + +static bool is_available(struct impl *backend) +{ + DBusMessage *m, *r; + DBusError err; + bool success = false; + + m = dbus_message_new_method_call(HSPHFPD_SERVICE, "/", + DBUS_INTERFACE_INTROSPECTABLE, "Introspect"); + if (m == NULL) + return false; + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + + if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN) + success = true; + + if (r) + dbus_message_unref(r); + else + dbus_error_free(&err); + + return success; +} + +struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *backend; + const char *str; + static const DBusObjectPathVTable vtable_application_object_manager = { + .message_function = application_object_manager_handler, + }; + static const DBusObjectPathVTable vtable_audio_agent_endpoint = { + .message_function = audio_agent_endpoint_handler, + }; + + backend = calloc(1, sizeof(struct impl)); + if (backend == NULL) + return NULL; + + spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); + + backend->this.name = "hsphfpd"; + backend->this.exclusive = true; + backend->monitor = monitor; + backend->quirks = quirks; + backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); + backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + backend->conn = dbus_connection; + if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc"))) + backend->msbc_supported = spa_atob(str); + else + backend->msbc_supported = false; + + spa_log_topic_init(backend->log, &log_topic); + + spa_list_init(&backend->endpoint_list); + + if (!dbus_connection_register_object_path(backend->conn, + APPLICATION_OBJECT_MANAGER_PATH, + &vtable_application_object_manager, backend)) { + free(backend); + return NULL; + } + + if (!dbus_connection_register_object_path(backend->conn, + HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ, + &vtable_audio_agent_endpoint, backend)) { + dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH); + free(backend); + return NULL; + } + + if (backend->msbc_supported && !dbus_connection_register_object_path(backend->conn, + HSPHFP_AUDIO_CLIENT_MSBC, + &vtable_audio_agent_endpoint, backend)) { + dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ); + dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH); + free(backend); + return NULL; + } + + if (add_filters(backend) < 0) { + dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_MSBC); + dbus_connection_unregister_object_path(backend->conn, HSPHFP_AUDIO_CLIENT_PCM_S16LE_8KHZ); + dbus_connection_unregister_object_path(backend->conn, APPLICATION_OBJECT_MANAGER_PATH); + free(backend); + return NULL; + } + + backend->this.available = is_available(backend); + + return &backend->this; +} diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c new file mode 100644 index 0000000..5eec82c --- /dev/null +++ b/spa/plugins/bluez5/backend-native.c @@ -0,0 +1,2838 @@ +/* Spa HSP/HFP native backend + * + * Copyright © 2018 Wim Taymans + * Copyright © 2021 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.native"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles" + +#define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000 +#define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000 + +#define INTERNATIONAL_NUMBER 145 +#define NATIONAL_NUMBER 129 + +#define MAX_HF_INDICATORS 16 + +enum { + HFP_AG_INITIAL_CODEC_SETUP_NONE = 0, + HFP_AG_INITIAL_CODEC_SETUP_SEND, + HFP_AG_INITIAL_CODEC_SETUP_WAIT +}; + +#define CIND_INDICATORS "(\"service\",(0-1)),(\"call\",(0-1)),(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5)),(\"roam\",(0-1)),(\"battchg\",(0-5))" +enum { + CIND_SERVICE = 1, + CIND_CALL, + CIND_CALLSETUP, + CIND_CALLHELD, + CIND_SIGNAL, + CIND_ROAM, + CIND_BATTERY_LEVEL, + CIND_MAX +}; + +struct modem { + bool network_has_service; + unsigned int signal_strength; + bool network_is_roaming; + char *operator_name; + char *own_number; + bool active_call; + unsigned int call_setup; +}; + +struct impl { + struct spa_bt_backend this; + + struct spa_bt_monitor *monitor; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_system *main_system; + struct spa_loop_utils *loop_utils; + struct spa_dbus *dbus; + DBusConnection *conn; + +#define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG) + enum spa_bt_profile enabled_profiles; + + struct spa_source sco; + + const struct spa_bt_quirks *quirks; + + struct spa_list rfcomm_list; + unsigned int defer_setup_enabled:1; + + struct modem modem; + unsigned int battery_level; + + void *modemmanager; + struct spa_source *ring_timer; + void *upower; +}; + +struct transport_data { + struct rfcomm *rfcomm; + struct spa_source sco; +}; + +enum hfp_hf_state { + hfp_hf_brsf, + hfp_hf_bac, + hfp_hf_cind1, + hfp_hf_cind2, + hfp_hf_cmer, + hfp_hf_slc1, + hfp_hf_slc2, + hfp_hf_vgs, + hfp_hf_vgm, + hfp_hf_bcs +}; + +enum hsp_hs_state { + hsp_hs_init1, + hsp_hs_init2, + hsp_hs_vgs, + hsp_hs_vgm, +}; + +struct rfcomm_volume { + bool active; + int hw_volume; +}; + +struct rfcomm { + struct spa_list link; + struct spa_source source; + struct impl *backend; + struct spa_bt_device *device; + struct spa_hook device_listener; + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + enum spa_bt_profile profile; + struct spa_source timer; + struct spa_source *volume_sync_timer; + char* path; + bool has_volume; + struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM]; + unsigned int broken_mic_hw_volume:1; +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + unsigned int slc_configured:1; + unsigned int codec_negotiation_supported:1; + unsigned int msbc_supported_by_hfp:1; + unsigned int hfp_ag_switching_codec:1; + unsigned int hfp_ag_initial_codec_setup:2; + unsigned int cind_call_active:1; + unsigned int cind_call_notify:1; + unsigned int extended_error_reporting:1; + unsigned int clip_notify:1; + enum hfp_hf_state hf_state; + enum hsp_hs_state hs_state; + unsigned int codec; + uint32_t cind_enabled_indicators; + char *hf_indicators[MAX_HF_INDICATORS]; +#endif +}; + +static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + DBusMessage *r; + + r = dbus_message_new_error(m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", + "Method not implemented"); + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void transport_destroy(void *data) +{ + struct rfcomm *rfcomm = data; + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "transport %p destroy", rfcomm->transport); + rfcomm->transport = NULL; +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, +}; + +static const struct spa_bt_transport_implementation sco_transport_impl; + +static struct spa_bt_transport *_transport_create(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct spa_bt_transport *t = NULL; + struct transport_data *td; + char* pathfd; + + if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL) + return NULL; + + t = spa_bt_transport_create(backend->monitor, pathfd, sizeof(struct transport_data)); + if (t == NULL) + goto finish; + spa_bt_transport_set_implementation(t, &sco_transport_impl, t); + + t->device = rfcomm->device; + spa_list_append(&t->device->transport_list, &t->device_link); + t->profile = rfcomm->profile; + t->backend = &backend->this; + t->n_channels = 1; + t->channels[0] = SPA_AUDIO_CHANNEL_MONO; + + td = t->user_data; + td->rfcomm = rfcomm; + + if (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) { + t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME; + t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME; + } else { + t->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME; + t->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME; + } + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) { + t->volumes[i].active = rfcomm->volumes[i].active; + t->volumes[i].hw_volume_max = SPA_BT_VOLUME_HS_MAX; + if (rfcomm->volumes[i].active && rfcomm->volumes[i].hw_volume != SPA_BT_VOLUME_INVALID) + t->volumes[i].volume = + spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t->volumes[i].hw_volume_max); + } + + spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm); + +finish: + return t; +} + +static int codec_switch_stop_timer(struct rfcomm *rfcomm); + +static void volume_sync_stop_timer(struct rfcomm *rfcomm); + +static void rfcomm_free(struct rfcomm *rfcomm) +{ + codec_switch_stop_timer(rfcomm); + for (int i = 0; i < MAX_HF_INDICATORS; i++) { + if (rfcomm->hf_indicators[i]) { + free(rfcomm->hf_indicators[i]); + } + } + spa_list_remove(&rfcomm->link); + if (rfcomm->path) + free(rfcomm->path); + if (rfcomm->transport) { + spa_hook_remove(&rfcomm->transport_listener); + spa_bt_transport_free(rfcomm->transport); + } + if (rfcomm->device) { + spa_bt_device_report_battery_level(rfcomm->device, SPA_BT_NO_BATTERY); + spa_hook_remove(&rfcomm->device_listener); + rfcomm->device = NULL; + } + if (rfcomm->source.fd >= 0) { + if (rfcomm->source.loop) + spa_loop_remove_source(rfcomm->source.loop, &rfcomm->source); + shutdown(rfcomm->source.fd, SHUT_RDWR); + close (rfcomm->source.fd); + rfcomm->source.fd = -1; + } + if (rfcomm->volume_sync_timer) + spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer); + free(rfcomm); +} + +#define RFCOMM_MESSAGE_MAX_LENGTH 256 + +/* from HF/HS to AG */ +SPA_PRINTF_FUNC(2, 3) +static ssize_t rfcomm_send_cmd(const struct rfcomm *rfcomm, const char *format, ...) +{ + struct impl *backend = rfcomm->backend; + char message[RFCOMM_MESSAGE_MAX_LENGTH + 1]; + ssize_t len; + va_list args; + + va_start(args, format); + len = vsnprintf(message, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args); + va_end(args); + + if (len < 0) + return -EINVAL; + + if (len > RFCOMM_MESSAGE_MAX_LENGTH) + return -E2BIG; + + spa_log_debug(backend->log, "RFCOMM >> %s", message); + + /* + * The format of an AT command from the HF to the AG shall be: + * - 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)); + } + + 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 = + spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max); + } + + spa_bt_transport_emit_volume_changed(rfcomm->transport); +} + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE +static bool rfcomm_hsp_ag(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int gain, dummy; + + /* There are only three HSP AT commands: + * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain. + * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain. + * AT+CKPD=200: Sent by HS when headset button is pressed. */ + if (sscanf(buf, "AT+VGS=%d", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + rfcomm_send_reply(rfcomm, "ERROR"); + } + } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + if (!rfcomm->broken_mic_hw_volume) + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + rfcomm_send_reply(rfcomm, "ERROR"); + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + } + } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) { + rfcomm_send_reply(rfcomm, "OK"); + } else { + return false; + } + + return true; +} + +static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id) +{ + struct spa_bt_transport_volume *t_volume; + const char *format; + int hw_volume; + + if (!rfcomm_volume_enabled(rfcomm)) + return false; + + t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL; + + if (!(t_volume && t_volume->active)) + return false; + + hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max); + rfcomm->volumes[id].hw_volume = hw_volume; + + if (id == SPA_BT_VOLUME_ID_TX) + format = "AT+VGM"; + else if (id == SPA_BT_VOLUME_ID_RX) + format = "AT+VGS"; + else + spa_assert_not_reached(); + + rfcomm_send_cmd(rfcomm, "%s=%d", format, hw_volume); + + return true; +} + +static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int gain; + + /* There are only three HSP AT result codes: + * +VGS=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGS command + * or when the gain is changed on the AG side. + * +VGM=value: value between 0 and 15, sent by AG to HS as a response to an AT+VGM command + * or when the gain is changed on the AG side. + * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because + * it does not expect a reply. */ + if (sscanf(buf, "\r\n+VGS=%d\r\n", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + } + } else if (sscanf(buf, "\r\n+VGM=%d\r\n", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + } + } else if (spa_strstartswith(buf, "\r\nOK\r\n")) { + if (rfcomm->hs_state == hsp_hs_init2) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hs_state = hsp_hs_vgs; + else + rfcomm->hs_state = hsp_hs_init1; + } else if (rfcomm->hs_state == hsp_hs_vgs) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) + rfcomm->hs_state = hsp_hs_vgm; + else + rfcomm->hs_state = hsp_hs_init1; + } + } + + return true; +} +#endif + +#ifdef HAVE_LIBUSB +static bool check_usb_altsetting_6(struct impl *backend, uint16_t vendor_id, uint16_t product_id) +{ + libusb_context *ctx = NULL; + struct libusb_config_descriptor *cfg = NULL; + libusb_device **devices = NULL; + + ssize_t ndev, idev; + int res; + bool ok = false; + + if ((res = libusb_init(&ctx)) < 0) { + ctx = NULL; + goto fail; + } + + if ((ndev = libusb_get_device_list(ctx, &devices)) < 0) { + res = ndev; + devices = NULL; + goto fail; + } + + for (idev = 0; idev < ndev; ++idev) { + libusb_device *dev = devices[idev]; + struct libusb_device_descriptor desc; + int icfg; + + libusb_get_device_descriptor(dev, &desc); + if (vendor_id != desc.idVendor || product_id != desc.idProduct) + continue; + + /* Check the device has Bluetooth isoch. altsetting 6 interface */ + + for (icfg = 0; icfg < desc.bNumConfigurations; ++icfg) { + int iiface; + + if ((res = libusb_get_config_descriptor(dev, icfg, &cfg)) != 0) { + cfg = NULL; + goto fail; + } + + for (iiface = 0; iiface < cfg->bNumInterfaces; ++iiface) { + const struct libusb_interface *iface = &cfg->interface[iiface]; + int ialt; + + for (ialt = 0; ialt < iface->num_altsetting; ++ialt) { + const struct libusb_interface_descriptor *idesc = &iface->altsetting[ialt]; + int iep; + + if (idesc->bInterfaceClass != LIBUSB_CLASS_WIRELESS || + idesc->bInterfaceSubClass != 1 /* RF */ || + idesc->bInterfaceProtocol != 1 /* Bluetooth */ || + idesc->bAlternateSetting != 6) + continue; + + for (iep = 0; iep < idesc->bNumEndpoints; ++iep) { + const struct libusb_endpoint_descriptor *ep = &idesc->endpoint[iep]; + if ((ep->bmAttributes & 0x3) == 0x1 /* isochronous */) { + ok = true; + goto done; + } + } + } + } + + libusb_free_config_descriptor(cfg); + cfg = NULL; + } + } + +done: + if (cfg) + libusb_free_config_descriptor(cfg); + if (devices) + libusb_free_device_list(devices, 0); + if (ctx) + libusb_exit(ctx); + return ok; + +fail: + spa_log_info(backend->log, "failed to acquire USB device info: %d (%s)", + res, libusb_strerror(res)); + ok = false; + goto done; +} +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + +static bool device_supports_required_mSBC_transport_modes( + struct impl *backend, struct spa_bt_device *device) +{ + int res; + bool msbc_ok, msbc_alt1_ok; + uint32_t bt_features; + + if (device->adapter == NULL) + return false; + + if (backend->quirks && spa_bt_quirks_get_features(backend->quirks, device->adapter, device, &bt_features) == 0) { + msbc_ok = bt_features & SPA_BT_FEATURE_MSBC; + msbc_alt1_ok = bt_features & (SPA_BT_FEATURE_MSBC_ALT1 | SPA_BT_FEATURE_MSBC_ALT1_RTL); + } else { + msbc_ok = true; + msbc_alt1_ok = true; + } + + spa_log_info(backend->log, + "bluez-monitor/hardware.conf: msbc:%d msbc-alt1:%d", (int)msbc_ok, (int)msbc_alt1_ok); + + if (!msbc_ok && !msbc_alt1_ok) + return false; + + res = spa_bt_adapter_has_msbc(device->adapter); + if (res < 0) { + spa_log_warn(backend->log, + "adapter %s: failed to determine msbc/esco capability (%d)", + device->adapter->path, res); + } else if (res == 0) { + spa_log_info(backend->log, + "adapter %s: no msbc/esco transport", + device->adapter->path); + return false; + } else { + spa_log_debug(backend->log, + "adapter %s: has msbc/esco transport", + device->adapter->path); + } + + /* Check if USB ALT6 is really available on the device */ + if (device->adapter->bus_type == BUS_TYPE_USB && !msbc_alt1_ok && msbc_ok) { +#ifdef HAVE_LIBUSB + if (device->adapter->source_id == SOURCE_ID_USB) { + msbc_ok = check_usb_altsetting_6(backend, device->adapter->vendor_id, + device->adapter->product_id); + } else { + msbc_ok = false; + } + if (!msbc_ok) + spa_log_info(backend->log, "bluetooth host adapter does not support USB ALT6"); +#else + spa_log_info(backend->log, + "compiled without libusb; can't check if bluetooth adapter has USB ALT6"); + msbc_ok = false; +#endif + } + if (device->adapter->bus_type != BUS_TYPE_USB) + msbc_alt1_ok = false; + + return msbc_ok || msbc_alt1_ok; +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec); + +static void process_iphoneaccev_indicator(struct rfcomm *rfcomm, unsigned int key, unsigned int value) +{ + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "key:%u value:%u", key, value); + + switch (key) { + case SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL: { + // Battery level is reported in range of 0-9, convert to 10-100% + uint8_t level = (SPA_CLAMP(value, 0u, 9u) + 1) * 10; + spa_log_debug(backend->log, "battery level: %d%%", (int) level); + + // TODO: report without Battery Provider (using props) + spa_bt_device_report_battery_level(rfcomm->device, level); + break; + } + case SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE: + break; + default: + spa_log_warn(backend->log, "unknown AT+IPHONEACCEV key:%u value:%u", key, value); + break; + } +} + +static void process_hfp_hf_indicator(struct rfcomm *rfcomm, unsigned int indicator, unsigned int value) +{ + struct impl *backend = rfcomm->backend; + + spa_log_debug(backend->log, "indicator:%u value:%u", indicator, value); + + switch (indicator) { + case SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY: + break; + case SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL: + // Battery level is reported in range 0-100 + spa_log_debug(backend->log, "battery level: %u%%", value); + + if (value <= 100) { + // TODO: report without Battery Provider (using props) + spa_bt_device_report_battery_level(rfcomm->device, value); + } else { + spa_log_warn(backend->log, "battery HF indicator %u outside of range [0, 100]: %u", indicator, value); + } + break; + default: + spa_log_warn(backend->log, "unknown HF indicator:%u value:%u", indicator, value); + break; + } +} + +static void rfcomm_hfp_ag_set_cind(struct rfcomm *rfcomm, bool call_active) +{ + if (rfcomm->profile != SPA_BT_PROFILE_HFP_HF) + return; + + if (call_active == rfcomm->cind_call_active) + return; + + rfcomm->cind_call_active = call_active; + + if (!rfcomm->cind_call_notify) + return; + + rfcomm_send_reply(rfcomm, "+CIEV: 2,%d", rfcomm->cind_call_active); +} + +static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int features; + unsigned int gain; + unsigned int count, r; + unsigned int selected_codec; + unsigned int indicator; + unsigned int indicator_value; + unsigned int value; + int xapl_vendor; + int xapl_product; + int xapl_features; + + spa_debug_log_mem(backend->log, SPA_LOG_LEVEL_DEBUG, 2, buf, strlen(buf)); + + /* Some devices send initial \n: be permissive */ + while (*buf == '\n') + ++buf; + + if (sscanf(buf, "AT+BRSF=%u", &features) == 1) { + unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE; + + /* + * Determine device volume control. Some headsets only support control of + * TX volume, but not RX, even if they have a microphone. Determine this + * separately based on whether we also get AT+VGS/AT+VGM, and quirks. + */ + rfcomm->has_volume = (features & SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL); + + /* Decide if we want to signal that the computer supports mSBC negotiation + This should be done when the computers bluetooth adapter supports the necessary transport mode */ + if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) { + + /* set the feature bit that indicates AG (=computer) supports codec negotiation */ + ag_features |= SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION; + + /* let's see if the headset supports codec negotiation */ + if ((features & (SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION)) != 0) { + spa_log_debug(backend->log, + "RFCOMM features = %i, codec negotiation supported by headset", + features); + /* Prepare reply: Audio Gateway (=computer) supports codec negotiation */ + rfcomm->codec_negotiation_supported = true; + rfcomm->msbc_supported_by_hfp = false; + } else { + /* Codec negotiation not supported */ + spa_log_debug(backend->log, + "RFCOMM features = %i, codec negotiation NOT supported by headset", + features); + + rfcomm->codec_negotiation_supported = false; + rfcomm->msbc_supported_by_hfp = false; + } + } + + /* send reply to HF with the features supported by Audio Gateway (=computer) */ + ag_features |= mm_supported_features(); + ag_features |= SPA_BT_HFP_AG_FEATURE_HF_INDICATORS; + rfcomm_send_reply(rfcomm, "+BRSF: %u", ag_features); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BAC=")) { + /* retrieve supported codecs */ + /* response has the form AT+BAC=,, + strategy: split the string into tokens */ + + char* token; + int cntr = 0; + + while ((token = strsep(&buf, "=,"))) { + unsigned int codec_id; + + /* skip token 0 i.e. the "AT+BAC=" part */ + if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) { + spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id); + if (codec_id == HFP_AUDIO_CODEC_MSBC) { + rfcomm->msbc_supported_by_hfp = true; + spa_log_debug(backend->log, "RFCOMM headset supports mSBC codec"); + } + } + cntr++; + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CIND=?")) { + rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CIND?")) { + rfcomm_send_reply(rfcomm, "+CIND: %d,%d,%d,0,%d,%d,%d", backend->modem.network_has_service, + backend->modem.active_call, backend->modem.call_setup, backend->modem.signal_strength, + backend->modem.network_is_roaming, backend->battery_level); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CMER")) { + int mode, keyp, disp, ind; + + rfcomm->slc_configured = true; + rfcomm_send_reply(rfcomm, "OK"); + + rfcomm->cind_call_active = false; + if (sscanf(buf, "AT+CMER= %d , %d , %d , %d", &mode, &keyp, &disp, &ind) == 4) + rfcomm->cind_call_notify = ind ? true : false; + else + rfcomm->cind_call_notify = false; + + /* switch codec to mSBC by sending unsolicited +BCS message */ + if (rfcomm->codec_negotiation_supported && rfcomm->msbc_supported_by_hfp) { + spa_log_debug(backend->log, "RFCOMM initial codec setup"); + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND; + rfcomm_send_reply(rfcomm, "+BCS: 2"); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC); + } else { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID); + } + } + } else if (spa_streq(buf, "\r")) { + /* No commands, reply OK (ITU-T Rec. V.250 Sec. 5.2.1 & 5.6) */ + rfcomm_send_reply(rfcomm, "OK"); + } else if (!rfcomm->slc_configured) { + spa_log_warn(backend->log, "RFCOMM receive command before SLC completed: %s", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + + /* ***** + * Following commands requires a Service Level Connection + * ***** */ + + } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) { + /* parse BCS(=Bluetooth Codec Selection) reply */ + bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL); + rfcomm->hfp_ag_switching_codec = false; + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; + codec_switch_stop_timer(rfcomm); + volume_sync_stop_timer(rfcomm); + + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) { + spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); + return true; + } + + rfcomm->codec = selected_codec; + + spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec); + + /* Recreate transport, since previous connection may now be invalid */ + if (rfcomm->transport) + spa_bt_transport_free(rfcomm->transport); + + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, -ENOMEM); + return true; + } + rfcomm->transport->codec = selected_codec; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + rfcomm_emit_volume_changed(rfcomm, -1, SPA_BT_VOLUME_INVALID); + + rfcomm_send_reply(rfcomm, "OK"); + if (was_switching_codec) + spa_bt_device_emit_codec_switched(rfcomm->device, 0); + } else if (spa_strstartswith(buf, "AT+BIA=")) { + /* retrieve indicators activation + * form: AT+BIA=[indrep1],[indrep2],[indrepx] */ + char *str = buf + 7; + unsigned int ind = 1; + + while (*str && ind < CIND_MAX && *str != '\r' && *str != '\n') { + if (*str == ',') { + ind++; + goto next_indicator; + } + + /* Ignore updates to mandantory indicators which are always ON */ + if (ind == CIND_CALL || ind == CIND_CALLSETUP || ind == CIND_CALLHELD) + goto next_indicator; + + switch (*str) { + case '0': + rfcomm->cind_enabled_indicators &= ~(1 << ind); + break; + case '1': + rfcomm->cind_enabled_indicators |= (1 << ind); + break; + default: + spa_log_warn(backend->log, "Unsupported entry in %s: %c", buf, *str); + } +next_indicator: + str++; + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CLCC")) { + struct spa_list *calls; + struct call *call; + unsigned int type; + + if (backend->modemmanager) { + calls = mm_get_calls(backend->modemmanager); + spa_list_for_each(call, calls, link) { + if (!call->number) { + rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u", call->index, call->direction, call->state, call->multiparty); + } else { + if (spa_strstartswith(call->number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + rfcomm_send_reply(rfcomm, "+CLCC: %u,%u,%u,0,%u,\"%s\",%d", call->index, call->direction, call->state, + call->multiparty, call->number, type); + } + } + } + + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+CLIP=%u", &value) == 1) { + if (value > 1) { + spa_log_debug(backend->log, "Unsupported AT+CLIP value: %u", value); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + rfcomm->clip_notify = value; + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+CMEE=%u", &value) == 1) { + if (value > 1) { + spa_log_debug(backend->log, "Unsupported AT+CMEE value: %u", value); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + rfcomm->extended_error_reporting = value; + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+CNUM")) { + if (backend->modem.own_number) { + unsigned int type; + if (spa_strstartswith(backend->modem.own_number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + rfcomm_send_reply(rfcomm, "+CNUM: ,\"%s\",%u", backend->modem.own_number, type); + } + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+COPS=")) { + unsigned int mode, val; + + if (sscanf(buf, "AT+COPS=%u,%u", &mode, &val) != 2 || + mode != 3 || val != 0) { + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + } else { + rfcomm_send_reply(rfcomm, "OK"); + } + } else if (spa_strstartswith(buf, "AT+COPS?")) { + if (!backend->modem.network_has_service) { + rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE); + } else { + if (backend->modem.operator_name) + rfcomm_send_reply(rfcomm, "+COPS: 0,0,\"%s\"", backend->modem.operator_name); + else + rfcomm_send_reply(rfcomm, "+COPS: 0,,"); + rfcomm_send_reply(rfcomm, "OK"); + } + } else if (sscanf(buf, "AT+VGM=%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + if (!rfcomm->broken_mic_hw_volume) + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED); + } + } else if (sscanf(buf, "AT+VGS=%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + rfcomm_send_reply(rfcomm, "OK"); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_ALLOWED); + } + } else if (spa_strstartswith(buf, "AT+BIND=?")) { + rfcomm_send_reply(rfcomm, "+BIND: (2)"); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BIND?")) { + rfcomm_send_reply(rfcomm, "+BIND: 2,1"); + rfcomm_send_reply(rfcomm, "OK"); + } else if (spa_strstartswith(buf, "AT+BIND=")) { + // BIND=... should return a comma separated list of indicators and + // 2 should be among the other numbers telling that battery charge + // is supported + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+BIEV=%u,%u", &indicator, &indicator_value) == 2) { + process_hfp_hf_indicator(rfcomm, indicator, indicator_value); + } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%*[^,],%u", &xapl_vendor, &xapl_product, &xapl_features) == 3) { + if (xapl_features & SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING) { + /* claim, that we support battery status reports */ + rfcomm_send_reply(rfcomm, "+XAPL=iPhone,%u", SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING); + } + rfcomm_send_reply(rfcomm, "OK"); + } else if (sscanf(buf, "AT+IPHONEACCEV=%u%n", &count, &r) == 1) { + if (count < 1 || count > 100) + return false; + + buf += r; + + for (unsigned int i = 0; i < count; i++) { + unsigned int key, value; + + if (sscanf(buf, " , %u , %u%n", &key, &value, &r) != 2) + return false; + + process_iphoneaccev_indicator(rfcomm, key, value); + buf += r; + } + } else if (spa_strstartswith(buf, "AT+APLSIRI?")) { + // This command is sent when we activate Apple extensions + rfcomm_send_reply(rfcomm, "OK"); + } else if (!mm_is_available(backend->modemmanager)) { + spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf); + rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE); + return true; + + /* ***** + * Following commands requires a Service Level Connection + * and acces to a modem + * ***** */ + + } else if (!backend->modem.network_has_service) { + spa_log_warn(backend->log, "RFCOMM receive command but network not available: %s", buf); + rfcomm_send_error(rfcomm, CMEE_NO_NETWORK_SERVICE); + return true; + + /* ***** + * Following commands requires a Service Level Connection, + * acces to a modem and to the network + * ***** */ + + } else if (spa_strstartswith(buf, "ATA")) { + enum cmee_error error; + + if (!mm_answer_call(backend->modemmanager, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "ATD")) { + char number[31], sep; + enum cmee_error error; + + if (sscanf(buf, "ATD%30[^;]%c", number, &sep) != 2 || sep != ';') { + spa_log_debug(backend->log, "Failed to parse ATD: \"%s\"", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + if (!mm_do_call(backend->modemmanager, number, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "AT+CHUP")) { + enum cmee_error error; + + if (!mm_hangup_call(backend->modemmanager, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else if (spa_strstartswith(buf, "AT+VTS=")) { + char *dtmf; + enum cmee_error error; + + dtmf = calloc(1, 2); + if (sscanf(buf, "AT+VTS=%1s", dtmf) != 1) { + spa_log_debug(backend->log, "Failed to parse AT+VTS: \"%s\"", buf); + rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); + return true; + } + + if (!mm_send_dtmf(backend->modemmanager, dtmf, rfcomm, &error)) { + rfcomm_send_error(rfcomm, error); + return true; + } + } else { + return false; + } + + return true; +} + +static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* buf) +{ + struct impl *backend = rfcomm->backend; + unsigned int features, gain, selected_codec, indicator, value; + char* token; + + while ((token = strsep(&buf, "\r\n"))) { + if (sscanf(token, "+BRSF:%u", &features) == 1) { + if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) && + rfcomm->msbc_supported_by_hfp) + rfcomm->codec_negotiation_supported = true; + } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) { + if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC) { + spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); + } else { + spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec); + + /* send codec selection to AG */ + rfcomm_send_cmd(rfcomm, "AT+BCS=%u", selected_codec); + + rfcomm->hf_state = hfp_hf_bcs; + + if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) { + if (rfcomm->transport) + spa_bt_transport_free(rfcomm->transport); + + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = selected_codec; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + } + } else if (sscanf(token, "+VGM%*1[:=]%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_TX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", token); + } + } else if (sscanf(token, "+VGS%*1[:=]%u", &gain) == 1) { + if (gain <= SPA_BT_VOLUME_HS_MAX) { + rfcomm_emit_volume_changed(rfcomm, SPA_BT_VOLUME_ID_RX, gain); + } else { + spa_log_debug(backend->log, "RFCOMM receive unsupported VGS gain: %s", token); + } + } else if (spa_strstartswith(token, "+CIND: (")) { + uint8_t i = 1; + while (strstr(token, "\"")) { + token += strcspn(token, "\"") + 1; + token[strcspn(token, "\"")] = 0; + rfcomm->hf_indicators[i] = strdup(token); + token += strcspn(token, "\"") + 1; + i++; + if (i == MAX_HF_INDICATORS) { + break; + } + } + } else if (spa_strstartswith(token, "+CIND: ")) { + token[strcspn(token, "\r")] = 0; + token[strcspn(token, "\n")] = 0; + token += strlen("+CIND: "); + uint8_t i = 1; + while (strlen(token)) { + if (i >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[i]) { + break; + } + token[strcspn(token, ",")] = 0; + spa_log_info(backend->log, "AG indicator state: %s = %i", rfcomm->hf_indicators[i], atoi(token)); + + if (spa_streq(rfcomm->hf_indicators[i], "battchg")) { + spa_bt_device_report_battery_level(rfcomm->device, atoi(token) * 100 / 5); + } + + token += strcspn(token, "\0") + 1; + i++; + } + } else if (sscanf(token, "+CIEV: %u,%u", &indicator, &value) == 2) { + if (indicator >= MAX_HF_INDICATORS || !rfcomm->hf_indicators[indicator]) { + spa_log_warn(backend->log, "indicator %u has not been registered, ignoring", indicator); + } else { + spa_log_info(backend->log, "AG indicator update: %s = %u", rfcomm->hf_indicators[indicator], value); + + if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) { + spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5); + } + } + } else if (spa_strstartswith(token, "OK")) { + switch(rfcomm->hf_state) { + case hfp_hf_brsf: + if (rfcomm->codec_negotiation_supported) { + rfcomm_send_cmd(rfcomm, "AT+BAC=1,2"); + rfcomm->hf_state = hfp_hf_bac; + } else { + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + } + break; + case hfp_hf_bac: + rfcomm_send_cmd(rfcomm, "AT+CIND=?"); + rfcomm->hf_state = hfp_hf_cind1; + break; + case hfp_hf_cind1: + rfcomm_send_cmd(rfcomm, "AT+CIND?"); + rfcomm->hf_state = hfp_hf_cind2; + break; + case hfp_hf_cind2: + rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1"); + rfcomm->hf_state = hfp_hf_cmer; + break; + case hfp_hf_cmer: + rfcomm->hf_state = hfp_hf_slc1; + rfcomm->slc_configured = true; + if (!rfcomm->codec_negotiation_supported) { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + // TODO: We should manage the missing transport + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + /* Report volume on SLC establishment */ + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + break; + case hfp_hf_slc2: + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + break; + case hfp_hf_vgs: + rfcomm->hf_state = hfp_hf_slc1; + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) + rfcomm->hf_state = hfp_hf_vgm; + break; + default: + break; + } + } + } + + return true; +} + +#endif + +static void rfcomm_event(struct spa_source *source) +{ + struct rfcomm *rfcomm = source->data; + struct impl *backend = rfcomm->backend; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_info(backend->log, "lost RFCOMM connection."); + rfcomm_free(rfcomm); + return; + } + + if (source->rmask & SPA_IO_IN) { + char buf[512]; + ssize_t len; + bool res = false; + + len = read(source->fd, buf, 511); + if (len < 0) { + spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno)); + return; + } + buf[len] = 0; + spa_log_debug(backend->log, "RFCOMM << %s", buf); + + switch (rfcomm->profile) { +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + case SPA_BT_PROFILE_HSP_HS: + res = rfcomm_hsp_ag(rfcomm, buf); + break; + case SPA_BT_PROFILE_HSP_AG: + res = rfcomm_hsp_hs(rfcomm, buf); + break; +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + case SPA_BT_PROFILE_HFP_HF: + res = rfcomm_hfp_ag(rfcomm, buf); + break; + case SPA_BT_PROFILE_HFP_AG: + res = rfcomm_hfp_hf(rfcomm, buf); + break; +#endif + default: + break; + } + + if (!res) { + spa_log_debug(backend->log, "RFCOMM received unsupported command: %s", buf); + rfcomm_send_error(rfcomm, CMEE_OPERATION_NOT_SUPPORTED); + } + } +} + +static int sco_create_socket(struct impl *backend, struct spa_bt_adapter *adapter, bool msbc) +{ + struct sockaddr_sco addr; + socklen_t len; + bdaddr_t src; + int sock = -1; + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO); + if (sock < 0) { + spa_log_error(backend->log, "socket(SEQPACKET, SCO) %s", strerror(errno)); + goto fail; + } + + str2ba(adapter->address, &src); + + len = sizeof(addr); + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &src); + + if (bind(sock, (struct sockaddr *) &addr, len) < 0) { + spa_log_error(backend->log, "bind(): %s", strerror(errno)); + goto fail; + } + + spa_log_debug(backend->log, "msbc=%d", (int)msbc); + if (msbc) { + /* set correct socket options for mSBC */ + struct bt_voice voice_config; + memset(&voice_config, 0, sizeof(voice_config)); + voice_config.setting = BT_VOICE_TRANSPARENT; + if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) { + spa_log_error(backend->log, "setsockopt(): %s", strerror(errno)); + goto fail; + } + } + + return sock; + +fail: + if (sock >= 0) + close(sock); + return -1; +} + +static int sco_do_connect(struct spa_bt_transport *t) +{ + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + struct spa_bt_device *d = t->device; + struct transport_data *td = t->user_data; + struct sockaddr_sco addr; + socklen_t len; + int err; + int sock; + bdaddr_t dst; + int retry = 2; + + spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u", + t, t->codec); + + if (d->adapter == NULL) + return -EIO; + + str2ba(d->address, &dst); + +again: + sock = sco_create_socket(backend, d->adapter, (t->codec == HFP_AUDIO_CODEC_MSBC)); + if (sock < 0) + return -1; + + len = sizeof(addr); + memset(&addr, 0, len); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, &dst); + + spa_log_debug(backend->log, "transport %p: doing connect", t); + err = connect(sock, (struct sockaddr *) &addr, len); + if (err < 0 && errno == ECONNABORTED && retry-- > 0) { + spa_log_warn(backend->log, "connect(): %s. Remaining retry:%d", + strerror(errno), retry); + close(sock); + goto again; + } else if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) { + spa_log_error(backend->log, "connect(): %s", strerror(errno)); +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (errno == EOPNOTSUPP && t->codec == HFP_AUDIO_CODEC_MSBC && + td->rfcomm->msbc_supported_by_hfp) { + /* Adapter doesn't support msbc. Renegotiate. */ + d->adapter->msbc_probed = true; + d->adapter->has_msbc = false; + td->rfcomm->msbc_supported_by_hfp = false; + if (t->profile == SPA_BT_PROFILE_HFP_HF) { + td->rfcomm->hfp_ag_switching_codec = true; + rfcomm_send_reply(td->rfcomm, "+BCS: 1"); + } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { + rfcomm_send_cmd(td->rfcomm, "AT+BAC=1"); + } + } +#endif + goto fail_close; + } + + return sock; + +fail_close: + close(sock); + return -1; +} + +static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later); + +static void wait_for_socket(int fd) +{ + struct pollfd fds[1]; + const int timeout_ms = 500; + + fds[0].fd = fd; + fds[0].events = POLLIN | POLLERR | POLLHUP; + poll(fds, 1, timeout_ms); +} + +static int sco_acquire_cb(void *data, bool optional) +{ + struct spa_bt_transport *t = data; + struct transport_data *td = t->user_data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + int sock; + socklen_t len; + + spa_log_debug(backend->log, "transport %p: enter sco_acquire_cb", t); + + if (optional || t->fd > 0) + sock = t->fd; + else + sock = sco_do_connect(t); + + if (sock < 0) + goto fail; + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + rfcomm_hfp_ag_set_cind(td->rfcomm, true); +#endif + + /* + * Send RFCOMM volume after connection is ready, and also after + * a timeout. + * + * Some headsets adjust their HFP volume when in A2DP mode + * without reporting via RFCOMM to us, so the volume level can + * be out of sync, and we can't know what it is. Moreover, they may + * take the first +VGS command after connection only partially + * into account, and need a long enough timeout. + * + * E.g. with Sennheiser HD-250BT, the first +VGS changes the + * actual volume, but does not update the level in the hardware + * volume buttons, which is updated by an +VGS event only after + * sufficient time is elapsed from the connection. + */ + wait_for_socket(sock); + rfcomm_ag_sync_volume(td->rfcomm, false); + rfcomm_ag_sync_volume(td->rfcomm, true); + + t->fd = sock; + + /* Fallback value */ + t->read_mtu = 48; + t->write_mtu = 48; + + if (true) { + struct sco_options sco_opt; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + + if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) + spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults"); + else { + spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu); + t->read_mtu = sco_opt.mtu; + t->write_mtu = sco_opt.mtu; + } + } + spa_log_debug(backend->log, "transport %p: read_mtu=%u, write_mtu=%u", t, t->read_mtu, t->write_mtu); + + return 0; + +fail: + return -1; +} + +static int sco_release_cb(void *data) +{ + struct spa_bt_transport *t = data; + struct transport_data *td = t->user_data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + + spa_log_info(backend->log, "Transport %s released", t->path); + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + rfcomm_hfp_ag_set_cind(td->rfcomm, false); +#endif + + if (t->sco_io) { + spa_bt_sco_io_destroy(t->sco_io); + t->sco_io = NULL; + } + + if (t->fd > 0) { + /* Shutdown and close the socket */ + shutdown(t->fd, SHUT_RDWR); + close(t->fd); + t->fd = -1; + } + + return 0; +} + +static void sco_event(struct spa_source *source) +{ + struct spa_bt_transport *t = source->data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno)); + if (t->fd >= 0) { + if (source->loop) + spa_loop_remove_source(source->loop, source); + shutdown(t->fd, SHUT_RDWR); + close (t->fd); + t->fd = -1; + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE); + } + } +} + +static void sco_listen_event(struct spa_source *source) +{ + struct impl *backend = source->data; + struct sockaddr_sco addr; + socklen_t addrlen; + int sock = -1; + char local_address[18], remote_address[18]; + struct rfcomm *rfcomm; + struct spa_bt_transport *t = NULL; + struct transport_data *td; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_error(backend->log, "error listening SCO connection: %s", strerror(errno)); + goto fail; + } + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + spa_log_debug(backend->log, "doing accept"); + sock = accept(source->fd, (struct sockaddr *) &addr, &addrlen); + if (sock < 0) { + if (errno != EAGAIN) + spa_log_error(backend->log, "SCO accept(): %s", strerror(errno)); + goto fail; + } + + ba2str(&addr.sco_bdaddr, remote_address); + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + if (getsockname(sock, (struct sockaddr *) &addr, &addrlen) < 0) { + spa_log_error(backend->log, "SCO getsockname(): %s", strerror(errno)); + goto fail; + } + + ba2str(&addr.sco_bdaddr, local_address); + + /* Find transport for local and remote address */ + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->transport && spa_streq(rfcomm->transport->device->address, remote_address) && + spa_streq(rfcomm->transport->device->adapter->address, local_address)) { + t = rfcomm->transport; + break; + } + } + if (!t) { + spa_log_debug(backend->log, "No transport for adapter %s and remote %s", + local_address, remote_address); + goto fail; + } + + /* The Synchronous Connection shall always be established by the AG, i.e. the remote profile + should be a HSP AG or HFP AG profile */ + if ((t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) == 0) { + spa_log_debug(backend->log, "transport %p: Rejecting incoming audio connection to an AG profile", t); + goto fail; + } + + if (t->fd >= 0) { + spa_log_debug(backend->log, "transport %p: Rejecting, audio already connected", t); + goto fail; + } + + spa_log_debug(backend->log, "transport %p: codec=%u", t, t->codec); + if (backend->defer_setup_enabled) { + /* In BT_DEFER_SETUP mode, when a connection is accepted, the listening socket is unblocked but + * the effective connection setup happens only on first receive, allowing to configure the + * accepted socket. */ + char buff; + + if (t->codec == HFP_AUDIO_CODEC_MSBC) { + /* set correct socket options for mSBC */ + struct bt_voice voice_config; + memset(&voice_config, 0, sizeof(voice_config)); + voice_config.setting = BT_VOICE_TRANSPARENT; + if (setsockopt(sock, SOL_BLUETOOTH, BT_VOICE, &voice_config, sizeof(voice_config)) < 0) { + spa_log_error(backend->log, "transport %p: setsockopt(): %s", t, strerror(errno)); + goto fail; + } + } + + /* First read from the accepted socket is non-blocking and returns a zero length buffer. */ + if (read(sock, &buff, 1) == -1) { + spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(errno)); + goto fail; + } + } + + t->fd = sock; + + td = t->user_data; + td->sco.func = sco_event; + td->sco.data = t; + td->sco.fd = sock; + td->sco.mask = SPA_IO_HUP | SPA_IO_ERR; + td->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &td->sco); + + spa_log_debug(backend->log, "transport %p: audio connected", t); + + /* Report initial volume to remote */ + if (t->profile == SPA_BT_PROFILE_HSP_AG) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hs_state = hsp_hs_vgs; + else + rfcomm->hs_state = hsp_hs_init1; + } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { + if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) + rfcomm->hf_state = hfp_hf_vgs; + else + rfcomm->hf_state = hfp_hf_slc1; + } + + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING); + return; + +fail: + if (sock >= 0) + close(sock); + return; +} + +static int sco_listen(struct impl *backend) +{ + struct sockaddr_sco addr; + int sock; + uint32_t defer = 1; + + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, BTPROTO_SCO); + if (sock < 0) { + spa_log_error(backend->log, "socket(SEQPACKET, SCO) %m"); + return -errno; + } + + /* Bind to local address */ + memset(&addr, 0, sizeof(addr)); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, BDADDR_ANY); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + spa_log_error(backend->log, "bind(): %m"); + goto fail_close; + } + + if (setsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, &defer, sizeof(defer)) < 0) { + spa_log_warn(backend->log, "Can't enable deferred setup: %s", strerror(errno)); + backend->defer_setup_enabled = 0; + } else { + backend->defer_setup_enabled = 1; + } + + spa_log_debug(backend->log, "doing listen"); + if (listen(sock, 1) < 0) { + spa_log_error(backend->log, "listen(): %m"); + goto fail_close; + } + + backend->sco.func = sco_listen_event; + backend->sco.data = backend; + backend->sco.fd = sock; + backend->sco.mask = SPA_IO_IN; + backend->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &backend->sco); + + return sock; + +fail_close: + close(sock); + return -1; +} + +static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id) +{ + struct transport_data *td = t->user_data; + struct rfcomm *rfcomm = td->rfcomm; + const char *format; + int value; + + if (!rfcomm_volume_enabled(rfcomm) + || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + || !(rfcomm->has_volume && rfcomm->volumes[id].active)) + return -ENOTSUP; + + value = rfcomm->volumes[id].hw_volume; + + if (id == SPA_BT_VOLUME_ID_RX) + if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF) + format = "+VGM: %d"; + else + format = "+VGM=%d"; + else if (id == SPA_BT_VOLUME_ID_TX) + if (rfcomm->profile & SPA_BT_PROFILE_HFP_HF) + format = "+VGS: %d"; + else + format = "+VGS=%d"; + else + spa_assert_not_reached(); + + if (rfcomm->transport) + rfcomm_send_reply(rfcomm, format, value); + + return 0; +} + +static int sco_set_volume_cb(void *data, int id, float volume) +{ + struct spa_bt_transport *t = data; + struct spa_bt_transport_volume *t_volume = &t->volumes[id]; + struct transport_data *td = t->user_data; + struct rfcomm *rfcomm = td->rfcomm; + int value; + + if (!rfcomm_volume_enabled(rfcomm) + || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + || !(rfcomm->has_volume && rfcomm->volumes[id].active)) + return -ENOTSUP; + + value = spa_bt_volume_linear_to_hw(volume, t_volume->hw_volume_max); + t_volume->volume = volume; + + if (rfcomm->volumes[id].hw_volume == value) + return 0; + rfcomm->volumes[id].hw_volume = value; + + return rfcomm_ag_set_volume(t, id); +} + +static const struct spa_bt_transport_implementation sco_transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = sco_acquire_cb, + .release = sco_release_cb, + .set_volume = sco_set_volume_cb, +}; + +static struct rfcomm *device_find_rfcomm(struct impl *backend, struct spa_bt_device *device) +{ + struct rfcomm *rfcomm; + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->device == device) + return rfcomm; + } + return NULL; +} + +static int backend_native_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL || rfcomm->profile != SPA_BT_PROFILE_HFP_HF) + return -ENOTSUP; + + if (codec == HFP_AUDIO_CODEC_CVSD) + return 1; + + return (codec == HFP_AUDIO_CODEC_MSBC && + (rfcomm->profile == SPA_BT_PROFILE_HFP_AG || + rfcomm->profile == SPA_BT_PROFILE_HFP_HF) && + rfcomm->msbc_supported_by_hfp && + rfcomm->codec_negotiation_supported) ? 1 : 0; +#else + return -ENOTSUP; +#endif +} + +static int codec_switch_stop_timer(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + if (rfcomm->timer.data == NULL) + return 0; + + spa_loop_remove_source(backend->main_loop, &rfcomm->timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + spa_system_close(backend->main_system, rfcomm->timer.fd); + rfcomm->timer.data = NULL; + return 0; +} + +static void volume_sync_stop_timer(struct rfcomm *rfcomm) +{ + if (rfcomm->volume_sync_timer) + spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer, + NULL, NULL, false); +} + +static void volume_sync_timer_event(void *data, uint64_t expirations) +{ + struct rfcomm *rfcomm = data; + + volume_sync_stop_timer(rfcomm); + + if (rfcomm->transport) { + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX); + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX); + } +} + +static int volume_sync_start_timer(struct rfcomm *rfcomm) +{ + struct timespec ts; + const uint64_t timeout = 1500 * SPA_NSEC_PER_MSEC; + + if (rfcomm->volume_sync_timer == NULL) + rfcomm->volume_sync_timer = spa_loop_utils_add_timer(rfcomm->backend->loop_utils, + volume_sync_timer_event, rfcomm); + + if (rfcomm->volume_sync_timer == NULL) + return -EIO; + + ts.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.tv_nsec = timeout % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer, + &ts, NULL, false); + + return 0; +} + +static int rfcomm_ag_sync_volume(struct rfcomm *rfcomm, bool later) +{ + if (rfcomm->transport == NULL) + return -ENOENT; + + if (!later) { + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_TX); + rfcomm_ag_set_volume(rfcomm->transport, SPA_BT_VOLUME_ID_RX); + } else { + volume_sync_start_timer(rfcomm); + } + + return 0; +} + +static void codec_switch_timer_event(struct spa_source *source) +{ + struct rfcomm *rfcomm = source->data; + struct impl *backend = rfcomm->backend; + uint64_t exp; + + if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0) + spa_log_warn(backend->log, "error reading timerfd: %s", strerror(errno)); + + codec_switch_stop_timer(rfcomm); + + spa_log_debug(backend->log, "rfcomm %p: codec switch timeout", rfcomm); + + switch (rfcomm->hfp_ag_initial_codec_setup) { + case HFP_AG_INITIAL_CODEC_SETUP_SEND: + /* Retry codec selection */ + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT; + rfcomm_send_reply(rfcomm, "+BCS: 2"); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + return; + case HFP_AG_INITIAL_CODEC_SETUP_WAIT: + /* Failure, try falling back to CVSD. */ + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; + if (rfcomm->transport == NULL) { + rfcomm->transport = _transport_create(rfcomm); + if (rfcomm->transport == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + } else { + rfcomm->transport->codec = HFP_AUDIO_CODEC_CVSD; + spa_bt_device_connect_profile(rfcomm->device, rfcomm->profile); + } + } + rfcomm_send_reply(rfcomm, "+BCS: 1"); + return; + default: + break; + } + + if (rfcomm->hfp_ag_switching_codec) { + rfcomm->hfp_ag_switching_codec = false; + if (rfcomm->device) + spa_bt_device_emit_codec_switched(rfcomm->device, -EIO); + } +} + +static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec) +{ + struct impl *backend = rfcomm->backend; + struct itimerspec ts; + + spa_log_debug(backend->log, "rfcomm %p: start timer", rfcomm); + if (rfcomm->timer.data == NULL) { + rfcomm->timer.data = rfcomm; + rfcomm->timer.func = codec_switch_timer_event; + rfcomm->timer.fd = spa_system_timerfd_create(backend->main_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + rfcomm->timer.mask = SPA_IO_IN; + rfcomm->timer.rmask = 0; + spa_loop_add_source(backend->main_loop, &rfcomm->timer); + } + ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC; + ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(backend->main_system, rfcomm->timer.fd, 0, &ts, NULL); + return 0; +} + +static int backend_native_ensure_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct impl *backend = data; + struct rfcomm *rfcomm; + int res; + + res = backend_native_supports_codec(data, device, codec); + if (res <= 0) + return -EINVAL; + + rfcomm = device_find_rfcomm(backend, device); + if (rfcomm == NULL) + return -ENOTSUP; + + if (!rfcomm->codec_negotiation_supported) + return -ENOTSUP; + + if (rfcomm->codec == codec) { + spa_bt_device_emit_codec_switched(device, 0); + return 0; + } + + if ((res = rfcomm_send_reply(rfcomm, "+BCS: %u", codec)) < 0) + return res; + + rfcomm->hfp_ag_switching_codec = true; + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + + return 0; +#else + return -ENOTSUP; +#endif +} + +static void device_destroy(void *data) +{ + struct rfcomm *rfcomm = data; + rfcomm_free(rfcomm); +} + +static const struct spa_bt_device_events device_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .destroy = device_destroy, +}; + +static enum spa_bt_profile path_to_profile(const char *path) +{ +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (spa_streq(path, PROFILE_HSP_AG)) + return SPA_BT_PROFILE_HSP_HS; + + if (spa_streq(path, PROFILE_HSP_HS)) + return SPA_BT_PROFILE_HSP_AG; +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (spa_streq(path, PROFILE_HFP_AG)) + return SPA_BT_PROFILE_HFP_HF; + + if (spa_streq(path, PROFILE_HFP_HF)) + return SPA_BT_PROFILE_HFP_AG; +#endif + + return SPA_BT_PROFILE_NULL; +} + +static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + DBusMessage *r; + DBusMessageIter it[5]; + const char *handler, *path; + enum spa_bt_profile profile; + struct rfcomm *rfcomm; + struct spa_bt_device *d; + struct spa_bt_transport *t = NULL; + int fd; + + if (!dbus_message_has_signature(m, "oha{sv}")) { + spa_log_warn(backend->log, "invalid NewConnection() signature"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + handler = dbus_message_get_path(m); + profile = path_to_profile(handler); + if (profile == SPA_BT_PROFILE_NULL) { + spa_log_warn(backend->log, "invalid handler %s", handler); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_init(m, &it[0]); + dbus_message_iter_get_basic(&it[0], &path); + + d = spa_bt_device_find(backend->monitor, path); + if (d == NULL || d->adapter == NULL) { + spa_log_warn(backend->log, "unknown device for path %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + spa_bt_device_add_profile(d, profile); + + dbus_message_iter_next(&it[0]); + dbus_message_iter_get_basic(&it[0], &fd); + + spa_log_debug(backend->log, "NewConnection path=%s, fd=%d, profile %s", path, fd, handler); + + rfcomm = calloc(1, sizeof(struct rfcomm)); + if (rfcomm == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + rfcomm->backend = backend; + rfcomm->profile = profile; + rfcomm->device = d; + rfcomm->path = strdup(path); + rfcomm->source.func = rfcomm_event; + rfcomm->source.data = rfcomm; + rfcomm->source.fd = fd; + rfcomm->source.mask = SPA_IO_IN; + rfcomm->source.rmask = 0; + /* By default all indicators are enabled */ + rfcomm->cind_enabled_indicators = 0xFFFFFFFF; + memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators); + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) { + if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) + rfcomm->volumes[i].active = true; + rfcomm->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID; + } + + spa_bt_device_add_listener(d, &rfcomm->device_listener, &device_events, rfcomm); + spa_loop_add_source(backend->main_loop, &rfcomm->source); + spa_list_append(&backend->rfcomm_list, &rfcomm->link); + + if (profile == SPA_BT_PROFILE_HSP_HS || profile == SPA_BT_PROFILE_HSP_AG) { + t = _transport_create(rfcomm); + if (t == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + goto fail_need_memory; + } + rfcomm->transport = t; + rfcomm->has_volume = rfcomm_volume_enabled(rfcomm); + + if (profile == SPA_BT_PROFILE_HSP_AG) { + rfcomm->hs_state = hsp_hs_init1; + } + + spa_bt_device_connect_profile(t->device, profile); + + spa_log_debug(backend->log, "Transport %s available for profile %s", t->path, handler); + } else if (profile == SPA_BT_PROFILE_HFP_AG) { + /* Start SLC connection */ + unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_NONE; + + /* Decide if we want to signal that the HF supports mSBC negotiation + This should be done when the bluetooth adapter supports the necessary transport mode */ + if (device_supports_required_mSBC_transport_modes(backend, rfcomm->device)) { + /* set the feature bit that indicates HF supports codec negotiation */ + hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION; + rfcomm->msbc_supported_by_hfp = true; + rfcomm->codec_negotiation_supported = false; + } else { + rfcomm->msbc_supported_by_hfp = false; + rfcomm->codec_negotiation_supported = false; + } + + if (rfcomm_volume_enabled(rfcomm)) { + rfcomm->has_volume = true; + hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL; + } + + /* send command to AG with the features supported by Hands-Free */ + rfcomm_send_cmd(rfcomm, "AT+BRSF=%u", hf_features); + + rfcomm->hf_state = hfp_hf_brsf; + } + + if (rfcomm_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) { + uint32_t device_features; + if (spa_bt_quirks_get_features(backend->quirks, d->adapter, d, &device_features) == 0) { + rfcomm->broken_mic_hw_volume = !(device_features & SPA_BT_FEATURE_HW_VOLUME_MIC); + if (rfcomm->broken_mic_hw_volume) + spa_log_debug(backend->log, "microphone HW volume disabled by quirk"); + } + } + + if ((r = dbus_message_new_method_return(m)) == NULL) + goto fail_need_memory; + if (!dbus_connection_send(conn, r, NULL)) + goto fail_need_memory; + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; + +fail_need_memory: + if (rfcomm) + rfcomm_free(rfcomm); + return DBUS_HANDLER_RESULT_NEED_MEMORY; +} + +static DBusHandlerResult profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + DBusMessage *r; + const char *handler, *path; + struct spa_bt_device *d; + enum spa_bt_profile profile = SPA_BT_PROFILE_NULL; + DBusMessageIter it[5]; + struct rfcomm *rfcomm, *rfcomm_tmp; + + if (!dbus_message_has_signature(m, "o")) { + spa_log_warn(backend->log, "invalid RequestDisconnection() signature"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + handler = dbus_message_get_path(m); + profile = path_to_profile(handler); + if (profile == SPA_BT_PROFILE_NULL) { + spa_log_warn(backend->log, "invalid handler %s", handler); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_init(m, &it[0]); + dbus_message_iter_get_basic(&it[0], &path); + + d = spa_bt_device_find(backend->monitor, path); + if (d == NULL || d->adapter == NULL) { + spa_log_warn(backend->log, "unknown device for path %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + spa_list_for_each_safe(rfcomm, rfcomm_tmp, &backend->rfcomm_list, link) { + if (rfcomm->device == d && rfcomm->profile == profile) { + rfcomm_free(rfcomm); + } + } + spa_bt_device_check_profiles(d, false); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + DBusMessage *r; + DBusHandlerResult res; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(backend->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = PROFILE_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release")) + res = profile_release(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection")) + res = profile_request_disconnection(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection")) + res = profile_new_connection(c, m, userdata); + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void register_profile_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + spa_log_warn(backend->log, "Register profile not supported"); + goto finish; + } + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(backend->log, "Error registering profile"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "RegisterProfile() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + finish: + dbus_message_unref(r); + dbus_pending_call_unref(pending); +} + +static int register_profile(struct impl *backend, const char *profile, const char *uuid) +{ + DBusMessage *m; + DBusMessageIter it[4]; + dbus_bool_t autoconnect; + dbus_uint16_t version, chan, features; + char *str; + DBusPendingCall *call; + + if (!(backend->enabled_profiles & spa_bt_profile_from_uuid(uuid))) + return -ECANCELED; + + spa_log_debug(backend->log, "Registering Profile %s %s", profile, uuid); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", + BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile"); + if (m == NULL) + return -ENOMEM; + + dbus_message_iter_init_append(m, &it[0]); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_OBJECT_PATH, &profile); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &uuid); + dbus_message_iter_open_container(&it[0], DBUS_TYPE_ARRAY, "{sv}", &it[1]); + + if (spa_streq(uuid, SPA_BT_UUID_HSP_HS) || + spa_streq(uuid, SPA_BT_UUID_HSP_HS_ALT)) { + + /* In the headset role, the connection will only be initiated from the remote side */ + str = "AutoConnect"; + autoconnect = 0; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "b", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_BOOLEAN, &autoconnect); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + str = "Channel"; + chan = HSP_HS_DEFAULT_CHANNEL; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &chan); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HSP version 1.2 */ + str = "Version"; + version = 0x0102; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } else if (spa_streq(uuid, SPA_BT_UUID_HFP_AG)) { + str = "Features"; + + /* We announce wideband speech support anyway */ + features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HFP version 1.7 */ + str = "Version"; + version = 0x0107; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } else if (spa_streq(uuid, SPA_BT_UUID_HFP_HF)) { + str = "Features"; + + /* We announce wideband speech support anyway */ + features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &features); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + + /* HFP version 1.7 */ + str = "Version"; + version = 0x0107; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); + dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); + dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); + dbus_message_iter_append_basic(&it[3], DBUS_TYPE_UINT16, &version); + dbus_message_iter_close_container(&it[2], &it[3]); + dbus_message_iter_close_container(&it[1], &it[2]); + } + dbus_message_iter_close_container(&it[0], &it[1]); + + dbus_connection_send_with_reply(backend->conn, m, &call, -1); + dbus_pending_call_set_notify(call, register_profile_reply, backend, NULL); + dbus_message_unref(m); + return 0; +} + +static void unregister_profile(struct impl *backend, const char *profile) +{ + DBusMessage *m, *r; + DBusError err; + + spa_log_debug(backend->log, "Unregistering Profile %s", profile); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", + BLUEZ_PROFILE_MANAGER_INTERFACE, "UnregisterProfile"); + if (m == NULL) + return; + + dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &profile, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_info(backend->log, "Unregistering Profile %s failed", profile); + dbus_error_free(&err); + return; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "UnregisterProfile() returned error: %s", dbus_message_get_error_name(r)); + return; + } + + dbus_message_unref(r); +} + +static int backend_native_register_profiles(void *data) +{ + struct impl *backend = data; + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + register_profile(backend, PROFILE_HSP_AG, SPA_BT_UUID_HSP_AG); + register_profile(backend, PROFILE_HSP_HS, SPA_BT_UUID_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + register_profile(backend, PROFILE_HFP_AG, SPA_BT_UUID_HFP_AG); + register_profile(backend, PROFILE_HFP_HF, SPA_BT_UUID_HFP_HF); +#endif + + if (backend->enabled_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + sco_listen(backend); + + return 0; +} + +static void sco_close(struct impl *backend) +{ + if (backend->sco.fd >= 0) { + if (backend->sco.loop) + spa_loop_remove_source(backend->sco.loop, &backend->sco); + shutdown(backend->sco.fd, SHUT_RDWR); + close (backend->sco.fd); + backend->sco.fd = -1; + } +} + +static int backend_native_unregister_profiles(void *data) +{ + struct impl *backend = data; + + sco_close(backend); + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_AG) + unregister_profile(backend, PROFILE_HSP_AG); + if (backend->enabled_profiles & SPA_BT_PROFILE_HSP_HS) + unregister_profile(backend, PROFILE_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_AG) + unregister_profile(backend, PROFILE_HFP_AG); + if (backend->enabled_profiles & SPA_BT_PROFILE_HFP_HF) + unregister_profile(backend, PROFILE_HFP_HF); +#endif + + return 0; +} + +static void send_ciev_for_each_rfcomm(struct impl *backend, int indicator, int value) +{ + struct rfcomm *rfcomm; + + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->slc_configured && + ((indicator == CIND_CALL || indicator == CIND_CALLSETUP || indicator == CIND_CALLHELD) || + (rfcomm->cind_call_notify && (rfcomm->cind_enabled_indicators & (1 << indicator))))) + rfcomm_send_reply(rfcomm, "+CIEV: %d,%d", indicator, value); + } +} + +static void ring_timer_event(void *data, uint64_t expirations) +{ + struct impl *backend = data; + const char *number; + unsigned int type; + struct timespec ts; + const uint64_t timeout = 1 * SPA_NSEC_PER_SEC; + struct rfcomm *rfcomm; + + number = mm_get_incoming_call_number(backend->modemmanager); + if (number) { + if (spa_strstartswith(number, "+")) + type = INTERNATIONAL_NUMBER; + else + type = NATIONAL_NUMBER; + } + + ts.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.tv_nsec = timeout % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, &ts, NULL, false); + + spa_list_for_each(rfcomm, &backend->rfcomm_list, link) { + if (rfcomm->slc_configured) { + rfcomm_send_reply(rfcomm, "RING"); + if (rfcomm->clip_notify && number) + rfcomm_send_reply(rfcomm, "+CLIP: \"%s\",%u", number, type); + } + } +} + +static void set_call_active(bool active, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.active_call != active) { + backend->modem.active_call = active; + send_ciev_for_each_rfcomm(backend, CIND_CALL, active); + } +} + +static void set_call_setup(enum call_setup value, void *user_data) +{ + struct impl *backend = user_data; + enum call_setup old = backend->modem.call_setup; + + if (backend->modem.call_setup != value) { + backend->modem.call_setup = value; + send_ciev_for_each_rfcomm(backend, CIND_CALLSETUP, value); + } + + if (value == CIND_CALLSETUP_INCOMING) { + if (backend->ring_timer == NULL) + backend->ring_timer = spa_loop_utils_add_timer(backend->loop_utils, ring_timer_event, backend); + + if (backend->ring_timer == NULL) { + spa_log_warn(backend->log, "Failed to create ring timer"); + return; + } + + ring_timer_event(backend, 0); + } else if (old == CIND_CALLSETUP_INCOMING) { + spa_loop_utils_update_timer(backend->loop_utils, backend->ring_timer, NULL, NULL, false); + } +} + +void set_battery_level(unsigned int level, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->battery_level != level) { + backend->battery_level = level; + send_ciev_for_each_rfcomm(backend, CIND_BATTERY_LEVEL, level); + } +} + +static void set_modem_operator_name(const char *name, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.operator_name) { + free(backend->modem.operator_name); + backend->modem.operator_name = NULL; + } + + if (name) + backend->modem.operator_name = strdup(name); +} + +static void set_modem_roaming(bool is_roaming, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.network_is_roaming != is_roaming) { + backend->modem.network_is_roaming = is_roaming; + send_ciev_for_each_rfcomm(backend, CIND_ROAM, is_roaming); + } +} + +static void set_modem_own_number(const char *number, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.own_number) { + free(backend->modem.own_number); + backend->modem.own_number = NULL; + } + + if (number) + backend->modem.own_number = strdup(number); +} + +static void set_modem_service(bool available, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.network_has_service != available) { + backend->modem.network_has_service = available; + send_ciev_for_each_rfcomm(backend, CIND_SERVICE, available); + } +} + +static void set_modem_signal_strength(unsigned int strength, void *user_data) +{ + struct impl *backend = user_data; + + if (backend->modem.signal_strength != strength) { + backend->modem.signal_strength = strength; + send_ciev_for_each_rfcomm(backend, CIND_SIGNAL, strength); + } +} + +static void send_cmd_result(bool success, enum cmee_error error, void *user_data) +{ + struct rfcomm *rfcomm = user_data; + + if (success) { + rfcomm_send_reply(rfcomm, "OK"); + return; + } + + rfcomm_send_error(rfcomm, error); +} + +static int backend_native_free(void *data) +{ + struct impl *backend = data; + + struct rfcomm *rfcomm; + + sco_close(backend); + + if (backend->modemmanager) { + mm_unregister(backend); + backend->modemmanager = NULL; + } + + if (backend->upower) { + upower_unregister(backend->upower); + backend->upower = NULL; + } + + if (backend->ring_timer) + spa_loop_utils_destroy_source(backend->loop_utils, backend->ring_timer); + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG); + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS); +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG); + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_HF); +#endif + + spa_list_consume(rfcomm, &backend->rfcomm_list, link) + rfcomm_free(rfcomm); + + if (backend->modem.operator_name) + free(backend->modem.operator_name); + free(backend); + + return 0; +} + +static int parse_headset_roles(struct impl *backend, const struct spa_dict *info) +{ + const char *str; + int profiles = SPA_BT_PROFILE_NULL; + + if (info == NULL || + (str = spa_dict_lookup(info, PROP_KEY_HEADSET_ROLES)) == NULL) + goto fallback; + + profiles = spa_bt_profiles_from_json_array(str); + if (profiles < 0) + goto fallback; + + backend->enabled_profiles = profiles & SPA_BT_PROFILE_HEADSET_AUDIO; + return 0; +fallback: + backend->enabled_profiles = DEFAULT_ENABLED_PROFILES; + return 0; +} + +static const struct spa_bt_backend_implementation backend_impl = { + SPA_VERSION_BT_BACKEND_IMPLEMENTATION, + .free = backend_native_free, + .register_profiles = backend_native_register_profiles, + .unregister_profiles = backend_native_unregister_profiles, + .ensure_codec = backend_native_ensure_codec, + .supports_codec = backend_native_supports_codec, +}; + +static const struct mm_ops mm_ops = { + .send_cmd_result = send_cmd_result, + .set_modem_service = set_modem_service, + .set_modem_signal_strength = set_modem_signal_strength, + .set_modem_operator_name = set_modem_operator_name, + .set_modem_own_number = set_modem_own_number, + .set_modem_roaming = set_modem_roaming, + .set_call_active = set_call_active, + .set_call_setup = set_call_setup, +}; + +struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *backend; + + static const DBusObjectPathVTable vtable_profile = { + .message_function = profile_handler, + }; + + backend = calloc(1, sizeof(struct impl)); + if (backend == NULL) + return NULL; + + spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); + + backend->this.name = "native"; + backend->monitor = monitor; + backend->quirks = quirks; + backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); + backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + backend->conn = dbus_connection; + backend->sco.fd = -1; + + spa_log_topic_init(backend->log, &log_topic); + + spa_list_init(&backend->rfcomm_list); + + if (parse_headset_roles(backend, info) < 0) + goto fail; + +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HSP_AG, + &vtable_profile, backend)) { + goto fail; + } + + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HSP_HS, + &vtable_profile, backend)) { + goto fail1; + } +#endif + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HFP_AG, + &vtable_profile, backend)) { + goto fail2; + } + + if (!dbus_connection_register_object_path(backend->conn, + PROFILE_HFP_HF, + &vtable_profile, backend)) { + goto fail3; + } +#endif + + backend->modemmanager = mm_register(backend->log, backend->conn, info, &mm_ops, backend); + backend->upower = upower_register(backend->log, backend->conn, set_battery_level, backend); + + return &backend->this; + +#ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE +fail3: + dbus_connection_unregister_object_path(backend->conn, PROFILE_HFP_AG); +fail2: +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_HS); +fail1: + dbus_connection_unregister_object_path(backend->conn, PROFILE_HSP_AG); +#endif +fail: + free(backend); + return NULL; +} diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c new file mode 100644 index 0000000..3ba2b03 --- /dev/null +++ b/spa/plugins/bluez5/backend-ofono.c @@ -0,0 +1,947 @@ +/* Spa oFono backend + * + * Copyright © 2020 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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) + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.ofono"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + struct spa_bt_backend this; + + struct spa_bt_monitor *monitor; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_system *main_system; + struct spa_dbus *dbus; + struct spa_loop_utils *loop_utils; + DBusConnection *conn; + + const struct spa_bt_quirks *quirks; + + struct spa_source *timer; + + unsigned int filters_added:1; + unsigned int msbc_supported:1; +}; + +struct transport_data { + struct spa_source sco; + unsigned int broken:1; + unsigned int activated:1; +}; + +#define OFONO_HF_AUDIO_MANAGER_INTERFACE OFONO_SERVICE ".HandsfreeAudioManager" +#define OFONO_HF_AUDIO_CARD_INTERFACE OFONO_SERVICE ".HandsfreeAudioCard" +#define OFONO_HF_AUDIO_AGENT_INTERFACE OFONO_SERVICE ".HandsfreeAudioAgent" + +#define OFONO_AUDIO_CLIENT "/Profile/ofono" + +#define OFONO_INTROSPECT_XML \ + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +#define OFONO_ERROR_INVALID_ARGUMENTS "org.ofono.Error.InvalidArguments" +#define OFONO_ERROR_NOT_IMPLEMENTED "org.ofono.Error.NotImplemented" +#define OFONO_ERROR_IN_USE "org.ofono.Error.InUse" +#define OFONO_ERROR_FAILED "org.ofono.Error.Failed" + +static void ofono_transport_get_mtu(struct impl *backend, struct spa_bt_transport *t) +{ + struct sco_options sco_opt; + socklen_t len; + + /* Fallback values */ + t->read_mtu = 48; + t->write_mtu = 48; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + + if (getsockopt(t->fd, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) + spa_log_warn(backend->log, "getsockopt(SCO_OPTIONS) failed, loading defaults"); + else { + spa_log_debug(backend->log, "autodetected mtu = %u", sco_opt.mtu); + t->read_mtu = sco_opt.mtu; + t->write_mtu = sco_opt.mtu; + } +} + +static struct spa_bt_transport *_transport_create(struct impl *backend, + const char *path, + struct spa_bt_device *device, + enum spa_bt_profile profile, + int codec, + struct spa_callbacks *impl) +{ + struct spa_bt_transport *t = NULL; + char *t_path = strdup(path); + + t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct transport_data)); + if (t == NULL) { + spa_log_warn(backend->log, "can't create transport: %m"); + free(t_path); + goto finish; + } + spa_bt_transport_set_implementation(t, impl, t); + + t->device = device; + spa_list_append(&t->device->transport_list, &t->device_link); + t->backend = &backend->this; + t->profile = profile; + t->codec = codec; + t->n_channels = 1; + t->channels[0] = SPA_AUDIO_CHANNEL_MONO; + +finish: + return t; +} + +static int _audio_acquire(struct impl *backend, const char *path, uint8_t *codec) +{ + DBusMessage *m, *r; + DBusError err; + int ret = 0; + + m = dbus_message_new_method_call(OFONO_SERVICE, path, + OFONO_HF_AUDIO_CARD_INTERFACE, + "Acquire"); + if (m == NULL) + return -ENOMEM; + + dbus_error_init(&err); + + /* + * XXX: We assume here oFono replies. It however can happen that the headset does + * XXX: not properly respond to the codec negotiation RFCOMM commands. + * XXX: oFono (1.34) fails to handle this condition, and will not send DBus reply + * XXX: in this case. The transport acquire API is synchronous, so we can't + * XXX: do better here right now. + */ + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_error(backend->log, "Transport Acquire() failed for transport %s (%s)", + path, err.message); + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Acquire returned error: %s", dbus_message_get_error_name(r)); + ret = -EIO; + goto finish; + } + + if (!dbus_message_get_args(r, &err, + DBUS_TYPE_UNIX_FD, &ret, + DBUS_TYPE_BYTE, codec, + DBUS_TYPE_INVALID)) { + spa_log_error(backend->log, "Failed to parse Acquire() reply: %s", err.message); + dbus_error_free(&err); + ret = -EIO; + goto finish; + } + +finish: + dbus_message_unref(r); + return ret; +} + +static int ofono_audio_acquire(void *data, bool optional) +{ + struct spa_bt_transport *transport = data; + struct transport_data *td = transport->user_data; + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + uint8_t codec; + int ret = 0; + + if (transport->fd >= 0) + goto finish; + if (td->broken) { + ret = -EIO; + goto finish; + } + + spa_bt_device_update_last_bluez_action_time(transport->device); + + ret = _audio_acquire(backend, transport->path, &codec); + if (ret < 0) + goto finish; + + transport->fd = ret; + + if (transport->codec != codec) { + struct timespec ts; + + spa_log_info(backend->log, "transport %p: acquired codec (%d) differs from transport one (%d)", + transport, codec, transport->codec); + + /* shutdown to make sure connection is dropped immediately */ + shutdown(transport->fd, SHUT_RDWR); + close(transport->fd); + transport->fd = -1; + + /* schedule immediate profile update, from main loop */ + transport->codec = codec; + td->broken = true; + ts.tv_sec = 0; + ts.tv_nsec = 1; + spa_loop_utils_update_timer(backend->loop_utils, backend->timer, + &ts, NULL, false); + return -EIO; + } + + td->broken = false; + + spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %d", transport, + transport->path, transport->fd, transport->codec); + + ofono_transport_get_mtu(backend, transport); + ret = 0; + +finish: + return ret; +} + +static int ofono_audio_release(void *data) +{ + struct spa_bt_transport *transport = data; + struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); + + spa_log_debug(backend->log, "transport %p: Release %s", + transport, transport->path); + + if (transport->sco_io) { + spa_bt_sco_io_destroy(transport->sco_io); + transport->sco_io = NULL; + } + + /* shutdown to make sure connection is dropped immediately */ + shutdown(transport->fd, SHUT_RDWR); + close(transport->fd); + transport->fd = -1; + + return 0; +} + +static DBusHandlerResult ofono_audio_card_removed(struct impl *backend, const char *path) +{ + struct spa_bt_transport *transport; + + spa_assert(backend); + spa_assert(path); + + spa_log_debug(backend->log, "card removed: %s", path); + + transport = spa_bt_transport_find(backend->monitor, path); + + if (transport != NULL) { + struct spa_bt_device *device = transport->device; + + spa_log_debug(backend->log, "transport %p: free %s", + transport, transport->path); + + spa_bt_transport_free(transport); + if (device != NULL) + spa_bt_device_check_profiles(device, false); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static const struct spa_bt_transport_implementation ofono_transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = ofono_audio_acquire, + .release = ofono_audio_release, +}; + +bool activate_transport(struct spa_bt_transport *t, const void *data) +{ + struct impl *backend = (void *)data; + struct transport_data *td = t->user_data; + struct timespec ts; + uint64_t now, threshold; + + if (t->backend != &backend->this) + return false; + + /* Check device-specific rate limit */ + spa_system_clock_gettime(backend->main_system, CLOCK_MONOTONIC, &ts); + now = SPA_TIMESPEC_TO_NSEC(&ts); + threshold = t->device->last_bluez_action_time + ACTION_INTERVAL_NSEC; + if (now < threshold) { + ts.tv_sec = (threshold - now) / SPA_NSEC_PER_SEC; + ts.tv_nsec = (threshold - now) % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(backend->loop_utils, backend->timer, + &ts, NULL, false); + return false; + } + + if (!td->activated) { + /* Connect profile */ + spa_log_debug(backend->log, "Transport %s activated", t->path); + td->activated = true; + spa_bt_device_connect_profile(t->device, t->profile); + } + + if (td->broken) { + /* Recreate the transport */ + struct spa_bt_transport *t_copy; + + t_copy = _transport_create(backend, t->path, t->device, + t->profile, t->codec, (struct spa_callbacks *)&ofono_transport_impl); + spa_bt_transport_free(t); + + if (t_copy) + spa_bt_device_connect_profile(t_copy->device, t_copy->profile); + + return true; + } + + return false; +} + +static void activate_transports(struct impl *backend) +{ + while (spa_bt_transport_find_full(backend->monitor, activate_transport, backend)); +} + +static void activate_timer_event(void *userdata, uint64_t expirations) +{ + struct impl *backend = userdata; + spa_loop_utils_update_timer(backend->loop_utils, backend->timer, NULL, NULL, false); + activate_transports(backend); +} + +static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path, DBusMessageIter *props_i) +{ + const char *remote_address = NULL; + const char *local_address = NULL; + struct spa_bt_device *d; + struct spa_bt_transport *t; + struct transport_data *td; + enum spa_bt_profile profile = SPA_BT_PROFILE_HFP_AG; + uint8_t codec = backend->msbc_supported ? + HFP_AUDIO_CODEC_MSBC : HFP_AUDIO_CODEC_CVSD; + + spa_assert(backend); + spa_assert(path); + spa_assert(props_i); + + spa_log_debug(backend->log, "new card: %s", path); + + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key, *value; + char c; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if ((c = dbus_message_iter_get_arg_type(&value_i)) != DBUS_TYPE_STRING) { + spa_log_error(backend->log, "Invalid properties for %s: expected 's', received '%c'", path, c); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_get_basic(&value_i, &value); + + if (spa_streq(key, "RemoteAddress")) { + remote_address = value; + } else if (spa_streq(key, "LocalAddress")) { + local_address = value; + } else if (spa_streq(key, "Type")) { + if (spa_streq(value, "gateway")) + profile = SPA_BT_PROFILE_HFP_HF; + } + + spa_log_debug(backend->log, "%s: %s", key, value); + + dbus_message_iter_next(props_i); + } + + if (!remote_address || !local_address) { + spa_log_error(backend->log, "Missing addresses for %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + d = spa_bt_device_find_by_address(backend->monitor, remote_address, local_address); + if (!d || !d->adapter) { + spa_log_error(backend->log, "Device doesn’t exist for %s", path); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + spa_bt_device_add_profile(d, profile); + + t = _transport_create(backend, path, d, profile, codec, (struct spa_callbacks *)&ofono_transport_impl); + if (t == NULL) { + spa_log_error(backend->log, "failed to create transport: %s", spa_strerror(-errno)); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + td = t->user_data; + + /* + * For HF profile, delay profile connect, so that we likely don't do it at the + * same time as the device is busy with A2DP connect. This avoids some oFono + * misbehavior (see comment in _audio_acquire above). + * + * For AG mode, we delay the emission of the nodes, so it is not necessary + * to know the codec in advance. + */ + if (profile == SPA_BT_PROFILE_HFP_HF) { + struct timespec ts; + ts.tv_sec = INITIAL_INTERVAL_NSEC / SPA_NSEC_PER_SEC; + ts.tv_nsec = INITIAL_INTERVAL_NSEC % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(backend->loop_utils, backend->timer, + &ts, NULL, false); + } else { + td->activated = true; + spa_bt_device_connect_profile(t->device, t->profile); + } + + spa_log_debug(backend->log, "Transport %s available, codec %d", t->path, t->codec); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult ofono_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + DBusMessage *r; + + spa_log_warn(backend->log, "release"); + + r = dbus_message_new_error(m, OFONO_HF_AUDIO_AGENT_INTERFACE ".Error.NotImplemented", + "Method not implemented"); + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void sco_event(struct spa_source *source) +{ + struct spa_bt_transport *t = source->data; + struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno)); + if (t->fd >= 0) { + if (source->loop) + spa_loop_remove_source(source->loop, source); + shutdown(t->fd, SHUT_RDWR); + close (t->fd); + t->fd = -1; + spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_IDLE); + } + } +} + +static int enable_sco_socket(int sock) +{ + char c; + struct pollfd pfd; + + if (sock < 0) + return ENOTCONN; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = sock; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, 0) < 0) + return errno; + + /* + * If socket already writable then it is not in defer setup state, + * otherwise it needs to be read to authorize the connection. + */ + if ((pfd.revents & POLLOUT)) + return 0; + + /* Enable socket by reading 1 byte */ + if (read(sock, &c, 1) < 0) + return errno; + + return 0; +} + +static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path; + int fd; + uint8_t codec; + struct spa_bt_transport *t; + struct transport_data *td; + DBusMessage *r = NULL; + + if (dbus_message_get_args(m, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_BYTE, &codec, + DBUS_TYPE_INVALID) == FALSE) { + r = dbus_message_new_error(m, OFONO_ERROR_INVALID_ARGUMENTS, "Invalid arguments in method call"); + goto fail; + } + + t = spa_bt_transport_find(backend->monitor, path); + if (t && (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) { + int err; + + err = enable_sco_socket(fd); + if (err) { + spa_log_error(backend->log, "transport %p: Couldn't authorize SCO connection: %s", t, strerror(err)); + r = dbus_message_new_error(m, OFONO_ERROR_FAILED, "SCO authorization failed"); + shutdown(fd, SHUT_RDWR); + close(fd); + goto fail; + } + + t->fd = fd; + t->codec = codec; + + spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %d", + t, t->path, t->fd, t->codec); + + td = t->user_data; + td->sco.func = sco_event; + td->sco.data = t; + td->sco.fd = fd; + td->sco.mask = SPA_IO_HUP | SPA_IO_ERR; + td->sco.rmask = 0; + spa_loop_add_source(backend->main_loop, &td->sco); + + ofono_transport_get_mtu(backend, t); + spa_bt_transport_set_state (t, SPA_BT_TRANSPORT_STATE_PENDING); + } + else if (fd) { + spa_log_debug(backend->log, "ignoring NewConnection"); + r = dbus_message_new_error(m, OFONO_ERROR_NOT_IMPLEMENTED, "Method not implemented"); + shutdown(fd, SHUT_RDWR); + close(fd); + } + +fail: + if (r) { + DBusHandlerResult res = DBUS_HANDLER_RESULT_HANDLED; + if (!dbus_connection_send(backend->conn, r, NULL)) + res = DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_unref(r); + return res; + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult ofono_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *backend = userdata; + const char *path, *interface, *member; + DBusMessage *r; + DBusHandlerResult res; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(backend->log, "path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = OFONO_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(backend->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "Release")) + res = ofono_release(c, m, userdata); + else if (dbus_message_is_method_call(m, OFONO_HF_AUDIO_AGENT_INTERFACE, "NewConnection")) + res = ofono_new_audio_connection(c, m, userdata); + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void ofono_getcards_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + DBusMessageIter i, array_i, struct_i, props_i; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Failed to get a list of handsfree audio cards: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a(oa{sv})")) { + spa_log_error(backend->log, "Invalid arguments in GetCards() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + char *path; + + dbus_message_iter_recurse(&array_i, &struct_i); + dbus_message_iter_get_basic(&struct_i, &path); + dbus_message_iter_next(&struct_i); + + dbus_message_iter_recurse(&struct_i, &props_i); + + ofono_audio_card_found(backend, path, &props_i); + + dbus_message_iter_next(&array_i); + } + +finish: + dbus_message_unref(r); + dbus_pending_call_unref(pending); +} + +static int backend_ofono_register(void *data) +{ + struct impl *backend = data; + + DBusMessage *m, *r; + const char *path = OFONO_AUDIO_CLIENT; + uint8_t codecs[2]; + const uint8_t *pcodecs = codecs; + int ncodecs = 0, res; + DBusPendingCall *call; + DBusError err; + + spa_log_debug(backend->log, "Registering"); + + m = dbus_message_new_method_call(OFONO_SERVICE, "/", + OFONO_HF_AUDIO_MANAGER_INTERFACE, "Register"); + if (m == NULL) + return -ENOMEM; + + codecs[ncodecs++] = HFP_AUDIO_CODEC_CVSD; + if (backend->msbc_supported) + codecs[ncodecs++] = HFP_AUDIO_CODEC_MSBC; + + dbus_message_append_args(m, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &pcodecs, ncodecs, + DBUS_TYPE_INVALID); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + + if (r == NULL) { + if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) { + spa_log_info(backend->log, "oFono not available: %s", + err.message); + res = -ENOTSUP; + } else { + spa_log_warn(backend->log, "Registering Profile %s failed: %s (%s)", + path, err.message, err.name); + res = -EIO; + } + dbus_error_free(&err); + return res; + } + + if (dbus_message_is_error(r, OFONO_ERROR_INVALID_ARGUMENTS)) { + spa_log_warn(backend->log, "invalid arguments"); + goto finish; + } + if (dbus_message_is_error(r, OFONO_ERROR_IN_USE)) { + spa_log_warn(backend->log, "already in use"); + goto finish; + } + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(backend->log, "Error registering profile"); + goto finish; + } + if (dbus_message_is_error(r, DBUS_ERROR_SERVICE_UNKNOWN)) { + spa_log_info(backend->log, "oFono not available, disabling"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Register() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + dbus_message_unref(r); + + spa_log_debug(backend->log, "registered"); + + m = dbus_message_new_method_call(OFONO_SERVICE, "/", + OFONO_HF_AUDIO_MANAGER_INTERFACE, "GetCards"); + if (m == NULL) + goto finish; + + dbus_connection_send_with_reply(backend->conn, m, &call, -1); + dbus_pending_call_set_notify(call, ofono_getcards_reply, backend, NULL); + dbus_message_unref(m); + + return 0; + +finish: + dbus_message_unref(r); + return -EIO; +} + +static DBusHandlerResult ofono_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct impl *backend = user_data; + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardAdded")) { + char *p; + DBusMessageIter arg_i, props_i; + + if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sv}")) { + spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardAdded"); + goto fail; + } + + dbus_message_iter_get_basic(&arg_i, &p); + + dbus_message_iter_next(&arg_i); + spa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&arg_i, &props_i); + + return ofono_audio_card_found(backend, p, &props_i); + } else if (dbus_message_is_signal(m, OFONO_HF_AUDIO_MANAGER_INTERFACE, "CardRemoved")) { + const char *p; + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_OBJECT_PATH, &p, DBUS_TYPE_INVALID)) { + spa_log_error(backend->log, "Failed to parse org.ofono.HandsfreeAudioManager.CardRemoved: %s", err.message); + goto fail; + } + + return ofono_audio_card_removed(backend, p); + } + +fail: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int add_filters(struct impl *backend) +{ + DBusError err; + + if (backend->filters_added) + return 0; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(backend->conn, ofono_filter_cb, backend, NULL)) { + spa_log_error(backend->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(backend->conn, + "type='signal',sender='" OFONO_SERVICE "'," + "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardAdded'", &err); + dbus_bus_add_match(backend->conn, + "type='signal',sender='" OFONO_SERVICE "'," + "interface='" OFONO_HF_AUDIO_MANAGER_INTERFACE "',member='CardRemoved'", &err); + + backend->filters_added = true; + + return 0; + +fail: + dbus_error_free(&err); + return -EIO; +} + +static int backend_ofono_free(void *data) +{ + struct impl *backend = data; + + if (backend->filters_added) { + dbus_connection_remove_filter(backend->conn, ofono_filter_cb, backend); + backend->filters_added = false; + } + + if (backend->timer) + spa_loop_utils_destroy_source(backend->loop_utils, backend->timer); + + dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT); + + free(backend); + + return 0; +} + +static const struct spa_bt_backend_implementation backend_impl = { + SPA_VERSION_BT_BACKEND_IMPLEMENTATION, + .free = backend_ofono_free, + .register_profiles = backend_ofono_register, +}; + +static bool is_available(struct impl *backend) +{ + DBusMessage *m, *r; + DBusError err; + bool success = false; + + m = dbus_message_new_method_call(OFONO_SERVICE, "/", + DBUS_INTERFACE_INTROSPECTABLE, "Introspect"); + if (m == NULL) + return false; + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(backend->conn, m, -1, &err); + dbus_message_unref(m); + + if (r && dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_METHOD_RETURN) + success = true; + + if (r) + dbus_message_unref(r); + else + dbus_error_free(&err); + + return success; +} + +struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *backend; + const char *str; + static const DBusObjectPathVTable vtable_profile = { + .message_function = ofono_handler, + }; + + backend = calloc(1, sizeof(struct impl)); + if (backend == NULL) + return NULL; + + spa_bt_backend_set_implementation(&backend->this, &backend_impl, backend); + + backend->this.name = "ofono"; + backend->this.exclusive = true; + backend->monitor = monitor; + backend->quirks = quirks; + backend->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + backend->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); + backend->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + backend->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + backend->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + backend->conn = dbus_connection; + if (info && (str = spa_dict_lookup(info, "bluez5.enable-msbc"))) + backend->msbc_supported = spa_atob(str); + else + backend->msbc_supported = false; + + spa_log_topic_init(backend->log, &log_topic); + + backend->timer = spa_loop_utils_add_timer(backend->loop_utils, activate_timer_event, backend); + if (backend->timer == NULL) { + free(backend); + return NULL; + } + + if (!dbus_connection_register_object_path(backend->conn, + OFONO_AUDIO_CLIENT, + &vtable_profile, backend)) { + free(backend); + return NULL; + } + + if (add_filters(backend) < 0) { + dbus_connection_unregister_object_path(backend->conn, OFONO_AUDIO_CLIENT); + free(backend); + return NULL; + } + + backend->this.available = is_available(backend); + + return &backend->this; +} diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h new file mode 100644 index 0000000..7bfac35 --- /dev/null +++ b/spa/plugins/bluez5/bap-codec-caps.h @@ -0,0 +1,142 @@ +/* Spa BAP codec API + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef SPA_BLUEZ5_BAP_CODEC_CAPS_H_ +#define SPA_BLUEZ5_BAP_CODEC_CAPS_H_ + +#define BAP_CODEC_LC3 0x06 + +#define LC3_TYPE_FREQ 0x01 +#define LC3_FREQ_8KHZ (1 << 0) +#define LC3_FREQ_11KHZ (1 << 1) +#define LC3_FREQ_16KHZ (1 << 2) +#define LC3_FREQ_22KHZ (1 << 3) +#define LC3_FREQ_24KHZ (1 << 4) +#define LC3_FREQ_32KHZ (1 << 5) +#define LC3_FREQ_44KHZ (1 << 6) +#define LC3_FREQ_48KHZ (1 << 7) +#define LC3_FREQ_ANY (LC3_FREQ_8KHZ | \ + LC3_FREQ_11KHZ | \ + LC3_FREQ_16KHZ | \ + LC3_FREQ_22KHZ | \ + LC3_FREQ_24KHZ | \ + LC3_FREQ_32KHZ | \ + LC3_FREQ_44KHZ | \ + LC3_FREQ_48KHZ) + +#define LC3_TYPE_DUR 0x02 +#define LC3_DUR_7_5 (1 << 0) +#define LC3_DUR_10 (1 << 1) +#define LC3_DUR_ANY (LC3_DUR_7_5 | \ + LC3_DUR_10) + +#define LC3_TYPE_CHAN 0x03 +#define LC3_CHAN_1 (1 << 0) +#define LC3_CHAN_2 (1 << 1) + +#define LC3_TYPE_FRAMELEN 0x04 +#define LC3_TYPE_BLKS 0x05 + +/* LC3 config parameters */ +#define LC3_CONFIG_FREQ_8KHZ 0x01 +#define LC3_CONFIG_FREQ_11KHZ 0x02 +#define LC3_CONFIG_FREQ_16KHZ 0x03 +#define LC3_CONFIG_FREQ_22KHZ 0x04 +#define LC3_CONFIG_FREQ_24KHZ 0x05 +#define LC3_CONFIG_FREQ_32KHZ 0x06 +#define LC3_CONFIG_FREQ_44KHZ 0x07 +#define LC3_CONFIG_FREQ_48KHZ 0x08 + +#define LC3_CONFIG_DURATION_7_5 0x00 +#define LC3_CONFIG_DURATION_10 0x01 + +#define LC3_CONFIG_CHNL_NOT_ALLOWED 0x00000000 +#define LC3_CONFIG_CHNL_FL 0x00000001 /* front left */ +#define LC3_CONFIG_CHNL_FR 0x00000002 /* front right */ +#define LC3_CONFIG_CHNL_FC 0x00000004 /* front center */ +#define LC3_CONFIG_CHNL_LFE 0x00000008 /* LFE */ +#define LC3_CONFIG_CHNL_BL 0x00000010 /* back left */ +#define LC3_CONFIG_CHNL_BR 0x00000020 /* back right */ +#define LC3_CONFIG_CHNL_FLC 0x00000040 /* front left center */ +#define LC3_CONFIG_CHNL_FRC 0x00000080 /* front right center */ +#define LC3_CONFIG_CHNL_BC 0x00000100 /* back center */ +#define LC3_CONFIG_CHNL_LFE2 0x00000200 /* LFE 2 */ +#define LC3_CONFIG_CHNL_SL 0x00000400 /* side left */ +#define LC3_CONFIG_CHNL_SR 0x00000800 /* side right */ +#define LC3_CONFIG_CHNL_TFL 0x00001000 /* top front left */ +#define LC3_CONFIG_CHNL_TFR 0x00002000 /* top front right */ +#define LC3_CONFIG_CHNL_TFC 0x00004000 /* top front center */ +#define LC3_CONFIG_CHNL_TC 0x00008000 /* top center */ +#define LC3_CONFIG_CHNL_TBL 0x00010000 /* top back left */ +#define LC3_CONFIG_CHNL_TBR 0x00020000 /* top back right */ +#define LC3_CONFIG_CHNL_TSL 0x00040000 /* top side left */ +#define LC3_CONFIG_CHNL_TSR 0x00080000 /* top side right */ +#define LC3_CONFIG_CHNL_TBC 0x00100000 /* top back center */ +#define LC3_CONFIG_CHNL_BFC 0x00200000 /* bottom front center */ +#define LC3_CONFIG_CHNL_BFL 0x00400000 /* bottom front left */ +#define LC3_CONFIG_CHNL_BFR 0x00800000 /* bottom front right */ +#define LC3_CONFIG_CHNL_FLW 0x01000000 /* front left wide */ +#define LC3_CONFIG_CHNL_FRW 0x02000000 /* front right wide */ +#define LC3_CONFIG_CHNL_LS 0x04000000 /* left surround */ +#define LC3_CONFIG_CHNL_RS 0x08000000 /* right surround */ + +#define LC3_MAX_CHANNELS 28 + +typedef struct { + uint8_t rate; + uint8_t frame_duration; + uint32_t channels; + uint16_t framelen; + uint8_t n_blks; +} __attribute__ ((packed)) bap_lc3_t; + +#define BT_ISO_QOS_CIG_UNSET 0xff +#define BT_ISO_QOS_CIS_UNSET 0xff + +#define BT_ISO_QOS_TARGET_LATENCY_LOW 0x01 +#define BT_ISO_QOS_TARGET_LATENCY_BALANCED 0x02 +#define BT_ISO_QOS_TARGET_LATENCY_RELIABILITY 0x03 + +struct bap_endpoint_qos { + uint8_t framing; + uint8_t phy; + uint8_t retransmission; + uint16_t latency; + uint32_t delay_min; + uint32_t delay_max; + uint32_t preferred_delay_min; + uint32_t preferred_delay_max; +}; + +struct bap_codec_qos { + uint32_t interval; + uint8_t framing; + uint8_t phy; + uint16_t sdu; + uint8_t retransmission; + uint16_t latency; + uint32_t delay; + uint8_t target_latency; +}; + +#endif diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c new file mode 100644 index 0000000..fe81168 --- /dev/null +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -0,0 +1,859 @@ +/* Spa BAP LC3 codec + * + * Copyright © 2020 Wim Taymans + * Copyright © 2022 Pauli Virtanen + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "media-codecs.h" +#include "bap-codec-caps.h" + +#define MAX_PACS 64 + +struct impl { + lc3_encoder_t enc[LC3_MAX_CHANNELS]; + lc3_decoder_t dec[LC3_MAX_CHANNELS]; + + int mtu; + int samplerate; + int channels; + int frame_dus; + int framelen; + int samples; + unsigned int codesize; +}; + +struct __attribute__((packed)) ltv { + uint8_t len; + uint8_t type; + uint8_t value[]; +}; + +struct pac_data { + const uint8_t *data; + size_t size; +}; + +static int write_ltv(uint8_t *dest, uint8_t type, void* value, size_t len) +{ + struct ltv *ltv = (struct ltv *)dest; + + ltv->len = len + 1; + ltv->type = type; + memcpy(ltv->value, value, len); + + return len + 2; +} + +static int write_ltv_uint8(uint8_t *dest, uint8_t type, uint8_t value) +{ + return write_ltv(dest, type, &value, sizeof(value)); +} + +static int write_ltv_uint16(uint8_t *dest, uint8_t type, uint16_t value) +{ + return write_ltv(dest, type, &value, sizeof(value)); +} + +static int write_ltv_uint32(uint8_t *dest, uint8_t type, uint32_t value) +{ + return write_ltv(dest, type, &value, sizeof(value)); +} + +static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]) +{ + uint8_t *data = caps; + uint16_t framelen[2] = {htobs(LC3_MIN_FRAME_BYTES), htobs(LC3_MAX_FRAME_BYTES)}; + + data += write_ltv_uint16(data, LC3_TYPE_FREQ, + htobs(LC3_FREQ_48KHZ | LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ)); + data += write_ltv_uint8(data, LC3_TYPE_DUR, LC3_DUR_ANY); + data += write_ltv_uint8(data, LC3_TYPE_CHAN, LC3_CHAN_1 | LC3_CHAN_2); + data += write_ltv(data, LC3_TYPE_FRAMELEN, framelen, sizeof(framelen)); + data += write_ltv_uint8(data, LC3_TYPE_BLKS, 2); + + return data - caps; +} + +static int parse_bluez_pacs(const uint8_t *data, size_t data_size, struct pac_data pacs[MAX_PACS]) +{ + /* + * BlueZ capabilites for the same codec may contain multiple + * PACs separated by zero-length LTV (see BlueZ b907befc2d80) + */ + int pac = 0; + + pacs[pac] = (struct pac_data){ data, 0 }; + + while (data_size > 0) { + struct ltv *ltv = (struct ltv *)data; + + if (ltv->len == 0) { + /* delimiter */ + if (pac + 1 >= MAX_PACS) + break; + + ++pac; + pacs[pac] = (struct pac_data){ data + 1, 0 }; + } else if (ltv->len >= data_size) { + return -EINVAL; + } else { + pacs[pac].size += ltv->len + 1; + } + data_size -= ltv->len + 1; + data += ltv->len + 1; + } + + return pac + 1; +} + +static bool parse_capabilities(bap_lc3_t *conf, const uint8_t *data, size_t data_size) +{ + uint16_t framelen_min = 0, framelen_max = 0; + + if (!data_size) + return false; + memset(conf, 0, sizeof(*conf)); + + conf->frame_duration = 0xFF; + + while (data_size > 0) { + struct ltv *ltv = (struct ltv *)data; + + if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size) + return false; + + switch (ltv->type) { + case LC3_TYPE_FREQ: + spa_return_val_if_fail(ltv->len == 3, false); + { + uint16_t rate = ltv->value[0] + (ltv->value[1] << 8); + if (rate & LC3_FREQ_48KHZ) + conf->rate = LC3_CONFIG_FREQ_48KHZ; + else if (rate & LC3_FREQ_24KHZ) + conf->rate = LC3_CONFIG_FREQ_24KHZ; + else if (rate & LC3_FREQ_16KHZ) + conf->rate = LC3_CONFIG_FREQ_16KHZ; + else if (rate & LC3_FREQ_8KHZ) + conf->rate = LC3_CONFIG_FREQ_8KHZ; + else + return false; + } + break; + case LC3_TYPE_DUR: + spa_return_val_if_fail(ltv->len == 2, false); + { + uint8_t duration = ltv->value[0]; + if (duration & LC3_DUR_10) + conf->frame_duration = LC3_CONFIG_DURATION_10; + else if (duration & LC3_DUR_7_5) + conf->frame_duration = LC3_CONFIG_DURATION_7_5; + else + return false; + } + break; + case LC3_TYPE_CHAN: + spa_return_val_if_fail(ltv->len == 2, false); + { + uint8_t channels = ltv->value[0]; + /* Only mono or stereo streams are currently supported, + * in both case Audio location is defined as both Front Left + * and Front Right, difference is done by the n_blks parameter. + */ + if ((channels & LC3_CHAN_2) || (channels & LC3_CHAN_1)) + conf->channels = LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL; + else + return false; + } + break; + case LC3_TYPE_FRAMELEN: + spa_return_val_if_fail(ltv->len == 5, false); + framelen_min = ltv->value[0] + (ltv->value[1] << 8); + framelen_max = ltv->value[2] + (ltv->value[3] << 8); + break; + case LC3_TYPE_BLKS: + spa_return_val_if_fail(ltv->len == 2, false); + conf->n_blks = ltv->value[0]; + if (!conf->n_blks) + return false; + break; + default: + return false; + } + data_size -= ltv->len + 1; + data += ltv->len + 1; + } + + if (framelen_min < LC3_MIN_FRAME_BYTES || framelen_max > LC3_MAX_FRAME_BYTES) + return false; + if (conf->frame_duration == 0xFF || !conf->rate) + return false; + if (!conf->channels) + conf->channels = LC3_CONFIG_CHNL_FL; + + switch (conf->rate) { + case LC3_CONFIG_FREQ_48KHZ: + if (conf->frame_duration == LC3_CONFIG_DURATION_7_5) + conf->framelen = 117; + else + conf->framelen = 120; + break; + case LC3_CONFIG_FREQ_24KHZ: + if (conf->frame_duration == LC3_CONFIG_DURATION_7_5) + conf->framelen = 45; + else + conf->framelen = 60; + break; + case LC3_CONFIG_FREQ_16KHZ: + if (conf->frame_duration == LC3_CONFIG_DURATION_7_5) + conf->framelen = 30; + else + conf->framelen = 40; + break; + case LC3_CONFIG_FREQ_8KHZ: + if (conf->frame_duration == LC3_CONFIG_DURATION_7_5) + conf->framelen = 26; + else + conf->framelen = 30; + break; + default: + return false; + } + + return true; +} + +static bool parse_conf(bap_lc3_t *conf, const uint8_t *data, size_t data_size) +{ + if (!data_size) + return false; + memset(conf, 0, sizeof(*conf)); + + conf->frame_duration = 0xFF; + + while (data_size > 0) { + struct ltv *ltv = (struct ltv *)data; + + if (ltv->len < sizeof(struct ltv) || ltv->len >= data_size) + return false; + + switch (ltv->type) { + case LC3_TYPE_FREQ: + spa_return_val_if_fail(ltv->len == 2, false); + conf->rate = ltv->value[0]; + break; + case LC3_TYPE_DUR: + spa_return_val_if_fail(ltv->len == 2, false); + conf->frame_duration = ltv->value[0]; + break; + case LC3_TYPE_CHAN: + spa_return_val_if_fail(ltv->len == 5, false); + conf->channels = ltv->value[0] + (ltv->value[1] << 8) + (ltv->value[2] << 16) + (ltv->value[3] << 24); + break; + case LC3_TYPE_FRAMELEN: + spa_return_val_if_fail(ltv->len == 3, false); + conf->framelen = ltv->value[0] + (ltv->value[1] << 8); + break; + case LC3_TYPE_BLKS: + spa_return_val_if_fail(ltv->len == 2, false); + conf->n_blks = ltv->value[0]; + if (!conf->n_blks) + return false; + break; + default: + return false; + } + data_size -= ltv->len + 1; + data += ltv->len + 1; + } + + if (conf->frame_duration == 0xFF || !conf->rate) + return false; + + return true; +} + +static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, int res2) +{ + const bap_lc3_t *conf; + int a, b; + +#define PREFER_EXPR(expr) \ + do { \ + conf = conf1; \ + a = (expr); \ + conf = conf2; \ + b = (expr); \ + if (a != b) \ + return b - a; \ + } while (0) + +#define PREFER_BOOL(expr) PREFER_EXPR((expr) ? 1 : 0) + + /* Prefer valid */ + a = (res1 > 0 && (size_t)res1 == sizeof(bap_lc3_t)) ? 1 : 0; + b = (res2 > 0 && (size_t)res2 == sizeof(bap_lc3_t)) ? 1 : 0; + if (!a || !b) + return b - a; + + PREFER_BOOL(conf->channels & LC3_CHAN_2); + PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_48KHZ | LC3_CONFIG_FREQ_24KHZ | LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_8KHZ)); + PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_48KHZ); + + return 0; + +#undef PREFER_EXPR +#undef PREFER_BOOL +} + +static int pac_cmp(const void *p1, const void *p2) +{ + const struct pac_data *pac1 = p1; + const struct pac_data *pac2 = p2; + bap_lc3_t conf1, conf2; + int res1, res2; + + res1 = parse_capabilities(&conf1, pac1->data, pac1->size) ? (int)sizeof(bap_lc3_t) : -EINVAL; + res2 = parse_capabilities(&conf2, pac2->data, pac2->size) ? (int)sizeof(bap_lc3_t) : -EINVAL; + + return conf_cmp(&conf1, res1, &conf2, res2); +} + +static int codec_select_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *settings, uint8_t config[A2DP_MAX_CAPS_SIZE]) +{ + struct pac_data pacs[MAX_PACS]; + int npacs; + bap_lc3_t conf; + uint8_t *data = config; + + if (caps == NULL) + return -EINVAL; + + /* Select best conf from those possible */ + npacs = parse_bluez_pacs(caps, caps_size, pacs); + if (npacs < 0) + return npacs; + else if (npacs == 0) + return -EINVAL; + + qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp); + + if (!parse_capabilities(&conf, pacs[0].data, pacs[0].size)) + return -ENOTSUP; + + data += write_ltv_uint8(data, LC3_TYPE_FREQ, conf.rate); + data += write_ltv_uint8(data, LC3_TYPE_DUR, conf.frame_duration); + data += write_ltv_uint32(data, LC3_TYPE_CHAN, htobl(conf.channels)); + data += write_ltv_uint16(data, LC3_TYPE_FRAMELEN, htobs(conf.framelen)); + data += write_ltv_uint8(data, LC3_TYPE_BLKS, conf.n_blks); + + return data - config; +} + +static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size, + const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, const struct spa_dict *global_settings) +{ + bap_lc3_t conf1, conf2; + int res1, res2; + + /* Order selected configurations by preference */ + res1 = codec->select_config(codec, 0, caps1, caps1_size, info, NULL, (uint8_t *)&conf1); + res2 = codec->select_config(codec, 0, caps2, caps2_size, info , NULL, (uint8_t *)&conf2); + + return conf_cmp(&conf1, res1, &conf2, res2); +} + +static uint8_t channels_to_positions(uint32_t channels, uint8_t n_channels, uint32_t *position) +{ + uint8_t n_positions = 0; + + spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS); + + /* First check if stream is configure for Mono, i.e. 1 block for both Front + * Left anf Front Right, + * else map LE Audio locations to PipeWire locations in the ascending order + * which will be used as block order in stream. + */ + if ((channels & (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL)) == (LC3_CONFIG_CHNL_FR | LC3_CONFIG_CHNL_FL) && + n_channels == 1) { + position[0] = SPA_AUDIO_CHANNEL_MONO; + n_positions = 1; + } else { +#define CHANNEL_2_SPACHANNEL(channel,spa_channel) if (channels & channel) position[n_positions++] = spa_channel; + + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FL, SPA_AUDIO_CHANNEL_FL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FR, SPA_AUDIO_CHANNEL_FR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FC, SPA_AUDIO_CHANNEL_FC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE, SPA_AUDIO_CHANNEL_LFE); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BL, SPA_AUDIO_CHANNEL_RL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BR, SPA_AUDIO_CHANNEL_RR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLC, SPA_AUDIO_CHANNEL_FLC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRC, SPA_AUDIO_CHANNEL_FRC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BC, SPA_AUDIO_CHANNEL_BC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LFE2, SPA_AUDIO_CHANNEL_LFE2); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SL, SPA_AUDIO_CHANNEL_SL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_SR, SPA_AUDIO_CHANNEL_SR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFL, SPA_AUDIO_CHANNEL_TFL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFR, SPA_AUDIO_CHANNEL_TFR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TFC, SPA_AUDIO_CHANNEL_TFC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TC, SPA_AUDIO_CHANNEL_TC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBL, SPA_AUDIO_CHANNEL_TRL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBR, SPA_AUDIO_CHANNEL_TRR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSL, SPA_AUDIO_CHANNEL_TSL); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TSR, SPA_AUDIO_CHANNEL_TSR); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_TBC, SPA_AUDIO_CHANNEL_TRC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFC, SPA_AUDIO_CHANNEL_BC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFL, SPA_AUDIO_CHANNEL_BLC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_BFR, SPA_AUDIO_CHANNEL_BRC); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FLW, SPA_AUDIO_CHANNEL_FLW); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_FRW, SPA_AUDIO_CHANNEL_FRW); + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_LS, SPA_AUDIO_CHANNEL_LLFE); /* is it the right mapping? */ + CHANNEL_2_SPACHANNEL(LC3_CONFIG_CHNL_RS, SPA_AUDIO_CHANNEL_RLFE); /* is it the right mapping? */ + +#undef CHANNEL_2_SPACHANNEL + } + + return n_positions; +} + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + bap_lc3_t conf; + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i = 0; + uint8_t res; + + if (!parse_conf(&conf, caps, caps_size)) + return -EINVAL; + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S24_32), + 0); + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + i = 0; + if (conf.rate & LC3_CONFIG_FREQ_48KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 48000); + spa_pod_builder_int(b, 48000); + } + if (conf.rate & LC3_CONFIG_FREQ_24KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 24000); + spa_pod_builder_int(b, 24000); + } + if (conf.rate & LC3_CONFIG_FREQ_16KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 16000); + spa_pod_builder_int(b, 16000); + } + if (conf.rate & LC3_CONFIG_FREQ_8KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 8000); + spa_pod_builder_int(b, 8000); + } + if (i == 0) + return -EINVAL; + if (i > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + res = channels_to_positions(conf.channels, conf.n_blks, position); + if (res == 0) + return -EINVAL; + spa_pod_builder_add(b, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(res), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, res, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + bap_lc3_t conf; + uint8_t res; + + if (caps == NULL) + return -EINVAL; + + if (!parse_conf(&conf, caps, caps_size)) + return -ENOTSUP; + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S24_32; + + switch (conf.rate) { + case LC3_CONFIG_FREQ_48KHZ: + info->info.raw.rate = 48000U; + break; + case LC3_CONFIG_FREQ_24KHZ: + info->info.raw.rate = 24000U; + break; + case LC3_CONFIG_FREQ_16KHZ: + info->info.raw.rate = 16000U; + break; + case LC3_CONFIG_FREQ_8KHZ: + info->info.raw.rate = 8000U; + break; + default: + return -EINVAL; + } + + res = channels_to_positions(conf.channels, conf.n_blks, info->info.raw.position); + if (res == 0) + return -EINVAL; + info->info.raw.channels = res; + + switch (conf.frame_duration) { + case LC3_CONFIG_DURATION_10: + case LC3_CONFIG_DURATION_7_5: + break; + default: + return -EINVAL; + } + + return 0; +} + +static int codec_get_qos(const struct media_codec *codec, + const void *config, size_t config_size, + const struct bap_endpoint_qos *endpoint_qos, + struct bap_codec_qos *qos) +{ + bap_lc3_t conf; + + spa_zero(*qos); + + if (!parse_conf(&conf, config, config_size)) + return -EINVAL; + + qos->framing = false; + if (endpoint_qos->phy & 0x2) + qos->phy = 0x2; + else if (endpoint_qos->phy & 0x1) + qos->phy = 0x1; + else + qos->phy = 0x2; + qos->retransmission = 2; /* default */ + qos->sdu = conf.framelen * conf.n_blks; + qos->latency = 20; /* default */ + qos->delay = 40000U; + qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); + qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED; + + switch (conf.rate) { + case LC3_CONFIG_FREQ_8KHZ: + case LC3_CONFIG_FREQ_16KHZ: + case LC3_CONFIG_FREQ_24KHZ: + case LC3_CONFIG_FREQ_32KHZ: + qos->retransmission = 2; + qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 8 : 10); + break; + case LC3_CONFIG_FREQ_48KHZ: + qos->retransmission = 5; + qos->latency = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 15 : 20); + break; + } + + /* Clamp to ASE values */ + if (endpoint_qos->latency >= 0x0005 && endpoint_qos->latency <= 0x0FA0) + /* Values outside the range are RFU */ + qos->latency = SPA_MAX(qos->latency, endpoint_qos->latency); + + if (endpoint_qos->delay_min) + qos->delay = SPA_MAX(qos->delay, endpoint_qos->delay_min); + if (endpoint_qos->delay_max) + qos->delay = SPA_MIN(qos->delay, endpoint_qos->delay_max); + + return 0; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + bap_lc3_t conf; + struct impl *this = NULL; + struct spa_audio_info config_info; + int res, ich; + + if (info->media_type != SPA_MEDIA_TYPE_audio || + info->media_subtype != SPA_MEDIA_SUBTYPE_raw || + info->info.raw.format != SPA_AUDIO_FORMAT_S24_32) { + res = -EINVAL; + goto error; + } + + if ((this = calloc(1, sizeof(struct impl))) == NULL) + goto error_errno; + + if ((res = codec_validate_config(codec, flags, config, config_len, &config_info)) < 0) + goto error; + + if (!parse_conf(&conf, config, config_len)) { + res = -ENOTSUP; + goto error; + } + + this->mtu = mtu; + this->samplerate = config_info.info.raw.rate; + this->channels = config_info.info.raw.channels; + this->framelen = conf.framelen; + + switch (conf.frame_duration) { + case LC3_CONFIG_DURATION_10: + this->frame_dus = 10000; + break; + case LC3_CONFIG_DURATION_7_5: + this->frame_dus = 7500; + break; + default: + res = -EINVAL; + goto error; + } + + this->samples = lc3_frame_samples(this->frame_dus, this->samplerate); + if (this->samples < 0) { + res = -EINVAL; + goto error; + } + this->codesize = this->samples * this->channels * sizeof(int32_t); + + if (!(flags & MEDIA_CODEC_FLAG_SINK)) { + for (ich = 0; ich < this->channels; ich++) { + this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_encoder_size(this->frame_dus, this->samplerate))); + if (this->enc[ich] == NULL) { + res = -EINVAL; + goto error; + } + } + } else { + for (ich = 0; ich < this->channels; ich++) { + this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_decoder_size(this->frame_dus, this->samplerate))); + if (this->dec[ich] == NULL) { + res = -EINVAL; + goto error; + } + } + } + + return this; + +error_errno: + res = -errno; + goto error; + +error: + if (this) { + for (ich = 0; ich < this->channels; ich++) { + if (this->enc[ich]) + free(this->enc[ich]); + if (this->dec[ich]) + free(this->dec[ich]); + } + } + free(this); + errno = -res; + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + int ich; + + for (ich = 0; ich < this->channels; ich++) { + if (this->enc[ich]) + free(this->enc[ich]); + if (this->dec[ich]) + free(this->dec[ich]); + } + free(this); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + return this->codesize; +} + +static int codec_abr_process (void *data, size_t unsent) +{ + return -ENOTSUP; +} + +static int codec_start_encode (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) +{ + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int frame_bytes; + int ich, res; + int size, processed; + + frame_bytes = lc3_frame_bytes(this->frame_dus, this->samplerate); + processed = 0; + size = 0; + + if (src_size < (size_t)this->codesize) + goto done; + if (dst_size < (size_t)frame_bytes) + goto done; + + for (ich = 0; ich < this->channels; ich++) { + uint8_t *in = (uint8_t *)src + (ich * 4); + uint8_t *out = (uint8_t *)dst + ich * this->framelen; + res = lc3_encode(this->enc[ich], LC3_PCM_FORMAT_S24, in, this->channels, this->framelen, out); + size += this->framelen; + if (SPA_UNLIKELY(res != 0)) + return -EINVAL; + } + *dst_out = size; + + processed += this->codesize; + +done: + spa_assert(size <= this->mtu); + *need_flush = NEED_FLUSH_ALL; + + return processed; +} + +static SPA_UNUSED int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + return 0; +} + +static SPA_UNUSED int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int ich, res; + int consumed; + int samples; + + spa_return_val_if_fail((size_t)(this->framelen * this->channels) == src_size, -EINVAL); + consumed = 0; + + samples = lc3_frame_samples(this->frame_dus, this->samplerate); + if (samples == -1) + return -EINVAL; + if (dst_size < this->codesize) + return -EINVAL; + + for (ich = 0; ich < this->channels; ich++) { + uint8_t *in = (uint8_t *)src + ich * this->framelen; + uint8_t *out = (uint8_t *)dst + (ich * 4); + res = lc3_decode(this->dec[ich], in, this->framelen, LC3_PCM_FORMAT_S24, out, this->channels); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + consumed += this->framelen; + } + + *dst_out = this->codesize; + + return consumed; +} + +static int codec_reduce_bitpool(void *data) +{ + return -ENOTSUP; +} + +static int codec_increase_bitpool(void *data) +{ + return -ENOTSUP; +} + +const struct media_codec bap_codec_lc3 = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3, + .name = "lc3", + .codec_id = BAP_CODEC_LC3, + .bap = true, + .description = "LC3", + .fill_caps = codec_fill_caps, + .select_config = codec_select_config, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .get_qos = codec_get_qos, + .caps_preference_cmp = codec_caps_preference_cmp, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .abr_process = codec_abr_process, + .start_encode = codec_start_encode, + .encode = codec_encode, + .start_decode = codec_start_decode, + .decode = codec_decode, + .reduce_bitpool = codec_reduce_bitpool, + .increase_bitpool = codec_increase_bitpool +}; + +MEDIA_CODEC_EXPORT_DEF( + "lc3", + &bap_codec_lc3 +); diff --git a/spa/plugins/bluez5/bluez-hardware.conf b/spa/plugins/bluez5/bluez-hardware.conf new file mode 100644 index 0000000..0247f75 --- /dev/null +++ b/spa/plugins/bluez5/bluez-hardware.conf @@ -0,0 +1,103 @@ +# List of hardware/kernel features, which cannot be detected generically. +# +# The `feature` is enabled only if all three of adapter, device, and +# kernel have it. +# +# For each of the adapter/device/kernel, the match rules are processed +# one at a time, and the first one that matches is used. +# +# Features and tags: +# msbc "standard" mSBC (60 byte tx packet) +# msbc-alt1 USB adapters with mSBC in ALT1 setting (24 byte tx packet) +# msbc-alt1-rtl Realtek USB adapters with mSBC in ALT1 setting (24 byte tx packet) +# hw-volume AVRCP and HSP/HFP hardware volume support +# hw-volume-mic Functional HSP/HFP microphone volume support +# sbc-xq "nonstandard" SBC codec setting with better sound quality +# faststream FastStream codec support +# a2dp-duplex A2DP duplex codec support +# +# Features are disabled with the key "no-features" whose value is an +# array of strings in the match rule. + +bluez5.features.device = [ + # properties: + # - name + # - address ("ff:ff:ff:ff:ff:ff") + # - vendor-id ("bluetooth:ffff", "usb:ffff") + # - product-id + # - version-id + + { name = "Air 1 Plus", no-features = [ hw-volume-mic ] }, + { name = "AirPods", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { name = "AirPods Pro", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { name = "AXLOIE Goin", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { name = "BAA 100", no-features = [ hw-volume ] }, # Buxton BAA 100, doesn't remember volume, #pipewire-1449 + { name = "D50s", address = "~^00:13:ef:", no-features = [ hw-volume ] }, # volume has no effect, #pipewire-1562 + { name = "FiiO BTR3", address = "~^40:ed:98:", no-features = [ faststream ] }, # #pipewire-1658 + { name = "JBL Endurance RUN BT", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { name = "JBL LIVE650BTNC" }, + { name = "Motorola DC800", no-features = [ sbc-xq ] }, # #pipewire-1590 + { name = "Motorola S305", no-features = [ sbc-xq ] }, # #pipewire-1590 + { name = "Soundcore Life P2-L", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { name = "Soundcore Motion B", no-features = [ hw-volume ] }, + { name = "SoundCore mini", no-features = [ hw-volume ] }, # #pipewire-1686 + { name = "SoundCore 2", no-features = [ sbc-xq ] }, # #pipewire-2291 + { name = "Tribit MAXSound Plus", no-features = [ hw-volume ] }, # #pipewire-1592 + { name = "Urbanista Stockholm Plus", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + + { address = "~^44:5e:cd:", no-features = [ faststream, a2dp-duplex ]}, # #pipewire-1756 + + { address = "~^94:16:25:", no-features = [ hw-volume ]}, # AirPods 2 + { address = "~^9c:64:8b:", no-features = [ hw-volume ]}, # AirPods 2 + { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # Ausdom M05 + { address = "~^0c:a6:94:", no-features = [ hw-volume ]}, # deepblue2 + { address = "~^00:14:02:", no-features = [ hw-volume ]}, # iKross IKBT83B HS + { address = "~^44:5e:f3:", no-features = [ hw-volume ]}, # JayBird BlueBuds X + { address = "~^d4:9c:28:", no-features = [ hw-volume ]}, # JayBird BlueBuds X + { address = "~^00:18:6b:", no-features = [ hw-volume ]}, # LG Tone HBS-730 + { address = "~^b8:ad:3e:", no-features = [ hw-volume ]}, # LG Tone HBS-730 + { address = "~^a0:e9:db:", no-features = [ hw-volume ]}, # LG Tone HV-800 + { address = "~^00:24:1c:", no-features = [ hw-volume ]}, # Motorola Roadster + { address = "~^00:11:b1:", no-features = [ hw-volume ]}, # Mpow Cheetah + { address = "~^a4:15:66:", no-features = [ hw-volume ]}, # SOL REPUBLIC Tracks Air + { address = "~^00:14:f1:", no-features = [ hw-volume ]}, # Swage Rokitboost HS + { address = "~^00:26:7e:", no-features = [ hw-volume ]}, # VW Car Kit + { address = "~^90:03:b7:", no-features = [ hw-volume ]}, # VW Car Kit + + # All features are enabled by default; it's simpler to block non-working devices one by one. +] + +bluez5.features.adapter = [ + # properties: + # - address ("ff:ff:ff:ff:ff:ff") + # - bus-type ("usb", "other") + # - vendor-id ("usb:ffff") + # - product-id ("ffff") + + # Realtek Semiconductor Corp. + { bus-type = "usb", vendor-id = "usb:0bda" }, + + # Generic USB adapters + { bus-type = "usb", no-features = [ msbc-alt1-rtl ] }, + + # Other adapters + { no-features = [ msbc-alt1-rtl ] }, +] + +bluez5.features.kernel = [ + # properties (as in uname): + # - sysname + # - release + # - version + + # See https://lore.kernel.org/linux-bluetooth/20201210012003.133000-1-tpiepho@gmail.com/ + # https://lore.kernel.org/linux-bluetooth/b86543908684cc6cd9afaf4de10fac7af1a49665.camel@iki.fi/ + { sysname = "Linux", release = "~^[0-4]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { sysname = "Linux", release = "~^5\\.[1-7]\\.", no-features = [ msbc-alt1, msbc-alt1-rtl ] }, + { sysname = "Linux", release = "~^5\\.(8|9)\\.", no-features = [ msbc-alt1 ] }, + { sysname = "Linux", release = "~^5\\.10\\.(1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|51|52|53|54|55|56|57|58|59|60|61)($|[^0-9])", no-features = [ msbc-alt1 ] }, + { sysname = "Linux", release = "~^5\\.12\\.(18|19)($|[^0-9])", no-features = [ msbc-alt1 ] }, + { sysname = "Linux", release = "~^5\\.13\\.(3|4|5|6|7|8|9|10|11|12|13)($|[^0-9])", no-features = [ msbc-alt1 ] }, + + { no-features = [] }, +] diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c new file mode 100644 index 0000000..4034f99 --- /dev/null +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -0,0 +1,5182 @@ +/* Spa V4l2 dbus + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 "defs.h" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +enum backend_selection { + BACKEND_NONE = -2, + BACKEND_ANY = -1, + BACKEND_HSPHFPD = 0, + BACKEND_OFONO = 1, + BACKEND_NATIVE = 2, + BACKEND_NUM, +}; + +/* + * Rate limit for BlueZ SetConfiguration calls. + * + * Too rapid calls to BlueZ API may cause A2DP profile to disappear, as the + * internal BlueZ/connection state gets confused. Use some reasonable minimum + * interval. + * + * AVDTP v1.3 Sec. 6.13 mentions 3 seconds as a reasonable timeout in one case + * (ACP connection reset timeout, if no INT response). The case here is + * different, but we assume a similar value is fine here. + */ +#define BLUEZ_ACTION_RATE_MSEC 3000 + +#define CODEC_SWITCH_RETRIES 1 + + +struct spa_bt_monitor { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_system *main_system; + struct spa_plugin_loader *plugin_loader; + struct spa_dbus *dbus; + struct spa_dbus_connection *dbus_connection; + DBusConnection *conn; + + struct spa_hook_list hooks; + + uint32_t id; + + const struct media_codec * const * media_codecs; + + /* + * Lists of BlueZ objects, kept up-to-date by following DBus events + * initiated by BlueZ. Object lifetime is also determined by that. + */ + struct spa_list adapter_list; + struct spa_list device_list; + struct spa_list remote_endpoint_list; + struct spa_list transport_list; + + unsigned int filters_added:1; + unsigned int objects_listed:1; + DBusPendingCall *get_managed_objects_call; + + struct spa_bt_backend *backend; + struct spa_bt_backend *backends[BACKEND_NUM]; + enum backend_selection backend_selection; + + struct spa_dict enabled_codecs; + + unsigned int connection_info_supported:1; + unsigned int dummy_avrcp_player:1; + + struct spa_bt_quirks *quirks; + +#define MAX_SETTINGS 128 + struct spa_dict_item global_setting_items[MAX_SETTINGS]; + struct spa_dict global_settings; + + /* A reference audio info for A2DP codec configuration. */ + struct media_codec_audio_info default_audio_info; + + bool le_audio_supported; +}; + +/* Stream endpoints owned by BlueZ for each device */ +struct spa_bt_remote_endpoint { + struct spa_list link; + struct spa_list device_link; + struct spa_bt_monitor *monitor; + char *path; + + char *uuid; + unsigned int codec; + struct spa_bt_device *device; + uint8_t *capabilities; + int capabilities_len; + bool delay_reporting; + bool acceptor; +}; + +/* + * Codec switching tries various codec/remote endpoint combinations + * in order, until an acceptable one is found. This triggers BlueZ + * to initiate DBus calls that result to the creation of a transport + * with the desired capabilities. + * The codec switch struct tracks candidates still to be tried. + */ +struct spa_bt_media_codec_switch { + struct spa_bt_device *device; + struct spa_list device_link; + + /* + * Codec switch may be waiting for either DBus reply from BlueZ + * or a timeout (but not both). + */ + struct spa_source timer; + DBusPendingCall *pending; + + uint32_t profile; + + /* + * Called asynchronously, so endpoint paths instead of pointers (which may be + * invalidated in the meantime). + */ + const struct media_codec **codecs; + char **paths; + + const struct media_codec **codec_iter; /**< outer iterator over codecs */ + char **path_iter; /**< inner iterator over endpoint paths */ + + uint16_t retries; + size_t num_paths; +}; + +#define DEFAULT_RECONNECT_PROFILES SPA_BT_PROFILE_NULL +#define DEFAULT_HW_VOLUME_PROFILES (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY | SPA_BT_PROFILE_HEADSET_HEAD_UNIT | \ + SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK) + +#define BT_DEVICE_DISCONNECTED 0 +#define BT_DEVICE_CONNECTED 1 +#define BT_DEVICE_INIT -1 + +/* + * SCO socket connect may fail with ECONNABORTED if it is done too soon after + * previous close. To avoid this in cases where nodes are toggled between + * stopped/started rapidly, postpone release until the transport has remained + * unused for a time. Since this appears common to multiple SCO backends, we do + * it for all SCO backends here. + */ +#define SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC 1000 +#define SPA_BT_TRANSPORT_IS_SCO(transport) (transport->backend != NULL) + +#define TRANSPORT_VOLUME_TIMEOUT_MSEC 200 + +static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport); +static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport); +static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport); +static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport); + +static int device_start_timer(struct spa_bt_device *device); +static int device_stop_timer(struct spa_bt_device *device); + +// Working with BlueZ Battery Provider. +// Developed using https://github.com/dgreid/adhd/commit/655b58f as an example of DBus calls. + +// Name of battery, formatted as /org/freedesktop/pipewire/battery/org/bluez/hciX/dev_XX_XX_XX_XX_XX_XX +static char *battery_get_name(const char *device_path) +{ + char *path = malloc(strlen(PIPEWIRE_BATTERY_PROVIDER) + strlen(device_path) + 1); + sprintf(path, PIPEWIRE_BATTERY_PROVIDER "%s", device_path); + return path; +} + +// Unregister virtual battery of device +static void battery_remove(struct spa_bt_device *device) { + DBusMessageIter i, entry; + DBusMessage *m; + const char *interface; + + if (device->battery_pending_call) { + spa_log_debug(device->monitor->log, "Cancelling and freeing pending battery provider register call"); + dbus_pending_call_cancel(device->battery_pending_call); + dbus_pending_call_unref(device->battery_pending_call); + device->battery_pending_call = NULL; + } + + if (!device->adapter || !device->adapter->has_battery_provider || !device->has_battery) + return; + + spa_log_debug(device->monitor->log, "Removing virtual battery: %s", device->battery_path); + + m = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER, + DBUS_INTERFACE_OBJECT_MANAGER, + DBUS_SIGNAL_INTERFACES_REMOVED); + + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, + &device->battery_path); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &entry); + interface = BLUEZ_INTERFACE_BATTERY_PROVIDER; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, + &interface); + dbus_message_iter_close_container(&i, &entry); + + if (!dbus_connection_send(device->monitor->conn, m, NULL)) { + spa_log_error(device->monitor->log, "sending " DBUS_SIGNAL_INTERFACES_REMOVED " failed"); + } + + dbus_message_unref(m); + + device->has_battery = false; +} + +// Create properties for Battery Provider request +static void battery_write_properties(DBusMessageIter *iter, struct spa_bt_device *device) +{ + DBusMessageIter dict, entry, variant; + + dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, + &entry); + const char *prop_percentage = "Percentage"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_percentage); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &device->battery); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *prop_device = "Device"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &prop_device); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_OBJECT_PATH_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_OBJECT_PATH, &device->path); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + + dbus_message_iter_close_container(iter, &dict); +} + +// Send current percentage to BlueZ +static void battery_update(struct spa_bt_device *device) +{ + spa_log_debug(device->monitor->log, "updating battery: %s", device->battery_path); + + DBusMessage *msg; + DBusMessageIter iter; + + msg = dbus_message_new_signal(device->battery_path, + DBUS_INTERFACE_PROPERTIES, + DBUS_SIGNAL_PROPERTIES_CHANGED); + + dbus_message_iter_init_append(msg, &iter); + const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, + &interface); + + battery_write_properties(&iter, device); + + if (!dbus_connection_send(device->monitor->conn, msg, NULL)) + spa_log_error(device->monitor->log, "Error updating battery"); + + dbus_message_unref(msg); +} + +// Create new virtual battery with value stored in current device object +static void battery_create(struct spa_bt_device *device) { + DBusMessage *msg; + DBusMessageIter iter, entry, dict; + msg = dbus_message_new_signal(PIPEWIRE_BATTERY_PROVIDER, + DBUS_INTERFACE_OBJECT_MANAGER, + DBUS_SIGNAL_INTERFACES_ADDED); + + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, + &device->battery_path); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sa{sv}}", &dict); + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *interface = BLUEZ_INTERFACE_BATTERY_PROVIDER; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, + &interface); + + battery_write_properties(&entry, device); + + dbus_message_iter_close_container(&dict, &entry); + dbus_message_iter_close_container(&iter, &dict); + + if (!dbus_connection_send(device->monitor->conn, msg, NULL)) { + spa_log_error(device->monitor->log, "Failed to create virtual battery for %s", device->address); + return; + } + + dbus_message_unref(msg); + + spa_log_debug(device->monitor->log, "Created virtual battery for %s", device->address); + device->has_battery = true; +} + +static void on_battery_provider_registered(DBusPendingCall *pending_call, + void *data) +{ + DBusMessage *reply; + struct spa_bt_device *device = data; + + reply = dbus_pending_call_steal_reply(pending_call); + dbus_pending_call_unref(pending_call); + + device->battery_pending_call = NULL; + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(device->monitor->log, "Failed to register battery provider. Error: %s", dbus_message_get_error_name(reply)); + spa_log_error(device->monitor->log, "BlueZ Battery Provider is not available, won't retry to register it. Make sure you are running BlueZ 5.56+ with experimental features to use Battery Provider."); + device->adapter->battery_provider_unavailable = true; + dbus_message_unref(reply); + return; + } + + spa_log_debug(device->monitor->log, "Registered Battery Provider"); + + device->adapter->has_battery_provider = true; + + if (!device->has_battery) + battery_create(device); + + dbus_message_unref(reply); +} + +// Register Battery Provider for adapter and then create virtual battery for device +static void register_battery_provider(struct spa_bt_device *device) +{ + DBusMessage *method_call; + DBusMessageIter message_iter; + + if (device->battery_pending_call) { + spa_log_debug(device->monitor->log, "Already registering battery provider"); + return; + } + + method_call = dbus_message_new_method_call( + BLUEZ_SERVICE, device->adapter_path, + BLUEZ_INTERFACE_BATTERY_PROVIDER_MANAGER, + "RegisterBatteryProvider"); + + if (!method_call) { + spa_log_error(device->monitor->log, "Failed to register battery provider"); + return; + } + + dbus_message_iter_init_append(method_call, &message_iter); + const char *object_path = PIPEWIRE_BATTERY_PROVIDER; + dbus_message_iter_append_basic(&message_iter, DBUS_TYPE_OBJECT_PATH, + &object_path); + + if (!dbus_connection_send_with_reply(device->monitor->conn, method_call, &device->battery_pending_call, + DBUS_TIMEOUT_USE_DEFAULT)) { + dbus_message_unref(method_call); + spa_log_error(device->monitor->log, "Failed to register battery provider"); + return; + } + + dbus_message_unref(method_call); + + if (!device->battery_pending_call) { + spa_log_error(device->monitor->log, "Failed to register battery provider"); + return; + } + + if (!dbus_pending_call_set_notify( + device->battery_pending_call, on_battery_provider_registered, + device, NULL)) { + spa_log_error(device->monitor->log, "Failed to register battery provider"); + dbus_pending_call_cancel(device->battery_pending_call); + dbus_pending_call_unref(device->battery_pending_call); + device->battery_pending_call = NULL; + } +} + +static int media_codec_to_endpoint(const struct media_codec *codec, + enum spa_bt_media_direction direction, + char** object_path) +{ + const char * endpoint; + + if (direction == SPA_BT_MEDIA_SOURCE) + endpoint = codec->bap ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT; + else + endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT; + + *object_path = spa_aprintf("%s/%s", endpoint, + codec->endpoint_name ? codec->endpoint_name : codec->name); + if (*object_path == NULL) + return -errno; + return 0; +} + +static const struct media_codec *media_endpoint_to_codec(struct spa_bt_monitor *monitor, const char *endpoint, bool *sink, const struct media_codec *preferred) +{ + const char *ep_name; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const struct media_codec *found = NULL; + int i; + + if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) { + ep_name = endpoint + strlen(A2DP_SINK_ENDPOINT "/"); + *sink = true; + } else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) { + ep_name = endpoint + strlen(A2DP_SOURCE_ENDPOINT "/"); + *sink = false; + } else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) { + ep_name = endpoint + strlen(BAP_SOURCE_ENDPOINT "/"); + *sink = false; + } else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) { + ep_name = endpoint + strlen(BAP_SINK_ENDPOINT "/"); + *sink = true; + } else { + *sink = true; + return NULL; + } + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + const char *codec_ep_name = + codec->endpoint_name ? codec->endpoint_name : codec->name; + + if (!spa_streq(ep_name, codec_ep_name)) + continue; + if ((*sink && !codec->decode) || (!*sink && !codec->encode)) + continue; + + /* Same endpoint may be shared with multiple codec objects, + * which may e.g. correspond to different encoder settings. + * Look up which one we selected. + */ + if ((preferred && codec == preferred) || found == NULL) + found = codec; + } + return found; +} + +static int media_endpoint_to_profile(const char *endpoint) +{ + + if (spa_strstartswith(endpoint, A2DP_SINK_ENDPOINT "/")) + return SPA_BT_PROFILE_A2DP_SOURCE; + else if (spa_strstartswith(endpoint, A2DP_SOURCE_ENDPOINT "/")) + return SPA_BT_PROFILE_A2DP_SINK; + else if (spa_strstartswith(endpoint, BAP_SINK_ENDPOINT "/")) + return SPA_BT_PROFILE_BAP_SOURCE; + else if (spa_strstartswith(endpoint, BAP_SOURCE_ENDPOINT "/")) + return SPA_BT_PROFILE_BAP_SINK; + else + return SPA_BT_PROFILE_NULL; +} + +static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec) +{ + return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL; +} + +static bool codec_has_direction(const struct media_codec *codec, enum spa_bt_media_direction direction) +{ + switch (direction) { + case SPA_BT_MEDIA_SOURCE: + return codec->encode; + case SPA_BT_MEDIA_SINK: + return codec->decode; + default: + spa_assert_not_reached(); + } +} + +static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor, + const struct media_codec *codec, + enum spa_bt_media_direction direction) +{ + /* Codecs with fill_caps == NULL share endpoint with another codec, + * and don't have their own endpoint + */ + return is_media_codec_enabled(monitor, codec) && + codec_has_direction(codec, direction) && + codec->fill_caps; +} + +static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + const char *path; + uint8_t *cap, config[A2DP_MAX_CAPS_SIZE]; + uint8_t *pconf = (uint8_t *) config; + DBusMessage *r; + DBusError err; + int size, res; + const struct media_codec *codec; + bool sink; + + dbus_error_init(&err); + + path = dbus_message_get_path(m); + + if (!dbus_message_get_args(m, &err, DBUS_TYPE_ARRAY, + DBUS_TYPE_BYTE, &cap, &size, DBUS_TYPE_INVALID)) { + spa_log_error(monitor->log, "Endpoint SelectConfiguration(): %s", err.message); + dbus_error_free(&err); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + spa_log_info(monitor->log, "%p: %s select conf %d", monitor, path, size); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, cap, (size_t)size); + + /* For codecs sharing the same endpoint, BlueZ-initiated connections + * always pick the default one. The session manager will + * switch the codec to a saved value after connection, so this generally + * does not matter. + */ + codec = media_endpoint_to_codec(monitor, path, &sink, NULL); + spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : ""); + + if (codec != NULL) + /* FIXME: We can't determine which device the SelectConfiguration() + * call is associated with, therefore device settings are not passed. + * This causes inconsistency with SelectConfiguration() triggered + * by codec switching. + */ + res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, cap, size, &monitor->default_audio_info, + &monitor->global_settings, config); + else + res = -ENOTSUP; + + if (res < 0 || res != size) { + spa_log_error(monitor->log, "can't select config: %d (%s)", + res, spa_strerror(res)); + if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", + "Unable to select configuration")) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + goto exit_send; + } + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, pconf, (size_t)size); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_ARRAY, + DBUS_TYPE_BYTE, &pconf, size, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + +exit_send: + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant); +static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size); +static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path); + +static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + const char *path; + DBusMessageIter args, props, iter; + DBusMessage *r = NULL; + int res; + const struct media_codec *codec; + bool sink; + const char *err_msg = "Unknown error"; + + const char *endpoint_path = NULL; + uint8_t caps[A2DP_MAX_CAPS_SIZE]; + uint8_t config[A2DP_MAX_CAPS_SIZE]; + int caps_size = 0; + int conf_size; + DBusMessageIter dict; + struct bap_endpoint_qos endpoint_qos; + + spa_zero(endpoint_qos); + + if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "a{sv}")) { + spa_log_error(monitor->log, "Invalid signature for method SelectProperties()"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_recurse(&args, &props); + if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + path = dbus_message_get_path(m); + + /* TODO: for codecs with shared endpoint, this currently always picks the default + * one. However, currently we don't have BAP codecs with shared endpoint, so + * this does not matter, but in case they are needed later we should pick the + * right one here. + */ + codec = media_endpoint_to_codec(monitor, path, &sink, NULL); + spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : ""); + if (!codec) { + spa_log_error(monitor->log, "Unsupported codec"); + err_msg = "Unsupported codec"; + goto error; + } + + /* Parse transport properties */ + while (dbus_message_iter_get_arg_type(&props) == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + int type; + + dbus_message_iter_recurse(&props, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + type = dbus_message_iter_get_arg_type(&value); + + if (spa_streq(key, "Capabilities")) { + DBusMessageIter array; + uint8_t *buf; + + if (type != DBUS_TYPE_ARRAY) { + spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type); + goto error_invalid; + } + + dbus_message_iter_recurse(&value, &array); + type = dbus_message_iter_get_arg_type(&array); + if (type != DBUS_TYPE_BYTE) { + spa_log_error(monitor->log, "%s is an array of wrong type %c", key, (char)type); + goto error_invalid; + } + + dbus_message_iter_get_fixed_array(&array, &buf, &caps_size); + if (caps_size > (int)sizeof(caps)) { + spa_log_error(monitor->log, "%s size:%d too large", key, (int)caps_size); + goto error_invalid; + } + memcpy(caps, buf, caps_size); + + spa_log_info(monitor->log, "%p: %s %s size:%d", monitor, path, key, caps_size); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', caps, (size_t)caps_size); + } else if (spa_streq(key, "Endpoint")) { + if (type != DBUS_TYPE_OBJECT_PATH) { + spa_log_error(monitor->log, "Property %s of wrong type %c", key, (char)type); + goto error_invalid; + } + + dbus_message_iter_get_basic(&value, &endpoint_path); + + spa_log_info(monitor->log, "%p: %s %s %s", monitor, path, key, endpoint_path); + } else if (type == DBUS_TYPE_BYTE) { + uint8_t v; + dbus_message_iter_get_basic(&value, &v); + + spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v); + + if (spa_streq(key, "Framing")) + endpoint_qos.framing = v; + else if (spa_streq(key, "PHY")) + endpoint_qos.phy = v; + else + spa_log_info(monitor->log, "Unknown property %s", key); + } else if (type == DBUS_TYPE_UINT16) { + dbus_uint16_t v; + dbus_message_iter_get_basic(&value, &v); + + spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v); + + if (spa_streq(key, "Latency")) + endpoint_qos.latency = v; + else + spa_log_info(monitor->log, "Unknown property %s", key); + } else if (type == DBUS_TYPE_UINT32) { + dbus_uint32_t v; + dbus_message_iter_get_basic(&value, &v); + + spa_log_info(monitor->log, "%p: %s %s 0x%x", monitor, path, key, (unsigned int)v); + + if (spa_streq(key, "MinimumDelay")) + endpoint_qos.delay_min = v; + else if (spa_streq(key, "MaximumDelay")) + endpoint_qos.delay_max = v; + else if (spa_streq(key, "PreferredMinimumDelay")) + endpoint_qos.preferred_delay_min = v; + else if (spa_streq(key, "PreferredMaximumDelay")) + endpoint_qos.preferred_delay_max = v; + else + spa_log_info(monitor->log, "Unknown property %s", key); + } else { + spa_log_info(monitor->log, "Unknown property %s", key); + } + + dbus_message_iter_next(&props); + } + + if (codec->bap) { + struct spa_bt_remote_endpoint *ep; + + ep = remote_endpoint_find(monitor, endpoint_path); + if (!ep) { + spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path); + goto error_invalid; + } + + /* Call of SelectProperties means that local device acts as an initiator + * and therefor remote endpoint is an acceptor + */ + ep->acceptor = true; + } + + /* TODO: determine which device the SelectConfiguration() call is associated + * with; it's known here based on the remote endpoint. + */ + conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, NULL, config); + if (conf_size < 0) { + spa_log_error(monitor->log, "can't select config: %d (%s)", + conf_size, spa_strerror(conf_size)); + goto error_invalid; + } + spa_log_info(monitor->log, "%p: selected conf %d", monitor, conf_size); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', (uint8_t *)config, (size_t)conf_size); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_iter_init_append(r, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING + DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, + &dict); + append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, &config, conf_size); + + if (codec->get_qos) { + struct bap_codec_qos qos; + dbus_bool_t framing; + const char *phy_str; + + spa_zero(qos); + + res = codec->get_qos(codec, config, conf_size, &endpoint_qos, &qos); + if (res < 0) { + spa_log_error(monitor->log, "can't select QOS config: %d (%s)", + res, spa_strerror(res)); + goto error_invalid; + } + + append_basic_variant_dict_entry(&dict, "Interval", DBUS_TYPE_UINT32, "u", &qos.interval); + framing = (qos.framing ? TRUE : FALSE); + append_basic_variant_dict_entry(&dict, "Framing", DBUS_TYPE_BOOLEAN, "b", &framing); + if (qos.phy == 0x1) + phy_str = "1M"; + else if (qos.phy == 0x2) + phy_str = "2M"; + else + spa_assert_not_reached(); + append_basic_variant_dict_entry(&dict, "PHY", DBUS_TYPE_STRING, "s", &phy_str); + append_basic_variant_dict_entry(&dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu); + append_basic_variant_dict_entry(&dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission); + append_basic_variant_dict_entry(&dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency); + append_basic_variant_dict_entry(&dict, "Delay", DBUS_TYPE_UINT32, "u", &qos.delay); + append_basic_variant_dict_entry(&dict, "TargetLatency", DBUS_TYPE_BYTE, "y", &qos.target_latency); + } + + dbus_message_iter_close_container(&iter, &dict); + + if (r) { + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + } + + return DBUS_HANDLER_RESULT_HANDLED; + +error_invalid: + err_msg = "Invalid property"; + goto error; + +error: + if (r) + dbus_message_unref(r); + if ((r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", err_msg)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) { + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_adapter *d; + spa_list_for_each(d, &monitor->adapter_list, link) + if (spa_streq(d->path, path)) + return d; + return NULL; +} + +static bool check_iter_signature(DBusMessageIter *it, const char *sig) +{ + char *v; + bool res; + v = dbus_message_iter_get_signature(it); + res = spa_streq(v, sig); + dbus_free(v); + return res; +} + +static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vendor, + uint16_t *product, uint16_t *version) +{ + char *pos; + unsigned int src, i, j, k; + + if (spa_strstartswith(modalias, "bluetooth:")) + src = SOURCE_ID_BLUETOOTH; + else if (spa_strstartswith(modalias, "usb:")) + src = SOURCE_ID_USB; + else + return -EINVAL; + + pos = strchr(modalias, ':'); + if (pos == NULL) + return -EINVAL; + + if (sscanf(pos + 1, "v%04Xp%04Xd%04X", &i, &j, &k) != 3) + return -EINVAL; + + /* Ignore BlueZ placeholder value */ + if (src == SOURCE_ID_USB && i == 0x1d6b && j == 0x0246) + return -ENXIO; + + *source = src; + *vendor = i; + *product = j; + *version = k; + + return 0; +} + +static int adapter_update_props(struct spa_bt_adapter *adapter, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + struct spa_bt_monitor *monitor = adapter->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "adapter %p: %s=%s", adapter, key, value); + + if (spa_streq(key, "Alias")) { + free(adapter->alias); + adapter->alias = strdup(value); + } + else if (spa_streq(key, "Name")) { + free(adapter->name); + adapter->name = strdup(value); + } + else if (spa_streq(key, "Address")) { + free(adapter->address); + adapter->address = strdup(value); + } + else if (spa_streq(key, "Modalias")) { + int ret; + ret = parse_modalias(value, &adapter->source_id, &adapter->vendor_id, + &adapter->product_id, &adapter->version_id); + if (ret < 0) + spa_log_debug(monitor->log, "adapter %p: %s=%s ignored: %s", + adapter, key, value, spa_strerror(ret)); + } + } + else if (type == DBUS_TYPE_UINT32) { + uint32_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value); + + if (spa_streq(key, "Class")) + adapter->bluetooth_class = value; + + } + else if (type == DBUS_TYPE_BOOLEAN) { + int value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "adapter %p: %s=%d", adapter, key, value); + + if (spa_streq(key, "Powered")) { + adapter->powered = value; + } + } + else if (spa_streq(key, "UUIDs")) { + DBusMessageIter iter; + + if (!check_iter_signature(&it[1], "as")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *uuid; + enum spa_bt_profile profile; + + dbus_message_iter_get_basic(&iter, &uuid); + + profile = spa_bt_profile_from_uuid(uuid); + + if (profile && (adapter->profiles & profile) == 0) { + spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, uuid); + adapter->profiles |= profile; + } else if (strcasecmp(uuid, SPA_BT_UUID_PACS) == 0 && + (adapter->profiles & SPA_BT_PROFILE_BAP_SINK) == 0) { + spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SINK); + adapter->profiles |= SPA_BT_PROFILE_BAP_SINK; + spa_log_debug(monitor->log, "adapter %p: add UUID=%s", adapter, SPA_BT_UUID_BAP_SOURCE); + adapter->profiles |= SPA_BT_PROFILE_BAP_SOURCE; + } + dbus_message_iter_next(&iter); + } + } + else + spa_log_debug(monitor->log, "adapter %p: unhandled key %s", adapter, key); + +next: + dbus_message_iter_next(props_iter); + } + return 0; +} + +static void adapter_update_devices(struct spa_bt_adapter *adapter) +{ + struct spa_bt_monitor *monitor = adapter->monitor; + struct spa_bt_device *device; + + /* + * Update devices when new adapter appears. + * Devices may appear on DBus before or after the adapter does. + */ + + spa_list_for_each(device, &monitor->device_list, link) { + if (device->adapter == NULL && spa_streq(device->adapter_path, adapter->path)) + device->adapter = adapter; + } +} + +static void adapter_register_player(struct spa_bt_adapter *adapter) +{ + if (adapter->player_registered || !adapter->monitor->dummy_avrcp_player) + return; + + if (spa_bt_player_register(adapter->dummy_player, adapter->path) == 0) + adapter->player_registered = true; +} + +static int adapter_init_bus_type(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d) +{ + char path[1024], buf[1024]; + const char *str; + ssize_t res = -EINVAL; + + d->bus_type = BUS_TYPE_OTHER; + + str = strrchr(d->path, '/'); /* hciXX */ + if (str == NULL) + return -ENOENT; + + snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/subsystem", str); + if ((res = readlink(path, buf, sizeof(buf)-1)) < 0) + return -errno; + buf[res] = '\0'; + + str = strrchr(buf, '/'); + if (str && spa_streq(str, "/usb")) + d->bus_type = BUS_TYPE_USB; + return 0; +} + +static int adapter_init_modalias(struct spa_bt_monitor *monitor, struct spa_bt_adapter *d) +{ + char path[1024]; + FILE *f = NULL; + int vendor_id, product_id; + const char *str; + int res = -EINVAL; + + /* Lookup vendor/product id for the device, if present */ + str = strrchr(d->path, '/'); /* hciXX */ + if (str == NULL) + goto fail; + snprintf(path, sizeof(path), "/sys/class/bluetooth/%s/device/modalias", str); + if ((f = fopen(path, "rbe")) == NULL) { + res = -errno; + goto fail; + } + if (fscanf(f, "usb:v%04Xp%04X", &vendor_id, &product_id) != 2) + goto fail; + d->source_id = SOURCE_ID_USB; + d->vendor_id = vendor_id; + d->product_id = product_id; + fclose(f); + + spa_log_debug(monitor->log, "adapter %p: usb vendor:%04x product:%04x", + d, vendor_id, product_id); + return 0; + +fail: + if (f) + fclose(f); + return res; +} + +static struct spa_bt_adapter *adapter_create(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_adapter *d; + + d = calloc(1, sizeof(struct spa_bt_adapter)); + if (d == NULL) + return NULL; + + d->dummy_player = spa_bt_player_new(monitor->conn, monitor->log); + if (d->dummy_player == NULL) { + free(d); + return NULL; + } + + d->monitor = monitor; + d->path = strdup(path); + + spa_list_prepend(&monitor->adapter_list, &d->link); + + adapter_init_bus_type(monitor, d); + adapter_init_modalias(monitor, d); + + return d; +} + +static void device_free(struct spa_bt_device *device); + +static void adapter_free(struct spa_bt_adapter *adapter) +{ + struct spa_bt_monitor *monitor = adapter->monitor; + struct spa_bt_device *d, *td; + + spa_log_debug(monitor->log, "%p", adapter); + + /* Devices should be destroyed before their assigned adapter */ + spa_list_for_each_safe(d, td, &monitor->device_list, link) + if (d->adapter == adapter) + device_free(d); + + spa_bt_player_destroy(adapter->dummy_player); + + spa_list_remove(&adapter->link); + free(adapter->alias); + free(adapter->name); + free(adapter->address); + free(adapter->path); + free(adapter); +} + +static uint32_t adapter_connectable_profiles(struct spa_bt_adapter *adapter) +{ + const uint32_t profiles = adapter->profiles; + uint32_t mask = 0; + + if (profiles & SPA_BT_PROFILE_A2DP_SINK) + mask |= SPA_BT_PROFILE_A2DP_SOURCE; + if (profiles & SPA_BT_PROFILE_A2DP_SOURCE) + mask |= SPA_BT_PROFILE_A2DP_SINK; + + if (profiles & SPA_BT_PROFILE_BAP_SINK) + mask |= SPA_BT_PROFILE_BAP_SOURCE; + if (profiles & SPA_BT_PROFILE_BAP_SOURCE) + mask |= SPA_BT_PROFILE_BAP_SINK; + + if (profiles & SPA_BT_PROFILE_HSP_AG) + mask |= SPA_BT_PROFILE_HSP_HS; + if (profiles & SPA_BT_PROFILE_HSP_HS) + mask |= SPA_BT_PROFILE_HSP_AG; + + if (profiles & SPA_BT_PROFILE_HFP_AG) + mask |= SPA_BT_PROFILE_HFP_HF; + if (profiles & SPA_BT_PROFILE_HFP_HF) + mask |= SPA_BT_PROFILE_HFP_AG; + + return mask; +} + +struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_device *d; + spa_list_for_each(d, &monitor->device_list, link) + if (spa_streq(d->path, path)) + return d; + return NULL; +} + +struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address) +{ + struct spa_bt_device *d; + spa_list_for_each(d, &monitor->device_list, link) + if (spa_streq(d->address, remote_address) && spa_streq(d->adapter->address, local_address)) + return d; + return NULL; +} + +void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device) +{ + struct timespec ts; + spa_system_clock_gettime(device->monitor->main_system, CLOCK_MONOTONIC, &ts); + device->last_bluez_action_time = SPA_TIMESPEC_TO_NSEC(&ts); +} + +static struct spa_bt_device *device_create(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_device *d; + + d = calloc(1, sizeof(struct spa_bt_device)); + if (d == NULL) + return NULL; + + d->id = monitor->id++; + d->monitor = monitor; + d->path = strdup(path); + d->battery_path = battery_get_name(d->path); + d->reconnect_profiles = DEFAULT_RECONNECT_PROFILES; + d->hw_volume_profiles = DEFAULT_HW_VOLUME_PROFILES; + + spa_list_init(&d->remote_endpoint_list); + spa_list_init(&d->transport_list); + spa_list_init(&d->codec_switch_list); + + spa_hook_list_init(&d->listener_list); + + spa_list_prepend(&monitor->device_list, &d->link); + + spa_bt_device_update_last_bluez_action_time(d); + + return d; +} + +static int device_stop_timer(struct spa_bt_device *device); + +static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw); + +static void device_clear_sub(struct spa_bt_device *device) +{ + battery_remove(device); + spa_bt_device_release_transports(device); +} + +static void device_free(struct spa_bt_device *device) +{ + struct spa_bt_remote_endpoint *ep, *tep; + struct spa_bt_media_codec_switch *sw; + struct spa_bt_transport *t, *tt; + struct spa_bt_monitor *monitor = device->monitor; + + spa_log_debug(monitor->log, "%p", device); + + spa_bt_device_emit_destroy(device); + + device_clear_sub(device); + device_stop_timer(device); + + if (device->added) { + spa_device_emit_object_info(&monitor->hooks, device->id, NULL); + } + + spa_list_for_each_safe(ep, tep, &device->remote_endpoint_list, device_link) { + if (ep->device == device) { + spa_list_remove(&ep->device_link); + ep->device = NULL; + } + } + + spa_list_for_each_safe(t, tt, &device->transport_list, device_link) { + if (t->device == device) { + spa_list_remove(&t->device_link); + t->device = NULL; + } + } + + spa_list_consume(sw, &device->codec_switch_list, device_link) + media_codec_switch_free(sw); + + spa_list_remove(&device->link); + free(device->path); + free(device->alias); + free(device->address); + free(device->adapter_path); + free(device->battery_path); + free(device->name); + free(device->icon); + free(device); +} + +int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id, uint16_t product_id, + char *vendor_str, int vendor_str_size, char *product_str, int product_str_size) +{ + char *source_str; + + switch (source_id) { + case SOURCE_ID_USB: + source_str = "usb"; + break; + case SOURCE_ID_BLUETOOTH: + source_str = "bluetooth"; + break; + default: + return -EINVAL; + } + + spa_scnprintf(vendor_str, vendor_str_size, "%s:%04x", source_str, (unsigned int)vendor_id); + spa_scnprintf(product_str, product_str_size, "%04x", (unsigned int)product_id); + return 0; +} + +static void emit_device_info(struct spa_bt_monitor *monitor, + struct spa_bt_device *device, bool with_connection) +{ + struct spa_device_object_info info; + char dev[32], name[128], class[16], vendor_id[64], product_id[64], product_id_tot[67]; + struct spa_dict_item items[23]; + uint32_t n_items = 0; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_BLUEZ5_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + snprintf(name, sizeof(name), "bluez_card.%s", device->address); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, name); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, device->alias); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ALIAS, device->name); + if (spa_bt_format_vendor_product_id( + device->source_id, device->vendor_id, device->product_id, + vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) { + snprintf(product_id_tot, sizeof(product_id_tot), "0x%s", product_id); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, vendor_id); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, product_id_tot); + } + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, + spa_bt_form_factor_name( + spa_bt_form_factor_from_class(device->bluetooth_class))); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_STRING, device->address); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, device->icon); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, device->path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address); + snprintf(dev, sizeof(dev), "pointer:%p", device); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_DEVICE, dev); + snprintf(class, sizeof(class), "0x%06x", device->bluetooth_class); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class); + + if (with_connection) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CONNECTION, + device->connected ? "connected": "disconnected"); + } + + info.props = &SPA_DICT_INIT(items, n_items); + spa_device_emit_object_info(&monitor->hooks, device->id, &info); +} + +static int device_connected_old(struct spa_bt_monitor *monitor, + struct spa_bt_device *device, int connected) +{ + + if (connected == BT_DEVICE_INIT) + return 0; + + device->connected = connected; + + if (device->connected) { + emit_device_info(monitor, device, false); + device->added = true; + } else { + if (!device->added) + return 0; + + device_clear_sub(device); + spa_device_emit_object_info(&monitor->hooks, device->id, NULL); + device->added = false; + } + + return 0; +} + +enum { + BT_DEVICE_RECONNECT_INIT = 0, + BT_DEVICE_RECONNECT_PROFILE, + BT_DEVICE_RECONNECT_STOP +}; + +static int device_connected(struct spa_bt_monitor *monitor, + struct spa_bt_device *device, int status) +{ + bool connected, init = (status == BT_DEVICE_INIT); + + connected = init ? 0 : status; + + if (!init) { + device->reconnect_state = + connected ? BT_DEVICE_RECONNECT_STOP + : BT_DEVICE_RECONNECT_PROFILE; + } + + if ((device->connected_profiles != 0) ^ connected) { + spa_log_error(monitor->log, + "device %p: unexpected call, connected_profiles:%08x connected:%d", + device, device->connected_profiles, device->connected); + return -EINVAL; + } + + if (!monitor->connection_info_supported) + return device_connected_old(monitor, device, status); + + if (init) { + device->connected = connected; + } else { + if (!device->added || !(connected ^ device->connected)) + return 0; + + device->connected = connected; + spa_bt_device_emit_connected(device, device->connected); + + if (!device->connected) + device_clear_sub(device); + } + + emit_device_info(monitor, device, true); + device->added = true; + + return 0; +} + +/* + * Add profile to device based on bluez actions + * (update property UUIDs, trigger profile handlers), + * in case UUIDs is empty on signal InterfaceAdded for + * org.bluez.Device1. And emit device info if there is + * at least 1 profile on device. This should be called + * before any device setting accessing. + */ +int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile) +{ + struct spa_bt_monitor *monitor = device->monitor; + + if (profile && (device->profiles & profile) == 0) { + spa_log_info(monitor->log, "device %p: add new profile %08x", device, profile); + device->profiles |= profile; + } + + if (!device->added && device->profiles) { + device_connected(monitor, device, BT_DEVICE_INIT); + if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT) + device_start_timer(device); + } + + return 0; +} + + +static int device_try_connect_profile(struct spa_bt_device *device, + const char *profile_uuid) +{ + struct spa_bt_monitor *monitor = device->monitor; + DBusMessage *m; + + spa_log_info(monitor->log, "device %p %s: profile %s not connected; try ConnectProfile()", + device, device->path, profile_uuid); + + /* Call org.bluez.Device1.ConnectProfile() on device, ignoring result */ + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + device->path, + BLUEZ_DEVICE_INTERFACE, + "ConnectProfile"); + if (m == NULL) + return -ENOMEM; + dbus_message_append_args(m, DBUS_TYPE_STRING, &profile_uuid, DBUS_TYPE_INVALID); + if (!dbus_connection_send(monitor->conn, m, NULL)) { + dbus_message_unref(m); + return -EIO; + } + dbus_message_unref(m); + + return 0; +} + +static int reconnect_device_profiles(struct spa_bt_device *device) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_device *d; + uint32_t reconnect = device->profiles + & device->reconnect_profiles + & (device->connected_profiles ^ device->profiles); + + /* Don't try to connect to same device via multiple adapters */ + spa_list_for_each(d, &monitor->device_list, link) { + if (d != device && spa_streq(d->address, device->address)) { + if (d->paired && d->trusted && !d->blocked && + d->reconnect_state == BT_DEVICE_RECONNECT_STOP) + reconnect &= ~d->reconnect_profiles; + if (d->connected_profiles) + reconnect = 0; + } + } + + /* Connect only profiles the adapter has a counterpart for */ + if (device->adapter) + reconnect &= adapter_connectable_profiles(device->adapter); + + if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) { + if (reconnect & SPA_BT_PROFILE_HFP_HF) { + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_HS); + } else if (reconnect & SPA_BT_PROFILE_HSP_HS) { + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_HF); + } + } else + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_HEAD_UNIT); + + if (!(device->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) { + if (reconnect & SPA_BT_PROFILE_HFP_AG) + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HSP_AG); + else if (reconnect & SPA_BT_PROFILE_HSP_AG) + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HFP_AG); + } else + SPA_FLAG_CLEAR(reconnect, SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + + if (reconnect & SPA_BT_PROFILE_HFP_HF) + device_try_connect_profile(device, SPA_BT_UUID_HFP_HF); + if (reconnect & SPA_BT_PROFILE_HSP_HS) + device_try_connect_profile(device, SPA_BT_UUID_HSP_HS); + if (reconnect & SPA_BT_PROFILE_HFP_AG) + device_try_connect_profile(device, SPA_BT_UUID_HFP_AG); + if (reconnect & SPA_BT_PROFILE_HSP_AG) + device_try_connect_profile(device, SPA_BT_UUID_HSP_AG); + if (reconnect & SPA_BT_PROFILE_A2DP_SINK) + device_try_connect_profile(device, SPA_BT_UUID_A2DP_SINK); + if (reconnect & SPA_BT_PROFILE_A2DP_SOURCE) + device_try_connect_profile(device, SPA_BT_UUID_A2DP_SOURCE); + if (reconnect & SPA_BT_PROFILE_BAP_SINK) + device_try_connect_profile(device, SPA_BT_UUID_BAP_SINK); + if (reconnect & SPA_BT_PROFILE_BAP_SOURCE) + device_try_connect_profile(device, SPA_BT_UUID_BAP_SOURCE); + + return reconnect; +} + +#define DEVICE_RECONNECT_TIMEOUT_SEC 2 +#define DEVICE_PROFILE_TIMEOUT_SEC 6 + +static void device_timer_event(struct spa_source *source) +{ + struct spa_bt_device *device = source->data; + struct spa_bt_monitor *monitor = device->monitor; + uint64_t exp; + + if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0) + spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno)); + + spa_log_debug(monitor->log, "device %p: timeout %08x %08x", + device, device->profiles, device->connected_profiles); + device_stop_timer(device); + if (BT_DEVICE_RECONNECT_STOP != device->reconnect_state) { + device->reconnect_state = BT_DEVICE_RECONNECT_STOP; + if (device->paired + && device->trusted + && !device->blocked + && device->reconnect_profiles != 0 + && reconnect_device_profiles(device)) + { + device_start_timer(device); + return; + } + } + if (device->connected_profiles) + device_connected(device->monitor, device, BT_DEVICE_CONNECTED); +} + +static int device_start_timer(struct spa_bt_device *device) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct itimerspec ts; + + spa_log_debug(monitor->log, "device %p: start timer", device); + if (device->timer.data == NULL) { + device->timer.data = device; + device->timer.func = device_timer_event; + device->timer.fd = spa_system_timerfd_create(monitor->main_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + device->timer.mask = SPA_IO_IN; + device->timer.rmask = 0; + spa_loop_add_source(monitor->main_loop, &device->timer); + } + ts.it_value.tv_sec = device->reconnect_state == BT_DEVICE_RECONNECT_STOP + ? DEVICE_PROFILE_TIMEOUT_SEC + : DEVICE_RECONNECT_TIMEOUT_SEC; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL); + return 0; +} + +static int device_stop_timer(struct spa_bt_device *device) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct itimerspec ts; + + if (device->timer.data == NULL) + return 0; + + spa_log_debug(monitor->log, "device %p: stop timer", device); + spa_loop_remove_source(monitor->main_loop, &device->timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, device->timer.fd, 0, &ts, NULL); + spa_system_close(monitor->main_system, device->timer.fd); + device->timer.data = NULL; + return 0; +} + +int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force) +{ + struct spa_bt_monitor *monitor = device->monitor; + uint32_t connected_profiles = device->connected_profiles; + uint32_t connectable_profiles = + device->adapter ? adapter_connectable_profiles(device->adapter) : 0; + uint32_t direction_masks[3] = { + SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_HEADSET_HEAD_UNIT, + SPA_BT_PROFILE_MEDIA_SOURCE, + SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY, + }; + bool direction_connected = false; + bool all_connected; + size_t i; + + if (connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + connected_profiles |= SPA_BT_PROFILE_HEADSET_HEAD_UNIT; + if (connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) + connected_profiles |= SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; + + for (i = 0; i < SPA_N_ELEMENTS(direction_masks); ++i) { + uint32_t mask = direction_masks[i] & device->profiles & connectable_profiles; + if (mask && (connected_profiles & mask) == mask) + direction_connected = true; + } + + all_connected = (device->profiles & connected_profiles) == device->profiles; + + spa_log_debug(monitor->log, "device %p: profiles %08x %08x connectable:%08x added:%d all:%d dir:%d", + device, device->profiles, connected_profiles, connectable_profiles, + device->added, all_connected, direction_connected); + + if (connected_profiles == 0 && spa_list_is_empty(&device->codec_switch_list)) { + device_stop_timer(device); + device_connected(monitor, device, BT_DEVICE_DISCONNECTED); + } else if (force || direction_connected || all_connected) { + device_stop_timer(device); + device_connected(monitor, device, BT_DEVICE_CONNECTED); + } else { + /* The initial reconnect event has not been triggered, + * the connecting is triggered by bluez. */ + if (device->reconnect_state == BT_DEVICE_RECONNECT_INIT) + device->reconnect_state = BT_DEVICE_RECONNECT_PROFILE; + device_start_timer(device); + } + return 0; +} + +static void device_set_connected(struct spa_bt_device *device, int connected) +{ + struct spa_bt_monitor *monitor = device->monitor; + + if (device->connected && !connected) + device->connected_profiles = 0; + + if (connected) + spa_bt_device_check_profiles(device, false); + else { + /* Stop codec switch on disconnect */ + struct spa_bt_media_codec_switch *sw; + spa_list_consume(sw, &device->codec_switch_list, device_link) + media_codec_switch_free(sw); + + if (device->reconnect_state != BT_DEVICE_RECONNECT_INIT) + device_stop_timer(device); + device_connected(monitor, device, BT_DEVICE_DISCONNECTED); + } +} + +int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile) +{ + uint32_t prev_connected = device->connected_profiles; + device->connected_profiles |= profile; + spa_bt_device_check_profiles(device, false); + if (device->connected_profiles != prev_connected) + spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected); + return 0; +} + +static void device_update_hw_volume_profiles(struct spa_bt_device *device) +{ + struct spa_bt_monitor *monitor = device->monitor; + uint32_t bt_features = 0; + + if (!monitor->quirks) + return; + + if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) != 0) + return; + + if (!(bt_features & SPA_BT_FEATURE_HW_VOLUME)) + device->hw_volume_profiles = 0; + + spa_log_debug(monitor->log, "hw-volume-profiles:%08x", (int)device->hw_volume_profiles); +} + +static int device_update_props(struct spa_bt_device *device, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + struct spa_bt_monitor *monitor = device->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "device %p: %s=%s", device, key, value); + + if (spa_streq(key, "Alias")) { + free(device->alias); + device->alias = strdup(value); + } + else if (spa_streq(key, "Name")) { + free(device->name); + device->name = strdup(value); + } + else if (spa_streq(key, "Address")) { + free(device->address); + device->address = strdup(value); + } + else if (spa_streq(key, "Adapter")) { + free(device->adapter_path); + device->adapter_path = strdup(value); + + device->adapter = adapter_find(monitor, value); + if (device->adapter == NULL) { + spa_log_info(monitor->log, "unknown adapter %s", value); + } + } + else if (spa_streq(key, "Icon")) { + free(device->icon); + device->icon = strdup(value); + } + else if (spa_streq(key, "Modalias")) { + int ret; + ret = parse_modalias(value, &device->source_id, &device->vendor_id, + &device->product_id, &device->version_id); + if (ret < 0) + spa_log_debug(monitor->log, "device %p: %s=%s ignored: %s", + device, key, value, spa_strerror(ret)); + } + } + else if (type == DBUS_TYPE_UINT32) { + uint32_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "device %p: %s=%08x", device, key, value); + + if (spa_streq(key, "Class")) + device->bluetooth_class = value; + } + else if (type == DBUS_TYPE_UINT16) { + uint16_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value); + + if (spa_streq(key, "Appearance")) + device->appearance = value; + } + else if (type == DBUS_TYPE_INT16) { + int16_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value); + + if (spa_streq(key, "RSSI")) + device->RSSI = value; + } + else if (type == DBUS_TYPE_BOOLEAN) { + int value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "device %p: %s=%d", device, key, value); + + if (spa_streq(key, "Paired")) { + device->paired = value; + } + else if (spa_streq(key, "Trusted")) { + device->trusted = value; + } + else if (spa_streq(key, "Connected")) { + device_set_connected(device, value); + } + else if (spa_streq(key, "Blocked")) { + device->blocked = value; + } + else if (spa_streq(key, "ServicesResolved")) { + if (value) + spa_bt_device_check_profiles(device, false); + } + } + else if (spa_streq(key, "UUIDs")) { + DBusMessageIter iter; + uint32_t prev_profiles = device->profiles; + + if (!check_iter_signature(&it[1], "as")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *uuid; + enum spa_bt_profile profile; + + dbus_message_iter_get_basic(&iter, &uuid); + + profile = spa_bt_profile_from_uuid(uuid); + + /* Only add A2DP/BAP profiles if HSP/HFP backed is none. + * This allows BT device to connect instantly instead of waiting for + * profile timeout, because all available profiles are connected. + */ + if (monitor->backend_selection != BACKEND_NONE || (monitor->backend_selection == BACKEND_NONE && + profile & (SPA_BT_PROFILE_MEDIA_SINK | SPA_BT_PROFILE_MEDIA_SOURCE))) { + if (profile && (device->profiles & profile) == 0) { + spa_log_debug(monitor->log, "device %p: add UUID=%s", device, uuid); + device->profiles |= profile; + } + } + dbus_message_iter_next(&iter); + } + + if (device->profiles != prev_profiles) + spa_bt_device_emit_profiles_changed( + device, prev_profiles, device->connected_profiles); + } + else + spa_log_debug(monitor->log, "device %p: unhandled key %s type %d", device, key, type); + +next: + dbus_message_iter_next(props_iter); + } + return 0; +} + +static bool device_props_ready(struct spa_bt_device *device) +{ + /* + * In some cases, BlueZ device props may be missing part of + * the information required when the interface first appears. + */ + return device->adapter && device->address; +} + +bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink) +{ + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_remote_endpoint *ep; + const struct { enum spa_bluetooth_audio_codec codec; uint32_t mask; } quirks[] = { + { SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, SPA_BT_FEATURE_SBC_XQ }, + { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, SPA_BT_FEATURE_FASTSTREAM }, + { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_FASTSTREAM }, + { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX }, + { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX }, + }; + size_t i; + + if (!is_media_codec_enabled(device->monitor, codec)) + return false; + + if (!device->adapter->application_registered) { + /* Codec switching not supported: only plain SBC allowed */ + return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc")); + } + + /* Check codec quirks */ + for (i = 0; i < SPA_N_ELEMENTS(quirks); ++i) { + uint32_t bt_features; + + if (codec->id != quirks[i].codec) + continue; + if (monitor->quirks == NULL) + break; + if (spa_bt_quirks_get_features(monitor->quirks, device->adapter, device, &bt_features) < 0) + break; + if (!(bt_features & quirks[i].mask)) + return false; + } + + spa_list_for_each(ep, &device->remote_endpoint_list, device_link) { + const enum spa_bt_profile profile = spa_bt_profile_from_uuid(ep->uuid); + enum spa_bt_profile expected; + + if (codec->bap) + expected = sink ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_BAP_SOURCE; + else + expected = sink ? SPA_BT_PROFILE_A2DP_SINK : SPA_BT_PROFILE_A2DP_SOURCE; + + if (profile != expected) + continue; + + if (media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len, + &ep->monitor->default_audio_info, &monitor->global_settings)) + return true; + } + + return false; +} + +const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink) +{ + struct spa_bt_monitor *monitor = device->monitor; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const struct media_codec **supported_codecs; + size_t i, j, size; + + *count = 0; + + size = 8; + supported_codecs = malloc(size * sizeof(const struct media_codec *)); + if (supported_codecs == NULL) + return NULL; + + j = 0; + for (i = 0; media_codecs[i] != NULL; ++i) { + if (spa_bt_device_supports_media_codec(device, media_codecs[i], sink)) { + supported_codecs[j] = media_codecs[i]; + ++j; + } + + if (j >= size) { + const struct media_codec **p; + size = size * 2; +#ifdef HAVE_REALLOCARRRAY + p = reallocarray(supported_codecs, size, sizeof(const struct media_codec *)); +#else + p = realloc(supported_codecs, size * sizeof(const struct media_codec *)); +#endif + if (p == NULL) { + free(supported_codecs); + return NULL; + } + supported_codecs = p; + } + } + + supported_codecs[j] = NULL; + *count = j; + + return supported_codecs; +} + +static struct spa_bt_remote_endpoint *device_remote_endpoint_find(struct spa_bt_device *device, const char *path) +{ + struct spa_bt_remote_endpoint *ep; + spa_list_for_each(ep, &device->remote_endpoint_list, device_link) + if (spa_streq(ep->path, path)) + return ep; + return NULL; +} + +static struct spa_bt_remote_endpoint *remote_endpoint_find(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_remote_endpoint *ep; + spa_list_for_each(ep, &monitor->remote_endpoint_list, link) + if (spa_streq(ep->path, path)) + return ep; + return NULL; +} + +static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_endpoint, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + struct spa_bt_monitor *monitor = remote_endpoint->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%s", remote_endpoint, key, value); + + if (spa_streq(key, "UUID")) { + free(remote_endpoint->uuid); + remote_endpoint->uuid = strdup(value); + } + else if (spa_streq(key, "Device")) { + struct spa_bt_device *device; + device = spa_bt_device_find(monitor, value); + if (device == NULL) + goto next; + spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device); + + if (remote_endpoint->device != device) { + if (remote_endpoint->device != NULL) + spa_list_remove(&remote_endpoint->device_link); + remote_endpoint->device = device; + if (device != NULL) + spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); + } + } + } + else if (type == DBUS_TYPE_BOOLEAN) { + int value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, value); + + if (spa_streq(key, "DelayReporting")) { + remote_endpoint->delay_reporting = value; + } + } + else if (type == DBUS_TYPE_BYTE) { + uint8_t value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value); + + if (spa_streq(key, "Codec")) { + remote_endpoint->codec = value; + } + } + else if (spa_streq(key, "Capabilities")) { + DBusMessageIter iter; + uint8_t *value; + int len; + + if (!check_iter_signature(&it[1], "ay")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + dbus_message_iter_get_fixed_array(&iter, &value, &len); + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len); + + free(remote_endpoint->capabilities); + remote_endpoint->capabilities_len = 0; + + remote_endpoint->capabilities = malloc(len); + if (remote_endpoint->capabilities) { + memcpy(remote_endpoint->capabilities, value, len); + remote_endpoint->capabilities_len = len; + } + } + else + spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key); + +next: + dbus_message_iter_next(props_iter); + } + return 0; +} + +static struct spa_bt_remote_endpoint *remote_endpoint_create(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_remote_endpoint *ep; + + ep = calloc(1, sizeof(struct spa_bt_remote_endpoint)); + if (ep == NULL) + return NULL; + + ep->monitor = monitor; + ep->path = strdup(path); + + spa_list_prepend(&monitor->remote_endpoint_list, &ep->link); + + return ep; +} + +static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint) +{ + struct spa_bt_monitor *monitor = remote_endpoint->monitor; + + spa_log_debug(monitor->log, "remote endpoint %p: free %s", + remote_endpoint, remote_endpoint->path); + + if (remote_endpoint->device) + spa_list_remove(&remote_endpoint->device_link); + + spa_list_remove(&remote_endpoint->link); + free(remote_endpoint->path); + free(remote_endpoint->uuid); + free(remote_endpoint->capabilities); + free(remote_endpoint); +} + +struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path) +{ + struct spa_bt_transport *t; + spa_list_for_each(t, &monitor->transport_list, link) + if (spa_streq(t->path, path)) + return t; + return NULL; +} + +struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor, + bool (*callback) (struct spa_bt_transport *t, const void *data), + const void *data) +{ + struct spa_bt_transport *t; + + spa_list_for_each(t, &monitor->transport_list, link) + if (callback(t, data) == true) + return t; + return NULL; +} + + +struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra) +{ + struct spa_bt_transport *t; + + t = calloc(1, sizeof(struct spa_bt_transport) + extra); + if (t == NULL) + return NULL; + + t->acquire_refcount = 0; + t->monitor = monitor; + t->path = path; + t->fd = -1; + t->sco_io = NULL; + t->delay = SPA_BT_UNKNOWN_DELAY; + t->user_data = SPA_PTROFF(t, sizeof(struct spa_bt_transport), void); + spa_hook_list_init(&t->listener_list); + spa_list_init(&t->bap_transport_linked); + + spa_list_append(&monitor->transport_list, &t->link); + + return t; +} + +bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport) +{ + return transport->device != NULL + && (transport->device->hw_volume_profiles & transport->profile); +} + +static void transport_sync_volume(struct spa_bt_transport *transport) +{ + if (!spa_bt_transport_volume_enabled(transport)) + return; + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) + spa_bt_transport_set_volume(transport, i, transport->volumes[i].volume); + spa_bt_transport_emit_volume_changed(transport); +} + +void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state) +{ + struct spa_bt_monitor *monitor = transport->monitor; + enum spa_bt_transport_state old = transport->state; + + if (old != state) { + transport->state = state; + spa_log_debug(monitor->log, "transport %p: %s state changed %d -> %d", + transport, transport->path, old, state); + spa_bt_transport_emit_state_changed(transport, old, state); + if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING) + transport_sync_volume(transport); + } +} + +void spa_bt_transport_free(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_device *device = transport->device; + uint32_t prev_connected = 0; + + spa_log_debug(monitor->log, "transport %p: free %s", transport, transport->path); + + spa_bt_transport_set_state(transport, SPA_BT_TRANSPORT_STATE_IDLE); + + spa_bt_transport_keepalive(transport, false); + + spa_bt_transport_emit_destroy(transport); + + spa_bt_transport_stop_volume_timer(transport); + spa_bt_transport_stop_release_timer(transport); + + if (transport->sco_io) { + spa_bt_sco_io_destroy(transport->sco_io); + transport->sco_io = NULL; + } + + spa_bt_transport_destroy(transport); + + if (transport->fd >= 0) { + spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED); + + shutdown(transport->fd, SHUT_RDWR); + close(transport->fd); + transport->fd = -1; + } + + spa_list_remove(&transport->link); + if (transport->device) { + prev_connected = transport->device->connected_profiles; + transport->device->connected_profiles &= ~transport->profile; + spa_list_remove(&transport->device_link); + } + + if (device && device->connected_profiles != prev_connected) + spa_bt_device_emit_profiles_changed(device, device->profiles, prev_connected); + + spa_list_remove(&transport->bap_transport_linked); + + free(transport->endpoint_path); + free(transport->path); + free(transport); +} + +int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive) +{ + if (keepalive) { + t->keepalive = true; + return 0; + } + + t->keepalive = false; + + if (t->acquire_refcount == 0 && t->acquired) { + t->acquire_refcount = 1; + return spa_bt_transport_release(t); + } + + return 0; +} + +int spa_bt_transport_acquire(struct spa_bt_transport *transport, bool optional) +{ + struct spa_bt_monitor *monitor = transport->monitor; + int res; + + if (transport->acquire_refcount > 0) { + spa_log_debug(monitor->log, "transport %p: incref %s", transport, transport->path); + transport->acquire_refcount += 1; + return 0; + } + spa_assert(transport->acquire_refcount == 0); + + if (!transport->acquired) + res = spa_bt_transport_impl(transport, acquire, 0, optional); + else + res = 0; + + if (res >= 0) { + transport->acquire_refcount = 1; + transport->acquired = true; + } + + return res; +} + +int spa_bt_transport_release(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + int res; + + if (transport->acquire_refcount > 1) { + spa_log_debug(monitor->log, "transport %p: decref %s", transport, transport->path); + transport->acquire_refcount -= 1; + return 0; + } + else if (transport->acquire_refcount == 0) { + spa_log_info(monitor->log, "transport %s already released", transport->path); + return 0; + } + spa_assert(transport->acquire_refcount == 1); + spa_assert(transport->acquired); + + if (SPA_BT_TRANSPORT_IS_SCO(transport)) { + /* Postpone SCO transport releases, since we might need it again soon */ + res = spa_bt_transport_start_release_timer(transport); + } else if (transport->keepalive) { + res = 0; + transport->acquire_refcount = 0; + spa_log_debug(monitor->log, "transport %p: keepalive %s on release", + transport, transport->path); + } else { + res = spa_bt_transport_impl(transport, release, 0); + if (res >= 0) { + transport->acquire_refcount = 0; + transport->acquired = false; + } + } + + return res; +} + +static int spa_bt_transport_release_now(struct spa_bt_transport *transport) +{ + int res; + + if (!transport->acquired) + return 0; + + spa_bt_transport_stop_release_timer(transport); + res = spa_bt_transport_impl(transport, release, 0); + if (res >= 0) { + transport->acquire_refcount = 0; + transport->acquired = false; + } + + return res; +} + +int spa_bt_device_release_transports(struct spa_bt_device *device) +{ + struct spa_bt_transport *t; + spa_list_for_each(t, &device->transport_list, device_link) + spa_bt_transport_release_now(t); + return 0; +} + +static int start_timeout_timer(struct spa_bt_monitor *monitor, + struct spa_source *timer, spa_source_func_t timer_event, + time_t timeout_msec, void *data) +{ + struct itimerspec ts; + if (timer->data == NULL) { + timer->data = data; + timer->func = timer_event; + timer->fd = spa_system_timerfd_create( + monitor->main_system, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + timer->mask = SPA_IO_IN; + timer->rmask = 0; + spa_loop_add_source(monitor->main_loop, timer); + } + ts.it_value.tv_sec = timeout_msec / SPA_MSEC_PER_SEC; + ts.it_value.tv_nsec = (timeout_msec % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL); + return 0; +} + +static int stop_timeout_timer(struct spa_bt_monitor *monitor, struct spa_source *timer) +{ + struct itimerspec ts; + + if (timer->data == NULL) + return 0; + + spa_loop_remove_source(monitor->main_loop, timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, timer->fd, 0, &ts, NULL); + spa_system_close(monitor->main_system, timer->fd); + timer->data = NULL; + return 0; +} + +static void spa_bt_transport_release_timer_event(struct spa_source *source) +{ + struct spa_bt_transport *transport = source->data; + struct spa_bt_monitor *monitor = transport->monitor; + + spa_assert(transport->acquire_refcount >= 1); + spa_assert(transport->acquired); + + spa_bt_transport_stop_release_timer(transport); + + if (transport->acquire_refcount == 1) { + if (!transport->keepalive) { + spa_bt_transport_impl(transport, release, 0); + transport->acquired = false; + } else { + spa_log_debug(monitor->log, "transport %p: keepalive %s on release", + transport, transport->path); + } + } else { + spa_log_debug(monitor->log, "transport %p: delayed decref %s", transport, transport->path); + } + transport->acquire_refcount -= 1; +} + +static int spa_bt_transport_start_release_timer(struct spa_bt_transport *transport) +{ + return start_timeout_timer(transport->monitor, + &transport->release_timer, + spa_bt_transport_release_timer_event, + SCO_TRANSPORT_RELEASE_TIMEOUT_MSEC, transport); +} + +static int spa_bt_transport_stop_release_timer(struct spa_bt_transport *transport) +{ + return stop_timeout_timer(transport->monitor, &transport->release_timer); +} + +static void spa_bt_transport_volume_changed(struct spa_bt_transport *transport) +{ + struct spa_bt_monitor *monitor = transport->monitor; + struct spa_bt_transport_volume * t_volume; + int volume_id; + + if (transport->profile & SPA_BT_PROFILE_A2DP_SINK) + volume_id = SPA_BT_VOLUME_ID_TX; + else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE) + volume_id = SPA_BT_VOLUME_ID_RX; + else + return; + + t_volume = &transport->volumes[volume_id]; + + if (t_volume->hw_volume != t_volume->new_hw_volume) { + t_volume->hw_volume = t_volume->new_hw_volume; + t_volume->volume = spa_bt_volume_hw_to_linear(t_volume->hw_volume, + t_volume->hw_volume_max); + spa_log_debug(monitor->log, "transport %p: volume changed %d(%f) ", + transport, t_volume->new_hw_volume, t_volume->volume); + if (spa_bt_transport_volume_enabled(transport)) { + transport->device->a2dp_volume_active[volume_id] = true; + spa_bt_transport_emit_volume_changed(transport); + } + } +} + +static void spa_bt_transport_volume_timer_event(struct spa_source *source) +{ + struct spa_bt_transport *transport = source->data; + struct spa_bt_monitor *monitor = transport->monitor; + uint64_t exp; + + if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0) + spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno)); + + spa_bt_transport_volume_changed(transport); +} + +static int spa_bt_transport_start_volume_timer(struct spa_bt_transport *transport) +{ + return start_timeout_timer(transport->monitor, + &transport->volume_timer, + spa_bt_transport_volume_timer_event, + TRANSPORT_VOLUME_TIMEOUT_MSEC, transport); +} + +static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport) +{ + return stop_timeout_timer(transport->monitor, &transport->volume_timer); +} + + +int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop) +{ + if (t->sco_io == NULL) { + t->sco_io = spa_bt_sco_io_create(data_loop, + t->fd, + t->read_mtu, + t->write_mtu); + if (t->sco_io == NULL) + return -ENOMEM; + } + return 0; +} + +int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t) +{ + if (t->delay != SPA_BT_UNKNOWN_DELAY) + return (int64_t)t->delay * 100 * SPA_NSEC_PER_USEC; + + /* Fallback values when device does not provide information */ + + if (t->media_codec == NULL) + return 30 * SPA_NSEC_PER_MSEC; + + switch (t->media_codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_SBC: + case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ: + return 200 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_MPEG: + case SPA_BLUETOOTH_AUDIO_CODEC_AAC: + return 200 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_APTX: + case SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD: + return 150 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_LDAC: + return 175 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL: + case SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX: + case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM: + case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX: + case SPA_BLUETOOTH_AUDIO_CODEC_LC3: + return 40 * SPA_NSEC_PER_MSEC; + default: + break; + }; + return 150 * SPA_NSEC_PER_MSEC; +} + +static int transport_update_props(struct spa_bt_transport *transport, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + struct spa_bt_monitor *monitor = transport->monitor; + + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + int type; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + type = dbus_message_iter_get_arg_type(&it[1]); + + if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + const char *value; + + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "transport %p: %s=%s", transport, key, value); + + if (spa_streq(key, "UUID")) { + switch (spa_bt_profile_from_uuid(value)) { + case SPA_BT_PROFILE_A2DP_SOURCE: + transport->profile = SPA_BT_PROFILE_A2DP_SINK; + break; + case SPA_BT_PROFILE_A2DP_SINK: + transport->profile = SPA_BT_PROFILE_A2DP_SOURCE; + break; + case SPA_BT_PROFILE_BAP_SOURCE: + transport->profile = SPA_BT_PROFILE_BAP_SINK; + break; + case SPA_BT_PROFILE_BAP_SINK: + transport->profile = SPA_BT_PROFILE_BAP_SOURCE; + break; + default: + spa_log_warn(monitor->log, "unknown profile %s", value); + break; + } + } + else if (spa_streq(key, "State")) { + spa_bt_transport_set_state(transport, spa_bt_transport_state_from_string(value)); + } + else if (spa_streq(key, "Device")) { + struct spa_bt_device *device = spa_bt_device_find(monitor, value); + if (transport->device != device) { + if (transport->device != NULL) + spa_list_remove(&transport->device_link); + transport->device = device; + if (device != NULL) + spa_list_append(&device->transport_list, &transport->device_link); + else + spa_log_warn(monitor->log, "could not find device %s", value); + } + } + else if (spa_streq(key, "Endpoint")) { + struct spa_bt_remote_endpoint *ep = remote_endpoint_find(monitor, value); + if (!ep) { + spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", value); + goto next; + } + + // If the remote endpoint is an acceptor this transport is an initiator + transport->bap_initiator = ep->acceptor; + } + } + else if (spa_streq(key, "Codec")) { + uint8_t value; + + if (type != DBUS_TYPE_BYTE) + goto next; + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value); + + transport->codec = value; + } + else if (spa_streq(key, "Configuration")) { + DBusMessageIter iter; + uint8_t *value; + int len; + + if (!check_iter_signature(&it[1], "ay")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + dbus_message_iter_get_fixed_array(&iter, &value, &len); + + spa_log_debug(monitor->log, "transport %p: %s=%d", transport, key, len); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len); + + free(transport->configuration); + transport->configuration_len = 0; + + transport->configuration = malloc(len); + if (transport->configuration) { + memcpy(transport->configuration, value, len); + transport->configuration_len = len; + } + } + else if (spa_streq(key, "Volume")) { + uint16_t value; + struct spa_bt_transport_volume * t_volume; + + if (type != DBUS_TYPE_UINT16) + goto next; + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "transport %p: %s=%d", transport, key, value); + + if (transport->profile & SPA_BT_PROFILE_A2DP_SINK) + t_volume = &transport->volumes[SPA_BT_VOLUME_ID_TX]; + else if (transport->profile & SPA_BT_PROFILE_A2DP_SOURCE) + t_volume = &transport->volumes[SPA_BT_VOLUME_ID_RX]; + else + goto next; + + t_volume->active = true; + t_volume->new_hw_volume = value; + + if (transport->profile & SPA_BT_PROFILE_A2DP_SINK) + spa_bt_transport_start_volume_timer(transport); + else + spa_bt_transport_volume_changed(transport); + } + else if (spa_streq(key, "Delay")) { + uint16_t value; + + if (type != DBUS_TYPE_UINT16) + goto next; + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value); + + transport->delay = value; + spa_bt_transport_emit_delay_changed(transport); + } + else if (spa_streq(key, "PresentationDelay")) { + uint32_t value; + + if (type != DBUS_TYPE_UINT32) + goto next; + dbus_message_iter_get_basic(&it[1], &value); + + spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value); + + transport->delay = value / 100; + spa_bt_transport_emit_delay_changed(transport); + } + else if (spa_streq(key, "Links")) { + DBusMessageIter iter; + + if (!check_iter_signature(&it[1], "ao")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *transport_path; + struct spa_bt_transport *t; + + dbus_message_iter_get_basic(&iter, &transport_path); + + spa_log_debug(monitor->log, "transport %p: Linked with=%s", transport, transport_path); + t = spa_bt_transport_find(monitor, transport_path); + if (!t) { + spa_log_warn(monitor->log, "Unable to find linked transport"); + dbus_message_iter_next(&iter); + continue; + } + + if (spa_list_is_empty(&t->bap_transport_linked)) + spa_list_append(&transport->bap_transport_linked, &t->bap_transport_linked); + else if (spa_list_is_empty(&transport->bap_transport_linked)) + spa_list_append(&t->bap_transport_linked, &transport->bap_transport_linked); + + dbus_message_iter_next(&iter); + } + } +next: + dbus_message_iter_next(props_iter); + } + return 0; +} + +static int transport_set_property_volume(struct spa_bt_transport *transport, uint16_t value) +{ + struct spa_bt_monitor *monitor = transport->monitor; + DBusMessage *m, *r; + DBusMessageIter it[2]; + DBusError err; + const char *interface = BLUEZ_MEDIA_TRANSPORT_INTERFACE; + const char *name = "Volume"; + int res = 0; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + DBUS_INTERFACE_PROPERTIES, + "Set"); + if (m == NULL) + return -ENOMEM; + + dbus_message_iter_init_append(m, &it[0]); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &interface); + dbus_message_iter_append_basic(&it[0], DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&it[0], DBUS_TYPE_VARIANT, + DBUS_TYPE_UINT16_AS_STRING, &it[1]); + dbus_message_iter_append_basic(&it[1], DBUS_TYPE_UINT16, &value); + dbus_message_iter_close_container(&it[0], &it[1]); + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err); + + dbus_message_unref(m); + + if (r == NULL) { + spa_log_error(monitor->log, "set volume %u failed for transport %s (%s)", + value, transport->path, err.message); + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) + res = -EIO; + + dbus_message_unref(r); + + spa_log_debug(monitor->log, "transport %p: set volume to %d", transport, value); + + return res; +} + +static int transport_set_volume(void *data, int id, float volume) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_transport_volume *t_volume = &transport->volumes[id]; + uint16_t value; + + if (!t_volume->active || !spa_bt_transport_volume_enabled(transport)) + return -ENOTSUP; + + value = spa_bt_volume_linear_to_hw(volume, 127); + t_volume->volume = volume; + + /* AVRCP volume would not applied on remote sink device + * if transport is not acquired (idle). */ + if (transport->fd < 0 && (transport->profile & SPA_BT_PROFILE_A2DP_SINK)) { + t_volume->hw_volume = SPA_BT_VOLUME_INVALID; + return 0; + } else if (t_volume->hw_volume != value) { + t_volume->hw_volume = value; + spa_bt_transport_stop_volume_timer(transport); + transport_set_property_volume(transport, value); + } + return 0; +} + +static int transport_acquire(void *data, bool optional) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_monitor *monitor = transport->monitor; + DBusMessage *m, *r = NULL; + DBusError err; + int ret = 0; + const char *method = optional ? "TryAcquire" : "Acquire"; + struct spa_bt_transport *t_linked; + + /* For LE Audio, multiple transport from the same device may share the same + * stream (CIS) and group (CIG) but for different direction, e.g. a speaker and + * a microphone. In this case they are linked. + * If one of them has already been acquired this function should not call Acquire + * or TryAcquire but re-use values from the previously acquired transport. + */ + spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) { + if (t_linked->acquired && t_linked->device == transport->device) { + transport->fd = t_linked->fd; + transport->read_mtu = t_linked->read_mtu; + transport->write_mtu = t_linked->write_mtu; + spa_log_debug(monitor->log, "transport %p: linked transport %s", transport, t_linked->path); + goto done; + } + } + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + BLUEZ_MEDIA_TRANSPORT_INTERFACE, + method); + if (m == NULL) + return -ENOMEM; + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + if (optional && spa_streq(err.name, "org.bluez.Error.NotAvailable")) { + spa_log_info(monitor->log, "Failed optional acquire of unavailable transport %s", + transport->path); + } + else { + spa_log_error(monitor->log, "Transport %s() failed for transport %s (%s)", + method, transport->path, err.message); + } + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "%s returned error: %s", method, dbus_message_get_error_name(r)); + ret = -EIO; + goto finish; + } + + if (!dbus_message_get_args(r, &err, + DBUS_TYPE_UNIX_FD, &transport->fd, + DBUS_TYPE_UINT16, &transport->read_mtu, + DBUS_TYPE_UINT16, &transport->write_mtu, + DBUS_TYPE_INVALID)) { + spa_log_error(monitor->log, "Failed to parse %s() reply: %s", method, err.message); + dbus_error_free(&err); + ret = -EIO; + goto finish; + } +done: + spa_log_debug(monitor->log, "transport %p: %s %s, fd %d MTU %d:%d", transport, method, + transport->path, transport->fd, transport->read_mtu, transport->write_mtu); + + spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_PLAYING); + + transport_sync_volume(transport); + +finish: + if (r) + dbus_message_unref(r); + return ret; +} + +static int transport_release(void *data) +{ + struct spa_bt_transport *transport = data; + struct spa_bt_monitor *monitor = transport->monitor; + DBusMessage *m, *r; + DBusError err; + bool is_idle = (transport->state == SPA_BT_TRANSPORT_STATE_IDLE); + struct spa_bt_transport *t_linked; + bool linked = false; + + spa_log_debug(monitor->log, "transport %p: Release %s", + transport, transport->path); + + spa_bt_player_set_state(transport->device->adapter->dummy_player, SPA_BT_PLAYER_STOPPED); + + /* For LE Audio, multiple transport stream (CIS) can be linked together (CIG). + * If they are part of the same device they re-use the same fd, and call to + * release should be done for the last one only. + */ + spa_list_for_each(t_linked, &transport->bap_transport_linked, bap_transport_linked) { + if (t_linked->acquired && t_linked->device == transport->device) { + linked = true; + break; + } + } + if (linked) { + spa_log_info(monitor->log, "Linked transport %s released", transport->path); + transport->fd = -1; + return 0; + } + + close(transport->fd); + transport->fd = -1; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + transport->path, + BLUEZ_MEDIA_TRANSPORT_INTERFACE, + "Release"); + if (m == NULL) + return -ENOMEM; + + dbus_error_init(&err); + + r = dbus_connection_send_with_reply_and_block(monitor->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r != NULL) + dbus_message_unref(r); + + if (dbus_error_is_set(&err)) { + if (is_idle) { + /* XXX: The fd always needs to be closed. However, Release() + * XXX: apparently doesn't need to be called on idle transports + * XXX: and fails. We call it just to be sure (e.g. in case + * XXX: there's a race with updating the property), but tone down the error. + */ + spa_log_debug(monitor->log, "Failed to release idle transport %s: %s", + transport->path, err.message); + } else { + spa_log_error(monitor->log, "Failed to release transport %s: %s", + transport->path, err.message); + } + dbus_error_free(&err); + } + else + spa_log_info(monitor->log, "Transport %s released", transport->path); + + return 0; +} + +static const struct spa_bt_transport_implementation transport_impl = { + SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION, + .acquire = transport_acquire, + .release = transport_release, + .set_volume = transport_set_volume, +}; + +static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata); + +static int media_codec_switch_cmp(const void *a, const void *b); + +static struct spa_bt_media_codec_switch *media_codec_switch_cmp_sw; /* global for qsort */ + +static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout); + +static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw); + +static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw) +{ + char **p; + + media_codec_switch_stop_timer(sw); + + if (sw->pending != NULL) { + dbus_pending_call_cancel(sw->pending); + dbus_pending_call_unref(sw->pending); + } + + if (sw->device != NULL) + spa_list_remove(&sw->device_link); + + if (sw->paths != NULL) + for (p = sw->paths; *p != NULL; ++p) + free(*p); + + free(sw->paths); + free(sw->codecs); + free(sw); +} + +static void media_codec_switch_next(struct spa_bt_media_codec_switch *sw) +{ + spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL); + + ++sw->path_iter; + if (*sw->path_iter == NULL) { + ++sw->codec_iter; + sw->path_iter = sw->paths; + } + + sw->retries = CODEC_SWITCH_RETRIES; +} + +static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch *sw) +{ + struct spa_bt_remote_endpoint *ep; + struct spa_bt_transport *t; + const struct media_codec *codec; + uint8_t config[A2DP_MAX_CAPS_SIZE]; + enum spa_bt_media_direction direction; + char *local_endpoint = NULL; + int res, config_size; + dbus_bool_t dbus_ret; + DBusMessage *m; + DBusMessageIter iter, d; + int i; + bool sink; + + /* Try setting configuration for current codec on current endpoint in list */ + + codec = *sw->codec_iter; + + spa_log_debug(sw->device->monitor->log, "media codec switch %p: consider codec %s for remote endpoint %s", + sw, (*sw->codec_iter)->name, *sw->path_iter); + + ep = device_remote_endpoint_find(sw->device, *sw->path_iter); + + if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s not valid, try next", + sw, *sw->path_iter); + goto next; + } + + /* Setup and check compatible configuration */ + if (ep->codec != codec->codec_id) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: different codec, try next", sw); + goto next; + } + + if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: wrong uuid (%s) for profile, try next", + sw, ep->uuid); + goto next; + } + + if ((sw->profile & SPA_BT_PROFILE_A2DP_SINK) || (sw->profile & SPA_BT_PROFILE_BAP_SINK) ) { + direction = SPA_BT_MEDIA_SOURCE; + sink = false; + } else if ((sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) || (sw->profile & SPA_BT_PROFILE_BAP_SOURCE) ) { + direction = SPA_BT_MEDIA_SINK; + sink = true; + } else { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: bad profile (%d), try next", + sw, sw->profile); + goto next; + } + + if (media_codec_to_endpoint(codec, direction, &local_endpoint) < 0) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: no endpoint for codec %s, try next", + sw, codec->name); + goto next; + } + + /* Each endpoint can be used by only one device at a time (on each adapter) */ + spa_list_for_each(t, &sw->device->monitor->transport_list, link) { + if (t->device == sw->device) + continue; + if (t->device->adapter != sw->device->adapter) + continue; + if (spa_streq(t->endpoint_path, local_endpoint)) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s in use, try next", + sw, local_endpoint); + goto next; + } + } + + res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len, + &sw->device->monitor->default_audio_info, + &sw->device->monitor->global_settings, config); + if (res < 0) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: incompatible capabilities (%d), try next", + sw, res); + goto next; + } + config_size = res; + + spa_log_debug(sw->device->monitor->log, "media codec switch %p: configuration %d", sw, config_size); + for (i = 0; i < config_size; i++) + spa_log_debug(sw->device->monitor->log, "media codec switch %p: %d: %02x", sw, i, config[i]); + + /* Codecs may share the same endpoint, so indicate which one we are using */ + sw->device->preferred_codec = codec; + + /* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */ + m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration"); + if (m == NULL) { + spa_log_debug(sw->device->monitor->log, "media codec switch %p: dbus allocation failure, try next", sw); + goto next; + } + + spa_bt_device_update_last_bluez_action_time(sw->device); + + spa_log_info(sw->device->monitor->log, "media codec switch %p: trying codec %s for endpoint %s, local endpoint %s", + sw, codec->name, ep->path, local_endpoint); + + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &local_endpoint); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d); + append_basic_array_variant_dict_entry(&d, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, config, config_size); + dbus_message_iter_close_container(&iter, &d); + + spa_assert(sw->pending == NULL); + dbus_ret = dbus_connection_send_with_reply(sw->device->monitor->conn, m, &sw->pending, -1); + + if (!dbus_ret || sw->pending == NULL) { + spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw); + dbus_message_unref(m); + goto next; + } + + dbus_ret = dbus_pending_call_set_notify(sw->pending, media_codec_switch_reply, sw, NULL); + dbus_message_unref(m); + + if (!dbus_ret) { + spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus set notify failure", sw); + goto next; + } + + free(local_endpoint); + return true; + +next: + free(local_endpoint); + return false; +} + +static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw) +{ + while (*sw->codec_iter != NULL && *sw->path_iter != NULL) { + struct timespec ts; + uint64_t now, threshold; + + /* Rate limit BlueZ calls */ + spa_system_clock_gettime(sw->device->monitor->main_system, CLOCK_MONOTONIC, &ts); + now = SPA_TIMESPEC_TO_NSEC(&ts); + threshold = sw->device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC; + if (now < threshold) { + /* Wait for timeout */ + media_codec_switch_start_timer(sw, threshold - now); + return; + } + + if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) { + /* Sort endpoints according to codec preference, when at a new codec. */ + media_codec_switch_cmp_sw = sw; + qsort(sw->paths, sw->num_paths, sizeof(char *), media_codec_switch_cmp); + } + + if (media_codec_switch_process_current(sw)) { + /* Wait for dbus reply */ + return; + } + + media_codec_switch_next(sw); + }; + + /* Didn't find any suitable endpoint. Report failure. */ + spa_log_info(sw->device->monitor->log, "media codec switch %p: failed to get an endpoint", sw); + spa_bt_device_emit_codec_switched(sw->device, -ENODEV); + spa_bt_device_check_profiles(sw->device, false); + media_codec_switch_free(sw); +} + +static bool media_codec_switch_goto_active(struct spa_bt_media_codec_switch *sw) +{ + struct spa_bt_device *device = sw->device; + struct spa_bt_media_codec_switch *active_sw; + + active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_media_codec_switch, device_link); + + if (active_sw != sw) { + struct spa_bt_media_codec_switch *t; + + /* This codec switch has been canceled. Switch to the newest one. */ + spa_log_debug(sw->device->monitor->log, + "media codec switch %p: canceled, go to new switch", sw); + + spa_list_for_each_safe(sw, t, &device->codec_switch_list, device_link) { + if (sw != active_sw) + media_codec_switch_free(sw); + } + + media_codec_switch_process(active_sw); + return false; + } + + return true; +} + +static void media_codec_switch_timer_event(struct spa_source *source) +{ + struct spa_bt_media_codec_switch *sw = source->data; + struct spa_bt_device *device = sw->device; + struct spa_bt_monitor *monitor = device->monitor; + uint64_t exp; + + if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0) + spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno)); + + spa_log_debug(monitor->log, "media codec switch %p: rate limit timer event", sw); + + media_codec_switch_stop_timer(sw); + + if (!media_codec_switch_goto_active(sw)) + return; + + media_codec_switch_process(sw); +} + +static void media_codec_switch_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_media_codec_switch *sw = user_data; + struct spa_bt_device *device = sw->device; + DBusMessage *r; + + r = dbus_pending_call_steal_reply(pending); + + spa_assert(sw->pending == pending); + dbus_pending_call_unref(pending); + sw->pending = NULL; + + spa_bt_device_update_last_bluez_action_time(device); + + if (!media_codec_switch_goto_active(sw)) { + if (r != NULL) + dbus_message_unref(r); + return; + } + + if (r == NULL) { + spa_log_error(sw->device->monitor->log, + "media codec switch %p: empty reply from dbus, trying next", + sw); + goto next; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_debug(sw->device->monitor->log, + "media codec switch %p: failed (%s), trying next", + sw, dbus_message_get_error_name(r)); + dbus_message_unref(r); + goto next; + } + + dbus_message_unref(r); + + /* Success */ + spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw); + spa_bt_device_emit_codec_switched(sw->device, 0); + spa_bt_device_check_profiles(sw->device, false); + media_codec_switch_free(sw); + return; + +next: + if (sw->retries > 0) + --sw->retries; + else + media_codec_switch_next(sw); + + media_codec_switch_process(sw); + return; +} + +static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout) +{ + struct spa_bt_monitor *monitor = sw->device->monitor; + struct itimerspec ts; + + spa_assert(sw->timer.data == NULL); + + spa_log_debug(monitor->log, "media codec switch %p: starting rate limit timer", sw); + + if (sw->timer.data == NULL) { + sw->timer.data = sw; + sw->timer.func = media_codec_switch_timer_event; + sw->timer.fd = spa_system_timerfd_create(monitor->main_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + sw->timer.mask = SPA_IO_IN; + sw->timer.rmask = 0; + spa_loop_add_source(monitor->main_loop, &sw->timer); + } + ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL); + return 0; +} + +static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw) +{ + struct spa_bt_monitor *monitor = sw->device->monitor; + struct itimerspec ts; + + if (sw->timer.data == NULL) + return 0; + + spa_log_debug(monitor->log, "media codec switch %p: stopping rate limit timer", sw); + + spa_loop_remove_source(monitor->main_loop, &sw->timer); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL); + spa_system_close(monitor->main_system, sw->timer.fd); + sw->timer.data = NULL; + return 0; +} + +static int media_codec_switch_cmp(const void *a, const void *b) +{ + struct spa_bt_media_codec_switch *sw = media_codec_switch_cmp_sw; + const struct media_codec *codec = *sw->codec_iter; + const char *path1 = *(char **)a, *path2 = *(char **)b; + struct spa_bt_remote_endpoint *ep1, *ep2; + uint32_t flags; + + ep1 = device_remote_endpoint_find(sw->device, path1); + ep2 = device_remote_endpoint_find(sw->device, path2); + + if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id || ep1->capabilities == NULL)) + ep1 = NULL; + if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL)) + ep2 = NULL; + if (ep1 && ep2 && !spa_streq(ep1->uuid, ep2->uuid)) { + ep1 = NULL; + ep2 = NULL; + } + + if (ep1 == NULL && ep2 == NULL) + return 0; + else if (ep1 == NULL) + return 1; + else if (ep2 == NULL) + return -1; + + if (codec->bap) + flags = spa_streq(ep1->uuid, SPA_BT_UUID_BAP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0; + else + flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0; + + return codec->caps_preference_cmp(codec, flags, ep1->capabilities, ep1->capabilities_len, + ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info, + &sw->device->monitor->global_settings); +} + +/* Ensure there's a transport for at least one of the listed codecs */ +int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs) +{ + struct spa_bt_media_codec_switch *sw; + struct spa_bt_remote_endpoint *ep; + struct spa_bt_transport *t; + const struct media_codec *preferred_codec = NULL; + size_t i, j, num_codecs, num_eps; + + if (!device->adapter->application_registered) { + /* Codec switching not supported */ + return -ENOTSUP; + } + + for (i = 0; codecs[i] != NULL; ++i) { + if (spa_bt_device_supports_media_codec(device, codecs[i], true)) { + preferred_codec = codecs[i]; + break; + } + } + + /* Check if we already have an enabled transport for the most preferred codec. + * However, if there already was a codec switch running, these transports may + * disappear soon. In that case, we have to do the full thing. + */ + if (spa_list_is_empty(&device->codec_switch_list) && preferred_codec != NULL) { + spa_list_for_each(t, &device->transport_list, device_link) { + if (t->media_codec != preferred_codec) + continue; + + if ((device->connected_profiles & t->profile) != t->profile) + continue; + + spa_bt_device_emit_codec_switched(device, 0); + return 0; + } + } + + /* Setup and start iteration */ + + sw = calloc(1, sizeof(struct spa_bt_media_codec_switch)); + if (sw == NULL) + return -ENOMEM; + + num_eps = 0; + spa_list_for_each(ep, &device->remote_endpoint_list, device_link) + ++num_eps; + + num_codecs = 0; + while (codecs[num_codecs] != NULL) + ++num_codecs; + + sw->codecs = calloc(num_codecs + 1, sizeof(const struct media_codec *)); + sw->paths = calloc(num_eps + 1, sizeof(char *)); + sw->num_paths = num_eps; + + if (sw->codecs == NULL || sw->paths == NULL) { + media_codec_switch_free(sw); + return -ENOMEM; + } + + for (i = 0, j = 0; i < num_codecs; ++i) { + if (is_media_codec_enabled(device->monitor, codecs[i])) { + sw->codecs[j] = codecs[i]; + ++j; + } + } + sw->codecs[j] = NULL; + + i = 0; + spa_list_for_each(ep, &device->remote_endpoint_list, device_link) { + sw->paths[i] = strdup(ep->path); + if (sw->paths[i] == NULL) { + media_codec_switch_free(sw); + return -ENOMEM; + } + ++i; + } + sw->paths[i] = NULL; + + sw->codec_iter = sw->codecs; + sw->path_iter = sw->paths; + sw->retries = CODEC_SWITCH_RETRIES; + + sw->profile = device->connected_profiles; + + sw->device = device; + + if (!spa_list_is_empty(&device->codec_switch_list)) { + /* + * There's a codec switch already running, either waiting for timeout or + * BlueZ reply. + * + * BlueZ does not appear to allow calling dbus_pending_call_cancel on an + * active request, so we have to wait for the reply to arrive first, and + * only then start processing this request. The timeout we would also have + * to wait to pass in any case, so we don't cancel it either. + */ + spa_log_debug(sw->device->monitor->log, + "media codec switch %p: already in progress, canceling previous", + sw); + + spa_list_prepend(&device->codec_switch_list, &sw->device_link); + } else { + spa_list_prepend(&device->codec_switch_list, &sw->device_link); + media_codec_switch_process(sw); + } + + return 0; +} + +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec) +{ + struct spa_bt_monitor *monitor = device->monitor; + return spa_bt_backend_ensure_codec(monitor->backend, device, codec); +} + +int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec) +{ + struct spa_bt_monitor *monitor = device->monitor; + return spa_bt_backend_supports_codec(monitor->backend, device, codec); +} + +static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn, + const char *path, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + const char *transport_path, *endpoint; + DBusMessageIter it[2]; + DBusMessage *r; + struct spa_bt_transport *transport; + const struct media_codec *codec; + int profile; + bool sink; + + if (!dbus_message_has_signature(m, "oa{sv}")) { + spa_log_warn(monitor->log, "invalid SetConfiguration() signature"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + endpoint = dbus_message_get_path(m); + + profile = media_endpoint_to_profile(endpoint); + codec = media_endpoint_to_codec(monitor, endpoint, &sink, NULL); + if (codec == NULL) { + spa_log_warn(monitor->log, "unknown SetConfiguration() codec"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + dbus_message_iter_init(m, &it[0]); + dbus_message_iter_get_basic(&it[0], &transport_path); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + transport = spa_bt_transport_find(monitor, transport_path); + + if (transport == NULL) { + char *tpath = strdup(transport_path); + + transport = spa_bt_transport_create(monitor, tpath, 0); + if (transport == NULL) { + free(tpath); + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + + spa_bt_transport_set_implementation(transport, &transport_impl, transport); + + if (profile & SPA_BT_PROFILE_A2DP_SOURCE) { + transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_AG_VOLUME; + transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_AG_VOLUME; + } else { + transport->volumes[SPA_BT_VOLUME_ID_RX].volume = DEFAULT_RX_VOLUME; + transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME; + } + } + + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) { + transport->volumes[i].hw_volume = SPA_BT_VOLUME_INVALID; + transport->volumes[i].hw_volume_max = SPA_BT_VOLUME_A2DP_MAX; + } + + free(transport->endpoint_path); + transport->endpoint_path = strdup(endpoint); + transport->profile = profile; + transport->media_codec = codec; + transport_update_props(transport, &it[1], NULL); + + if (transport->device == NULL || transport->device->adapter == NULL) { + spa_log_warn(monitor->log, "no device found for transport"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + /* If multiple codecs share the endpoint, pick the one we wanted */ + transport->media_codec = codec = media_endpoint_to_codec(monitor, endpoint, &sink, + transport->device->preferred_codec); + spa_assert(codec != NULL); + spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : ""); + + spa_bt_device_update_last_bluez_action_time(transport->device); + + if (profile & SPA_BT_PROFILE_A2DP_SOURCE) { + /* PW is the rendering device so it's responsible for reporting hardware volume. */ + transport->volumes[SPA_BT_VOLUME_ID_RX].active = true; + } else if (profile & SPA_BT_PROFILE_A2DP_SINK) { + transport->volumes[SPA_BT_VOLUME_ID_TX].active + |= transport->device->a2dp_volume_active[SPA_BT_VOLUME_ID_TX]; + } + + if (codec->validate_config) { + struct spa_audio_info info; + if (codec->validate_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, + transport->configuration, transport->configuration_len, + &info) < 0) { + spa_log_error(monitor->log, "invalid transport configuration"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + transport->n_channels = info.info.raw.channels; + memcpy(transport->channels, info.info.raw.position, + transport->n_channels * sizeof(uint32_t)); + } else { + transport->n_channels = 2; + transport->channels[0] = SPA_AUDIO_CHANNEL_FL; + transport->channels[1] = SPA_AUDIO_CHANNEL_FR; + } + spa_log_info(monitor->log, "%p: %s validate conf channels:%d", + monitor, path, transport->n_channels); + + spa_bt_device_add_profile(transport->device, transport->profile); + + spa_bt_device_connect_profile(transport->device, transport->profile); + + /* Sync initial volumes */ + transport_sync_volume(transport); + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult endpoint_clear_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + DBusError err; + DBusMessage *r; + const char *transport_path; + struct spa_bt_transport *transport; + + dbus_error_init(&err); + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_OBJECT_PATH, &transport_path, + DBUS_TYPE_INVALID)) { + spa_log_warn(monitor->log, "Bad ClearConfiguration method call: %s", + err.message); + dbus_error_free(&err); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + transport = spa_bt_transport_find(monitor, transport_path); + + if (transport != NULL) { + struct spa_bt_device *device = transport->device; + + spa_log_debug(monitor->log, "transport %p: free %s", + transport, transport->path); + + spa_bt_transport_free(transport); + if (device != NULL) + spa_bt_device_check_profiles(device, false); + } + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult endpoint_release(DBusConnection *conn, DBusMessage *m, void *userdata) +{ + DBusMessage *r; + + r = dbus_message_new_error(m, + BLUEZ_MEDIA_ENDPOINT_INTERFACE ".Error.NotImplemented", + "Method not implemented"); + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult endpoint_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct spa_bt_monitor *monitor = userdata; + const char *path, *interface, *member; + DBusMessage *r; + DBusHandlerResult res; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = ENDPOINT_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(monitor->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration")) + res = endpoint_set_configuration(c, path, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectConfiguration")) + res = endpoint_select_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SelectProperties")) + res = endpoint_select_properties(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration")) + res = endpoint_clear_configuration(c, m, userdata); + else if (dbus_message_is_method_call(m, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Release")) + res = endpoint_release(c, m, userdata); + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void bluez_register_endpoint_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + DBusMessage *r; + + r = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "RegisterEndpoint() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + +finish: + dbus_message_unref(r); +} + +static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) { + DBusMessageIter dict_entry_it, variant_it; + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it); + dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key); + + dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it); + dbus_message_iter_append_basic(&variant_it, variant_type_int, variant); + dbus_message_iter_close_container(&dict_entry_it, &variant_it); + dbus_message_iter_close_container(dict, &dict_entry_it); +} + +static void append_basic_array_variant_dict_entry(DBusMessageIter *dict, const char* key, const char* variant_type_str, const char* array_type_str, int array_type_int, void* data, int data_size) { + DBusMessageIter dict_entry_it, variant_it, array_it; + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it); + dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key); + + dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it); + dbus_message_iter_open_container(&variant_it, DBUS_TYPE_ARRAY, array_type_str, &array_it); + dbus_message_iter_append_fixed_array (&array_it, array_type_int, &data, data_size); + dbus_message_iter_close_container(&variant_it, &array_it); + dbus_message_iter_close_container(&dict_entry_it, &variant_it); + dbus_message_iter_close_container(dict, &dict_entry_it); +} + +static int bluez_register_endpoint(struct spa_bt_monitor *monitor, + const char *path, enum spa_bt_media_direction direction, + const char *uuid, const struct media_codec *codec) +{ + char *object_path = NULL; + DBusMessage *m; + DBusMessageIter object_it, dict_it; + DBusPendingCall *call; + uint8_t caps[A2DP_MAX_CAPS_SIZE]; + int ret, caps_size; + uint16_t codec_id = codec->codec_id; + bool sink = (direction == SPA_BT_MEDIA_SINK); + + spa_assert(codec->fill_caps); + + ret = media_codec_to_endpoint(codec, direction, &object_path); + if (ret < 0) + goto error; + + ret = caps_size = codec->fill_caps(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, caps); + if (ret < 0) + goto error; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + path, + BLUEZ_MEDIA_INTERFACE, + "RegisterEndpoint"); + if (m == NULL) { + ret = -EIO; + goto error; + } + + dbus_message_iter_init_append(m, &object_it); + dbus_message_iter_append_basic(&object_it, DBUS_TYPE_OBJECT_PATH, &object_path); + + dbus_message_iter_open_container(&object_it, DBUS_TYPE_ARRAY, "{sv}", &dict_it); + + append_basic_variant_dict_entry(&dict_it,"UUID", DBUS_TYPE_STRING, "s", &uuid); + append_basic_variant_dict_entry(&dict_it, "Codec", DBUS_TYPE_BYTE, "y", &codec_id); + append_basic_array_variant_dict_entry(&dict_it, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size); + + dbus_message_iter_close_container(&object_it, &dict_it); + + dbus_connection_send_with_reply(monitor->conn, m, &call, -1); + dbus_pending_call_set_notify(call, bluez_register_endpoint_reply, monitor, NULL); + dbus_message_unref(m); + + free(object_path); + + return 0; + +error: + free(object_path); + return ret; +} + +static int adapter_register_endpoints(struct spa_bt_adapter *a) +{ + struct spa_bt_monitor *monitor = a->monitor; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + int i; + int err = 0; + + if (a->endpoints_registered) + return err; + + /* The legacy bluez5 api doesn't support codec switching + * It doesn't make sense to register codecs other than SBC + * as bluez5 will probably use SBC anyway and we have no control over it + * let's incentivize users to upgrade their bluez5 daemon + * if they want proper media codec support + * */ + spa_log_warn(monitor->log, + "Using legacy bluez5 API for A2DP - only SBC will be supported. " + "No LE Audio. Please upgrade bluez5."); + + monitor->le_audio_supported = false; + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + + if (codec->id != SPA_BLUETOOTH_AUDIO_CODEC_SBC) + continue; + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) { + if ((err = bluez_register_endpoint(monitor, a->path, + SPA_BT_MEDIA_SOURCE, + SPA_BT_UUID_A2DP_SOURCE, + codec))) + goto out; + } + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) { + if ((err = bluez_register_endpoint(monitor, a->path, + SPA_BT_MEDIA_SINK, + SPA_BT_UUID_A2DP_SINK, + codec))) + goto out; + } + + a->endpoints_registered = true; + break; + } + + if (!a->endpoints_registered) { + /* Should never happen as SBC support is always enabled */ + spa_log_error(monitor->log, "Broken PipeWire build - unable to locate SBC codec"); + err = -ENOSYS; + } + +out: + if (err) { + spa_log_error(monitor->log, "Failed to register bluez5 endpoints"); + } + return err; +} + +static void append_media_object(DBusMessageIter *iter, const char *endpoint, + const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size) +{ + const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE; + DBusMessageIter object, array, entry, dict; + dbus_bool_t delay_reporting; + + dbus_message_iter_open_container(iter, DBUS_TYPE_DICT_ENTRY, NULL, &object); + dbus_message_iter_append_basic(&object, DBUS_TYPE_OBJECT_PATH, &endpoint); + + dbus_message_iter_open_container(&object, DBUS_TYPE_ARRAY, "{sa{sv}}", &array); + + dbus_message_iter_open_container(&array, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &interface_name); + + dbus_message_iter_open_container(&entry, DBUS_TYPE_ARRAY, "{sv}", &dict); + + append_basic_variant_dict_entry(&dict, "UUID", DBUS_TYPE_STRING, "s", &uuid); + append_basic_variant_dict_entry(&dict, "Codec", DBUS_TYPE_BYTE, "y", &codec_id); + append_basic_array_variant_dict_entry(&dict, "Capabilities", "ay", "y", DBUS_TYPE_BYTE, caps, caps_size); + if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_A2DP_SOURCE) { + delay_reporting = TRUE; + append_basic_variant_dict_entry(&dict, "DelayReporting", DBUS_TYPE_BOOLEAN, "b", &delay_reporting); + } + + dbus_message_iter_close_container(&entry, &dict); + dbus_message_iter_close_container(&array, &entry); + dbus_message_iter_close_container(&object, &array); + dbus_message_iter_close_container(iter, &object); +} + +static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage *m, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const char *path, *interface, *member; + char *endpoint; + DBusMessage *r; + DBusMessageIter iter, array; + DBusHandlerResult res; + int i; + + path = dbus_message_get_path(m); + interface = dbus_message_get_interface(m); + member = dbus_message_get_member(m); + + spa_log_debug(monitor->log, "dbus: path=%s, interface=%s, member=%s", path, interface, member); + + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { + const char *xml = OBJECT_MANAGER_INTROSPECT_XML; + + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(monitor->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_unref(r); + res = DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_is_method_call(m, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects")) { + if ((r = dbus_message_new_method_return(m)) == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + + dbus_message_iter_init_append(r, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{oa{sa{sv}}}", &array); + + for (i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + uint8_t caps[A2DP_MAX_CAPS_SIZE]; + int caps_size, ret; + uint16_t codec_id = codec->codec_id; + + if (!is_media_codec_enabled(monitor, codec)) + continue; + + if (codec->bap && !monitor->le_audio_supported) { + /* The legacy bluez5 api doesn't support LE Audio + * It doesn't make sense to register unsupported codecs as it prevents + * registration of A2DP codecs + * let's incentivize users to upgrade their bluez5 daemon + * if they want proper media codec support + * */ + spa_log_warn(monitor->log, "Trying to use legacy bluez5 API for LE Audio - only A2DP will be supported. " + "Please upgrade bluez5."); + continue; + } + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK)) { + caps_size = codec->fill_caps(codec, MEDIA_CODEC_FLAG_SINK, caps); + if (caps_size < 0) + continue; + + ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK, &endpoint); + if (ret == 0) { + spa_log_info(monitor->log, "register media sink codec %s: %s", media_codecs[i]->name, endpoint); + append_media_object(&array, endpoint, + codec->bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK, + codec_id, caps, caps_size); + free(endpoint); + } + } + + if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE)) { + caps_size = codec->fill_caps(codec, 0, caps); + if (caps_size < 0) + continue; + + ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE, &endpoint); + if (ret == 0) { + spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint); + append_media_object(&array, endpoint, + codec->bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE, + codec_id, caps, caps_size); + free(endpoint); + } + } + } + + dbus_message_iter_close_container(&iter, &array); + if (!dbus_connection_send(monitor->conn, r, NULL)) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + res = DBUS_HANDLER_RESULT_HANDLED; + } + else + res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + return res; +} + +static void bluez_register_application_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_adapter *adapter = user_data; + struct spa_bt_monitor *monitor = adapter->monitor; + DBusMessage *r; + bool fallback = true; + + r = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + + if (r == NULL) + return; + + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { + spa_log_warn(monitor->log, "Registering media applications for adapter %s is disabled in bluez5", adapter->path); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "RegisterApplication() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + fallback = false; + adapter->application_registered = true; + +finish: + dbus_message_unref(r); + + if (fallback) + adapter_register_endpoints(adapter); +} + +static int register_media_endpoint(struct spa_bt_monitor *monitor, + const struct media_codec *codec, + enum spa_bt_media_direction direction) +{ + static const DBusObjectPathVTable vtable_endpoint = { + .message_function = endpoint_handler, + }; + + if (!endpoint_should_be_registered(monitor, codec, direction)) + return 0; + + char *object_path = NULL; + int ret = media_codec_to_endpoint(codec, direction, &object_path); + if (ret < 0) + return ret; + + spa_log_info(monitor->log, "registering endpoint: %s", object_path); + + if (!dbus_connection_register_object_path(monitor->conn, + object_path, + &vtable_endpoint, monitor)) + { + ret = -EIO; + } + + free(object_path); + return ret; +} + +static int register_media_application(struct spa_bt_monitor * monitor) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + const DBusObjectPathVTable vtable_object_manager = { + .message_function = object_manager_handler, + }; + + spa_log_info(monitor->log, "Registering media application object: " MEDIA_OBJECT_MANAGER_PATH); + + if (!dbus_connection_register_object_path(monitor->conn, + MEDIA_OBJECT_MANAGER_PATH, + &vtable_object_manager, monitor)) + return -EIO; + + for (int i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + + register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE); + register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK); + } + + return 0; +} + +static void unregister_media_endpoint(struct spa_bt_monitor *monitor, + const struct media_codec *codec, + enum spa_bt_media_direction direction) +{ + if (!endpoint_should_be_registered(monitor, codec, direction)) + return; + + char *object_path = NULL; + int ret = media_codec_to_endpoint(codec, direction, &object_path); + if (ret < 0) + return; + + spa_log_info(monitor->log, "unregistering endpoint: %s", object_path); + + if (!dbus_connection_unregister_object_path(monitor->conn, object_path)) + spa_log_warn(monitor->log, "failed to unregister %s\n", object_path); + + free(object_path); +} + +static void unregister_media_application(struct spa_bt_monitor * monitor) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + + for (int i = 0; media_codecs[i]; i++) { + const struct media_codec *codec = media_codecs[i]; + + unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE); + unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK); + } + + dbus_connection_unregister_object_path(monitor->conn, MEDIA_OBJECT_MANAGER_PATH); +} + +static int adapter_register_application(struct spa_bt_adapter *a) { + const char *object_manager_path = MEDIA_OBJECT_MANAGER_PATH; + struct spa_bt_monitor *monitor = a->monitor; + DBusMessage *m; + DBusMessageIter i, d; + DBusPendingCall *call; + + if (a->application_registered) + return 0; + + spa_log_debug(monitor->log, "Registering bluez5 media application on adapter %s", a->path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + a->path, + BLUEZ_MEDIA_INTERFACE, + "RegisterApplication"); + if (m == NULL) + return -EIO; + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &object_manager_path); + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, "{sv}", &d); + dbus_message_iter_close_container(&i, &d); + + dbus_connection_send_with_reply(monitor->conn, m, &call, -1); + dbus_pending_call_set_notify(call, bluez_register_application_reply, a, NULL); + dbus_message_unref(m); + + return 0; +} + +static int switch_backend(struct spa_bt_monitor *monitor, struct spa_bt_backend *backend) +{ + int res; + size_t i; + + spa_return_val_if_fail(backend != NULL, -EINVAL); + + if (!backend->available) + return -ENODEV; + + for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) { + struct spa_bt_backend *b = monitor->backends[i]; + if (backend != b && b && b->available && b->exclusive) + spa_log_warn(monitor->log, + "%s running, but not configured as HFP/HSP backend: " + "it may interfere with HFP/HSP functionality.", + b->name); + } + + if (monitor->backend == backend) + return 0; + + spa_log_info(monitor->log, "Switching to HFP/HSP backend %s", backend->name); + + spa_bt_backend_unregister_profiles(monitor->backend); + + if ((res = spa_bt_backend_register_profiles(backend)) < 0) { + monitor->backend = NULL; + return res; + } + + monitor->backend = backend; + return 0; +} + +static void reselect_backend(struct spa_bt_monitor *monitor, bool silent) +{ + struct spa_bt_backend *backend; + size_t i; + + spa_log_debug(monitor->log, "re-selecting HFP/HSP backend"); + + if (monitor->backend_selection == BACKEND_NONE) { + spa_bt_backend_unregister_profiles(monitor->backend); + monitor->backend = NULL; + return; + } else if (monitor->backend_selection == BACKEND_ANY) { + for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) { + backend = monitor->backends[i]; + if (backend && switch_backend(monitor, backend) == 0) + return; + } + } else { + backend = monitor->backends[monitor->backend_selection]; + if (backend && switch_backend(monitor, backend) == 0) + return; + } + + spa_bt_backend_unregister_profiles(monitor->backend); + monitor->backend = NULL; + + if (!silent) + spa_log_error(monitor->log, "Failed to start HFP/HSP backend %s", + backend ? backend->name : "none"); +} + +static int media_update_props(struct spa_bt_monitor *monitor, + DBusMessageIter *props_iter, + DBusMessageIter *invalidated_iter) +{ + while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { + DBusMessageIter it[2]; + const char *key; + + dbus_message_iter_recurse(props_iter, &it[0]); + dbus_message_iter_get_basic(&it[0], &key); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + if (spa_streq(key, "SupportedUUIDs")) { + DBusMessageIter iter; + + if (!check_iter_signature(&it[1], "as")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *uuid; + + dbus_message_iter_get_basic(&iter, &uuid); + + if (spa_streq(uuid, SPA_BT_UUID_BAP_SINK)) { + monitor->le_audio_supported = true; + spa_log_info(monitor->log, "LE Audio supported"); + } + dbus_message_iter_next(&iter); + } + } + else + spa_log_debug(monitor->log, "media: unhandled key %s", key); + +next: + dbus_message_iter_next(props_iter); + } + return 0; +} + +static void interface_added(struct spa_bt_monitor *monitor, + DBusConnection *conn, + const char *object_path, + const char *interface_name, + DBusMessageIter *props_iter) +{ + spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name); + + if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) { + struct spa_bt_adapter *a; + + a = adapter_find(monitor, object_path); + if (a == NULL) { + a = adapter_create(monitor, object_path); + if (a == NULL) { + spa_log_warn(monitor->log, "can't create adapter: %m"); + return; + } + } + adapter_update_props(a, props_iter, NULL); + adapter_register_application(a); + adapter_register_player(a); + adapter_update_devices(a); + } + else if (spa_streq(interface_name, BLUEZ_PROFILE_MANAGER_INTERFACE)) { + if (monitor->backends[BACKEND_NATIVE]) + monitor->backends[BACKEND_NATIVE]->available = true; + reselect_backend(monitor, false); + } + else if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) { + struct spa_bt_device *d; + + d = spa_bt_device_find(monitor, object_path); + if (d == NULL) { + d = device_create(monitor, object_path); + if (d == NULL) { + spa_log_warn(monitor->log, "can't create Bluetooth device %s: %m", + object_path); + return; + } + } + + device_update_props(d, props_iter, NULL); + d->reconnect_state = BT_DEVICE_RECONNECT_INIT; + + if (!device_props_ready(d)) + return; + + device_update_hw_volume_profiles(d); + + /* Trigger bluez device creation before bluez profile negotiation started so that + * profile connection handlers can receive per-device settings during profile negotiation. */ + spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL); + } + else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) { + struct spa_bt_remote_endpoint *ep; + struct spa_bt_device *d; + + ep = remote_endpoint_find(monitor, object_path); + if (ep == NULL) { + ep = remote_endpoint_create(monitor, object_path); + if (ep == NULL) { + spa_log_warn(monitor->log, "can't create Bluetooth remote endpoint %s: %m", + object_path); + return; + } + } + remote_endpoint_update_props(ep, props_iter, NULL); + + d = ep->device; + if (d) + spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles); + } + else if (spa_streq(interface_name, BLUEZ_MEDIA_INTERFACE)) { + media_update_props(monitor, props_iter, NULL); + } +} + +static void interfaces_added(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter) +{ + DBusMessageIter it[3]; + const char *object_path; + + dbus_message_iter_get_basic(arg_iter, &object_path); + dbus_message_iter_next(arg_iter); + dbus_message_iter_recurse(arg_iter, &it[0]); + + while (dbus_message_iter_get_arg_type(&it[0]) != DBUS_TYPE_INVALID) { + const char *interface_name; + + dbus_message_iter_recurse(&it[0], &it[1]); + dbus_message_iter_get_basic(&it[1], &interface_name); + dbus_message_iter_next(&it[1]); + dbus_message_iter_recurse(&it[1], &it[2]); + + interface_added(monitor, monitor->conn, + object_path, interface_name, + &it[2]); + + dbus_message_iter_next(&it[0]); + } +} + +static void interfaces_removed(struct spa_bt_monitor *monitor, DBusMessageIter *arg_iter) +{ + const char *object_path; + DBusMessageIter it; + + dbus_message_iter_get_basic(arg_iter, &object_path); + dbus_message_iter_next(arg_iter); + dbus_message_iter_recurse(arg_iter, &it); + + while (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_INVALID) { + const char *interface_name; + + dbus_message_iter_get_basic(&it, &interface_name); + + spa_log_debug(monitor->log, "Found object %s, interface %s", object_path, interface_name); + + if (spa_streq(interface_name, BLUEZ_DEVICE_INTERFACE)) { + struct spa_bt_device *d; + d = spa_bt_device_find(monitor, object_path); + if (d != NULL) + device_free(d); + } else if (spa_streq(interface_name, BLUEZ_ADAPTER_INTERFACE)) { + struct spa_bt_adapter *a; + a = adapter_find(monitor, object_path); + if (a != NULL) + adapter_free(a); + } else if (spa_streq(interface_name, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) { + struct spa_bt_remote_endpoint *ep; + ep = remote_endpoint_find(monitor, object_path); + if (ep != NULL) { + struct spa_bt_device *d = ep->device; + remote_endpoint_free(ep); + if (d) + spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles); + } + } + + dbus_message_iter_next(&it); + } +} + +static void get_managed_objects_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + DBusMessage *r; + DBusMessageIter it[6]; + + spa_assert(pending == monitor->get_managed_objects_call); + monitor->get_managed_objects_call = NULL; + + r = dbus_pending_call_steal_reply(pending); + dbus_pending_call_unref(pending); + + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(monitor->log, "BlueZ D-Bus ObjectManager not available"); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, "GetManagedObjects() failed: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &it[0]) || + !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { + spa_log_error(monitor->log, "Invalid reply signature for GetManagedObjects()"); + goto finish; + } + + dbus_message_iter_recurse(&it[0], &it[1]); + + while (dbus_message_iter_get_arg_type(&it[1]) != DBUS_TYPE_INVALID) { + dbus_message_iter_recurse(&it[1], &it[2]); + + interfaces_added(monitor, &it[2]); + + dbus_message_iter_next(&it[1]); + } + + reselect_backend(monitor, false); + + monitor->objects_listed = true; + +finish: + dbus_message_unref(r); + return; +} + +static void get_managed_objects(struct spa_bt_monitor *monitor) +{ + if (monitor->objects_listed || monitor->get_managed_objects_call) + return; + + DBusMessage *m; + DBusPendingCall *call; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, + "/", + "org.freedesktop.DBus.ObjectManager", + "GetManagedObjects"); + + dbus_message_set_auto_start(m, false); + + dbus_connection_send_with_reply(monitor->conn, m, &call, -1); + dbus_pending_call_set_notify(call, get_managed_objects_reply, monitor, NULL); + dbus_message_unref(m); + + monitor->get_managed_objects_call = call; +} + +static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct spa_bt_monitor *monitor = user_data; + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + spa_log_debug(monitor->log, "Name owner changed %s", dbus_message_get_path(m)); + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + spa_log_error(monitor->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto fail; + } + + if (spa_streq(name, BLUEZ_SERVICE)) { + bool has_old_owner = old_owner && *old_owner; + bool has_new_owner = new_owner && *new_owner; + + if (has_old_owner) { + spa_log_debug(monitor->log, "Bluetooth daemon disappeared"); + + if (monitor->backends[BACKEND_NATIVE]) + monitor->backends[BACKEND_NATIVE]->available = false; + + reselect_backend(monitor, true); + } + + if (has_old_owner || has_new_owner) { + struct spa_bt_adapter *a; + struct spa_bt_device *d; + struct spa_bt_remote_endpoint *ep; + struct spa_bt_transport *t; + + monitor->objects_listed = false; + + spa_list_consume(t, &monitor->transport_list, link) + spa_bt_transport_free(t); + spa_list_consume(ep, &monitor->remote_endpoint_list, link) + remote_endpoint_free(ep); + spa_list_consume(d, &monitor->device_list, link) + device_free(d); + spa_list_consume(a, &monitor->adapter_list, link) + adapter_free(a); + } + + if (has_new_owner) { + spa_log_debug(monitor->log, "Bluetooth daemon appeared"); + get_managed_objects(monitor); + } + } else if (spa_streq(name, OFONO_SERVICE)) { + if (monitor->backends[BACKEND_OFONO]) + monitor->backends[BACKEND_OFONO]->available = (new_owner && *new_owner); + reselect_backend(monitor, false); + } else if (spa_streq(name, HSPHFPD_SERVICE)) { + if (monitor->backends[BACKEND_HSPHFPD]) + monitor->backends[BACKEND_HSPHFPD]->available = (new_owner && *new_owner); + reselect_backend(monitor, false); + } + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded")) { + DBusMessageIter it; + + spa_log_debug(monitor->log, "interfaces added %s", dbus_message_get_path(m)); + + if (!monitor->objects_listed) + goto finish; + + if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { + spa_log_error(monitor->log, "Invalid signature found in InterfacesAdded"); + goto finish; + } + + interfaces_added(monitor, &it); + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.ObjectManager", "InterfacesRemoved")) { + DBusMessageIter it; + + spa_log_debug(monitor->log, "interfaces removed %s", dbus_message_get_path(m)); + + if (!monitor->objects_listed) + goto finish; + + if (!dbus_message_iter_init(m, &it) || !spa_streq(dbus_message_get_signature(m), "oas")) { + spa_log_error(monitor->log, "Invalid signature found in InterfacesRemoved"); + goto finish; + } + + interfaces_removed(monitor, &it); + } else if (dbus_message_is_signal(m, "org.freedesktop.DBus.Properties", "PropertiesChanged")) { + DBusMessageIter it[2]; + const char *iface, *path; + + if (!monitor->objects_listed) + goto finish; + + if (!dbus_message_iter_init(m, &it[0]) || + !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + spa_log_error(monitor->log, "Invalid signature found in PropertiesChanged"); + goto finish; + } + path = dbus_message_get_path(m); + + dbus_message_iter_get_basic(&it[0], &iface); + dbus_message_iter_next(&it[0]); + dbus_message_iter_recurse(&it[0], &it[1]); + + if (spa_streq(iface, BLUEZ_ADAPTER_INTERFACE)) { + struct spa_bt_adapter *a; + + a = adapter_find(monitor, path); + if (a == NULL) { + spa_log_warn(monitor->log, + "Properties changed in unknown adapter %s", path); + goto finish; + } + spa_log_debug(monitor->log, "Properties changed in adapter %s", path); + + adapter_update_props(a, &it[1], NULL); + } + else if (spa_streq(iface, BLUEZ_DEVICE_INTERFACE)) { + struct spa_bt_device *d; + + d = spa_bt_device_find(monitor, path); + if (d == NULL) { + spa_log_debug(monitor->log, + "Properties changed in unknown device %s", path); + goto finish; + } + spa_log_debug(monitor->log, "Properties changed in device %s", path); + + device_update_props(d, &it[1], NULL); + + if (!device_props_ready(d)) + goto finish; + + device_update_hw_volume_profiles(d); + + spa_bt_device_add_profile(d, SPA_BT_PROFILE_NULL); + } + else if (spa_streq(iface, BLUEZ_MEDIA_ENDPOINT_INTERFACE)) { + struct spa_bt_remote_endpoint *ep; + struct spa_bt_device *d; + + ep = remote_endpoint_find(monitor, path); + if (ep == NULL) { + spa_log_debug(monitor->log, + "Properties changed in unknown remote endpoint %s", path); + goto finish; + } + spa_log_debug(monitor->log, "Properties changed in remote endpoint %s", path); + + remote_endpoint_update_props(ep, &it[1], NULL); + + d = ep->device; + if (d) + spa_bt_device_emit_profiles_changed(d, d->profiles, d->connected_profiles); + } + else if (spa_streq(iface, BLUEZ_MEDIA_TRANSPORT_INTERFACE)) { + struct spa_bt_transport *transport; + + transport = spa_bt_transport_find(monitor, path); + if (transport == NULL) { + spa_log_warn(monitor->log, + "Properties changed in unknown transport %s", path); + goto finish; + } + + spa_log_debug(monitor->log, "Properties changed in transport %s", path); + + transport_update_props(transport, &it[1], NULL); + } + } + +fail: + dbus_error_free(&err); +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void add_filters(struct spa_bt_monitor *this) +{ + DBusError err; + + if (this->filters_added) + return; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(this->conn, filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" BLUEZ_SERVICE "'", &err); +#ifdef HAVE_BLUEZ_5_BACKEND_OFONO + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" OFONO_SERVICE "'", &err); +#endif +#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "arg0='" HSPHFPD_SERVICE "'", &err); +#endif + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," + "arg0='" BLUEZ_ADAPTER_INTERFACE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," + "arg0='" BLUEZ_DEVICE_INTERFACE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," + "arg0='" BLUEZ_MEDIA_ENDPOINT_INTERFACE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" BLUEZ_SERVICE "'," + "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'," + "arg0='" BLUEZ_MEDIA_TRANSPORT_INTERFACE "'", &err); + + this->filters_added = true; + + return; + +fail: + dbus_error_free(&err); +} + +static int +impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + struct spa_bt_monitor *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + add_filters(this); + get_managed_objects(this); + + struct spa_bt_device *device; + spa_list_for_each(device, &this->device_list, link) { + if (device->added) + emit_device_info(this, device, this->connection_info_supported); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct spa_bt_monitor *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct spa_bt_monitor *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct spa_bt_monitor *monitor; + struct spa_bt_adapter *a; + struct spa_bt_device *d; + struct spa_bt_remote_endpoint *ep; + struct spa_bt_transport *t; + const struct spa_dict_item *it; + size_t i; + + monitor = (struct spa_bt_monitor *) handle; + + /* + * We don't call BlueZ API unregister methods here, since BlueZ generally does the + * unregistration when the DBus connection is closed below. We'll unregister DBus + * object managers and filter callbacks though. + */ + + unregister_media_application(monitor); + + if (monitor->filters_added) { + dbus_connection_remove_filter(monitor->conn, filter_cb, monitor); + monitor->filters_added = false; + } + + if (monitor->get_managed_objects_call) { + dbus_pending_call_cancel(monitor->get_managed_objects_call); + dbus_pending_call_unref(monitor->get_managed_objects_call); + } + + spa_list_consume(t, &monitor->transport_list, link) + spa_bt_transport_free(t); + spa_list_consume(ep, &monitor->remote_endpoint_list, link) + remote_endpoint_free(ep); + spa_list_consume(d, &monitor->device_list, link) + device_free(d); + spa_list_consume(a, &monitor->adapter_list, link) + adapter_free(a); + + for (i = 0; i < SPA_N_ELEMENTS(monitor->backends); ++i) { + spa_bt_backend_free(monitor->backends[i]); + monitor->backends[i] = NULL; + } + + spa_dict_for_each(it, &monitor->global_settings) { + free((void *)it->key); + free((void *)it->value); + } + + free((void*)monitor->enabled_codecs.items); + spa_zero(monitor->enabled_codecs); + + dbus_connection_unref(monitor->conn); + spa_dbus_connection_destroy(monitor->dbus_connection); + monitor->dbus_connection = NULL; + monitor->conn = NULL; + + monitor->objects_listed = false; + + monitor->connection_info_supported = false; + + monitor->backend = NULL; + monitor->backend_selection = BACKEND_NATIVE; + + spa_bt_quirks_destroy(monitor->quirks); + + free_media_codecs(monitor->media_codecs); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct spa_bt_monitor); +} + +int spa_bt_profiles_from_json_array(const char *str) +{ + struct spa_json it, it_array; + char role_name[256]; + enum spa_bt_profile profiles = SPA_BT_PROFILE_NULL; + + spa_json_init(&it, str, strlen(str)); + + if (spa_json_enter_array(&it, &it_array) <= 0) + return -EINVAL; + + while (spa_json_get_string(&it_array, role_name, sizeof(role_name)) > 0) { + if (spa_streq(role_name, "hsp_hs")) { + profiles |= SPA_BT_PROFILE_HSP_HS; + } else if (spa_streq(role_name, "hsp_ag")) { + profiles |= SPA_BT_PROFILE_HSP_AG; + } else if (spa_streq(role_name, "hfp_hf")) { + profiles |= SPA_BT_PROFILE_HFP_HF; + } else if (spa_streq(role_name, "hfp_ag")) { + profiles |= SPA_BT_PROFILE_HFP_AG; + } else if (spa_streq(role_name, "a2dp_sink")) { + profiles |= SPA_BT_PROFILE_A2DP_SINK; + } else if (spa_streq(role_name, "a2dp_source")) { + profiles |= SPA_BT_PROFILE_A2DP_SOURCE; + } else if (spa_streq(role_name, "bap_sink")) { + profiles |= SPA_BT_PROFILE_BAP_SINK; + } else if (spa_streq(role_name, "bap_source")) { + profiles |= SPA_BT_PROFILE_BAP_SOURCE; + } + } + + return profiles; +} + +static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict *info) +{ + const struct media_codec * const * const media_codecs = this->media_codecs; + const char *str; + struct spa_dict_item *codecs; + struct spa_json it, it_array; + char codec_name[256]; + size_t num_codecs; + int i; + + /* Parse bluez5.codecs property to a dict of enabled codecs */ + + num_codecs = 0; + while (media_codecs[num_codecs]) + ++num_codecs; + + codecs = calloc(num_codecs, sizeof(struct spa_dict_item)); + if (codecs == NULL) + return -ENOMEM; + + if (info == NULL || (str = spa_dict_lookup(info, "bluez5.codecs")) == NULL) + goto fallback; + + spa_json_init(&it, str, strlen(str)); + + if (spa_json_enter_array(&it, &it_array) <= 0) { + spa_log_error(this->log, "property bluez5.codecs '%s' is not an array", str); + goto fallback; + } + + this->enabled_codecs = SPA_DICT_INIT(codecs, 0); + + while (spa_json_get_string(&it_array, codec_name, sizeof(codec_name)) > 0) { + int i; + + for (i = 0; media_codecs[i]; ++i) { + const struct media_codec *codec = media_codecs[i]; + + if (!spa_streq(codec->name, codec_name)) + continue; + + if (spa_dict_lookup_item(&this->enabled_codecs, codec->name) != NULL) + continue; + + spa_log_debug(this->log, "enabling codec %s", codec->name); + + spa_assert(this->enabled_codecs.n_items < num_codecs); + + codecs[this->enabled_codecs.n_items].key = codec->name; + codecs[this->enabled_codecs.n_items].value = "true"; + ++this->enabled_codecs.n_items; + + break; + } + } + + spa_dict_qsort(&this->enabled_codecs); + + for (i = 0; media_codecs[i]; ++i) { + const struct media_codec *codec = media_codecs[i]; + if (!is_media_codec_enabled(this, codec)) + spa_log_debug(this->log, "disabling codec %s", codec->name); + } + return 0; + +fallback: + for (i = 0; media_codecs[i]; ++i) { + const struct media_codec *codec = media_codecs[i]; + spa_log_debug(this->log, "enabling codec %s", codec->name); + codecs[i].key = codec->name; + codecs[i].value = "true"; + } + this->enabled_codecs = SPA_DICT_INIT(codecs, i); + spa_dict_qsort(&this->enabled_codecs); + return 0; +} + +static void get_global_settings(struct spa_bt_monitor *this, const struct spa_dict *dict) +{ + uint32_t n_items = 0; + uint32_t i; + + if (dict == NULL) { + this->global_settings = SPA_DICT_INIT(this->global_setting_items, 0); + return; + } + + for (i = 0; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->global_setting_items); i++) { + const struct spa_dict_item *it = &dict->items[i]; + if (spa_strstartswith(it->key, "bluez5.") && it->value != NULL) + this->global_setting_items[n_items++] = + SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value)); + } + + this->global_settings = SPA_DICT_INIT(this->global_setting_items, n_items); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct spa_bt_monitor *this; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct spa_bt_monitor *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + this->plugin_loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); + + spa_log_topic_init(this->log, &log_topic); + + if (this->dbus == NULL) { + spa_log_error(this->log, "a dbus is needed"); + return -EINVAL; + } + + if (this->plugin_loader == NULL) { + spa_log_error(this->log, "a plugin loader is needed"); + return -EINVAL; + } + + this->media_codecs = NULL; + this->quirks = NULL; + this->conn = NULL; + this->dbus_connection = NULL; + + this->media_codecs = load_media_codecs(this->plugin_loader, this->log); + if (this->media_codecs == NULL) { + spa_log_error(this->log, "failed to load required media codec plugins"); + res = -EIO; + goto fail; + } + + this->quirks = spa_bt_quirks_create(info, this->log); + if (this->quirks == NULL) { + spa_log_error(this->log, "failed to parse quirk table"); + res = -EINVAL; + goto fail; + } + + this->dbus_connection = spa_dbus_get_connection(this->dbus, SPA_DBUS_TYPE_SYSTEM); + if (this->dbus_connection == NULL) { + spa_log_error(this->log, "no dbus connection"); + res = -EIO; + goto fail; + } + this->conn = spa_dbus_connection_get(this->dbus_connection); + if (this->conn == NULL) { + spa_log_error(this->log, "failed to get dbus connection"); + res = -EIO; + goto fail; + } + + /* XXX: We should handle spa_dbus reconnecting, but we don't, so ref + * XXX: the handle so that we can keep it if spa_dbus unrefs it. + */ + dbus_connection_ref(this->conn); + + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + spa_list_init(&this->adapter_list); + spa_list_init(&this->device_list); + spa_list_init(&this->remote_endpoint_list); + spa_list_init(&this->transport_list); + + if ((res = parse_codec_array(this, info)) < 0) + goto fail; + + this->default_audio_info.rate = A2DP_CODEC_DEFAULT_RATE; + this->default_audio_info.channels = A2DP_CODEC_DEFAULT_CHANNELS; + + this->backend_selection = BACKEND_NATIVE; + + get_global_settings(this, info); + + if (info) { + const char *str; + uint32_t tmp; + + if ((str = spa_dict_lookup(info, "api.bluez5.connection-info")) != NULL && + spa_atob(str)) + this->connection_info_supported = true; + + if ((str = spa_dict_lookup(info, "bluez5.default.rate")) != NULL && + (tmp = atoi(str)) > 0) + this->default_audio_info.rate = tmp; + + if ((str = spa_dict_lookup(info, "bluez5.default.channels")) != NULL && + ((tmp = atoi(str)) > 0)) + this->default_audio_info.channels = tmp; + + if ((str = spa_dict_lookup(info, "bluez5.hfphsp-backend")) != NULL) { + if (spa_streq(str, "none")) + this->backend_selection = BACKEND_NONE; + else if (spa_streq(str, "any")) + this->backend_selection = BACKEND_ANY; + else if (spa_streq(str, "ofono")) + this->backend_selection = BACKEND_OFONO; + else if (spa_streq(str, "hsphfpd")) + this->backend_selection = BACKEND_HSPHFPD; + else if (spa_streq(str, "native")) + this->backend_selection = BACKEND_NATIVE; + } + + if ((str = spa_dict_lookup(info, "bluez5.dummy-avrcp-player")) != NULL) + this->dummy_avrcp_player = spa_atob(str); + else + this->dummy_avrcp_player = false; + } + + register_media_application(this); + + /* Create backends. They're started after we get a reply from Bluez. */ + this->backends[BACKEND_NATIVE] = backend_native_new(this, this->conn, info, this->quirks, support, n_support); + this->backends[BACKEND_OFONO] = backend_ofono_new(this, this->conn, info, this->quirks, support, n_support); + this->backends[BACKEND_HSPHFPD] = backend_hsphfpd_new(this, this->conn, info, this->quirks, support, n_support); + + return 0; + +fail: + if (this->media_codecs) + free_media_codecs(this->media_codecs); + if (this->quirks) + spa_bt_quirks_destroy(this->quirks); + if (this->conn) + dbus_connection_unref(this->conn); + if (this->dbus_connection) + spa_dbus_connection_destroy(this->dbus_connection); + this->media_codecs = NULL; + this->quirks = NULL; + this->conn = NULL; + this->dbus_connection = NULL; + return res; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +const struct spa_handle_factory spa_bluez5_dbus_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_BLUEZ5_ENUM_DBUS, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +// Report battery percentage to BlueZ using experimental (BlueZ 5.56) Battery Provider API. No-op if no changes occurred. +int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage) +{ + if (percentage == SPA_BT_NO_BATTERY) { + battery_remove(device); + return 0; + } + + // BlueZ likely is running without battery provider support, don't try to report battery + if (device->adapter->battery_provider_unavailable) return 0; + + // If everything is initialized and battery level has not changed we don't need to send anything to BlueZ + if (device->adapter->has_battery_provider && device->has_battery && device->battery == percentage) return 1; + + device->battery = percentage; + + if (!device->adapter->has_battery_provider) { + // No provider: register it, create battery when registered + register_battery_provider(device); + } else if (!device->has_battery) { + // Have provider but no battery: create battery with correct percentage + battery_create(device); + } else { + // Just update existing battery percentage + battery_update(device); + } + + return 1; +} diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c new file mode 100644 index 0000000..dcdfeaf --- /dev/null +++ b/spa/plugins/bluez5/bluez5-device.c @@ -0,0 +1,2398 @@ +/* Spa Bluez5 Device + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.device"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define MAX_DEVICES 64 + +#define DEVICE_ID_SOURCE 0 +#define DEVICE_ID_SINK 1 +#define DYNAMIC_NODE_ID_FLAG 0x1000 + +static struct spa_i18n *_i18n; + +#define _(_str) spa_i18n_text(_i18n,(_str)) +#define N_(_str) (_str) + +enum { + DEVICE_PROFILE_OFF = 0, + DEVICE_PROFILE_AG = 1, + DEVICE_PROFILE_A2DP = 2, + DEVICE_PROFILE_HSP_HFP = 3, + DEVICE_PROFILE_BAP = 4, + DEVICE_PROFILE_LAST = DEVICE_PROFILE_BAP, +}; + +struct props { + enum spa_bluetooth_audio_codec codec; + bool offload_active; +}; + +static void reset_props(struct props *props) +{ + props->codec = 0; + props->offload_active = false; +} + +struct impl; + +struct node { + struct impl *impl; + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + uint32_t id; + unsigned int active:1; + unsigned int mute:1; + unsigned int save:1; + unsigned int a2dp_duplex:1; + unsigned int offload_acquired:1; + uint32_t n_channels; + int64_t latency_offset; + uint32_t channels[SPA_AUDIO_MAX_CHANNELS]; + float volumes[SPA_AUDIO_MAX_CHANNELS]; + float soft_volumes[SPA_AUDIO_MAX_CHANNELS]; +}; + +struct dynamic_node +{ + struct impl *impl; + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + uint32_t id; + const char *factory_name; + bool a2dp_duplex; +}; + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + + uint32_t info_all; + struct spa_device_info info; +#define IDX_EnumProfile 0 +#define IDX_Profile 1 +#define IDX_EnumRoute 2 +#define IDX_Route 3 +#define IDX_PropInfo 4 +#define IDX_Props 5 + struct spa_param_info params[6]; + + struct spa_hook_list hooks; + + struct props props; + + struct spa_bt_device *bt_dev; + struct spa_hook bt_dev_listener; + + uint32_t profile; + unsigned int switching_codec:1; + unsigned int save_profile:1; + uint32_t prev_bt_connected_profiles; + + const struct media_codec **supported_codecs; + size_t supported_codec_count; + + struct dynamic_node dyn_media_source; + struct dynamic_node dyn_media_sink; + struct dynamic_node dyn_sco_source; + struct dynamic_node dyn_sco_sink; + +#define MAX_SETTINGS 32 + struct spa_dict_item setting_items[MAX_SETTINGS]; + struct spa_dict setting_dict; + + struct node nodes[2]; +}; + +static void init_node(struct impl *this, struct node *node, uint32_t id) +{ + uint32_t i; + + spa_zero(*node); + node->id = id; + for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + node->volumes[i] = 1.0f; + node->soft_volumes[i] = 1.0f; + } +} + +static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size) +{ + const struct media_codec * const *c; + + spa_assert(size > 0); + spa_assert(this->supported_codecs); + + for (c = this->supported_codecs; *c && size > 1; ++c) { + if ((*c)->id == id || id == 0) { + *codecs++ = *c; + --size; + } + } + + *codecs = NULL; +} + +static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, size_t *idx) +{ + const struct media_codec *media_codec = NULL; + size_t i; + for (i = 0; i < this->supported_codec_count; ++i) { + if (this->supported_codecs[i]->id == id) { + media_codec = this->supported_codecs[i]; + if (idx) + *idx = i; + } + } + return media_codec; +} + +static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id) +{ + switch (id) { + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: + return HFP_AUDIO_CODEC_CVSD; + case SPA_BLUETOOTH_AUDIO_CODEC_MSBC: + return HFP_AUDIO_CODEC_MSBC; + default: + return 0; + } +} + +static enum spa_bluetooth_audio_codec get_hfp_codec_id(unsigned int codec) +{ + switch (codec) { + case HFP_AUDIO_CODEC_MSBC: + return SPA_BLUETOOTH_AUDIO_CODEC_MSBC; + case HFP_AUDIO_CODEC_CVSD: + return SPA_BLUETOOTH_AUDIO_CODEC_CVSD; + } + return SPA_ID_INVALID; +} + +static const char *get_hfp_codec_description(unsigned int codec) +{ + switch (codec) { + case HFP_AUDIO_CODEC_MSBC: + return "mSBC"; + case HFP_AUDIO_CODEC_CVSD: + return "CVSD"; + } + return "unknown"; +} + +static const char *get_hfp_codec_name(unsigned int codec) +{ + switch (codec) { + case HFP_AUDIO_CODEC_MSBC: + return "msbc"; + case HFP_AUDIO_CODEC_CVSD: + return "cvsd"; + } + return "unknown"; +} + +static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex) +{ + if (t->media_codec != NULL) { + if (a2dp_duplex && t->media_codec->duplex_codec) + return t->media_codec->duplex_codec->name; + return t->media_codec->name; + } + return get_hfp_codec_name(t->codec); +} + +static void transport_destroy(void *userdata) +{ + struct node *node = userdata; + node->transport = NULL; +} + +static void emit_node_props(struct impl *this, struct node *node, bool full) +{ + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, node->id); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, node->n_channels, node->volumes), + SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, node->n_channels, node->soft_volumes), + SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, node->n_channels, node->channels)); + if (full) { + spa_pod_builder_add(&b, + SPA_PROP_mute, SPA_POD_Bool(node->mute), + SPA_PROP_softMute, SPA_POD_Bool(node->mute), + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(node->latency_offset), + 0); + } + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); +} + +static void emit_volume(struct impl *this, struct node *node) +{ + emit_node_props(this, node, false); +} + +static void emit_info(struct impl *this, bool full); + +static float get_soft_volume_boost(struct node *node) +{ + const struct media_codec *codec = node->transport ? node->transport->media_codec : NULL; + + /* + * For A2DP duplex, the duplex microphone channel sometimes does not appear + * to have hardware gain, and input volume is very low. + * + * Work around this by boosting the software volume level, i.e. adjust + * the scale on the user-visible volume control to something more sensible. + * If this causes clipping, the user can just reduce the mic volume to + * bring SW gain below 1. + */ + if (node->a2dp_duplex && node->transport && codec && codec->info && + spa_atob(spa_dict_lookup(codec->info, "duplex.boost")) && + node->id == DEVICE_ID_SOURCE && + !node->transport->volumes[SPA_BT_VOLUME_ID_RX].active) + return 10.0f; /* 20 dB boost */ + + /* In all other cases, no boost */ + return 1.0f; +} + +static float node_get_hw_volume(struct node *node) +{ + uint32_t i; + float hw_volume = 0.0f; + for (i = 0; i < node->n_channels; i++) + hw_volume = SPA_MAX(node->volumes[i], hw_volume); + return SPA_MIN(hw_volume, 1.0f); +} + +static void node_update_soft_volumes(struct node *node, float hw_volume) +{ + for (uint32_t i = 0; i < node->n_channels; ++i) { + node->soft_volumes[i] = hw_volume > 0.0f + ? node->volumes[i] / hw_volume + : 0.0f; + } +} + +static bool node_update_volume_from_transport(struct node *node, bool reset) +{ + struct impl *impl = node->impl; + struct spa_bt_transport_volume *t_volume; + float prev_hw_volume; + + if (!node->transport || !spa_bt_transport_volume_enabled(node->transport)) + return false; + + /* PW is the controller for remote device. */ + if (impl->profile != DEVICE_PROFILE_A2DP + && impl->profile != DEVICE_PROFILE_BAP + && impl->profile != DEVICE_PROFILE_HSP_HFP) + return false; + + t_volume = &node->transport->volumes[node->id]; + + if (!t_volume->active) + return false; + + prev_hw_volume = node_get_hw_volume(node); + + if (!reset) { + for (uint32_t i = 0; i < node->n_channels; ++i) { + node->volumes[i] = prev_hw_volume > 0.0f + ? node->volumes[i] * t_volume->volume / prev_hw_volume + : t_volume->volume; + } + } else { + for (uint32_t i = 0; i < node->n_channels; ++i) + node->volumes[i] = t_volume->volume; + } + + node_update_soft_volumes(node, t_volume->volume); + + /* + * Consider volume changes from the headset as requested + * by the user, and to be saved by the SM. + */ + node->save = true; + + return true; +} + +static void volume_changed(void *userdata) +{ + struct node *node = userdata; + struct impl *impl = node->impl; + + if (!node_update_volume_from_transport(node, false)) + return; + + emit_volume(impl, node); + + impl->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + impl->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(impl, false); +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .destroy = transport_destroy, + .volume_changed = volume_changed, +}; + +static int node_offload_set_active(struct node *node, bool active) +{ + int res = 0; + + if (node->transport == NULL || !node->active) + return -ENOTSUP; + + if (active && !node->offload_acquired) + res = spa_bt_transport_acquire(node->transport, false); + else if (!active && node->offload_acquired) + res = spa_bt_transport_release(node->transport); + + if (res >= 0) + node->offload_acquired = active; + + return res; +} + +static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels, uint32_t *channels) +{ + const struct media_codec *codec; + struct spa_audio_info info = { 0 }; + + if (!a2dp_duplex || !t->media_codec || !t->media_codec->duplex_codec) { + *n_channels = t->n_channels; + memcpy(channels, t->channels, t->n_channels * sizeof(uint32_t)); + return; + } + + codec = t->media_codec->duplex_codec; + + if (!codec->validate_config || + codec->validate_config(codec, 0, + t->configuration, t->configuration_len, + &info) < 0) { + *n_channels = 1; + channels[0] = SPA_AUDIO_CHANNEL_MONO; + return; + } + + *n_channels = info.info.raw.channels; + memcpy(channels, info.info.raw.position, + info.info.raw.channels * sizeof(uint32_t)); +} + +static void emit_node(struct impl *this, struct spa_bt_transport *t, + uint32_t id, const char *factory_name, bool a2dp_duplex) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_device_object_info info; + struct spa_dict_item items[8]; + uint32_t n_items = 0; + char transport[32], str_id[32]; + bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG); + + snprintf(transport, sizeof(transport), "pointer:%p", t); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_TRANSPORT, transport); + items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PROFILE, spa_bt_profile_name(t->profile)); + items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CODEC, get_codec_name(t, a2dp_duplex)); + items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, device->address); + items[4] = SPA_DICT_ITEM_INIT("device.routes", "1"); + n_items = 5; + if (!is_dyn_node) { + snprintf(str_id, sizeof(str_id), "%d", id); + items[5] = SPA_DICT_ITEM_INIT("card.profile.device", str_id); + n_items++; + } + if (spa_streq(spa_bt_profile_name(t->profile), "headset-head-unit")) { + items[n_items] = SPA_DICT_ITEM_INIT("device.intended-roles", "Communication"); + n_items++; + } + if (a2dp_duplex) { + items[n_items] = SPA_DICT_ITEM_INIT("api.bluez5.a2dp-duplex", "true"); + n_items++; + } + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + info.factory_name = factory_name; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.props = &SPA_DICT_INIT(items, n_items); + + SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG); + spa_device_emit_object_info(&this->hooks, id, &info); + + if (!is_dyn_node) { + uint32_t prev_channels = this->nodes[id].n_channels; + float boost; + + this->nodes[id].impl = this; + this->nodes[id].active = true; + this->nodes[id].offload_acquired = false; + this->nodes[id].a2dp_duplex = a2dp_duplex; + get_channels(t, a2dp_duplex, &this->nodes[id].n_channels, this->nodes[id].channels); + if (this->nodes[id].transport) + spa_hook_remove(&this->nodes[id].transport_listener); + this->nodes[id].transport = t; + spa_bt_transport_add_listener(t, &this->nodes[id].transport_listener, &transport_events, &this->nodes[id]); + + if (prev_channels > 0) { + size_t i; + /* + * Spread mono volume to all channels, if we had switched HFP -> A2DP. + * XXX: we should also use different route for hfp and a2dp + */ + for (i = prev_channels; i < this->nodes[id].n_channels; ++i) + this->nodes[id].volumes[i] = this->nodes[id].volumes[i % prev_channels]; + } + + node_update_volume_from_transport(&this->nodes[id], true); + + boost = get_soft_volume_boost(&this->nodes[id]); + if (boost != 1.0f) { + size_t i; + for (i = 0; i < this->nodes[id].n_channels; ++i) + this->nodes[id].soft_volumes[i] = this->nodes[id].volumes[i] * boost; + } + + emit_node_props(this, &this->nodes[id], true); + } +} + +static struct spa_bt_transport *find_transport(struct impl *this, int profile, enum spa_bluetooth_audio_codec codec) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_bt_transport *t; + + spa_list_for_each(t, &device->transport_list, device_link) { + bool codec_ok = codec == 0 || + (t->media_codec != NULL && t->media_codec->id == codec) || + get_hfp_codec_id(t->codec) == codec; + + if ((t->profile & device->connected_profiles) && + (t->profile & profile) == t->profile && + codec_ok) + return t; + } + + return NULL; +} + +static void dynamic_node_transport_destroy(void *data) +{ + struct dynamic_node *this = data; + spa_log_debug(this->impl->log, "transport %p destroy", this->transport); + this->transport = NULL; +} + +static void dynamic_node_transport_state_changed(void *data, + enum spa_bt_transport_state old, + enum spa_bt_transport_state state) +{ + struct dynamic_node *this = data; + struct impl *impl = this->impl; + struct spa_bt_transport *t = this->transport; + + spa_log_debug(impl->log, "transport %p state %d->%d", t, old, state); + + if (state >= SPA_BT_TRANSPORT_STATE_PENDING && old < SPA_BT_TRANSPORT_STATE_PENDING) { + if (!SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) { + SPA_FLAG_SET(this->id, DYNAMIC_NODE_ID_FLAG); + spa_bt_transport_keepalive(t, true); + emit_node(impl, t, this->id, this->factory_name, this->a2dp_duplex); + } + } else if (state < SPA_BT_TRANSPORT_STATE_PENDING && old >= SPA_BT_TRANSPORT_STATE_PENDING) { + if (SPA_FLAG_IS_SET(this->id, DYNAMIC_NODE_ID_FLAG)) { + SPA_FLAG_CLEAR(this->id, DYNAMIC_NODE_ID_FLAG); + spa_bt_transport_keepalive(t, false); + spa_device_emit_object_info(&impl->hooks, this->id, NULL); + } + } +} + +static void dynamic_node_volume_changed(void *data) +{ + struct dynamic_node *node = data; + struct impl *impl = node->impl; + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + struct spa_bt_transport_volume *t_volume; + int id = node->id, volume_id; + + SPA_FLAG_CLEAR(id, DYNAMIC_NODE_ID_FLAG); + + /* Remote device is the controller */ + if (!node->transport || impl->profile != DEVICE_PROFILE_AG + || !spa_bt_transport_volume_enabled(node->transport)) + return; + + if (id == 0 || id == 2) + volume_id = SPA_BT_VOLUME_ID_RX; + else if (id == 1) + volume_id = SPA_BT_VOLUME_ID_TX; + else + return; + + t_volume = &node->transport->volumes[volume_id]; + if (!t_volume->active) + return; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, id); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_volume, SPA_POD_Float(t_volume->volume)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_log_debug(impl->log, "dynamic node %p: volume %d changed %f, profile %d", + node, volume_id, t_volume->volume, node->transport->profile); + + /* Dynamic node doesn't has route, we can only set volume on adaptar node. */ + spa_device_emit_event(&impl->hooks, event); +} + +static const struct spa_bt_transport_events dynamic_node_transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = dynamic_node_transport_destroy, + .state_changed = dynamic_node_transport_state_changed, + .volume_changed = dynamic_node_volume_changed, +}; + +static void emit_dynamic_node(struct dynamic_node *this, struct impl *impl, + struct spa_bt_transport *t, uint32_t id, const char *factory_name, bool a2dp_duplex) +{ + spa_log_debug(impl->log, "dynamic node, transport: %p->%p id: %08x->%08x", + this->transport, t, this->id, id); + + if (this->transport) { + /* Session manager don't really handles transport ptr changing. */ + spa_assert(this->transport == t); + spa_hook_remove(&this->transport_listener); + } + + this->impl = impl; + this->transport = t; + this->id = id; + this->factory_name = factory_name; + this->a2dp_duplex = a2dp_duplex; + + spa_bt_transport_add_listener(this->transport, + &this->transport_listener, &dynamic_node_transport_events, this); + + /* emits the node if the state is already pending */ + dynamic_node_transport_state_changed (this, SPA_BT_TRANSPORT_STATE_IDLE, t->state); +} + +static void remove_dynamic_node(struct dynamic_node *this) +{ + if (this->transport == NULL) + return; + + /* destroy the node, if it exists */ + dynamic_node_transport_state_changed (this, this->transport->state, + SPA_BT_TRANSPORT_STATE_IDLE); + + spa_hook_remove(&this->transport_listener); + this->impl = NULL; + this->transport = NULL; + this->id = 0; + this->factory_name = NULL; +} + +static int emit_nodes(struct impl *this) +{ + struct spa_bt_transport *t; + + switch (this->profile) { + case DEVICE_PROFILE_OFF: + break; + case DEVICE_PROFILE_AG: + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) { + t = find_transport(this, SPA_BT_PROFILE_HFP_AG, 0); + if (!t) + t = find_transport(this, SPA_BT_PROFILE_HSP_AG, 0); + if (t) { + if (t->profile == SPA_BT_PROFILE_HSP_AG) + this->props.codec = 0; + else + this->props.codec = get_hfp_codec_id(t->codec); + emit_dynamic_node(&this->dyn_sco_source, this, t, + 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false); + emit_dynamic_node(&this->dyn_sco_sink, this, t, + 1, SPA_NAME_API_BLUEZ5_SCO_SINK, false); + } + } + if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_A2DP_SOURCE)) { + t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0); + if (t) { + this->props.codec = t->media_codec->id; + emit_dynamic_node(&this->dyn_media_source, this, t, + 2, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false); + + if (t->media_codec->duplex_codec) { + emit_dynamic_node(&this->dyn_media_sink, this, t, + 3, SPA_NAME_API_BLUEZ5_A2DP_SINK, true); + } + } + } + break; + case DEVICE_PROFILE_A2DP: + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) { + t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE, 0); + if (t) { + this->props.codec = t->media_codec->id; + emit_dynamic_node(&this->dyn_media_source, this, t, + DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, false); + + if (t->media_codec->duplex_codec) { + emit_node(this, t, + DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, true); + } + } + } + + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) { + t = find_transport(this, SPA_BT_PROFILE_A2DP_SINK, this->props.codec); + if (t) { + this->props.codec = t->media_codec->id; + emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_A2DP_SINK, false); + + if (t->media_codec->duplex_codec) { + emit_node(this, t, + DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_A2DP_SOURCE, true); + } + } + } + + if (get_supported_media_codec(this, this->props.codec, NULL) == NULL) + this->props.codec = 0; + break; + case DEVICE_PROFILE_BAP: + if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SOURCE)) { + t = find_transport(this, SPA_BT_PROFILE_BAP_SOURCE, 0); + if (t) { + this->props.codec = t->media_codec->id; + if (t->bap_initiator) + emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false); + else + emit_dynamic_node(&this->dyn_media_source, this, t, + DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_MEDIA_SOURCE, false); + } + } + + if (this->bt_dev->connected_profiles & (SPA_BT_PROFILE_BAP_SINK)) { + t = find_transport(this, SPA_BT_PROFILE_BAP_SINK, this->props.codec); + if (t) { + this->props.codec = t->media_codec->id; + if (t->bap_initiator) + emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + else + emit_dynamic_node(&this->dyn_media_sink, this, t, + DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + } + } + + if (get_supported_media_codec(this, this->props.codec, NULL) == NULL) + this->props.codec = 0; + break; + case DEVICE_PROFILE_HSP_HFP: + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) { + t = find_transport(this, SPA_BT_PROFILE_HFP_HF, this->props.codec); + if (!t) + t = find_transport(this, SPA_BT_PROFILE_HSP_HS, 0); + if (t) { + if (t->profile == SPA_BT_PROFILE_HSP_HS) + this->props.codec = 0; + else + this->props.codec = get_hfp_codec_id(t->codec); + emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false); + emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK, false); + } + } + + if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1) + this->props.codec = 0; + break; + default: + return -EINVAL; + } + return 0; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_DEVICE_BUS, "bluetooth" }, + { SPA_KEY_MEDIA_CLASS, "Audio/Device" }, +}; + +static void emit_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(info_items); + + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_remove_nodes(struct impl *this) +{ + remove_dynamic_node (&this->dyn_media_source); + remove_dynamic_node (&this->dyn_media_sink); + remove_dynamic_node (&this->dyn_sco_source); + remove_dynamic_node (&this->dyn_sco_sink); + + for (uint32_t i = 0; i < 2; i++) { + struct node * node = &this->nodes[i]; + node_offload_set_active(node, false); + if (node->transport) { + spa_hook_remove(&node->transport_listener); + node->transport = NULL; + } + if (node->active) { + spa_device_emit_object_info(&this->hooks, i, NULL); + node->active = false; + } + } + + this->props.offload_active = false; +} + +static bool validate_profile(struct impl *this, uint32_t profile, + enum spa_bluetooth_audio_codec codec); + +static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec, bool save) +{ + if (!validate_profile(this, profile, codec)) { + spa_log_warn(this->log, "trying to set invalid profile %d, codec %d, %08x %08x", + profile, codec, + this->bt_dev->profiles, this->bt_dev->connected_profiles); + return -EINVAL; + } + + this->save_profile = save; + + if (this->profile == profile && + (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) && + (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) && + (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec)) + return 0; + + emit_remove_nodes(this); + + spa_bt_device_release_transports(this->bt_dev); + + this->profile = profile; + this->prev_bt_connected_profiles = this->bt_dev->connected_profiles; + this->props.codec = codec; + + /* + * A2DP/BAP: ensure there's a transport with the selected codec (0 means any). + * Don't try to switch codecs when the device is in the A2DP source role, since + * devices do not appear to like that. + */ + if ((profile == DEVICE_PROFILE_A2DP || profile == DEVICE_PROFILE_BAP) + && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) { + int ret; + const struct media_codec *codecs[64]; + + get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs)); + + this->switching_codec = true; + + ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs); + if (ret < 0) { + if (ret != -ENOTSUP) + spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret); + } else { + return 0; + } + } else if (profile == DEVICE_PROFILE_HSP_HFP && get_hfp_codec(codec) && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) { + int ret; + + this->switching_codec = true; + + ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, get_hfp_codec(codec)); + if (ret < 0) { + if (ret != -ENOTSUP) + spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret); + } else { + return 0; + } + } + + this->switching_codec = false; + this->props.codec = 0; + emit_nodes(this); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(this, false); + + return 0; +} + +static void codec_switched(void *userdata, int status) +{ + struct impl *this = userdata; + + spa_log_debug(this->log, "codec switched (status %d)", status); + + this->switching_codec = false; + + if (status < 0) { + /* Failed to switch: return to a fallback profile */ + spa_log_error(this->log, "failed to switch codec (%d), setting fallback profile", status); + if (this->profile == DEVICE_PROFILE_A2DP && this->props.codec != 0) { + this->props.codec = 0; + } else if (this->profile == DEVICE_PROFILE_BAP && this->props.codec != 0) { + this->props.codec = 0; + } else if (this->profile == DEVICE_PROFILE_HSP_HFP && this->props.codec != 0) { + this->props.codec = 0; + } else { + this->profile = DEVICE_PROFILE_OFF; + } + } + + emit_remove_nodes(this); + emit_nodes(this); + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + if (this->prev_bt_connected_profiles != this->bt_dev->connected_profiles) + this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(this, false); +} + +static void profiles_changed(void *userdata, uint32_t prev_profiles, uint32_t prev_connected_profiles) +{ + struct impl *this = userdata; + uint32_t connected_change; + bool nodes_changed = false; + + connected_change = (this->bt_dev->connected_profiles ^ prev_connected_profiles); + + /* Profiles changed. We have to re-emit device information. */ + spa_log_info(this->log, "profiles changed to %08x %08x (prev %08x %08x, change %08x)" + " switching_codec:%d", + this->bt_dev->profiles, this->bt_dev->connected_profiles, + prev_profiles, prev_connected_profiles, connected_change, + this->switching_codec); + + if (this->switching_codec) + return; + + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK) { + free(this->supported_codecs); + this->supported_codecs = spa_bt_device_get_supported_media_codecs( + this->bt_dev, &this->supported_codec_count, true); + } + + switch (this->profile) { + case DEVICE_PROFILE_OFF: + /* Noop */ + nodes_changed = false; + break; + case DEVICE_PROFILE_AG: + nodes_changed = (connected_change & (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY | + SPA_BT_PROFILE_MEDIA_SOURCE)); + spa_log_debug(this->log, "profiles changed: AG nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_BAP: + if (get_supported_media_codec(this, this->props.codec, NULL) == NULL) + this->props.codec = 0; + nodes_changed = (connected_change & (SPA_BT_PROFILE_MEDIA_SINK | + SPA_BT_PROFILE_MEDIA_SOURCE)); + spa_log_debug(this->log, "profiles changed: media nodes changed: %d", + nodes_changed); + break; + case DEVICE_PROFILE_HSP_HFP: + if (spa_bt_device_supports_hfp_codec(this->bt_dev, get_hfp_codec(this->props.codec)) != 1) + this->props.codec = 0; + nodes_changed = (connected_change & SPA_BT_PROFILE_HEADSET_HEAD_UNIT); + spa_log_debug(this->log, "profiles changed: HSP/HFP nodes changed: %d", + nodes_changed); + break; + } + + if (nodes_changed) { + emit_remove_nodes(this); + emit_nodes(this); + } + + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; /* Profile changes may affect routes */ + this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[IDX_PropInfo].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(this, false); +} + +static void set_initial_profile(struct impl *this); + +static void device_connected(void *userdata, bool connected) { + struct impl *this = userdata; + + spa_log_debug(this->log, "connected: %d", connected); + + if (connected ^ (this->profile != DEVICE_PROFILE_OFF)) + set_initial_profile(this); +} + +static const struct spa_bt_device_events bt_dev_events = { + SPA_VERSION_BT_DEVICE_EVENTS, + .connected = device_connected, + .codec_switched = codec_switched, + .profiles_changed = profiles_changed, +}; + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info) + emit_info(this, true); + + if (events->object_info) + emit_nodes(this); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum spa_bluetooth_audio_codec codec) +{ + struct spa_bt_device *device = this->bt_dev; + uint32_t mask; + bool have_output = false, have_input = false; + const struct media_codec *media_codec; + + switch (index) { + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_BAP: + if (device->connected_profiles & SPA_BT_PROFILE_MEDIA_SINK) + have_output = true; + + media_codec = get_supported_media_codec(this, codec, NULL); + if (media_codec && media_codec->duplex_codec) + have_input = true; + break; + case DEVICE_PROFILE_HSP_HFP: + if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + have_output = have_input = true; + break; + default: + break; + } + + mask = 0; + if (have_output) + mask |= 1 << SPA_DIRECTION_OUTPUT; + if (have_input) + mask |= 1 << SPA_DIRECTION_INPUT; + return mask; +} + +static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32_t *next, enum spa_bluetooth_audio_codec *codec) +{ + /* + * XXX: The codecs should probably become a separate param, and not have + * XXX: separate profiles for each one. + */ + + *codec = 0; + *next = index + 1; + + if (index <= DEVICE_PROFILE_LAST) { + return index; + } else if (index != SPA_ID_INVALID) { + const struct spa_type_info *info; + uint32_t profile; + + *codec = index - DEVICE_PROFILE_LAST; + *next = SPA_ID_INVALID; + + for (info = spa_type_bluetooth_audio_codec; info->type; ++info) + if (info->type > *codec) + *next = SPA_MIN(info->type + DEVICE_PROFILE_LAST, *next); + + if (get_hfp_codec(*codec)) + profile = DEVICE_PROFILE_HSP_HFP; + else if (*codec == SPA_BLUETOOTH_AUDIO_CODEC_LC3) + profile = DEVICE_PROFILE_BAP; + else + profile = DEVICE_PROFILE_A2DP; + + return profile; + } + + *next = SPA_ID_INVALID; + return SPA_ID_INVALID; +} + +static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_audio_codec codec) +{ + if (profile == DEVICE_PROFILE_OFF || profile == DEVICE_PROFILE_AG) + return profile; + + if (profile == DEVICE_PROFILE_A2DP) { + if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_MEDIA_SOURCE)) + return profile; + + return codec + DEVICE_PROFILE_LAST; + } + + if (profile == DEVICE_PROFILE_BAP) { + if (codec == 0) + return profile; + + return codec + DEVICE_PROFILE_LAST; + } + + if (profile == DEVICE_PROFILE_HSP_HFP) { + if (codec == 0 || (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HFP_AG)) + return profile; + + return codec + DEVICE_PROFILE_LAST; + } + + return SPA_ID_INVALID; +} + +static bool set_initial_hsp_hfp_profile(struct impl *this) +{ + struct spa_bt_transport *t; + int i; + + for (i = SPA_BT_PROFILE_HSP_HS; i <= SPA_BT_PROFILE_HFP_AG; i <<= 1) { + if (!(this->bt_dev->connected_profiles & i)) + continue; + + t = find_transport(this, i, 0); + if (t) { + this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ? + DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP; + this->props.codec = get_hfp_codec_id(t->codec); + + spa_log_debug(this->log, "initial profile HSP/HFP profile:%d codec:%d", + this->profile, this->props.codec); + return true; + } + } + return false; +} + +static void set_initial_profile(struct impl *this) +{ + struct spa_bt_transport *t; + int i; + + this->switching_codec = false; + + if (this->supported_codecs) + free(this->supported_codecs); + this->supported_codecs = spa_bt_device_get_supported_media_codecs( + this->bt_dev, &this->supported_codec_count, true); + + /* Prefer BAP, then A2DP, then HFP, then null, but select AG if the device + appears not to have BAP_SINK, A2DP_SINK or any HEAD_UNIT profile */ + + /* If default profile is set to HSP/HFP, first try those and exit if found. */ + if (this->bt_dev->settings != NULL) { + const char *str = spa_dict_lookup(this->bt_dev->settings, "bluez5.profile"); + if (spa_streq(str, "off")) + goto off; + if (spa_streq(str, "headset-head-unit") && set_initial_hsp_hfp_profile(this)) + return; + } + + for (i = SPA_BT_PROFILE_BAP_SINK; i <= SPA_BT_PROFILE_A2DP_SOURCE; i <<= 1) { + if (!(this->bt_dev->connected_profiles & i)) + continue; + + t = find_transport(this, i, 0); + if (t) { + if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE) + this->profile = DEVICE_PROFILE_AG; + else if (i == SPA_BT_PROFILE_BAP_SINK) + this->profile = DEVICE_PROFILE_BAP; + else + this->profile = DEVICE_PROFILE_A2DP; + this->props.codec = t->media_codec->id; + spa_log_debug(this->log, "initial profile media profile:%d codec:%d", + this->profile, this->props.codec); + return; + } + } + + if (set_initial_hsp_hfp_profile(this)) + return; + +off: + spa_log_debug(this->log, "initial profile off"); + + this->profile = DEVICE_PROFILE_OFF; + this->props.codec = 0; +} + +static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b, + uint32_t id, uint32_t index, uint32_t profile_index, enum spa_bluetooth_audio_codec codec, + bool current) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_pod_frame f[2]; + const char *name, *desc; + char *name_and_codec = NULL; + char *desc_and_codec = NULL; + uint32_t n_source = 0, n_sink = 0; + uint32_t capture[1] = { DEVICE_ID_SOURCE }, playback[1] = { DEVICE_ID_SINK }; + int priority; + + switch (profile_index) { + case DEVICE_PROFILE_OFF: + name = "off"; + desc = _("Off"); + priority = 0; + break; + case DEVICE_PROFILE_AG: + { + uint32_t profile = device->connected_profiles & + (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + if (profile == 0) { + return NULL; + } else { + name = "audio-gateway"; + desc = _("Audio Gateway (A2DP Source & HSP/HFP AG)"); + } + priority = 256; + break; + } + case DEVICE_PROFILE_A2DP: + { + /* make this device profile visible only if there is an A2DP sink */ + uint32_t profile = device->connected_profiles & + (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE); + if (!(profile & SPA_BT_PROFILE_A2DP_SINK)) { + return NULL; + } + name = spa_bt_profile_name(profile); + n_sink++; + if (codec) { + size_t idx; + const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx); + if (media_codec == NULL) { + errno = EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, media_codec->name); + name = name_and_codec; + if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) { + desc_and_codec = spa_aprintf(_("High Fidelity Playback (A2DP Sink, codec %s)"), + media_codec->description); + } else { + desc_and_codec = spa_aprintf(_("High Fidelity Duplex (A2DP Source/Sink, codec %s)"), + media_codec->description); + + } + desc = desc_and_codec; + priority = 16 + this->supported_codec_count - idx; /* order as in codec list */ + } else { + if (profile == SPA_BT_PROFILE_A2DP_SINK) { + desc = _("High Fidelity Playback (A2DP Sink)"); + } else { + desc = _("High Fidelity Duplex (A2DP Source/Sink)"); + } + priority = 16; + } + break; + } + case DEVICE_PROFILE_BAP: + { + uint32_t profile = device->connected_profiles & + (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE); + size_t idx; + const struct media_codec *media_codec; + + if (profile == 0) + return NULL; + + if (!codec) { + errno = EINVAL; + return NULL; + } + + if (profile & (SPA_BT_PROFILE_BAP_SINK)) + n_sink++; + if (profile & (SPA_BT_PROFILE_BAP_SOURCE)) + n_source++; + + name = spa_bt_profile_name(profile); + + media_codec = get_supported_media_codec(this, codec, &idx); + if (media_codec == NULL) { + errno = EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, media_codec->name); + name = name_and_codec; + switch (profile) { + case SPA_BT_PROFILE_BAP_SINK: + desc_and_codec = spa_aprintf(_("High Fidelity Playback (BAP Sink, codec %s)"), + media_codec->description); + break; + case SPA_BT_PROFILE_BAP_SOURCE: + desc_and_codec = spa_aprintf(_("High Fidelity Input (BAP Source, codec %s)"), + media_codec->description); + break; + default: + desc_and_codec = spa_aprintf(_("High Fidelity Duplex (BAP Source/Sink, codec %s)"), + media_codec->description); + } + desc = desc_and_codec; + priority = 128 + this->supported_codec_count - idx; /* order as in codec list */ + break; + } + case DEVICE_PROFILE_HSP_HFP: + { + /* make this device profile visible only if there is a head unit */ + uint32_t profile = device->connected_profiles & + SPA_BT_PROFILE_HEADSET_HEAD_UNIT; + if (profile == 0) { + return NULL; + } + name = spa_bt_profile_name(profile); + n_source++; + n_sink++; + if (codec) { + bool codec_ok = !(profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + unsigned int hfp_codec = get_hfp_codec(codec); + if (spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1) + codec_ok = false; + if (!codec_ok) { + errno = EINVAL; + return NULL; + } + name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec)); + name = name_and_codec; + desc_and_codec = spa_aprintf(_("Headset Head Unit (HSP/HFP, codec %s)"), + get_hfp_codec_description(hfp_codec)); + desc = desc_and_codec; + priority = 1 + hfp_codec; /* prefer msbc over cvsd */ + } else { + desc = _("Headset Head Unit (HSP/HFP)"); + priority = 1; + } + break; + } + default: + errno = EINVAL; + return NULL; + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id); + spa_pod_builder_add(b, + SPA_PARAM_PROFILE_index, SPA_POD_Int(index), + SPA_PARAM_PROFILE_name, SPA_POD_String(name), + SPA_PARAM_PROFILE_description, SPA_POD_String(desc), + SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes), + SPA_PARAM_PROFILE_priority, SPA_POD_Int(priority), + 0); + if (n_source > 0 || n_sink > 0) { + spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0); + spa_pod_builder_push_struct(b, &f[1]); + if (n_source > 0) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Source"), + SPA_POD_Int(n_source), + SPA_POD_String("card.profile.devices"), + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, capture)); + } + if (n_sink > 0) { + spa_pod_builder_add_struct(b, + SPA_POD_String("Audio/Sink"), + SPA_POD_Int(n_sink), + SPA_POD_String("card.profile.devices"), + SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int, 1, playback)); + } + spa_pod_builder_pop(b, &f[1]); + } + if (current) { + spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0); + spa_pod_builder_bool(b, this->save_profile); + } + + if (name_and_codec) + free(name_and_codec); + if (desc_and_codec) + free(desc_and_codec); + + return spa_pod_builder_pop(b, &f[0]); +} + +static bool validate_profile(struct impl *this, uint32_t profile, + enum spa_bluetooth_audio_codec codec) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + return (build_profile(this, &b, 0, 0, profile, codec, false) != NULL); +} + +static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, + uint32_t id, uint32_t port, uint32_t profile) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_pod_frame f[2]; + enum spa_direction direction; + const char *name_prefix, *description, *hfp_description, *port_type; + enum spa_bt_form_factor ff; + enum spa_bluetooth_audio_codec codec; + char name[128]; + uint32_t i, j, mask, next; + uint32_t dev = SPA_ID_INVALID, enum_dev; + + ff = spa_bt_form_factor_from_class(device->bluetooth_class); + + switch (ff) { + case SPA_BT_FORM_FACTOR_HEADSET: + name_prefix = "headset"; + description = _("Headset"); + hfp_description = _("Handsfree"); + port_type = "headset"; + break; + case SPA_BT_FORM_FACTOR_HANDSFREE: + name_prefix = "handsfree"; + description = _("Handsfree"); + hfp_description = _("Handsfree (HFP)"); + port_type = "handsfree"; + break; + case SPA_BT_FORM_FACTOR_MICROPHONE: + name_prefix = "microphone"; + description = _("Microphone"); + hfp_description = _("Handsfree"); + port_type = "mic"; + break; + case SPA_BT_FORM_FACTOR_SPEAKER: + name_prefix = "speaker"; + description = _("Speaker"); + hfp_description = _("Handsfree"); + port_type = "speaker"; + break; + case SPA_BT_FORM_FACTOR_HEADPHONE: + name_prefix = "headphone"; + description = _("Headphone"); + hfp_description = _("Handsfree"); + port_type = "headphones"; + break; + case SPA_BT_FORM_FACTOR_PORTABLE: + name_prefix = "portable"; + description = _("Portable"); + hfp_description = _("Handsfree"); + port_type = "portable"; + break; + case SPA_BT_FORM_FACTOR_CAR: + name_prefix = "car"; + description = _("Car"); + hfp_description = _("Handsfree"); + port_type = "car"; + break; + case SPA_BT_FORM_FACTOR_HIFI: + name_prefix = "hifi"; + description = _("HiFi"); + hfp_description = _("Handsfree"); + port_type = "hifi"; + break; + case SPA_BT_FORM_FACTOR_PHONE: + name_prefix = "phone"; + description = _("Phone"); + hfp_description = _("Handsfree"); + port_type = "phone"; + break; + case SPA_BT_FORM_FACTOR_UNKNOWN: + default: + name_prefix = "bluetooth"; + description = _("Bluetooth"); + hfp_description = _("Bluetooth (HFP)"); + port_type = "bluetooth"; + break; + } + + switch (port) { + case 0: + direction = SPA_DIRECTION_INPUT; + snprintf(name, sizeof(name), "%s-input", name_prefix); + enum_dev = DEVICE_ID_SOURCE; + if (profile == DEVICE_PROFILE_A2DP) + dev = enum_dev; + else if (profile != SPA_ID_INVALID) + enum_dev = SPA_ID_INVALID; + break; + case 1: + direction = SPA_DIRECTION_OUTPUT; + snprintf(name, sizeof(name), "%s-output", name_prefix); + enum_dev = DEVICE_ID_SINK; + if (profile == DEVICE_PROFILE_A2DP) + dev = enum_dev; + else if (profile != SPA_ID_INVALID) + enum_dev = SPA_ID_INVALID; + break; + case 2: + direction = SPA_DIRECTION_INPUT; + snprintf(name, sizeof(name), "%s-hf-input", name_prefix); + description = hfp_description; + enum_dev = DEVICE_ID_SOURCE; + if (profile == DEVICE_PROFILE_HSP_HFP) + dev = enum_dev; + else if (profile != SPA_ID_INVALID) + enum_dev = SPA_ID_INVALID; + break; + case 3: + direction = SPA_DIRECTION_OUTPUT; + snprintf(name, sizeof(name), "%s-hf-output", name_prefix); + description = hfp_description; + enum_dev = DEVICE_ID_SINK; + if (profile == DEVICE_PROFILE_HSP_HFP) + dev = enum_dev; + else if (profile != SPA_ID_INVALID) + enum_dev = SPA_ID_INVALID; + break; + default: + errno = EINVAL; + return NULL; + } + + if (enum_dev == SPA_ID_INVALID) { + errno = EINVAL; + return NULL; + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id); + spa_pod_builder_add(b, + SPA_PARAM_ROUTE_index, SPA_POD_Int(port), + SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction), + SPA_PARAM_ROUTE_name, SPA_POD_String(name), + SPA_PARAM_ROUTE_description, SPA_POD_String(description), + SPA_PARAM_ROUTE_priority, SPA_POD_Int(0), + SPA_PARAM_ROUTE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes), + 0); + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, 0); + spa_pod_builder_push_struct(b, &f[1]); + spa_pod_builder_int(b, 1); + spa_pod_builder_add(b, + SPA_POD_String("port.type"), + SPA_POD_String(port_type), + NULL); + spa_pod_builder_pop(b, &f[1]); + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0); + spa_pod_builder_push_array(b, &f[1]); + + mask = 0; + for (i = 1; (j = get_profile_from_index(this, i, &next, &codec)) != SPA_ID_INVALID; i = next) { + uint32_t profile_mask; + + if (j == DEVICE_PROFILE_A2DP && !(port == 0 || port == 1)) + continue; + if (j == DEVICE_PROFILE_HSP_HFP && !(port == 2 || port == 3)) + continue; + + profile_mask = profile_direction_mask(this, j, codec); + if (!(profile_mask & (1 << direction))) + continue; + + /* Check the profile actually exists */ + if (!validate_profile(this, j, codec)) + continue; + + mask |= profile_mask; + spa_pod_builder_int(b, i); + } + spa_pod_builder_pop(b, &f[1]); + + if (!(mask & (1 << direction))) { + /* No profile has route direction */ + return NULL; + } + + if (dev != SPA_ID_INVALID) { + struct node *node = &this->nodes[dev]; + struct spa_bt_transport_volume *t_volume; + + mask = profile_direction_mask(this, this->profile, this->props.codec); + if (!(mask & (1 << direction))) + return NULL; + + t_volume = node->transport + ? &node->transport->volumes[node->id] + : NULL; + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0); + spa_pod_builder_int(b, dev); + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0); + spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id); + + spa_pod_builder_prop(b, SPA_PROP_mute, 0); + spa_pod_builder_bool(b, node->mute); + + spa_pod_builder_prop(b, SPA_PROP_channelVolumes, + (t_volume && t_volume->active) ? SPA_POD_PROP_FLAG_HARDWARE : 0); + spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float, + node->n_channels, node->volumes); + + if (t_volume && t_volume->active) { + spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY); + spa_pod_builder_float(b, 1.0f / (t_volume->hw_volume_max + 1)); + } + + spa_pod_builder_prop(b, SPA_PROP_channelMap, 0); + spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id, + node->n_channels, node->channels); + + if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) && + dev == DEVICE_ID_SINK) { + spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0); + spa_pod_builder_long(b, node->latency_offset); + } + + spa_pod_builder_pop(b, &f[1]); + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0); + spa_pod_builder_bool(b, node->save); + } + + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0); + spa_pod_builder_push_array(b, &f[1]); + spa_pod_builder_int(b, enum_dev); + spa_pod_builder_pop(b, &f[1]); + + if (profile != SPA_ID_INVALID) { + spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0); + spa_pod_builder_int(b, profile); + } + return spa_pod_builder_pop(b, &f[0]); +} + +static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec) +{ + int i; + +next: + *j = *j + 1; + spa_assert(*j >= 0); + if ((size_t)*j >= this->supported_codec_count) + return false; + + for (i = 0; i < *j; ++i) + if (this->supported_codecs[i]->id == this->supported_codecs[*j]->id) + goto next; + + *codec = this->supported_codecs[*j]; + return true; +} + +static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_builder *b, uint32_t id) +{ + struct spa_pod_frame f[2]; + struct spa_pod_choice *choice; + const struct media_codec *codec; + size_t n; + int j; + +#define FOR_EACH_MEDIA_CODEC(j, codec) \ + for (j = -1; iterate_supported_media_codecs(this, &j, &codec);) +#define FOR_EACH_HFP_CODEC(j) \ + for (j = HFP_AUDIO_CODEC_MSBC; j >= HFP_AUDIO_CODEC_CVSD; --j) \ + if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1) + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + + /* + * XXX: the ids in principle should use builder_id, not builder_int, + * XXX: but the type info for _type and _labels doesn't work quite right now. + */ + + /* Transport codec */ + spa_pod_builder_prop(b, SPA_PROP_INFO_id, 0); + spa_pod_builder_id(b, SPA_PROP_bluetoothAudioCodec); + spa_pod_builder_prop(b, SPA_PROP_INFO_description, 0); + spa_pod_builder_string(b, "Air codec"); + spa_pod_builder_prop(b, SPA_PROP_INFO_type, 0); + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]); + n = 0; + if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) { + FOR_EACH_MEDIA_CODEC(j, codec) { + if (n == 0) + spa_pod_builder_int(b, codec->id); + spa_pod_builder_int(b, codec->id); + ++n; + } + } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { + FOR_EACH_HFP_CODEC(j) { + if (n == 0) + spa_pod_builder_int(b, get_hfp_codec_id(j)); + spa_pod_builder_int(b, get_hfp_codec_id(j)); + ++n; + } + } + if (n == 0) + choice->body.type = SPA_CHOICE_None; + spa_pod_builder_pop(b, &f[1]); + spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(b, &f[1]); + if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) { + FOR_EACH_MEDIA_CODEC(j, codec) { + spa_pod_builder_int(b, codec->id); + spa_pod_builder_string(b, codec->description); + } + } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { + FOR_EACH_HFP_CODEC(j) { + spa_pod_builder_int(b, get_hfp_codec_id(j)); + spa_pod_builder_string(b, get_hfp_codec_description(j)); + } + } + spa_pod_builder_pop(b, &f[1]); + return spa_pod_builder_pop(b, &f[0]); + +#undef FOR_EACH_MEDIA_CODEC +#undef FOR_EACH_HFP_CODEC +} + +static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b, uint32_t id) +{ + struct props *p = &this->props; + + return spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_bluetoothAudioCodec, SPA_POD_Id(p->codec), + SPA_PROP_bluetoothOffloadActive, SPA_POD_Bool(p->offload_active)); +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[2048]; + struct spa_result_device_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumProfile: + { + uint32_t profile; + enum spa_bluetooth_audio_codec codec; + + profile = get_profile_from_index(this, result.index, &result.next, &codec); + + switch (profile) { + case DEVICE_PROFILE_OFF: + case DEVICE_PROFILE_AG: + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_HSP_HFP: + param = build_profile(this, &b, id, result.index, profile, codec, false); + if (param == NULL) + goto next; + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Profile: + { + uint32_t index; + + switch (result.index) { + case 0: + index = get_index_from_profile(this, this->profile, this->props.codec); + param = build_profile(this, &b, id, index, this->profile, this->props.codec, true); + if (param == NULL) + return 0; + break; + default: + return 0; + } + break; + } + case SPA_PARAM_EnumRoute: + { + switch (result.index) { + case 0: case 1: case 2: case 3: + param = build_route(this, &b, id, result.index, SPA_ID_INVALID); + if (param == NULL) + goto next; + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Route: + { + switch (result.index) { + case 0: case 1: case 2: case 3: + param = build_route(this, &b, id, result.index, this->profile); + if (param == NULL) + goto next; + break; + default: + return 0; + } + break; + } + case SPA_PARAM_PropInfo: + { + switch (result.index) { + case 0: + param = build_prop_info_codec(this, &b, id); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_bluetoothOffloadActive), + SPA_PROP_INFO_description, SPA_POD_String("Bluetooth audio offload active"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(false)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + switch (result.index) { + case 0: + param = build_props(this, &b, id); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_device_emit_result(&this->hooks, seq, 0, + SPA_RESULT_TYPE_DEVICE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int node_set_volume(struct impl *this, struct node *node, float volumes[], uint32_t n_volumes) +{ + uint32_t i; + int changed = 0; + struct spa_bt_transport_volume *t_volume; + + if (n_volumes == 0) + return -EINVAL; + + spa_log_info(this->log, "node %p volume %f", node, volumes[0]); + + for (i = 0; i < node->n_channels; i++) { + if (node->volumes[i] == volumes[i % n_volumes]) + continue; + ++changed; + node->volumes[i] = volumes[i % n_volumes]; + } + + t_volume = node->transport ? &node->transport->volumes[node->id]: NULL; + + if (t_volume && t_volume->active + && spa_bt_transport_volume_enabled(node->transport)) { + float hw_volume = node_get_hw_volume(node); + spa_log_debug(this->log, "node %p hardware volume %f", node, hw_volume); + + node_update_soft_volumes(node, hw_volume); + spa_bt_transport_set_volume(node->transport, node->id, hw_volume); + } else { + float boost = get_soft_volume_boost(node); + for (uint32_t i = 0; i < node->n_channels; ++i) + node->soft_volumes[i] = node->volumes[i] * boost; + } + + emit_volume(this, node); + + return changed; +} + +static int node_set_mute(struct impl *this, struct node *node, bool mute) +{ + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + int changed = 0; + + spa_log_info(this->log, "node %p mute %d", node, mute); + + changed = (node->mute != mute); + node->mute = mute; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, node->id); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_mute, SPA_POD_Bool(mute), + SPA_PROP_softMute, SPA_POD_Bool(mute)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); + + return changed; +} + +static int node_set_latency_offset(struct impl *this, struct node *node, int64_t latency_offset) +{ + struct spa_event *event; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[1]; + int changed = 0; + + spa_log_info(this->log, "node %p latency offset %"PRIi64" nsec", node, latency_offset); + + changed = (node->latency_offset != latency_offset); + node->latency_offset = latency_offset; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], + SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0); + spa_pod_builder_int(&b, node->id); + spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0); + + spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(latency_offset)); + event = spa_pod_builder_pop(&b, &f[0]); + + spa_device_emit_event(&this->hooks, event); + + return changed; +} + +static int apply_device_props(struct impl *this, struct node *node, struct spa_pod *props) +{ + float volume = 0; + bool mute = 0; + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) props; + int changed = 0; + float volumes[SPA_AUDIO_MAX_CHANNELS]; + uint32_t channels[SPA_AUDIO_MAX_CHANNELS]; + uint32_t n_volumes = 0, SPA_UNUSED n_channels = 0; + int64_t latency_offset = 0; + + if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props)) + return -EINVAL; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + if (spa_pod_get_float(&prop->value, &volume) == 0) { + int res = node_set_volume(this, node, &volume, 1); + if (res > 0) + ++changed; + } + break; + case SPA_PROP_mute: + if (spa_pod_get_bool(&prop->value, &mute) == 0) { + int res = node_set_mute(this, node, mute); + if (res > 0) + ++changed; + } + break; + case SPA_PROP_channelVolumes: + n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, + volumes, SPA_AUDIO_MAX_CHANNELS); + break; + case SPA_PROP_channelMap: + n_channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, + channels, SPA_AUDIO_MAX_CHANNELS); + break; + case SPA_PROP_latencyOffsetNsec: + if (spa_pod_get_long(&prop->value, &latency_offset) == 0) { + int res = node_set_latency_offset(this, node, latency_offset); + if (res > 0) + ++changed; + } + } + } + if (n_volumes > 0) { + int res = node_set_volume(this, node, volumes, n_volumes); + if (res > 0) + ++changed; + } + + return changed; +} + +static void apply_prop_offload_active(struct impl *this, bool active) +{ + bool old_value = this->props.offload_active; + + this->props.offload_active = active; + + for (int i = 0; i < 2; i++) { + node_offload_set_active(&this->nodes[i], active); + if (!this->nodes[i].offload_acquired) + this->props.offload_active = false; + } + + if (this->props.offload_active != old_value) { + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_info(this, false); + } +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Profile: + { + uint32_t idx, next; + uint32_t profile; + enum spa_bluetooth_audio_codec codec; + bool save = false; + + if (param == NULL) + return -EINVAL; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx), + SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) { + spa_log_warn(this->log, "can't parse profile"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + + profile = get_profile_from_index(this, idx, &next, &codec); + if (profile == SPA_ID_INVALID) + return -EINVAL; + + spa_log_debug(this->log, "setting profile %d codec:%d save:%d", profile, codec, (int)save); + return set_profile(this, profile, codec, save); + } + case SPA_PARAM_Route: + { + uint32_t idx, device; + struct spa_pod *props = NULL; + struct node *node; + bool save = false; + + if (param == NULL) + return -EINVAL; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamRoute, NULL, + SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx), + SPA_PARAM_ROUTE_device, SPA_POD_Int(&device), + SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props), + SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) { + spa_log_warn(this->log, "can't parse route"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + if (device > 1 || !this->nodes[device].active) + return -EINVAL; + + node = &this->nodes[device]; + node->save = save; + if (props) { + int changed = apply_device_props(this, node, props); + if (changed > 0) { + this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; + } + emit_info(this, false); + } + break; + } + case SPA_PARAM_Props: + { + uint32_t codec_id = SPA_ID_INVALID; + bool offload_active = this->props.offload_active; + + if (param == NULL) + return 0; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_bluetoothAudioCodec, SPA_POD_OPT_Id(&codec_id), + SPA_PROP_bluetoothOffloadActive, SPA_POD_OPT_Bool(&offload_active))) < 0) { + spa_log_warn(this->log, "can't parse props"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + + spa_log_debug(this->log, "setting props codec:%d offload:%d", (int)codec_id, (int)offload_active); + + apply_prop_offload_active(this, offload_active); + + if (codec_id == SPA_ID_INVALID) + return 0; + + if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) { + size_t j; + for (j = 0; j < this->supported_codec_count; ++j) { + if (this->supported_codecs[j]->id == codec_id) { + return set_profile(this, this->profile, codec_id, true); + } + } + } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { + if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD && + spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_CVSD) == 1) { + return set_profile(this, this->profile, codec_id, true); + } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_MSBC && + spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_MSBC) == 1) { + return set_profile(this, this->profile, codec_id, true); + } + } + return -EINVAL; + } + default: + return -ENOENT; + } + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + const struct spa_dict_item *it; + + emit_remove_nodes(this); + + free(this->supported_codecs); + if (this->bt_dev) { + this->bt_dev->settings = NULL; + spa_hook_remove(&this->bt_dev_listener); + } + + spa_dict_for_each(it, &this->setting_dict) { + if(it->key) + free((void *)it->key); + if(it->value) + free((void *)it->value); + } + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static const struct spa_dict* +filter_bluez_device_setting(struct impl *this, const struct spa_dict *dict) +{ + uint32_t n_items = 0; + for (uint32_t i = 0 + ; i < dict->n_items && n_items < SPA_N_ELEMENTS(this->setting_items) + ; i++) + { + const struct spa_dict_item *it = &dict->items[i]; + if (it->key != NULL && strncmp(it->key, "bluez", 5) == 0 && it->value != NULL) { + this->setting_items[n_items++] = + SPA_DICT_ITEM_INIT(strdup(it->key), strdup(it->value)); + } + } + this->setting_dict = SPA_DICT_INIT(this->setting_items, n_items); + return &this->setting_dict; +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + _i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N); + + spa_log_topic_init(this->log, &log_topic); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_DEVICE))) + sscanf(str, "pointer:%p", &this->bt_dev); + + if (this->bt_dev == NULL) { + spa_log_error(this->log, "a device is needed"); + return -EINVAL; + } + + if (info) { + int profiles; + this->bt_dev->settings = filter_bluez_device_setting(this, info); + + if ((str = spa_dict_lookup(info, "bluez5.auto-connect")) != NULL) { + if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0) + this->bt_dev->reconnect_profiles = profiles; + } + + if ((str = spa_dict_lookup(info, "bluez5.hw-volume")) != NULL) { + if ((profiles = spa_bt_profiles_from_json_array(str)) >= 0) + this->bt_dev->hw_volume_profiles = profiles; + } + } + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + init_node(this, &this->nodes[0], 0); + init_node(this, &this->nodes[1], 1); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS | + SPA_DEVICE_CHANGE_MASK_PARAMS; + + this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE); + this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ); + this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 6; + + spa_bt_device_add_listener(this->bt_dev, &this->bt_dev_listener, &bt_dev_events, this); + + set_initial_profile(this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +static const struct spa_dict_item handle_info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { 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/codec-loader.c b/spa/plugins/bluez5/codec-loader.c new file mode 100644 index 0000000..f8363f8 --- /dev/null +++ b/spa/plugins/bluez5/codec-loader.c @@ -0,0 +1,234 @@ +/* Spa A2DP codec API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include + +#include "defs.h" +#include "codec-loader.h" + +#define MEDIA_CODEC_LIB_BASE "bluez5/libspa-codec-bluez5-" + +/* AVDTP allows 0x3E endpoints, can't have more codecs than that */ +#define MAX_CODECS 0x3E +#define MAX_HANDLES MAX_CODECS + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.codecs"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + const struct media_codec *codecs[MAX_CODECS + 1]; + struct spa_handle *handles[MAX_HANDLES]; + size_t n_codecs; + size_t n_handles; + struct spa_plugin_loader *loader; + struct spa_log *log; +}; + +static int codec_order(const struct media_codec *c) +{ + static const enum spa_bluetooth_audio_codec order[] = { + SPA_BLUETOOTH_AUDIO_CODEC_LC3, + SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, + SPA_BLUETOOTH_AUDIO_CODEC_APTX, + SPA_BLUETOOTH_AUDIO_CODEC_AAC, + SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, + SPA_BLUETOOTH_AUDIO_CODEC_MPEG, + SPA_BLUETOOTH_AUDIO_CODEC_SBC, + SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_51, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_71, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, + }; + size_t i; + for (i = 0; i < SPA_N_ELEMENTS(order); ++i) + if (c->id == order[i]) + return i; + return SPA_N_ELEMENTS(order); +} + +static int codec_order_cmp(const void *a, const void *b) +{ + const struct media_codec * const *ca = a; + const struct media_codec * const *cb = b; + int ia = codec_order(*ca); + int ib = codec_order(*cb); + if (*ca == *cb) + return 0; + return (ia == ib) ? (*ca < *cb ? -1 : 1) : ia - ib; +} + +static int load_media_codecs_from(struct impl *impl, const char *factory_name, const char *libname) +{ + struct spa_handle *handle = NULL; + void *iface; + const struct spa_bluez5_codec_a2dp *bluez5_codec_a2dp; + int n_codecs = 0; + int res; + size_t i; + struct spa_dict_item info_items[] = { + { SPA_KEY_LIBRARY_NAME, libname }, + }; + struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + + handle = spa_plugin_loader_load(impl->loader, factory_name, &info); + if (handle == NULL) { + spa_log_info(impl->log, "Bluetooth codec plugin %s not available", factory_name); + return -ENOENT; + } + + spa_log_debug(impl->log, "loading codecs from %s", factory_name); + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Bluez5CodecMedia, &iface)) < 0) { + spa_log_warn(impl->log, "Bluetooth codec plugin %s has no codec interface", + factory_name); + goto fail; + } + + bluez5_codec_a2dp = iface; + + if (bluez5_codec_a2dp->iface.version != SPA_VERSION_BLUEZ5_CODEC_MEDIA) { + spa_log_warn(impl->log, "codec plugin %s has incompatible ABI version (%d != %d)", + factory_name, bluez5_codec_a2dp->iface.version, SPA_VERSION_BLUEZ5_CODEC_MEDIA); + res = -ENOENT; + goto fail; + } + + for (i = 0; bluez5_codec_a2dp->codecs[i]; ++i) { + const struct media_codec *c = bluez5_codec_a2dp->codecs[i]; + const char *ep = c->endpoint_name ? c->endpoint_name : c->name; + size_t j; + + if (!ep) + goto next_codec; + + if (impl->n_codecs >= MAX_CODECS) { + spa_log_error(impl->log, "too many A2DP codecs"); + break; + } + + /* Don't load duplicate endpoints */ + for (j = 0; j < impl->n_codecs; ++j) { + const struct media_codec *c2 = impl->codecs[j]; + const char *ep2 = c2->endpoint_name ? c2->endpoint_name : c2->name; + if (spa_streq(ep, ep2) && c->fill_caps && c2->fill_caps) { + spa_log_debug(impl->log, "media codec %s from %s duplicate endpoint %s", + c->name, factory_name, ep); + goto next_codec; + } + } + + spa_log_debug(impl->log, "loaded media codec %s from %s, endpoint:%s", + c->name, factory_name, ep); + + if (c->set_log) + c->set_log(impl->log); + + impl->codecs[impl->n_codecs++] = c; + ++n_codecs; + + next_codec: + continue; + } + + if (n_codecs > 0) + impl->handles[impl->n_handles++] = handle; + else + spa_plugin_loader_unload(impl->loader, handle); + + return 0; + +fail: + if (handle) + spa_plugin_loader_unload(impl->loader, handle); + return res; +} + +const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log) +{ + struct impl *impl; + bool has_sbc; + size_t i; + const struct { const char *factory; const char *lib; } plugins[] = { +#define MEDIA_CODEC_FACTORY_LIB(basename) \ + { MEDIA_CODEC_FACTORY_NAME(basename), MEDIA_CODEC_LIB_BASE basename } + MEDIA_CODEC_FACTORY_LIB("aac"), + MEDIA_CODEC_FACTORY_LIB("aptx"), + MEDIA_CODEC_FACTORY_LIB("faststream"), + MEDIA_CODEC_FACTORY_LIB("ldac"), + MEDIA_CODEC_FACTORY_LIB("sbc"), + MEDIA_CODEC_FACTORY_LIB("lc3plus"), + MEDIA_CODEC_FACTORY_LIB("opus"), + MEDIA_CODEC_FACTORY_LIB("lc3") +#undef MEDIA_CODEC_FACTORY_LIB + }; + + impl = calloc(sizeof(struct impl), 1); + if (impl == NULL) + return NULL; + + impl->loader = loader; + impl->log = log; + + spa_log_topic_init(impl->log, &log_topic); + + for (i = 0; i < SPA_N_ELEMENTS(plugins); ++i) + load_media_codecs_from(impl, plugins[i].factory, plugins[i].lib); + + has_sbc = false; + for (i = 0; i < impl->n_codecs; ++i) + if (impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_SBC) + has_sbc = true; + + if (!has_sbc) { + spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins"); + free_media_codecs(impl->codecs); + errno = ENOENT; + return NULL; + } + + qsort(impl->codecs, impl->n_codecs, sizeof(const struct media_codec *), codec_order_cmp); + + return impl->codecs; +} + +void free_media_codecs(const struct media_codec * const *media_codecs) +{ + struct impl *impl = SPA_CONTAINER_OF(media_codecs, struct impl, codecs); + size_t i; + + for (i = 0; i < impl->n_handles; ++i) + spa_plugin_loader_unload(impl->loader, impl->handles[i]); + + free(impl); +} diff --git a/spa/plugins/bluez5/codec-loader.h b/spa/plugins/bluez5/codec-loader.h new file mode 100644 index 0000000..b77d9c4 --- /dev/null +++ b/spa/plugins/bluez5/codec-loader.h @@ -0,0 +1,39 @@ +/* Spa A2DP codec API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUEZ5_CODEC_LOADER_H_ +#define SPA_BLUEZ5_CODEC_LOADER_H_ + +#include +#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..6fbfb2a --- /dev/null +++ b/spa/plugins/bluez5/dbus-monitor.c @@ -0,0 +1,265 @@ +/* Spa midi dbus + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include + +#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), "g-properties-changed", + G_CALLBACK(on_g_properties_changed), + monitor, NULL); + g_object_set_data(G_OBJECT(iface), "dbus-monitor-signals-connected", NULL); + } + + on_remove(monitor, G_DBUS_PROXY(iface)); +} + +static void on_object_added(GDBusObjectManager *self, GDBusObject *object, + gpointer user_data) +{ + struct dbus_monitor *monitor = user_data; + GList *interfaces = g_dbus_object_get_interfaces(object); + + /* + * on_interface_added won't necessarily be called on objects on + * name owner changes, so we have to call it here for all interfaces. + */ + for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) { + on_interface_added(dbus_monitor_manager(monitor), + object, G_DBUS_INTERFACE(lli->data), monitor); + } + + g_list_free_full(interfaces, g_object_unref); +} + +static void on_object_removed(GDBusObjectManager *manager, GDBusObject *object, + gpointer user_data) +{ + struct dbus_monitor *monitor = user_data; + GList *interfaces = g_dbus_object_get_interfaces(object); + + for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) { + on_interface_removed(dbus_monitor_manager(monitor), + object, G_DBUS_INTERFACE(lli->data), monitor); + } + + g_list_free_full(interfaces, g_object_unref); +} + +static void on_notify(GObject *gobject, GParamSpec *pspec, gpointer user_data) +{ + struct dbus_monitor *monitor = user_data; + + if (spa_streq(pspec->name, "name-owner") && monitor->on_name_owner_change) + monitor->on_name_owner_change(monitor); +} + +static GType get_proxy_type(GDBusObjectManagerClient *manager, const gchar *object_path, + const gchar *interface_name, gpointer user_data) +{ + struct dbus_monitor *monitor = user_data; + const struct dbus_monitor_proxy_type *p; + + for (p = monitor->proxy_types; p && p->proxy_type != G_TYPE_INVALID; ++p) { + if (spa_streq(p->interface_name, interface_name)) + return p->proxy_type; + } + + return G_TYPE_DBUS_PROXY; +} + +static void init_done(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + struct dbus_monitor *monitor = user_data; + GError *error = NULL; + GList *objects; + GObject *ret; + + g_clear_object(&monitor->call); + + ret = g_async_initable_new_finish(G_ASYNC_INITABLE(source_object), res, &error); + if (!ret) { + spa_log_error(monitor->log, "%p: creating DBus object monitor failed: %s", + monitor, error->message); + g_error_free(error); + return; + } + monitor->manager = G_DBUS_OBJECT_MANAGER_CLIENT(ret); + + spa_log_debug(monitor->log, "%p: DBus monitor started", monitor); + + g_signal_connect(monitor->manager, "interface-added", + G_CALLBACK(on_interface_added), monitor); + g_signal_connect(monitor->manager, "interface-removed", + G_CALLBACK(on_interface_removed), monitor); + g_signal_connect(monitor->manager, "object-added", + G_CALLBACK(on_object_added), monitor); + g_signal_connect(monitor->manager, "object-removed", + G_CALLBACK(on_object_removed), monitor); + g_signal_connect(monitor->manager, "notify", + G_CALLBACK(on_notify), monitor); + + /* List all objects now */ + objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor)); + for (GList *llo = g_list_first(objects); llo; llo = llo->next) { + GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data)); + + for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) { + on_interface_added(dbus_monitor_manager(monitor), + G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data), + monitor); + } + g_list_free_full(interfaces, g_object_unref); + } + g_list_free_full(objects, g_object_unref); +} + +void dbus_monitor_init(struct dbus_monitor *monitor, + GType client_type, + struct spa_log *log, GDBusConnection *conn, + const char *name, const char *object_path, + const struct dbus_monitor_proxy_type *proxy_types, + void (*on_name_owner_change)(struct dbus_monitor *monitor)) +{ + GDBusObjectManagerClientFlags flags = G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START; + size_t i; + + spa_zero(*monitor); + + monitor->log = log; + monitor->call = g_cancellable_new(); + monitor->on_name_owner_change = on_name_owner_change; + + spa_zero(monitor->proxy_types); + + for (i = 0; proxy_types && proxy_types[i].proxy_type != G_TYPE_INVALID; ++i) { + spa_assert(i < DBUS_MONITOR_MAX_TYPES); + monitor->proxy_types[i] = proxy_types[i]; + } + + g_async_initable_new_async(client_type, G_PRIORITY_DEFAULT, + monitor->call, init_done, monitor, + "flags", flags, "name", name, "connection", conn, + "object-path", object_path, + "get-proxy-type-func", get_proxy_type, + "get-proxy-type-user-data", monitor, + NULL); +} + +void dbus_monitor_clear(struct dbus_monitor *monitor) +{ + g_cancellable_cancel(monitor->call); + g_clear_object(&monitor->call); + + if (monitor->manager) { + /* + * Indicate all objects should stop now. + * + * This has to be a separate hook, because the proxy finalizers + * may be called later asynchronously via e.g. DBus callbacks. + */ + GList *objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(monitor)); + for (GList *llo = g_list_first(objects); llo; llo = llo->next) { + GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data)); + for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) { + on_interface_removed(dbus_monitor_manager(monitor), + G_DBUS_OBJECT(llo->data), G_DBUS_INTERFACE(lli->data), + monitor); + } + g_list_free_full(interfaces, g_object_unref); + } + g_list_free_full(objects, g_object_unref); + } + + g_clear_object(&monitor->manager); + spa_zero(*monitor); +} diff --git a/spa/plugins/bluez5/dbus-monitor.h b/spa/plugins/bluez5/dbus-monitor.h new file mode 100644 index 0000000..f9fa12b --- /dev/null +++ b/spa/plugins/bluez5/dbus-monitor.h @@ -0,0 +1,83 @@ +/* Spa midi dbus + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef DBUS_MONITOR_H_ +#define DBUS_MONITOR_H_ + +#include + +#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..434f735 --- /dev/null +++ b/spa/plugins/bluez5/decode-buffer.h @@ -0,0 +1,486 @@ +/* Spa Bluez5 decode buffer + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/** + * \file decode-buffer.h Buffering for Bluetooth sources + * + * A linear buffer, which is compacted when it gets half full. + * + * Also contains buffering logic, which calculates a rate correction + * factor to maintain the buffer level at the target value. + * + * Consider typical packet intervals with nominal frame duration + * of 10ms: + * + * ... 5ms | 5ms | 20ms | 5ms | 5ms | 20ms ... + * + * ... 3ms | 3ms | 4ms | 30ms | 3ms | 3ms | 4ms | 30ms ... + * + * plus random jitter; 10ms nominal may occasionally have 20+ms interval. + * The regular timer cycle cannot be aligned with this, so process() + * may occur at any time. + * + * The buffer level is the difference between the number of samples in + * buffer immediately after receiving a packet, and the samples consumed + * before receiving the next packet. + * + * The buffer level indicates how much any packet can be delayed without + * underrun. If it is positive, there are no underruns. + * + * The rate correction aims to maintain the average level at a safety margin. + */ + +#ifndef SPA_BLUEZ5_DECODE_BUFFER_H +#define SPA_BLUEZ5_DECODE_BUFFER_H + +#include +#include +#include + +#define BUFFERING_LONG_MSEC (2*60000) +#define BUFFERING_SHORT_MSEC 1000 +#define BUFFERING_RATE_DIFF_MAX 0.005 + +/** + * Safety margin. + * + * The spike is the long-window maximum difference + * between minimum and average buffer level. + */ +#define BUFFERING_TARGET(spike,packet_size) \ + SPA_CLAMP((spike)*3/2, (packet_size), 6*(packet_size)) + +/** + * Rate controller. + * + * It's here in a form, where it operates on the running average + * so it's compatible with the level spike determination, and + * clamping the rate to a range is easy. The impulse response + * is similar to spa_dll, and step response does not have sign changes. + * + * The controller iterates as + * + * avg(j+1) = (1 - beta) avg(j) + beta level(j) + * corr(j+1) = corr(j) + a [avg(j+1) - avg(j)] / duration + * + b [avg(j) - target] / duration + * + * with beta = duration/avg_period < 0.5 is the moving average parameter, + * and a = beta/3 + ..., b = beta^2/27 + .... + * + * This choice results to c(j) being low-pass filtered, and buffer level(j) + * converging towards target with stable damped evolution with eigenvalues + * real and close to each other around (1 - beta)^(1/3). + * + * Derivation: + * + * The deviation from the buffer level target evolves as + * + * delta(j) = level(j) - target + * delta(j+1) = delta(j) + r(j) - c(j+1) + * + * where r is samples received in one duration, and c corrected rate + * (samples per duration). + * + * The rate correction is in general determined by linear filter f + * + * c(j+1) = c(j) + \sum_{k=0}^\infty delta(j - k) f(k) + * + * If \sum_k f(k) is not zero, the only fixed point is c=r, delta=0, + * so this structure (if the filter is stable) rate matches and + * drives buffer level to target. + * + * The z-transform then is + * + * delta(z) = G(z) r(z) + * c(z) = F(z) delta(z) + * G(z) = (z - 1) / [(z - 1)^2 + z f(z)] + * F(z) = f(z) / (z - 1) + * + * We now want: poles of G(z) must be in |z|<1 for stability, F(z) + * should damp high frequencies, and f(z) is causal. + * + * To satisfy the conditions, take + * + * (z - 1)^2 + z f(z) = p(z) / q(z) + * + * where p(z) is polynomial with leading term z^n with wanted root + * structure, and q(z) is any polynomial with leading term z^{n-2}. + * This guarantees f(z) is causal, and G(z) = (z-1) q(z) / p(z). + * We can choose p(z) and q(z) to improve low-pass properties of F(z). + * + * Simplest choice is p(z)=(z-x)^2 and q(z)=1, but that gives flat + * high frequency response in F(z). Better choice is p(z) = (z-u)*(z-v)*(z-w) + * and q(z) = z - r. To make F(z) better lowpass, one can cancel + * a resulting 1/z pole in F(z) by setting r=u*v*w. Then, + * + * G(z) = (z - u*v*w)*(z - 1) / [(z - u)*(z - v)*(z - w)] + * F(z) = (a z + b - a) / (z - 1) * H(z) + * H(z) = beta / (z - 1 + beta) + * beta = 1 - u*v*w + * a = [(1-u) + (1-v) + (1-w) - beta] / beta + * b = (1-u)*(1-v)*(1-w) / beta + * + * which corresponds to iteration for c(j): + * + * avg(j+1) = (1 - beta) avg(j) + beta delta(j) + * c(j+1) = c(j) + a [avg(j+1) - avg(j)] + b avg(j) + * + * So the controller operates on the running average, + * which gives the low-pass property for c(j). + * + * The simplest filter is obtained by putting the poles at + * u=v=w=(1-beta)**(1/3). Since beta << 1, computing the root + * can be avoided by expanding in series. + * + * Overshoot in impulse response could be reduced by moving one of the + * poles closer to z=1, but this increases the step response time. + */ +struct spa_bt_rate_control +{ + double avg; + double corr; +}; + +static void spa_bt_rate_control_init(struct spa_bt_rate_control *this, double level) +{ + this->avg = level; + this->corr = 1.0; +} + +static double spa_bt_rate_control_update(struct spa_bt_rate_control *this, double level, + double target, double duration, double period) +{ + /* + * u = (1 - beta)^(1/3) + * x = a / beta + * y = b / beta + * a = (2 + u) * (1 - u)^2 / beta + * b = (1 - u)^3 / beta + * beta -> 0 + */ + const double beta = SPA_CLAMP(duration / period, 0, 0.5); + const double x = 1.0/3; + const double y = beta/27; + double avg; + + avg = beta * level + (1 - beta) * this->avg; + this->corr += x * (avg - this->avg) / period + + y * (this->avg - target) / period; + this->avg = avg; + + this->corr = SPA_CLAMP(this->corr, + 1 - BUFFERING_RATE_DIFF_MAX, + 1 + BUFFERING_RATE_DIFF_MAX); + + return this->corr; +} + + +/** Windowed min/max */ +struct spa_bt_ptp +{ + union { + int32_t min; + int32_t mins[4]; + }; + union { + int32_t max; + int32_t maxs[4]; + }; + uint32_t pos; + uint32_t period; +}; + +struct spa_bt_decode_buffer +{ + struct spa_log *log; + + uint32_t frame_size; + uint32_t rate; + + uint8_t *buffer_decoded; + uint32_t buffer_size; + uint32_t buffer_reserve; + uint32_t write_index; + uint32_t read_index; + + struct spa_bt_ptp spike; /**< spikes (long window) */ + struct spa_bt_ptp packet_size; /**< packet size (short window) */ + + struct spa_bt_rate_control ctl; + double corr; + + uint32_t prev_consumed; + uint32_t prev_avail; + uint32_t prev_duration; + uint32_t underrun; + uint32_t pos; + + uint8_t received:1; + uint8_t buffering:1; +}; + +static void spa_bt_ptp_init(struct spa_bt_ptp *p, int32_t period) +{ + size_t i; + + spa_zero(*p); + for (i = 0; i < SPA_N_ELEMENTS(p->mins); ++i) { + p->mins[i] = INT32_MAX; + p->maxs[i] = INT32_MIN; + } + p->period = period; +} + +static void spa_bt_ptp_update(struct spa_bt_ptp *p, int32_t value, uint32_t duration) +{ + const size_t n = SPA_N_ELEMENTS(p->mins); + size_t i; + + for (i = 0; i < n; ++i) { + p->mins[i] = SPA_MIN(p->mins[i], value); + p->maxs[i] = SPA_MAX(p->maxs[i], value); + } + + p->pos += duration; + if (p->pos >= p->period / (n - 1)) { + p->pos = 0; + for (i = 1; i < SPA_N_ELEMENTS(p->mins); ++i) { + p->mins[i-1] = p->mins[i]; + p->maxs[i-1] = p->maxs[i]; + } + p->mins[n-1] = INT32_MAX; + p->maxs[n-1] = INT32_MIN; + } +} + +static int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, struct spa_log *log, + uint32_t frame_size, uint32_t rate, uint32_t quantum_limit, uint32_t reserve) +{ + spa_zero(*this); + this->frame_size = frame_size; + this->rate = rate; + this->log = log; + this->buffer_reserve = this->frame_size * reserve; + this->buffer_size = this->frame_size * quantum_limit * 2; + this->buffer_size += this->buffer_reserve; + this->corr = 1.0; + this->buffering = true; + + spa_bt_rate_control_init(&this->ctl, 0); + + spa_bt_ptp_init(&this->spike, (uint64_t)this->rate * BUFFERING_LONG_MSEC / 1000); + spa_bt_ptp_init(&this->packet_size, (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000); + + if ((this->buffer_decoded = malloc(this->buffer_size)) == NULL) { + this->buffer_size = 0; + return -ENOMEM; + } + return 0; +} + +static void spa_bt_decode_buffer_clear(struct spa_bt_decode_buffer *this) +{ + free(this->buffer_decoded); + spa_zero(*this); +} + +static void spa_bt_decode_buffer_compact(struct spa_bt_decode_buffer *this) +{ + uint32_t avail; + + spa_assert(this->read_index <= this->write_index); + + if (this->read_index == this->write_index) { + this->read_index = 0; + this->write_index = 0; + goto done; + } + + if (this->write_index > this->read_index + this->buffer_size - this->buffer_reserve) { + /* Drop data to keep buffer reserve free */ + spa_log_info(this->log, "%p buffer overrun: dropping data", this); + this->read_index = this->write_index + this->buffer_reserve - this->buffer_size; + } + + if (this->write_index < (this->buffer_size - this->buffer_reserve) / 2 + || this->read_index == 0) + goto done; + + avail = this->write_index - this->read_index; + spa_memmove(this->buffer_decoded, + SPA_PTROFF(this->buffer_decoded, this->read_index, void), + avail); + this->read_index = 0; + this->write_index = avail; + +done: + spa_assert(this->buffer_size - this->write_index >= this->buffer_reserve); +} + +static void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer *this, uint32_t *avail) +{ + spa_bt_decode_buffer_compact(this); + spa_assert(this->buffer_size >= this->write_index); + *avail = this->buffer_size - this->write_index; + return SPA_PTROFF(this->buffer_decoded, this->write_index, void); +} + +static void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size) +{ + spa_assert(size % this->frame_size == 0); + this->write_index += size; + this->received = true; + spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size); +} + +static void *spa_bt_decode_buffer_get_read(struct spa_bt_decode_buffer *this, uint32_t *avail) +{ + spa_assert(this->write_index >= this->read_index); + if (!this->buffering) + *avail = this->write_index - this->read_index; + else + *avail = 0; + return SPA_PTROFF(this->buffer_decoded, this->read_index, void); +} + +static void spa_bt_decode_buffer_read(struct spa_bt_decode_buffer *this, uint32_t size) +{ + spa_assert(size % this->frame_size == 0); + this->read_index += size; +} + +static void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this) +{ + int32_t size = (this->write_index - this->read_index) / this->frame_size; + int32_t level; + + this->prev_avail = size * this->frame_size; + this->prev_consumed = this->prev_duration; + + level = (int32_t)this->prev_avail/this->frame_size + - (int32_t)this->prev_duration; + this->corr = 1.0; + + spa_bt_rate_control_init(&this->ctl, level); +} + +static void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration) +{ + const uint32_t data_size = samples * this->frame_size; + const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8); + const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration); + uint32_t avail; + + if (SPA_UNLIKELY(duration != this->prev_duration)) { + this->prev_duration = duration; + spa_bt_decode_buffer_recover(this); + } + + if (SPA_UNLIKELY(this->buffering)) { + int32_t size = (this->write_index - this->read_index) / this->frame_size; + + this->corr = 1.0; + + spa_log_trace(this->log, "%p buffering size:%d", this, (int)size); + + if (this->received && + packet_size > 0 && + size >= SPA_MAX(3*packet_size, (int32_t)duration)) + this->buffering = false; + else + return; + + spa_bt_decode_buffer_recover(this); + } + + spa_bt_decode_buffer_get_read(this, &avail); + + if (this->received) { + const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000; + int32_t level, target; + + /* Track buffer level */ + level = (int32_t)(this->prev_avail/this->frame_size) - (int32_t)this->prev_consumed; + level = SPA_MAX(level, -max_level); + this->prev_consumed = SPA_MIN(this->prev_consumed, avg_period); + + spa_bt_ptp_update(&this->spike, this->ctl.avg - level, this->prev_consumed); + + /* Update target level */ + target = BUFFERING_TARGET(this->spike.max, packet_size); + + if (level > SPA_MAX(4 * target, 2*(int32_t)duration) && + avail > data_size) { + /* Lagging too much: drop data */ + uint32_t size = SPA_MIN(avail - data_size, + (level - target*5/2) * this->frame_size); + + spa_bt_decode_buffer_read(this, size); + spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d", + this, (int)size/this->frame_size, + (int)level, (int)target); + + spa_bt_decode_buffer_recover(this); + } + + this->pos += this->prev_consumed; + if (this->pos > this->rate) { + spa_log_debug(this->log, + "%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f", + this, + (int)this->ctl.avg, + (int)target, + (int)level, + (int)(avail / this->frame_size), + (int)this->spike.max, + (double)this->corr); + this->pos = 0; + } + + this->corr = spa_bt_rate_control_update(&this->ctl, + level, target, this->prev_consumed, avg_period); + + spa_bt_decode_buffer_get_read(this, &avail); + + this->prev_consumed = 0; + this->prev_avail = avail; + this->underrun = 0; + this->received = false; + } + + if (avail < data_size) { + spa_log_trace(this->log, "%p underrun samples:%d", this, + (data_size - avail) / this->frame_size); + this->underrun += samples; + if (this->underrun >= SPA_MIN((uint32_t)max_level, this->buffer_size / this->frame_size)) { + this->buffering = true; + spa_log_debug(this->log, "%p underrun too much: start buffering", this); + } + } + + this->prev_consumed += samples; +} + +#endif diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h new file mode 100644 index 0000000..5d194b3 --- /dev/null +++ b/spa/plugins/bluez5/defs.h @@ -0,0 +1,822 @@ +/* Spa Bluez5 Monitor + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUEZ5_DEFS_H +#define SPA_BLUEZ5_DEFS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#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_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 PROFILE_HSP_AG "/Profile/HSPAG" +#define PROFILE_HSP_HS "/Profile/HSPHS" +#define PROFILE_HFP_AG "/Profile/HFPAG" +#define PROFILE_HFP_HF "/Profile/HFPHF" + +#define HSP_HS_DEFAULT_CHANNEL 3 + +#define SOURCE_ID_BLUETOOTH 0x1 /* Bluetooth SIG */ +#define SOURCE_ID_USB 0x2 /* USB Implementer's Forum */ + +#define BUS_TYPE_USB 1 +#define BUS_TYPE_OTHER 255 + +#define HFP_AUDIO_CODEC_CVSD 0x01 +#define HFP_AUDIO_CODEC_MSBC 0x02 + +#define MEDIA_OBJECT_MANAGER_PATH "/MediaEndpoint" +#define A2DP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSink" +#define A2DP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/A2DPSource" + +#define BAP_SINK_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSink" +#define BAP_SOURCE_ENDPOINT MEDIA_OBJECT_MANAGER_PATH "/BAPSource" + +#define SPA_BT_UNKNOWN_DELAY 0 + +#define SPA_BT_NO_BATTERY ((uint8_t)255) + +/* HFP uses SBC encoding with precisely defined parameters. Hence, the size + * of the input (number of PCM samples) and output is known up front. */ +#define MSBC_DECODED_SIZE 240 +#define MSBC_ENCODED_SIZE 60 /* 2 bytes header + 57 mSBC payload + 1 byte padding */ +#define MSBC_PAYLOAD_SIZE 57 + +enum spa_bt_media_direction { + SPA_BT_MEDIA_SOURCE, + SPA_BT_MEDIA_SINK, +}; + +enum spa_bt_profile { + SPA_BT_PROFILE_NULL = 0, + SPA_BT_PROFILE_BAP_SINK = (1 << 0), + SPA_BT_PROFILE_BAP_SOURCE = (1 << 1), + SPA_BT_PROFILE_A2DP_SINK = (1 << 2), + SPA_BT_PROFILE_A2DP_SOURCE = (1 << 3), + SPA_BT_PROFILE_HSP_HS = (1 << 4), + SPA_BT_PROFILE_HSP_AG = (1 << 5), + SPA_BT_PROFILE_HFP_HF = (1 << 6), + SPA_BT_PROFILE_HFP_AG = (1 << 7), + + SPA_BT_PROFILE_A2DP_DUPLEX = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE), + SPA_BT_PROFILE_BAP_DUPLEX = (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE), + SPA_BT_PROFILE_HEADSET_HEAD_UNIT = (SPA_BT_PROFILE_HSP_HS | SPA_BT_PROFILE_HFP_HF), + SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY = (SPA_BT_PROFILE_HSP_AG | SPA_BT_PROFILE_HFP_AG), + SPA_BT_PROFILE_HEADSET_AUDIO = (SPA_BT_PROFILE_HEADSET_HEAD_UNIT | SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY), + + SPA_BT_PROFILE_MEDIA_SINK = (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK), + SPA_BT_PROFILE_MEDIA_SOURCE = (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE), +}; + +static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid) +{ + if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SOURCE) == 0) + return SPA_BT_PROFILE_A2DP_SOURCE; + else if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SINK) == 0) + return SPA_BT_PROFILE_A2DP_SINK; + else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS) == 0) + return SPA_BT_PROFILE_HSP_HS; + else if (strcasecmp(uuid, SPA_BT_UUID_HSP_HS_ALT) == 0) + return SPA_BT_PROFILE_HSP_HS; + else if (strcasecmp(uuid, SPA_BT_UUID_HSP_AG) == 0) + return SPA_BT_PROFILE_HSP_AG; + else if (strcasecmp(uuid, SPA_BT_UUID_HFP_HF) == 0) + return SPA_BT_PROFILE_HFP_HF; + else if (strcasecmp(uuid, SPA_BT_UUID_HFP_AG) == 0) + return SPA_BT_PROFILE_HFP_AG; + else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SINK) == 0) + return SPA_BT_PROFILE_BAP_SINK; + else if (strcasecmp(uuid, SPA_BT_UUID_BAP_SOURCE) == 0) + return SPA_BT_PROFILE_BAP_SOURCE; + else + return 0; +} +int spa_bt_profiles_from_json_array(const char *str); + +int spa_bt_format_vendor_product_id(uint16_t source_id, uint16_t vendor_id, + uint16_t product_id, char *vendor_str, int vendor_str_size, + char *product_str, int product_str_size); + +enum spa_bt_hfp_ag_feature { + SPA_BT_HFP_AG_FEATURE_NONE = (0), + SPA_BT_HFP_AG_FEATURE_3WAY = (1 << 0), + SPA_BT_HFP_AG_FEATURE_ECNR = (1 << 1), + SPA_BT_HFP_AG_FEATURE_VOICE_RECOG = (1 << 2), + SPA_BT_HFP_AG_FEATURE_IN_BAND_RING_TONE = (1 << 3), + SPA_BT_HFP_AG_FEATURE_ATTACH_VOICE_TAG = (1 << 4), + SPA_BT_HFP_AG_FEATURE_REJECT_CALL = (1 << 5), + SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS = (1 << 6), + SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_CONTROL = (1 << 7), + SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE = (1 << 8), + SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION = (1 << 9), + SPA_BT_HFP_AG_FEATURE_HF_INDICATORS = (1 << 10), + SPA_BT_HFP_AG_FEATURE_ESCO_S4 = (1 << 11), +}; + +enum spa_bt_hfp_sdp_ag_features { + SPA_BT_HFP_SDP_AG_FEATURE_NONE = (0), + SPA_BT_HFP_SDP_AG_FEATURE_3WAY = (1 << 0), + SPA_BT_HFP_SDP_AG_FEATURE_ECNR = (1 << 1), + SPA_BT_HFP_SDP_AG_FEATURE_VOICE_RECOG = (1 << 2), + SPA_BT_HFP_SDP_AG_FEATURE_IN_BAND_RING_TONE = (1 << 3), + SPA_BT_HFP_SDP_AG_FEATURE_ATTACH_VOICE_TAG = (1 << 4), + SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH = (1 << 5), +}; + +enum spa_bt_hfp_hf_feature { + SPA_BT_HFP_HF_FEATURE_NONE = (0), + SPA_BT_HFP_HF_FEATURE_ECNR = (1 << 0), + SPA_BT_HFP_HF_FEATURE_3WAY = (1 << 1), + SPA_BT_HFP_HF_FEATURE_CLIP = (1 << 2), + SPA_BT_HFP_HF_FEATURE_VOICE_RECOGNITION = (1 << 3), + SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL = (1 << 4), + SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS = (1 << 5), + SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_CONTROL = (1 << 6), + SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION = (1 << 7), + SPA_BT_HFP_HF_FEATURE_HF_INDICATORS = (1 << 8), + SPA_BT_HFP_HF_FEATURE_ESCO_S4 = (1 << 9), +}; + +/* https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Hands-Free%20Profile.pdf */ +enum spa_bt_hfp_hf_indicator { + SPA_BT_HFP_HF_INDICATOR_ENHANCED_SAFETY = 1, + SPA_BT_HFP_HF_INDICATOR_BATTERY_LEVEL = 2, +}; + +/* HFP Command AT+IPHONEACCEV + * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf */ +enum spa_bt_hfp_hf_iphoneaccev_keys { + SPA_BT_HFP_HF_IPHONEACCEV_KEY_BATTERY_LEVEL = 1, + SPA_BT_HFP_HF_IPHONEACCEV_KEY_DOCK_STATE = 2, +}; + +/* HFP Command AT+XAPL + * https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf + * Bits 0, 5 and above are reserved. */ +enum spa_bt_hfp_hf_xapl_features { + SPA_BT_HFP_HF_XAPL_FEATURE_BATTERY_REPORTING = (1 << 1), + SPA_BT_HFP_HF_XAPL_FEATURE_DOCKED_OR_POWERED = (1 << 2), + SPA_BT_HFP_HF_XAPL_FEATURE_SIRI_STATUS_REPORTING = (1 << 3), + SPA_BT_HFP_HF_XAPL_FEATURE_NOISE_REDUCTION_REPORTING = (1 << 4), +}; + +enum spa_bt_hfp_sdp_hf_features { + SPA_BT_HFP_SDP_HF_FEATURE_NONE = (0), + SPA_BT_HFP_SDP_HF_FEATURE_ECNR = (1 << 0), + SPA_BT_HFP_SDP_HF_FEATURE_3WAY = (1 << 1), + SPA_BT_HFP_SDP_HF_FEATURE_CLIP = (1 << 2), + SPA_BT_HFP_SDP_HF_FEATURE_VOICE_RECOGNITION = (1 << 3), + SPA_BT_HFP_SDP_HF_FEATURE_REMOTE_VOLUME_CONTROL = (1 << 4), + SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH = (1 << 5), +}; + +static inline const char *spa_bt_profile_name (enum spa_bt_profile profile) { + switch (profile) { + case SPA_BT_PROFILE_A2DP_SOURCE: + return "a2dp-source"; + case SPA_BT_PROFILE_A2DP_SINK: + return "a2dp-sink"; + case SPA_BT_PROFILE_A2DP_DUPLEX: + return "a2dp-duplex"; + case SPA_BT_PROFILE_HSP_HS: + case SPA_BT_PROFILE_HFP_HF: + case SPA_BT_PROFILE_HEADSET_HEAD_UNIT: + return "headset-head-unit"; + case SPA_BT_PROFILE_HSP_AG: + case SPA_BT_PROFILE_HFP_AG: + case SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY: + return "headset-audio-gateway"; + case SPA_BT_PROFILE_HEADSET_AUDIO: + return "headset-audio"; + case SPA_BT_PROFILE_BAP_SOURCE: + return "bap-source"; + case SPA_BT_PROFILE_BAP_SINK: + return "bap-sink"; + case SPA_BT_PROFILE_BAP_DUPLEX: + return "bap-duplex"; + default: + break; + } + return "unknown"; +} + +struct spa_bt_monitor; +struct spa_bt_backend; +struct spa_bt_player; + +struct spa_bt_adapter { + struct spa_list link; + struct spa_bt_monitor *monitor; + struct spa_bt_player *dummy_player; + char *path; + char *alias; + char *address; + char *name; + int bus_type; + uint16_t source_id; + uint16_t vendor_id; + uint16_t product_id; + uint16_t version_id; + uint32_t bluetooth_class; + uint32_t profiles; + int powered; + unsigned int has_msbc:1; + unsigned int msbc_probed:1; + unsigned int endpoints_registered:1; + unsigned int application_registered:1; + unsigned int player_registered:1; + unsigned int has_battery_provider:1; + unsigned int battery_provider_unavailable:1; +}; + +enum spa_bt_form_factor { + SPA_BT_FORM_FACTOR_UNKNOWN, + SPA_BT_FORM_FACTOR_HEADSET, + SPA_BT_FORM_FACTOR_HANDSFREE, + SPA_BT_FORM_FACTOR_MICROPHONE, + SPA_BT_FORM_FACTOR_SPEAKER, + SPA_BT_FORM_FACTOR_HEADPHONE, + SPA_BT_FORM_FACTOR_PORTABLE, + SPA_BT_FORM_FACTOR_CAR, + SPA_BT_FORM_FACTOR_HIFI, + SPA_BT_FORM_FACTOR_PHONE, +}; + +static inline const char *spa_bt_form_factor_name(enum spa_bt_form_factor ff) +{ + switch (ff) { + case SPA_BT_FORM_FACTOR_HEADSET: + return "headset"; + case SPA_BT_FORM_FACTOR_HANDSFREE: + return "hands-free"; + case SPA_BT_FORM_FACTOR_MICROPHONE: + return "microphone"; + case SPA_BT_FORM_FACTOR_SPEAKER: + return "speaker"; + case SPA_BT_FORM_FACTOR_HEADPHONE: + return "headphone"; + case SPA_BT_FORM_FACTOR_PORTABLE: + return "portable"; + case SPA_BT_FORM_FACTOR_CAR: + return "car"; + case SPA_BT_FORM_FACTOR_HIFI: + return "hifi"; + case SPA_BT_FORM_FACTOR_PHONE: + return "phone"; + case SPA_BT_FORM_FACTOR_UNKNOWN: + default: + return "unknown"; + } +} + +static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t bluetooth_class) +{ + uint32_t major, minor; + /* See Bluetooth Assigned Numbers: + * https://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm */ + major = (bluetooth_class >> 8) & 0x1F; + minor = (bluetooth_class >> 2) & 0x3F; + + switch (major) { + case 2: + return SPA_BT_FORM_FACTOR_PHONE; + case 4: + switch (minor) { + case 1: + return SPA_BT_FORM_FACTOR_HEADSET; + case 2: + return SPA_BT_FORM_FACTOR_HANDSFREE; + case 4: + return SPA_BT_FORM_FACTOR_MICROPHONE; + case 5: + return SPA_BT_FORM_FACTOR_SPEAKER; + case 6: + return SPA_BT_FORM_FACTOR_HEADPHONE; + case 7: + return SPA_BT_FORM_FACTOR_PORTABLE; + case 8: + return SPA_BT_FORM_FACTOR_CAR; + case 10: + return SPA_BT_FORM_FACTOR_HIFI; + } + } + return SPA_BT_FORM_FACTOR_UNKNOWN; +} + +struct spa_bt_media_codec_switch; +struct spa_bt_transport; + +struct spa_bt_device_events { +#define SPA_VERSION_BT_DEVICE_EVENTS 0 + uint32_t version; + + /** Device connection status */ + void (*connected) (void *data, bool connected); + + /** Codec switching completed */ + void (*codec_switched) (void *data, int status); + + /** Profile configuration changed */ + void (*profiles_changed) (void *data, uint32_t prev_profiles, uint32_t prev_connected); + + /** Device freed */ + void (*destroy) (void *data); +}; + +struct media_codec; + +struct spa_bt_device { + struct spa_list link; + struct spa_bt_monitor *monitor; + struct spa_bt_adapter *adapter; + uint32_t id; + char *path; + char *alias; + char *address; + char *adapter_path; + char *battery_path; + char *name; + char *icon; + uint16_t source_id; + uint16_t vendor_id; + uint16_t product_id; + uint16_t version_id; + uint32_t bluetooth_class; + uint16_t appearance; + uint16_t RSSI; + int paired; + int trusted; + int connected; + int blocked; + uint32_t profiles; + uint32_t connected_profiles; + uint32_t reconnect_profiles; + int reconnect_state; + struct spa_source timer; + struct spa_list remote_endpoint_list; + struct spa_list transport_list; + struct spa_list codec_switch_list; + uint8_t battery; + int has_battery; + + uint32_t hw_volume_profiles; + /* Even though A2DP volume is exposed on transport interface, the + * volume activation info would not be variate between transports + * under same device. So it's safe to cache activation info here. */ + bool a2dp_volume_active[2]; + + uint64_t last_bluez_action_time; + + struct spa_hook_list listener_list; + bool added; + + const struct spa_dict *settings; + + DBusPendingCall *battery_pending_call; + + const struct media_codec *preferred_codec; +}; + +struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path); +struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monitor, const char *remote_address, const char *local_address); +int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile); +int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile); +int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force); +int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs); +bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, bool sink); +const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count, bool sink); +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec); +int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec); +int spa_bt_device_release_transports(struct spa_bt_device *device); +int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage); +void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device); + +#define spa_bt_device_emit(d,m,v,...) spa_hook_list_call(&(d)->listener_list, \ + struct spa_bt_device_events, \ + m, v, ##__VA_ARGS__) +#define spa_bt_device_emit_connected(d,...) spa_bt_device_emit(d, connected, 0, __VA_ARGS__) +#define spa_bt_device_emit_codec_switched(d,...) spa_bt_device_emit(d, codec_switched, 0, __VA_ARGS__) +#define spa_bt_device_emit_profiles_changed(d,...) spa_bt_device_emit(d, profiles_changed, 0, __VA_ARGS__) +#define spa_bt_device_emit_destroy(d) spa_bt_device_emit(d, destroy, 0) +#define spa_bt_device_add_listener(d,listener,events,data) \ + spa_hook_list_append(&(d)->listener_list, listener, events, data) + +struct spa_bt_sco_io; + +struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop, int fd, uint16_t read_mtu, uint16_t write_mtu); +void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io); +void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *userdata, uint8_t *data, int size), void *userdata); +void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *userdata), void *userdata); +int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *data, int size); + +#define SPA_BT_VOLUME_ID_RX 0 +#define SPA_BT_VOLUME_ID_TX 1 +#define SPA_BT_VOLUME_ID_TERM 2 + +#define SPA_BT_VOLUME_INVALID -1 +#define SPA_BT_VOLUME_HS_MAX 15 +#define SPA_BT_VOLUME_A2DP_MAX 127 + +enum spa_bt_transport_state { + SPA_BT_TRANSPORT_STATE_IDLE, + SPA_BT_TRANSPORT_STATE_PENDING, + SPA_BT_TRANSPORT_STATE_ACTIVE, +}; + +struct spa_bt_transport_events { +#define SPA_VERSION_BT_TRANSPORT_EVENTS 0 + uint32_t version; + + void (*destroy) (void *data); + void (*delay_changed) (void *data); + void (*state_changed) (void *data, enum spa_bt_transport_state old, + enum spa_bt_transport_state state); + void (*volume_changed) (void *data); +}; + +struct spa_bt_transport_implementation { +#define SPA_VERSION_BT_TRANSPORT_IMPLEMENTATION 0 + uint32_t version; + + int (*acquire) (void *data, bool optional); + int (*release) (void *data); + int (*set_volume) (void *data, int id, float volume); + int (*destroy) (void *data); +}; + +struct spa_bt_transport_volume { + bool active; + float volume; + int hw_volume_max; + + /* XXX: items below should be put to user_data */ + int hw_volume; + int new_hw_volume; +}; + +struct spa_bt_transport { + struct spa_list link; + struct spa_bt_monitor *monitor; + struct spa_bt_backend *backend; + char *path; + struct spa_bt_device *device; + struct spa_list device_link; + enum spa_bt_profile profile; + enum spa_bt_transport_state state; + const struct media_codec *media_codec; + unsigned int codec; + void *configuration; + int configuration_len; + char *endpoint_path; + bool bap_initiator; + struct spa_list bap_transport_linked; + + uint32_t n_channels; + uint32_t channels[64]; + + struct spa_bt_transport_volume volumes[SPA_BT_VOLUME_ID_TERM]; + + int acquire_refcount; + bool acquired; + bool keepalive; + int fd; + uint16_t read_mtu; + uint16_t write_mtu; + uint16_t delay; + + struct spa_bt_sco_io *sco_io; + + struct spa_source volume_timer; + struct spa_source release_timer; + + struct spa_hook_list listener_list; + struct spa_callbacks impl; + + /* user_data must be the last item in the struct */ + void *user_data; +}; + +struct spa_bt_transport *spa_bt_transport_create(struct spa_bt_monitor *monitor, char *path, size_t extra); +void spa_bt_transport_free(struct spa_bt_transport *transport); +void spa_bt_transport_set_state(struct spa_bt_transport *transport, enum spa_bt_transport_state state); +struct spa_bt_transport *spa_bt_transport_find(struct spa_bt_monitor *monitor, const char *path); +struct spa_bt_transport *spa_bt_transport_find_full(struct spa_bt_monitor *monitor, + bool (*callback) (struct spa_bt_transport *t, const void *data), + const void *data); +int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *transport); +bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport); + +int spa_bt_transport_acquire(struct spa_bt_transport *t, bool optional); +int spa_bt_transport_release(struct spa_bt_transport *t); +int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive); +int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop); + +#define spa_bt_transport_emit(t,m,v,...) spa_hook_list_call(&(t)->listener_list, \ + struct spa_bt_transport_events, \ + m, v, ##__VA_ARGS__) +#define spa_bt_transport_emit_destroy(t) spa_bt_transport_emit(t, destroy, 0) +#define spa_bt_transport_emit_delay_changed(t) spa_bt_transport_emit(t, delay_changed, 0) +#define spa_bt_transport_emit_state_changed(t,...) spa_bt_transport_emit(t, state_changed, 0, __VA_ARGS__) +#define spa_bt_transport_emit_volume_changed(t) spa_bt_transport_emit(t, volume_changed, 0) + +#define spa_bt_transport_add_listener(t,listener,events,data) \ + spa_hook_list_append(&(t)->listener_list, listener, events, data) + +#define spa_bt_transport_set_implementation(t,_impl,_data) \ + (t)->impl = SPA_CALLBACKS_INIT(_impl, _data) + +#define spa_bt_transport_impl(t,m,v,...) \ +({ \ + int res = 0; \ + spa_callbacks_call_res(&(t)->impl, \ + struct spa_bt_transport_implementation, \ + res, m, v, ##__VA_ARGS__); \ + res; \ +}) + +#define spa_bt_transport_destroy(t) spa_bt_transport_impl(t, destroy, 0) +#define spa_bt_transport_set_volume(t,...) spa_bt_transport_impl(t, set_volume, 0, __VA_ARGS__) + +static inline enum spa_bt_transport_state spa_bt_transport_state_from_string(const char *value) +{ + if (strcasecmp("idle", value) == 0) + return SPA_BT_TRANSPORT_STATE_IDLE; + else if (strcasecmp("pending", value) == 0) + return SPA_BT_TRANSPORT_STATE_PENDING; + else if (strcasecmp("active", value) == 0) + return SPA_BT_TRANSPORT_STATE_ACTIVE; + else + return SPA_BT_TRANSPORT_STATE_IDLE; +} + +#define DEFAULT_AG_VOLUME 1.0f +#define DEFAULT_RX_VOLUME 1.0f +#define DEFAULT_TX_VOLUME 0.064f /* spa_bt_volume_hw_to_linear(40, 100) */ + +/* AVRCP/HSP volume is considered as percentage, so map it to pulseaudio (cubic) volume. */ +static inline uint32_t spa_bt_volume_linear_to_hw(double v, uint32_t hw_volume_max) +{ + if (v <= 0.0) + return 0; + if (v >= 1.0) + return hw_volume_max; + return SPA_CLAMP((uint64_t) lround(cbrt(v) * hw_volume_max), + 0u, hw_volume_max); +} + +static inline double spa_bt_volume_hw_to_linear(uint32_t v, uint32_t hw_volume_max) +{ + double f; + if (v <= 0) + return 0.0; + if (v >= hw_volume_max) + return 1.0; + f = ((double) v / hw_volume_max); + return f * f * f; +} + +enum spa_bt_feature { + SPA_BT_FEATURE_MSBC = (1 << 0), + SPA_BT_FEATURE_MSBC_ALT1 = (1 << 1), + SPA_BT_FEATURE_MSBC_ALT1_RTL = (1 << 2), + SPA_BT_FEATURE_HW_VOLUME = (1 << 3), + SPA_BT_FEATURE_HW_VOLUME_MIC = (1 << 4), + SPA_BT_FEATURE_SBC_XQ = (1 << 5), + SPA_BT_FEATURE_FASTSTREAM = (1 << 6), + SPA_BT_FEATURE_A2DP_DUPLEX = (1 << 7), +}; + +struct spa_bt_quirks; + +struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log); +int spa_bt_quirks_get_features(const struct spa_bt_quirks *quirks, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device, + uint32_t *features); +void spa_bt_quirks_destroy(struct spa_bt_quirks *quirks); + +int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter); + +struct spa_bt_backend_implementation { +#define SPA_VERSION_BT_BACKEND_IMPLEMENTATION 0 + uint32_t version; + + int (*free) (void *data); + int (*register_profiles) (void *data); + int (*unregister_profiles) (void *data); + int (*ensure_codec) (void *data, struct spa_bt_device *device, unsigned int codec); + int (*supports_codec) (void *data, struct spa_bt_device *device, unsigned int codec); +}; + +struct spa_bt_backend { + struct spa_callbacks impl; + const char *name; + bool available; + bool exclusive; +}; + +#define spa_bt_backend_set_implementation(b,_impl,_data) \ + (b)->impl = SPA_CALLBACKS_INIT(_impl, _data) + +#define spa_bt_backend_impl(b,m,v,...) \ +({ \ + int res = -ENOTSUP; \ + if (b) \ + spa_callbacks_call_res(&(b)->impl, \ + struct spa_bt_backend_implementation, \ + res, m, v, ##__VA_ARGS__); \ + res; \ +}) + +#define spa_bt_backend_free(b) spa_bt_backend_impl(b, free, 0) +#define spa_bt_backend_register_profiles(b) spa_bt_backend_impl(b, register_profiles, 0) +#define spa_bt_backend_unregister_profiles(b) spa_bt_backend_impl(b, unregister_profiles, 0) +#define spa_bt_backend_ensure_codec(b,...) spa_bt_backend_impl(b, ensure_codec, 0, __VA_ARGS__) +#define spa_bt_backend_supports_codec(b,...) spa_bt_backend_impl(b, supports_codec, 0, __VA_ARGS__) + +static inline struct spa_bt_backend *dummy_backend_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support) +{ + return NULL; +} + +#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE +struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support); +#else +#define backend_native_new dummy_backend_new +#endif + +#define OFONO_SERVICE "org.ofono" +#ifdef HAVE_BLUEZ_5_BACKEND_OFONO +struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support); +#else +#define backend_ofono_new dummy_backend_new +#endif + +#define HSPHFPD_SERVICE "org.hsphfpd" +#ifdef HAVE_BLUEZ_5_BACKEND_HSPHFPD +struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor, + void *dbus_connection, + const struct spa_dict *info, + const struct spa_bt_quirks *quirks, + const struct spa_support *support, + uint32_t n_support); +#else +#define backend_hsphfpd_new dummy_backend_new +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_BLUEZ5_DEFS_H */ diff --git a/spa/plugins/bluez5/hci.c b/spa/plugins/bluez5/hci.c new file mode 100644 index 0000000..88dc1fc --- /dev/null +++ b/spa/plugins/bluez5/hci.c @@ -0,0 +1,93 @@ +/* Spa HSP/HFP native backend HCI support + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 + +int spa_bt_adapter_has_msbc(struct spa_bt_adapter *adapter) +{ + int hci_id, res; + int sock = -1; + uint8_t features[8], max_page = 0; + struct sockaddr_hci a; + const char *str; + + if (adapter->msbc_probed) + return adapter->has_msbc; + + str = strrchr(adapter->path, '/'); /* hciXX */ + if (str == NULL || sscanf(str, "/hci%d", &hci_id) != 1 || hci_id < 0) + return -ENOENT; + + sock = socket(AF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC, BTPROTO_HCI); + if (sock < 0) + goto error; + + memset(&a, 0, sizeof(a)); + a.hci_family = AF_BLUETOOTH; + a.hci_dev = hci_id; + if (bind(sock, (struct sockaddr *) &a, sizeof(a)) < 0) + goto error; + + if (hci_read_local_ext_features(sock, 0, &max_page, features, 1000) < 0) + goto error; + + close(sock); + + adapter->msbc_probed = true; + adapter->has_msbc = ((features[2] & LMP_TRSP_SCO) && (features[3] & LMP_ESCO)) ? 1 : 0; + return adapter->has_msbc; + +error: + res = -errno; + if (sock >= 0) + close(sock); + return res; +} + +#endif diff --git a/spa/plugins/bluez5/media-codecs.c b/spa/plugins/bluez5/media-codecs.c new file mode 100644 index 0000000..28445fc --- /dev/null +++ b/spa/plugins/bluez5/media-codecs.c @@ -0,0 +1,212 @@ +/* + * BlueALSA - bluez-a2dp.c + * Copyright (c) 2016-2017 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#include + +#include "media-codecs.h" + +int media_codec_select_config(const struct media_codec_config configs[], size_t n, + uint32_t cap, int preferred_value) +{ + size_t i; + int *scores, res; + unsigned int max_priority; + + if (n == 0) + return -EINVAL; + + scores = calloc(n, sizeof(int)); + if (scores == NULL) + return -errno; + + max_priority = configs[0].priority; + for (i = 1; i < n; ++i) { + if (configs[i].priority > max_priority) + max_priority = configs[i].priority; + } + + for (i = 0; i < n; ++i) { + if (!(configs[i].config & cap)) { + scores[i] = -1; + continue; + } + if (configs[i].value == preferred_value) + scores[i] = 100 * (max_priority + 1); + else if (configs[i].value > preferred_value) + scores[i] = 10 * (max_priority + 1); + else + scores[i] = 1; + + scores[i] *= configs[i].priority + 1; + } + + res = 0; + for (i = 1; i < n; ++i) { + if (scores[i] > scores[res]) + res = i; + } + + if (scores[res] < 0) + res = -EINVAL; + + free(scores); + return res; +} + +bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *global_settings) +{ + uint8_t config[A2DP_MAX_CAPS_SIZE]; + int res; + + if (codec_id != codec->codec_id) + return false; + + if (caps == NULL) + return false; + + res = codec->select_config(codec, 0, caps, caps_size, info, global_settings, config); + if (res < 0) + return false; + + if (codec->bap) + return true; + else + return ((size_t)res == caps_size); +} + +#ifdef CODEC_PLUGIN + +struct impl { + struct spa_handle handle; + struct spa_bluez5_codec_a2dp bluez5_codec_a2dp; +}; + +static int +impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Bluez5CodecMedia)) + *interface = &this->bluez5_codec_a2dp; + else + return -ENOENT; + + return 0; +} + +static int +impl_clear(struct spa_handle *handle) +{ + spa_return_val_if_fail(handle != NULL, -EINVAL); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->bluez5_codec_a2dp.codecs = codec_plugin_media_codecs; + this->bluez5_codec_a2dp.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Bluez5CodecMedia, + SPA_VERSION_BLUEZ5_CODEC_MEDIA, + NULL, + this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Bluez5CodecMedia,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_dict_item handle_info_items[] = { + { SPA_KEY_FACTORY_DESCRIPTION, "Bluetooth codec plugin" }, +}; + +static const struct spa_dict handle_info = SPA_DICT_INIT_ARRAY(handle_info_items); + +static struct spa_handle_factory handle_factory = { + SPA_VERSION_HANDLE_FACTORY, + NULL, + &handle_info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (handle_factory.name == NULL) + handle_factory.name = codec_plugin_factory_name; + + switch (*index) { + case 0: + *factory = &handle_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +#endif diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h new file mode 100644 index 0000000..d3447db --- /dev/null +++ b/spa/plugins/bluez5/media-codecs.h @@ -0,0 +1,178 @@ +/* Spa A2DP codec API + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef SPA_BLUEZ5_A2DP_CODECS_H_ +#define SPA_BLUEZ5_A2DP_CODECS_H_ + +#include +#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 7 + +struct spa_bluez5_codec_a2dp { + struct spa_interface iface; + const struct media_codec * const *codecs; /**< NULL terminated array */ +}; + +#define MEDIA_CODEC_FACTORY_NAME(basename) (SPA_NAME_API_CODEC_BLUEZ5_MEDIA "." basename) + +#ifdef CODEC_PLUGIN +#define MEDIA_CODEC_EXPORT_DEF(basename,...) \ + const char *codec_plugin_factory_name = MEDIA_CODEC_FACTORY_NAME(basename); \ + static const struct media_codec * const codec_plugin_media_codec_list[] = { __VA_ARGS__, NULL }; \ + const struct media_codec * const * const codec_plugin_media_codecs = codec_plugin_media_codec_list; + +extern const struct media_codec * const * const codec_plugin_media_codecs; +extern const char *codec_plugin_factory_name; +#endif + +#define MEDIA_CODEC_FLAG_SINK (1 << 0) + +#define A2DP_CODEC_DEFAULT_RATE 48000 +#define A2DP_CODEC_DEFAULT_CHANNELS 2 + +enum { + NEED_FLUSH_NO = 0, + NEED_FLUSH_ALL = 1, + NEED_FLUSH_FRAGMENT = 2, +}; + +struct media_codec_audio_info { + uint32_t rate; + uint32_t channels; +}; + +struct media_codec { + enum spa_bluetooth_audio_codec id; + uint8_t codec_id; + a2dp_vendor_codec_t vendor; + + bool bap; + + const char *name; + const char *description; + const char *endpoint_name; /**< Endpoint name. If NULL, same as name */ + const struct spa_dict *info; + + const size_t send_buf_size; + + const struct media_codec *duplex_codec; /**< Codec for non-standard A2DP duplex channel */ + + struct spa_log *log; + + /** If fill_caps is NULL, no endpoint is registered (for sharing with another codec). */ + int (*fill_caps) (const struct media_codec *codec, uint32_t flags, + uint8_t caps[A2DP_MAX_CAPS_SIZE]); + + int (*select_config) (const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + const struct media_codec_audio_info *info, + const struct spa_dict *global_settings, uint8_t config[A2DP_MAX_CAPS_SIZE]); + int (*enum_config) (const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *builder, struct spa_pod **param); + int (*validate_config) (const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info); + int (*get_qos)(const struct media_codec *codec, + const void *config, size_t config_size, + const struct bap_endpoint_qos *endpoint_qos, + struct bap_codec_qos *qos); + + /** qsort comparison sorting caps in order of preference for the codec. + * Used in codec switching to select best remote endpoints. + * The caps handed in correspond to this codec_id, but are + * otherwise not checked beforehand. + */ + int (*caps_preference_cmp) (const struct media_codec *codec, uint32_t flags, const void *caps1, size_t caps1_size, + const void *caps2, size_t caps2_size, const struct media_codec_audio_info *info, + const struct spa_dict *global_settings); + + void *(*init_props) (const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings); + void (*clear_props) (void *); + int (*enum_props) (void *props, const struct spa_dict *settings, uint32_t id, uint32_t idx, + struct spa_pod_builder *builder, struct spa_pod **param); + int (*set_props) (void *props, const struct spa_pod *param); + + void *(*init) (const struct media_codec *codec, uint32_t flags, void *config, size_t config_size, + const struct spa_audio_info *info, void *props, size_t mtu); + void (*deinit) (void *data); + + int (*update_props) (void *data, void *props); + + int (*get_block_size) (void *data); + + int (*abr_process) (void *data, size_t unsent); + + int (*start_encode) (void *data, + void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp); + int (*encode) (void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush); + + int (*start_decode) (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp); + int (*decode) (void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out); + + int (*reduce_bitpool) (void *data); + int (*increase_bitpool) (void *data); + + void (*set_log) (struct spa_log *global_log); +}; + +struct media_codec_config { + uint32_t config; + int value; + unsigned int priority; +}; + +int media_codec_select_config(const struct media_codec_config configs[], size_t n, + uint32_t cap, int preferred_value); + +bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_id, + const void *caps, size_t caps_size, const struct media_codec_audio_info *info, + const struct spa_dict *global_settings); + +#endif diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c new file mode 100644 index 0000000..29eec1f --- /dev/null +++ b/spa/plugins/bluez5/media-sink.c @@ -0,0 +1,1868 @@ +/* Spa Media Sink + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.media"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + int64_t latency_offset; + char clock_name[64]; +}; + +#define FILL_FRAMES 4 +#define MIN_BUFFERS 2 +#define MAX_BUFFERS 32 +#define BUFFER_SIZE (8192*8) + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + struct spa_audio_info current_format; + uint32_t frame_size; + unsigned int have_format:1; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_latency_info latency; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list free; + struct spa_list ready; + + size_t ready_offset; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint32_t quantum_limit; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + + struct port port; + + unsigned int started:1; + unsigned int following:1; + unsigned int is_output:1; + unsigned int flush_pending:1; + + unsigned int is_duplex:1; + + struct spa_source source; + int timerfd; + struct spa_source flush_source; + struct spa_source flush_timer_source; + int flush_timerfd; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint64_t current_time; + uint64_t next_time; + uint64_t last_error; + uint64_t process_time; + + uint64_t prev_flush_time; + uint64_t next_flush_time; + + const struct media_codec *codec; + bool codec_props_changed; + void *codec_props; + void *codec_data; + struct spa_audio_info codec_format; + + int need_flush; + bool fragment; + uint32_t block_size; + uint8_t buffer[BUFFER_SIZE]; + uint32_t buffer_used; + uint32_t header_size; + uint32_t block_count; + uint16_t seqnum; + uint32_t timestamp; + uint64_t sample_count; + uint8_t tmp_buffer[BUFFER_SIZE]; + uint32_t tmp_buffer_used; + uint32_t fd_buffer_size; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0) + +static void reset_props(struct impl *this, struct props *props) +{ + props->latency_offset = 0; + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0, index_offset = 0; + bool enum_codec = false; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec), + SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX)); + break; + default: + enum_codec = true; + index_offset = 1; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset)); + break; + default: + enum_codec = true; + index_offset = 1; + } + break; + } + default: + return -ENOENT; + } + + if (enum_codec) { + int res; + if (this->codec->enum_props == NULL || this->codec_props == NULL || + this->transport == NULL) + return 0; + else if ((res = this->codec->enum_props(this->codec_props, + this->transport->device->settings, + id, result.index - index_offset, &b, ¶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; + set_timers(this); + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + bool following; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + + following = is_following(this); + if (this->started && following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full); + +static void emit_port_info(struct impl *this, struct port *port, bool full); + +static void set_latency(struct impl *this, bool emit_latency) +{ + struct port *port = &this->port; + int64_t delay; + + if (this->transport == NULL) + return; + + delay = spa_bt_transport_get_delay_nsec(this->transport); + delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2); + port->latency.min_ns = port->latency.max_ns = delay; + + if (emit_latency) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + } +} + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct props new_props = this->props; + int changed = 0; + + if (param == NULL) { + reset_props(this, &new_props); + } else { + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset)); + } + + changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); + this->props = new_props; + + if (changed) + set_latency(this, true); + + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + int res, codec_res = 0; + res = apply_props(this, param); + if (this->codec_props && this->codec->set_props) { + codec_res = this->codec->set_props(this->codec_props, param); + if (codec_res > 0) + this->codec_props_changed = true; + } + if (res > 0 || codec_res > 0) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, false); + } + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int reset_buffer(struct impl *this) +{ + if (this->codec_props_changed && this->codec_props + && this->codec->update_props) { + this->codec->update_props(this->codec_data, this->codec_props); + this->codec_props_changed = false; + } + this->need_flush = 0; + this->block_count = 0; + this->fragment = false; + this->buffer_used = this->codec->start_encode(this->codec_data, + this->buffer, sizeof(this->buffer), + this->seqnum++, this->timestamp); + this->header_size = this->buffer_used; + this->timestamp = this->sample_count; + return 0; +} + +static int get_transport_unused_size(struct impl *this) +{ + int res, value; + res = ioctl(this->flush_source.fd, TIOCOUTQ, &value); + if (res < 0) { + spa_log_error(this->log, "%p: ioctl fail: %m", this); + return -errno; + } + spa_log_trace(this->log, "%p: fd unused buffer size:%d/%d", this, value, this->fd_buffer_size); + return value; +} + +static int send_buffer(struct impl *this) +{ + int written, unsent; + + unsent = get_transport_unused_size(this); + if (unsent >= 0) { + unsent = this->fd_buffer_size - unsent; + this->codec->abr_process(this->codec_data, unsent); + } + + written = send(this->flush_source.fd, this->buffer, + this->buffer_used, MSG_DONTWAIT | MSG_NOSIGNAL); + + if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { + struct timespec ts; + uint64_t now; + uint64_t dt; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts); + now = SPA_TIMESPEC_TO_NSEC(&ts); + dt = now - this->prev_flush_time; + this->prev_flush_time = now; + + spa_log_trace(this->log, + "%p: send blocks:%d block:%u seq:%u ts:%u size:%u " + "wrote:%d dt:%"PRIu64, + this, this->block_count, this->block_size, this->seqnum, + this->timestamp, this->buffer_used, written, dt); + } + + if (written < 0) { + spa_log_debug(this->log, "%p: %m", this); + return -errno; + } + + return written; +} + +static int encode_buffer(struct impl *this, const void *data, uint32_t size) +{ + int processed; + size_t out_encoded; + struct port *port = &this->port; + const void *from_data = data; + int from_size = size; + + spa_log_trace(this->log, "%p: encode %d used %d, %d %d %d", + this, size, this->buffer_used, port->frame_size, this->block_size, + this->block_count); + + if (this->need_flush) + return 0; + + if (this->buffer_used >= sizeof(this->buffer)) + return -ENOSPC; + + if (size < this->block_size - this->tmp_buffer_used) { + memcpy(this->tmp_buffer + this->tmp_buffer_used, data, size); + this->tmp_buffer_used += size; + return size; + } else if (this->tmp_buffer_used > 0) { + memcpy(this->tmp_buffer + this->tmp_buffer_used, data, this->block_size - this->tmp_buffer_used); + from_data = this->tmp_buffer; + from_size = this->block_size; + this->tmp_buffer_used = this->block_size - this->tmp_buffer_used; + } + + processed = this->codec->encode(this->codec_data, + from_data, from_size, + this->buffer + this->buffer_used, + sizeof(this->buffer) - this->buffer_used, + &out_encoded, &this->need_flush); + if (processed < 0) + return processed; + + this->sample_count += processed / port->frame_size; + this->block_count += processed / this->block_size; + this->buffer_used += out_encoded; + + spa_log_trace(this->log, "%p: processed %d %zd used %d", + this, processed, out_encoded, this->buffer_used); + + if (this->tmp_buffer_used) { + processed = this->tmp_buffer_used; + this->tmp_buffer_used = 0; + } + return processed; +} + +static int encode_fragment(struct impl *this) +{ + int res; + size_t out_encoded; + struct port *port = &this->port; + + spa_log_trace(this->log, "%p: encode fragment used %d, %d %d %d", + this, this->buffer_used, port->frame_size, this->block_size, + this->block_count); + + if (this->need_flush) + return 0; + + res = this->codec->encode(this->codec_data, + NULL, 0, + this->buffer + this->buffer_used, + sizeof(this->buffer) - this->buffer_used, + &out_encoded, &this->need_flush); + if (res < 0) + return res; + if (res != 0) + return -EINVAL; + + this->buffer_used += out_encoded; + + spa_log_trace(this->log, "%p: processed fragment %zd used %d", + this, out_encoded, this->buffer_used); + + return 0; +} + +static int flush_buffer(struct impl *this) +{ + spa_log_trace(this->log, "%p: used:%d block_size:%d", this, + this->buffer_used, this->block_size); + + if (this->need_flush) + return send_buffer(this); + + return 0; +} + +static int add_data(struct impl *this, const void *data, uint32_t size) +{ + int processed, total = 0; + + while (size > 0) { + processed = encode_buffer(this, data, size); + + if (processed <= 0) + return total > 0 ? total : processed; + + data = SPA_PTROFF(data, processed, void); + size -= processed; + total += processed; + } + return total; +} + +static void enable_flush_timer(struct impl *this, bool enabled) +{ + struct itimerspec ts; + + if (!enabled) + this->next_flush_time = 0; + + ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, + this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); + + this->flush_pending = enabled; +} + +static uint32_t get_queued_frames(struct impl *this) +{ + struct port *port = &this->port; + uint32_t bytes = 0; + struct buffer *b; + + spa_list_for_each(b, &port->ready, link) { + struct spa_data *d = b->buf->datas; + + bytes += d[0].chunk->size; + } + + if (bytes > port->ready_offset) + bytes -= port->ready_offset; + else + bytes = 0; + + return bytes / port->frame_size; +} + +static int flush_data(struct impl *this, uint64_t now_time) +{ + int written; + uint32_t total_frames; + struct port *port = &this->port; + int unused_buffer; + + if (!this->flush_source.loop) { + /* I/O in error state */ + return -EIO; + } + + total_frames = 0; +again: + written = 0; + if (this->fragment && !this->need_flush) { + int res; + this->fragment = false; + if ((res = encode_fragment(this)) < 0) { + /* Error */ + reset_buffer(this); + return res; + } + } + while (!spa_list_is_empty(&port->ready) && !this->need_flush) { + uint8_t *src; + uint32_t n_bytes, n_frames; + struct buffer *b; + struct spa_data *d; + uint32_t index, offs, avail, l0, l1; + + b = spa_list_first(&port->ready, struct buffer, link); + d = b->buf->datas; + + src = d[0].data; + + index = d[0].chunk->offset + port->ready_offset; + avail = d[0].chunk->size - port->ready_offset; + avail /= port->frame_size; + + offs = index % d[0].maxsize; + n_frames = avail; + n_bytes = n_frames * port->frame_size; + + l0 = SPA_MIN(n_bytes, d[0].maxsize - offs); + l1 = n_bytes - l0; + + written = add_data(this, src + offs, l0); + if (written > 0 && l1 > 0) + written += add_data(this, src, l1); + if (written <= 0) { + if (written < 0 && written != -ENOSPC) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + this->port.io->buffer_id = b->id; + spa_log_warn(this->log, "%p: error %s, reuse buffer %u", + this, spa_strerror(written), b->id); + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + port->ready_offset = 0; + } + break; + } + + n_frames = written / port->frame_size; + + port->ready_offset += written; + + if (port->ready_offset >= d[0].chunk->size) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + spa_log_trace(this->log, "%p: reuse buffer %u", this, b->id); + this->port.io->buffer_id = b->id; + + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + port->ready_offset = 0; + } + total_frames += n_frames; + + spa_log_trace(this->log, "%p: written %u frames", this, total_frames); + } + + if (written > 0 && this->buffer_used == this->header_size) { + enable_flush_timer(this, false); + return 0; + } + + if (this->flush_pending) { + spa_log_trace(this->log, "%p: wait for flush timer", this); + return 0; + } + + /* + * Get socket queue size before writing to it. + * This should be the same as buffer size to increase bitpool + * Bitpool shouldn't be increased when data is left over in the buffer + */ + unused_buffer = get_transport_unused_size(this); + + written = flush_buffer(this); + + if (written == -EAGAIN) { + spa_log_trace(this->log, "%p: fail flush", this); + if (now_time - this->last_error > SPA_NSEC_PER_SEC / 2) { + int res = this->codec->reduce_bitpool(this->codec_data); + + spa_log_debug(this->log, "%p: reduce bitpool: %i", this, res); + this->last_error = now_time; + } + + /* + * The socket buffer is full, and the device is not processing data + * fast enough, so should just skip this packet. There will be a sound + * glitch in any case. + */ + written = this->buffer_used; + } + + if (written < 0) { + spa_log_trace(this->log, "%p: error flushing %s", this, + spa_strerror(written)); + reset_buffer(this); + enable_flush_timer(this, false); + return written; + } + else if (written > 0) { + /* + * We cannot write all data we have at once, since this can exceed device + * buffers (esp. for the A2DP low-latency codecs) and socket buffers, so + * flush needs to be delayed. + */ + uint32_t packet_samples = this->block_count * this->block_size + / port->frame_size; + uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC + / port->current_format.info.raw.rate; + + if (SPA_LIKELY(this->position)) { + uint32_t frames = get_queued_frames(this); + uint64_t duration_ns; + + /* + * Flush at the time position of the next buffered sample. + */ + duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC + / this->position->clock.rate.denom); + this->next_flush_time = this->process_time + duration_ns + - ((uint64_t)frames * SPA_NSEC_PER_SEC + / port->current_format.info.raw.rate); + + /* + * We could delay the output by one packet to avoid waiting + * for the next buffer and so make send intervals exactly regular. + * However, this is not needed for A2DP or BAP. The controller + * will do the scheduling for us, and there's also the socket buffer + * in between. + */ +#if 0 + this->next_flush_time += SPA_MIN(packet_time, + duration_ns * (port->n_buffers - 1)); +#endif + } else { + if (this->next_flush_time == 0) + this->next_flush_time = this->process_time; + this->next_flush_time += packet_time; + } + + if (this->need_flush == NEED_FLUSH_FRAGMENT) { + reset_buffer(this); + this->fragment = true; + goto again; + } + + if (now_time - this->last_error > SPA_NSEC_PER_SEC) { + if (unused_buffer == (int)this->fd_buffer_size) { + int res = this->codec->increase_bitpool(this->codec_data); + + spa_log_debug(this->log, "%p: increase bitpool: %i", this, res); + } + this->last_error = now_time; + } + + spa_log_trace(this->log, "%p: flush at:%"PRIu64" process:%"PRIu64, this, + this->next_flush_time, this->process_time); + reset_buffer(this); + enable_flush_timer(this, true); + } + else { + /* Don't want to flush yet, or failed to write anything */ + spa_log_trace(this->log, "%p: skip flush", this); + enable_flush_timer(this, false); + } + return 0; +} + +static void media_on_flush_error(struct spa_source *source) +{ + struct impl *this = source->data; + + spa_log_trace(this->log, "%p: flush event", this); + + if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) { + spa_log_warn(this->log, "%p: error %d", this, source->rmask); + if (this->flush_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_source); + return; + } +} + +static void media_on_flush_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t exp; + int res; + + spa_log_trace(this->log, "%p: flush on timeout", this); + + if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); + return; + } + + if (this->transport == NULL) { + enable_flush_timer(this, false); + return; + } + + while (exp-- > 0) { + this->flush_pending = false; + flush_data(this, this->current_time); + } +} + +static void media_on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + uint64_t exp, duration; + uint32_t rate; + struct spa_io_buffers *io = port->io; + uint64_t prev_time, now_time; + int res; + + if (this->transport == NULL) + return; + + if (this->started) { + if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", + spa_strerror(res)); + return; + } + } + + prev_time = this->current_time; + now_time = this->current_time = this->next_time; + + spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, + now_time, now_time - prev_time); + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate; + + if (SPA_LIKELY(this->clock)) { + int64_t delay_nsec; + + this->clock->nsec = now_time; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->rate_diff = 1.0f; + this->clock->next_nsec = this->next_time; + + delay_nsec = spa_bt_transport_get_delay_nsec(this->transport); + + /* Negative delay doesn't work properly, so disallow it */ + delay_nsec += SPA_CLAMP(this->props.latency_offset, -delay_nsec, INT64_MAX / 2); + + this->clock->delay = (delay_nsec * this->clock->rate.denom) / SPA_NSEC_PER_SEC; + } + + + spa_log_trace(this->log, "%p: %d", this, io->status); + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); + + set_timeout(this, this->next_time); +} + +static int do_start(struct impl *this) +{ + int res, val, size; + struct port *port; + socklen_t len; + uint8_t *conf; + uint32_t flags; + + if (this->started) + return 0; + + spa_return_val_if_fail(this->transport, -EIO); + + this->following = is_following(this); + + spa_log_debug(this->log, "%p: start following:%d", this, this->following); + + if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) + return res; + + port = &this->port; + + conf = this->transport->configuration; + size = this->transport->configuration_len; + + spa_log_debug(this->log, "Transport configuration:"); + spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 2, conf, (size_t)size); + + flags = this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0; + + this->codec_data = this->codec->init(this->codec, + flags, + this->transport->configuration, + this->transport->configuration_len, + &port->current_format, + this->codec_props, + this->transport->write_mtu); + if (this->codec_data == NULL) + return -EIO; + + spa_log_info(this->log, "%p: using %s codec %s, delay:%"PRIi64" ms", this, + this->codec->bap ? "BAP" : "A2DP", this->codec->description, + (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC)); + + this->seqnum = 0; + + this->block_size = this->codec->get_block_size(this->codec_data); + if (this->block_size > sizeof(this->tmp_buffer)) { + spa_log_error(this->log, "block-size %d > %zu", + this->block_size, sizeof(this->tmp_buffer)); + return -EIO; + } + + spa_log_debug(this->log, "%p: block_size %d", this, this->block_size); + + val = this->codec->send_buf_size > 0 + /* The kernel doubles the SO_SNDBUF option value set by setsockopt(). */ + ? this->codec->send_buf_size / 2 + this->codec->send_buf_size % 2 + : FILL_FRAMES * this->transport->write_mtu; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "%p: SO_SNDBUF %m", this); + + len = sizeof(val); + if (getsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, &len) < 0) { + spa_log_warn(this->log, "%p: SO_SNDBUF %m", this); + } + else { + spa_log_debug(this->log, "%p: SO_SNDBUF: %d", this, val); + } + this->fd_buffer_size = val; + + val = FILL_FRAMES * this->transport->read_mtu; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "%p: SO_RCVBUF %m", this); + + val = 6; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "SO_PRIORITY failed: %m"); + + reset_buffer(this); + + this->source.data = this; + this->source.fd = this->timerfd; + this->source.func = media_on_timeout; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->source); + + this->flush_timer_source.data = this; + this->flush_timer_source.fd = this->flush_timerfd; + this->flush_timer_source.func = media_on_flush_timeout; + this->flush_timer_source.mask = SPA_IO_IN; + this->flush_timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->flush_timer_source); + + this->flush_source.data = this; + this->flush_source.fd = this->transport->fd; + this->flush_source.func = media_on_flush_error; + this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP; + this->flush_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->flush_source); + + this->flush_pending = false; + + set_timers(this); + this->started = true; + + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct itimerspec ts; + + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL); + + if (this->flush_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_source); + + if (this->flush_timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL); + + return 0; +} + +static int do_stop(struct impl *this) +{ + int res = 0; + + if (!this->started) + return 0; + + spa_log_trace(this->log, "%p: stop", this); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + this->started = false; + + if (this->transport) + res = spa_bt_transport_release(this->transport); + + if (this->codec_data) + this->codec->deinit(this->codec_data); + this->codec_data = NULL; + + return res; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if ((res = do_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if ((res = do_stop(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_output ? "Audio/Sink" : "Stream/Input/Audio" }, + { "media.name", ((this->transport && this->transport->device->name) ? + this->transport->device->name : this->codec->bap ? "BAP" : "A2DP" ) }, + { SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" }, + }; + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (this->codec == NULL) + return -EIO; + if (this->transport == NULL) + return -EIO; + + if ((res = this->codec->enum_config(this->codec, + this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0, + this->transport->configuration, + this->transport->configuration_len, + id, result.index, &b, ¶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; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: + param = spa_latency_build(&b, id, &port->latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + do_stop(this); + if (port->n_buffers > 0) { + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int err; + + if (format == NULL) { + spa_log_debug(this->log, "clear format"); + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + port->frame_size = info.info.raw.channels; + switch (info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16: + port->frame_size *= 2; + break; + case SPA_AUDIO_FORMAT_S24: + port->frame_size *= 3; + break; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_F32: + port->frame_size *= 4; + break; + default: + return -EINVAL; + } + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + res = 0; + break; + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + spa_log_debug(this->log, "use buffers %d", n_buffers); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + + b->buf = buffers[i]; + b->id = i; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (buffers[i]->datas[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + + if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) { + struct buffer *b = &port->buffers[io->buffer_id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + + spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id); + + spa_list_append(&port->ready, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + + io->buffer_id = SPA_ID_INVALID; + io->status = SPA_STATUS_OK; + } + + if (this->following) { + if (this->position) { + this->current_time = this->position->clock.nsec; + } else { + struct timespec now; + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->current_time = SPA_TIMESPEC_TO_NSEC(&now); + } + } + + this->process_time = this->current_time; + + if (!spa_list_is_empty(&port->ready)) { + spa_log_trace(this->log, "%p: flush on process", this); + flush_data(this, this->current_time); + } + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static void transport_delay_changed(void *data) +{ + struct impl *this = data; + spa_log_debug(this->log, "transport %p delay changed", this->transport); + set_latency(this, true); +} + +static int do_transport_destroy(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + this->transport = NULL; + return 0; +} + +static void transport_destroy(void *data) +{ + struct impl *this = data; + spa_log_debug(this->log, "transport %p destroy", this->transport); + spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); +} + +static void transport_state_changed(void *data, + enum spa_bt_transport_state old, + enum spa_bt_transport_state state) +{ + struct impl *this = data; + + spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state); + + if (state < SPA_BT_TRANSPORT_STATE_ACTIVE && old == SPA_BT_TRANSPORT_STATE_ACTIVE && + this->started) { + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + + spa_log_debug(this->log, "%p: transport %p becomes inactive: stop and indicate error", + this, this->transport); + + /* + * If establishing connection fails due to remote end not activating + * the transport, we won't get a write error, but instead see a transport + * state change. + * + * Stop and emit a node error, to let upper levels handle it. + */ + + do_stop(this); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_node_emit_event(&this->hooks, + spa_pod_builder_add_object(&b, + SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_Error)); + } +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .delay_changed = transport_delay_changed, + .state_changed = transport_state_changed, + .destroy = transport_destroy, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + + do_stop(this); + if (this->codec_props && this->codec->clear_props) + this->codec->clear_props(this->codec_props); + if (this->transport) + spa_hook_remove(&this->transport_listener); + spa_system_close(this->data_system, this->timerfd); + spa_system_close(this->data_system, this->flush_timerfd); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_log_topic_init(this->log, &log_topic); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS | + SPA_NODE_CHANGE_MASK_PROPS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = 0; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + port->latency.min_quantum = 1.0f; + port->latency.max_quantum = 1.0f; + + spa_list_init(&port->ready); + + this->quantum_limit = 8192; + + if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) + spa_atou32(str, &this->quantum_limit, 0); + + if (info && (str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL) + this->is_duplex = spa_atob(str); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT))) + sscanf(str, "pointer:%p", &this->transport); + + if (this->transport == NULL) { + spa_log_error(this->log, "a transport is needed"); + return -EINVAL; + } + if (this->transport->media_codec == NULL) { + spa_log_error(this->log, "a transport codec is needed"); + return -EINVAL; + } + + this->codec = this->transport->media_codec; + + if (this->is_duplex) { + if (!this->codec->duplex_codec) { + spa_log_error(this->log, "transport codec doesn't support duplex"); + return -EINVAL; + } + this->codec = this->codec->duplex_codec; + } + + if (this->codec->init_props != NULL) + this->codec_props = this->codec->init_props(this->codec, + this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0, + this->transport->device->settings); + + if (this->codec->bap) + this->is_output = this->transport->bap_initiator; + else + this->is_output = true; + + reset_props(this, &this->props); + + set_latency(this, false); + + spa_bt_transport_add_listener(this->transport, + &this->transport_listener, &transport_events, this); + + this->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + this->flush_timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { 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..360f812 --- /dev/null +++ b/spa/plugins/bluez5/media-source.c @@ -0,0 +1,1707 @@ +/* Spa Media Source + * + * Copyright © 2018 Wim Taymans + * Copyright © 2019 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.media"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#include "decode-buffer.h" + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + char clock_name[64]; +}; + +#define FILL_FRAMES 2 +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; + unsigned int outstanding:1; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + struct spa_audio_info current_format; + uint32_t frame_size; + unsigned int have_format:1; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_io_rate_match *rate_match; + struct spa_latency_info latency; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list free; + struct spa_list ready; + + struct spa_bt_decode_buffer buffer; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint32_t quantum_limit; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define IDX_NODE_IO 2 +#define N_NODE_PARAMS 3 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + + struct port port; + + unsigned int started:1; + unsigned int transport_acquired:1; + unsigned int following:1; + unsigned int matching:1; + unsigned int resampling:1; + + unsigned int is_input:1; + unsigned int is_duplex:1; + unsigned int use_duplex_source:1; + + int fd; + struct spa_source source; + + struct spa_source timer_source; + int timerfd; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint64_t current_time; + uint64_t next_time; + + const struct media_codec *codec; + bool codec_props_changed; + void *codec_props; + void *codec_data; + struct spa_audio_info codec_format; + + uint8_t buffer_read[4096]; + struct timespec now; + uint64_t sample_count; + + int duplex_timerfd; + uint64_t duplex_timeout; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) + +static void reset_props(struct props *props) +{ + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0, index_offset = 0; + bool enum_codec = false; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + switch (result.index) { + default: + enum_codec = true; + index_offset = 0; + } + break; + } + case SPA_PARAM_Props: + { + switch (result.index) { + default: + enum_codec = true; + index_offset = 0; + } + break; + } + default: + return -ENOENT; + } + + if (enum_codec) { + int res; + if (this->codec->enum_props == NULL || this->codec_props == NULL || + this->transport == NULL) + return 0; + else if ((res = this->codec->enum_props(this->codec_props, + this->transport->device->settings, + id, result.index - index_offset, + &b, ¶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); + spa_bt_decode_buffer_recover(&port->buffer); + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + bool following; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + + following = is_following(this); + if (this->started && following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full); + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct props new_props = this->props; + int changed = 0; + + if (param == NULL) { + reset_props(&new_props); + } else { + /* noop */ + } + + changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); + this->props = new_props; + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + int res, codec_res = 0; + res = apply_props(this, param); + if (this->codec_props && this->codec->set_props) { + codec_res = this->codec->set_props(this->codec_props, param); + if (codec_res > 0) + this->codec_props_changed = true; + } + if (res > 0 || codec_res > 0) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, false); + } + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static void reset_buffers(struct port *port) +{ + uint32_t i; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } +} + +static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + + if (b->outstanding) { + spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } +} + +static int32_t read_data(struct impl *this) { + const ssize_t b_size = sizeof(this->buffer_read); + int32_t size_read = 0; + +again: + /* read data from socket */ + size_read = recv(this->fd, this->buffer_read, b_size, MSG_DONTWAIT); + + if (size_read == 0) + return 0; + else if (size_read < 0) { + /* retry if interrupted */ + if (errno == EINTR) + goto again; + + /* return socket has no data */ + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + + /* go to 'stop' if socket has an error */ + spa_log_error(this->log, "read error: %s", strerror(errno)); + return -errno; + } + + return size_read; +} + +static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size, + uint8_t *dst, uint32_t dst_size) +{ + ssize_t processed; + size_t written, avail; + + if ((processed = this->codec->start_decode(this->codec_data, + src, src_size, NULL, NULL)) < 0) + return processed; + + src += processed; + src_size -= processed; + + /* decode */ + avail = dst_size; + while (src_size > 0) { + if ((processed = this->codec->decode(this->codec_data, + src, src_size, dst, avail, &written)) <= 0) + return processed; + + /* update source and dest pointers */ + spa_return_val_if_fail (avail > written, -ENOSPC); + src_size -= processed; + src += processed; + avail -= written; + dst += written; + } + return dst_size - avail; +} + +static void media_on_ready_read(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + struct timespec now; + void *buf; + int32_t size_read, decoded; + uint32_t avail; + uint64_t dt; + + /* make sure the source is an input */ + if ((source->rmask & SPA_IO_IN) == 0) { + spa_log_error(this->log, "source is not an input, rmask=%d", source->rmask); + goto stop; + } + if (this->transport == NULL) { + spa_log_debug(this->log, "no transport, stop reading"); + goto stop; + } + + spa_log_trace(this->log, "socket poll"); + + /* read */ + size_read = read_data (this); + if (size_read == 0) + return; + if (size_read < 0) { + spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read)); + goto stop; + } + + /* update the current pts */ + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + + if (this->codec_props_changed && this->codec_props + && this->codec->update_props) { + this->codec->update_props(this->codec_data, this->codec_props); + this->codec_props_changed = false; + } + + /* decode to buffer */ + buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + spa_log_trace(this->log, "read socket data size:%d, avail:%d", size_read, avail); + decoded = decode_data(this, this->buffer_read, size_read, buf, avail); + if (decoded < 0) { + spa_log_debug(this->log, "failed to decode data: %d", decoded); + return; + } + if (decoded == 0) { + spa_log_trace(this->log, "no decoded socket data"); + return; + } + + /* discard when not started */ + if (!this->started) + return; + + spa_bt_decode_buffer_write_packet(&port->buffer, decoded); + + dt = SPA_TIMESPEC_TO_NSEC(&this->now); + this->now = now; + dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt; + + spa_log_trace(this->log, "decoded socket data size:%d frames:%d dt:%d dms", + (int)decoded, (int)decoded/port->frame_size, + (int)(dt / 100000)); + + return; + +stop: + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); +} + +static int set_duplex_timeout(struct impl *this, uint64_t timeout) +{ + struct itimerspec ts; + ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + return spa_system_timerfd_settime(this->data_system, + this->duplex_timerfd, 0, &ts, NULL); +} + +static void media_on_duplex_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t exp; + int res; + + if ((res = spa_system_timerfd_read(this->data_system, this->duplex_timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); + return; + } + + set_duplex_timeout(this, this->duplex_timeout); + + media_on_ready_read(source); +} + +static int setup_matching(struct impl *this) +{ + struct port *port = &this->port; + + if (this->position && port->rate_match) { + port->rate_match->rate = 1 / port->buffer.corr; + + this->matching = this->following; + this->resampling = this->matching || + (port->current_format.info.raw.rate != this->position->clock.rate.denom); + } else { + this->matching = false; + this->resampling = false; + } + + if (port->rate_match) + SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching); + + return 0; +} + +static int produce_buffer(struct impl *this); + +static void media_on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + uint64_t exp, duration; + uint32_t rate; + uint64_t prev_time, now_time; + int res; + + if (this->transport == NULL) + return; + + if (this->started) { + if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); + return; + } + } + + prev_time = this->current_time; + now_time = this->current_time = this->next_time; + + spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, + now_time, now_time - prev_time); + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + setup_matching(this); + + this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->rate_diff = port->buffer.corr; + this->clock->next_nsec = this->next_time; + } + + if (port->io) { + int status = produce_buffer(this); + spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status); + } + + spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); + + set_timeout(this, this->next_time); +} + +static int transport_start(struct impl *this) +{ + int res, val; + struct port *port = &this->port; + uint32_t flags; + + if (this->transport_acquired) + return 0; + + spa_log_debug(this->log, "%p: transport %p acquire", this, + this->transport); + if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) + return res; + + this->transport_acquired = true; + + flags = this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK; + + this->codec_data = this->codec->init(this->codec, + flags, + this->transport->configuration, + this->transport->configuration_len, + &port->current_format, + this->codec_props, + this->transport->read_mtu); + if (this->codec_data == NULL) + return -EIO; + + spa_log_info(this->log, "%p: using %s codec %s", this, + this->codec->bap ? "BAP" : "A2DP", this->codec->description); + + val = fcntl(this->transport->fd, F_GETFL); + if (fcntl(this->transport->fd, F_SETFL, val | O_NONBLOCK) < 0) + spa_log_warn(this->log, "%p: fcntl %u %m", this, val | O_NONBLOCK); + + val = FILL_FRAMES * this->transport->write_mtu; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_SNDBUF, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "%p: SO_SNDBUF %m", this); + + val = FILL_FRAMES * this->transport->read_mtu; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_RCVBUF, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "%p: SO_RCVBUF %m", this); + + val = 6; + if (setsockopt(this->transport->fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0) + spa_log_warn(this->log, "SO_PRIORITY failed: %m"); + + reset_buffers(port); + + spa_bt_decode_buffer_clear(&port->buffer); + if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log, + port->frame_size, port->current_format.info.raw.rate, + this->quantum_limit, this->quantum_limit)) < 0) + return res; + + this->fd = this->transport->fd; + + this->source.data = this; + + if (!this->use_duplex_source) { + this->source.fd = this->transport->fd; + this->source.func = media_on_ready_read; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->source); + } else { + /* + * XXX: For an unknown reason (on Linux 5.13.10), the socket when working with + * XXX: "duplex" stream sometimes stops waking up from the poll, even though + * XXX: you can recv() from the socket with no problem. + * XXX: + * XXX: The reason for this should be found and fixed. + * XXX: To work around this, for now we just do the stupid thing and poll + * XXX: on a timer, chosen so that it's fast enough for the aptX-LL codec + * XXX: we currently support (which sends mSBC data), and also for Opus + * XXX: forward stream. + */ + this->source.fd = this->duplex_timerfd; + this->source.func = media_on_duplex_timeout; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->source); + + this->duplex_timeout = SPA_NSEC_PER_MSEC * 25/10; + set_duplex_timeout(this, this->duplex_timeout); + } + + this->timer_source.data = this; + this->timer_source.fd = this->timerfd; + this->timer_source.func = media_on_timeout; + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->timer_source); + + this->sample_count = 0; + + setup_matching(this); + + set_timers(this); + + return 0; +} + +static int do_start(struct impl *this) +{ + int res = 0; + + if (this->started) + return 0; + + spa_return_val_if_fail(this->transport != NULL, -EIO); + + this->following = is_following(this); + + spa_log_debug(this->log, "%p: start state:%d following:%d", + this, this->transport->state, this->following); + + if (this->transport->state >= SPA_BT_TRANSPORT_STATE_PENDING || + this->is_duplex || this->codec->bap) + res = transport_start(this); + + this->started = true; + + return res; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct itimerspec ts; + + spa_log_debug(this->log, "%p: remove source", this); + + set_duplex_timeout(this, 0); + + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL); + + return 0; +} + +static int transport_stop(struct impl *this) +{ + struct port *port = &this->port; + int res; + + spa_log_debug(this->log, "%p: transport stop", this); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + if (this->transport && this->transport_acquired) + res = spa_bt_transport_release(this->transport); + else + res = 0; + + this->transport_acquired = false; + + if (this->codec_data) + this->codec->deinit(this->codec_data); + this->codec_data = NULL; + + spa_bt_decode_buffer_clear(&port->buffer); + + return res; +} + +static int do_stop(struct impl *this) +{ + int res; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "%p: stop", this); + + res = transport_stop(this); + + this->started = false; + + return res; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if ((res = do_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if ((res = do_stop(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + + struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, this->is_input ? "Audio/Source" : "Stream/Output/Audio" }, + { SPA_KEY_NODE_LATENCY, this->is_input ? "" : "512/48000" }, + { "media.name", ((this->transport && this->transport->device->name) ? + this->transport->device->name : this->codec->bap ? "BAP" : "A2DP") }, + { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" }, + }; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (result.index > 0) + return 0; + if (this->codec == NULL) + return -EIO; + if (this->transport == NULL) + return -EIO; + + if ((res = this->codec->enum_config(this->codec, + this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK, + this->transport->configuration, + this->transport->configuration_len, + id, result.index, &b, ¶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: + param = spa_latency_build(&b, id, &port->latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + do_stop(this); + if (port->n_buffers > 0) { + spa_list_init(&port->free); + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int err; + + if (format == NULL) { + spa_log_debug(this->log, "clear format"); + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + port->frame_size = info.info.raw.channels; + + switch (info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16: + port->frame_size *= 2; + break; + case SPA_AUDIO_FORMAT_S24: + port->frame_size *= 3; + break; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_F32: + port->frame_size *= 4; + break; + default: + return -EINVAL; + } + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + res = 0; + break; + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + spa_log_debug(this->log, "use buffers %d", n_buffers); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + port->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + + if (port->n_buffers == 0) + return -EIO; + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + recycle_buffer(this, port, buffer_id); + + return 0; +} + +static uint32_t get_samples(struct impl *this, uint32_t *duration) +{ + struct port *port = &this->port; + uint32_t samples; + + if (SPA_LIKELY(port->rate_match) && this->resampling) { + samples = port->rate_match->size; + } else { + if (SPA_LIKELY(this->position)) + samples = this->position->clock.duration * port->current_format.info.raw.rate + / this->position->clock.rate.denom; + else + samples = 1024; + } + + if (SPA_LIKELY(this->position)) + *duration = this->position->clock.duration * port->current_format.info.raw.rate + / this->position->clock.rate.denom; + else if (SPA_LIKELY(this->clock)) + *duration = this->clock->duration * port->current_format.info.raw.rate + / this->clock->rate.denom; + else + *duration = 1024 * port->current_format.info.raw.rate / 48000; + + return samples; +} + +static void process_buffering(struct impl *this) +{ + struct port *port = &this->port; + uint32_t duration; + const uint32_t samples = get_samples(this, &duration); + uint32_t avail; + void *buf; + + spa_bt_decode_buffer_process(&port->buffer, samples, duration); + + setup_matching(this); + + buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); + + /* copy data to buffers */ + if (!spa_list_is_empty(&port->free) && avail > 0) { + struct buffer *buffer; + struct spa_data *datas; + uint32_t data_size; + + data_size = samples * port->frame_size; + + avail = SPA_MIN(avail, data_size); + + spa_bt_decode_buffer_read(&port->buffer, avail); + + buffer = spa_list_first(&port->free, struct buffer, link); + spa_list_remove(&buffer->link); + + spa_log_trace(this->log, "dequeue %d", buffer->id); + + if (buffer->h) { + buffer->h->seq = this->sample_count; + buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now); + buffer->h->dts_offset = 0; + } + + datas = buffer->buf->datas; + + spa_assert(datas[0].maxsize >= data_size); + + datas[0].chunk->offset = 0; + datas[0].chunk->size = avail; + datas[0].chunk->stride = port->frame_size; + + memcpy(datas[0].data, buf, avail); + + this->sample_count += avail / port->frame_size; + + /* ready buffer if full */ + spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size); + spa_list_append(&port->ready, &buffer->link); + } +} + +static int produce_buffer(struct impl *this) +{ + struct buffer *buffer; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + + if (io == NULL) + return -EIO; + + /* Return if we already have a buffer */ + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* Recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + /* Handle buffering */ + process_buffering(this); + + /* Return if there are no buffers ready to be processed */ + if (spa_list_is_empty(&port->ready)) + return SPA_STATUS_OK; + + /* Get the new buffer from the ready list */ + buffer = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&buffer->link); + buffer->outstanding = true; + + /* Set the new buffer in IO */ + io->buffer_id = buffer->id; + io->status = SPA_STATUS_HAVE_DATA; + + /* Notify we have a buffer ready to be processed */ + return SPA_STATUS_HAVE_DATA; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + spa_log_trace(this->log, "%p status:%d", this, io->status); + + /* Return if we already have a buffer */ + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* Recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + /* Follower produces buffers here, driver in timeout */ + if (this->following) + return produce_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int do_transport_destroy(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + this->transport = NULL; + this->transport_acquired = false; + return 0; +} + +static void transport_destroy(void *data) +{ + struct impl *this = data; + spa_log_debug(this->log, "transport %p destroy", this->transport); + spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + struct port *port = &this->port; + + do_stop(this); + if (this->codec_props && this->codec->clear_props) + this->codec->clear_props(this->codec_props); + if (this->transport) + spa_hook_remove(&this->transport_listener); + spa_system_close(this->data_system, this->timerfd); + if (this->duplex_timerfd >= 0) { + spa_system_close(this->data_system, this->duplex_timerfd); + this->duplex_timerfd = -1; + } + spa_bt_decode_buffer_clear(&port->buffer); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_log_topic_init(this->log, &log_topic); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + /* set the node info */ + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 0; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + /* set the port info */ + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_TERMINAL; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + port->latency.min_quantum = 1.0f; + port->latency.max_quantum = 1.0f; + + /* Init the buffer lists */ + spa_list_init(&port->ready); + spa_list_init(&port->free); + + this->quantum_limit = 8192; + + if (info != NULL) { + if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) + spa_atou32(str, &this->quantum_limit, 0); + if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL) + sscanf(str, "pointer:%p", &this->transport); + if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL) + this->is_input = spa_streq(str, "input"); + if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL) + this->is_duplex = spa_atob(str); + } + + if (this->transport == NULL) { + spa_log_error(this->log, "a transport is needed"); + return -EINVAL; + } + if (this->transport->media_codec == NULL) { + spa_log_error(this->log, "a transport codec is needed"); + return -EINVAL; + } + this->codec = this->transport->media_codec; + + if (this->is_duplex) { + if (!this->codec->duplex_codec) { + spa_log_error(this->log, "transport codec doesn't support duplex"); + return -EINVAL; + } + this->codec = this->codec->duplex_codec; + this->is_input = true; + } + this->use_duplex_source = this->is_duplex || (this->codec->duplex_codec != NULL); + + if (this->codec->bap) + this->is_input = this->transport->bap_initiator; + + if (this->codec->init_props != NULL) + this->codec_props = this->codec->init_props(this->codec, + this->is_duplex ? 0 : MEDIA_CODEC_FLAG_SINK, + this->transport->device->settings); + + spa_bt_transport_add_listener(this->transport, + &this->transport_listener, &transport_events, this); + + this->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + if (this->use_duplex_source) { + this->duplex_timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + } else { + this->duplex_timerfd = -1; + } + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. " }, + { 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..f189570 --- /dev/null +++ b/spa/plugins/bluez5/meson.build @@ -0,0 +1,206 @@ +gnome = import('gnome') + +bluez5_deps = [ mathlib, dbus_dep, glib2_dep, sbc_dep, bluez_dep, gio_dep, gio_unix_dep ] +foreach dep: bluez5_deps + if not dep.found() + subdir_done() + endif +endforeach + +cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE', + get_option('bluez5-backend-hsp-native').allowed() or + get_option('bluez5-backend-hfp-native').allowed()) +cdata.set('HAVE_BLUEZ_5_BACKEND_HSP_NATIVE', get_option('bluez5-backend-hsp-native').allowed()) +cdata.set('HAVE_BLUEZ_5_BACKEND_HFP_NATIVE', get_option('bluez5-backend-hfp-native').allowed()) +cdata.set('HAVE_BLUEZ_5_BACKEND_NATIVE_MM', get_option('bluez5-backend-native-mm').allowed()) +cdata.set('HAVE_BLUEZ_5_BACKEND_OFONO', get_option('bluez5-backend-ofono').allowed()) +cdata.set('HAVE_BLUEZ_5_BACKEND_HSPHFPD', get_option('bluez5-backend-hsphfpd').allowed()) +cdata.set('HAVE_BLUEZ_5_HCI', dependency('bluez', version: '< 6', required: false).found()) + +bluez5_sources = [ + 'plugin.c', + 'codec-loader.c', + 'media-codecs.c', + 'media-sink.c', + 'media-source.c', + 'sco-sink.c', + 'sco-source.c', + 'sco-io.c', + 'quirks.c', + 'player.c', + 'bluez5-device.c', + 'bluez5-dbus.c', + 'hci.c', + 'dbus-monitor.c', + 'midi-enum.c', + 'midi-parser.c', + 'midi-node.c', + 'midi-server.c', +] + +bluez5_interface_src = gnome.gdbus_codegen('bluez5-interface-gen', + sources: 'org.bluez.xml', + interface_prefix : 'org.bluez.', + object_manager: true, + namespace : 'Bluez5', + annotations : [ + ['org.bluez.GattCharacteristic1.AcquireNotify()', 'org.gtk.GDBus.C.UnixFD', 'true'], + ['org.bluez.GattCharacteristic1.AcquireWrite()', 'org.gtk.GDBus.C.UnixFD', 'true'], + ] +) +bluez5_sources += [ bluez5_interface_src ] + +bluez5_data = ['bluez-hardware.conf'] + +install_data(bluez5_data, install_dir : spa_datadir / 'bluez5') + +if get_option('bluez5-backend-hsp-native').allowed() or get_option('bluez5-backend-hfp-native').allowed() + if libusb_dep.found() + bluez5_deps += libusb_dep + endif + if mm_dep.found() + bluez5_deps += mm_dep + bluez5_sources += ['modemmanager.c'] + endif + bluez5_sources += ['backend-native.c', 'upower.c'] +endif + +if get_option('bluez5-backend-ofono').allowed() + bluez5_sources += ['backend-ofono.c'] +endif + +if get_option('bluez5-backend-hsphfpd').allowed() + bluez5_sources += ['backend-hsphfpd.c'] +endif + +# The library uses GObject, and cannot be unloaded +bluez5_link_args = [ '-Wl,-z', '-Wl,nodelete' ] + +bluez5lib = shared_library('spa-bluez5', + bluez5_sources, + include_directories : [ configinc ], + dependencies : [ spa_dep, bluez5_deps ], + link_args : bluez5_link_args, + install : true, + install_dir : spa_plugindir / 'bluez5') + +codec_args = [ '-DCODEC_PLUGIN' ] + +bluez_codec_sbc = shared_library('spa-codec-bluez5-sbc', + [ 'a2dp-codec-sbc.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, sbc_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') + +bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream', + [ 'a2dp-codec-faststream.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, sbc_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') + +if fdk_aac_dep.found() + bluez_codec_aac = shared_library('spa-codec-bluez5-aac', + [ 'a2dp-codec-aac.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, fdk_aac_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +if aptx_dep.found() + bluez_codec_aptx = shared_library('spa-codec-bluez5-aptx', + [ 'a2dp-codec-aptx.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, aptx_dep, sbc_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +if ldac_dep.found() + ldac_args = codec_args + ldac_dep = [ ldac_dep ] + if ldac_abr_dep.found() + ldac_args += [ '-DENABLE_LDAC_ABR' ] + ldac_dep += ldac_abr_dep + endif + bluez_codec_ldac = shared_library('spa-codec-bluez5-ldac', + [ 'a2dp-codec-ldac.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : ldac_args, + dependencies : [ spa_dep, ldac_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +if get_option('bluez5-codec-lc3plus').allowed() and lc3plus_dep.found() + bluez_codec_lc3plus = shared_library('spa-codec-bluez5-lc3plus', + [ 'a2dp-codec-lc3plus.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, lc3plus_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +if get_option('bluez5-codec-opus').allowed() and opus_dep.found() + opus_args = codec_args + opus_dep = [ opus_dep ] + bluez_codec_opus = shared_library('spa-codec-bluez5-opus', + [ 'a2dp-codec-opus.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : opus_args, + dependencies : [ spa_dep, opus_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found() + bluez_codec_lc3 = shared_library('spa-codec-bluez5-lc3', + [ 'bap-codec-lc3.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, lc3_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'bluez5') +endif + +test_apps = [ + 'test-midi', +] +bluez5_test_lib = static_library('bluez5_test_lib', + [ 'midi-parser.c' ], + include_directories : [ configinc ], + dependencies : [ spa_dep, bluez5_deps ], + install : false +) + +foreach a : test_apps + test(a, + executable(a, a + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib, bluez5_deps ], + include_directories : [ configinc ], + link_with : [ bluez5_test_lib ], + install_rpath : spa_plugindir / 'bluez5', + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'bluez5'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'bluez5' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'bluez5', + configuration: test_conf + ) + endif +endforeach diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c new file mode 100644 index 0000000..a966591 --- /dev/null +++ b/spa/plugins/bluez5/midi-enum.c @@ -0,0 +1,887 @@ +/* Spa midi dbus + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl +{ + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + + GDBusConnection *conn; + struct dbus_monitor monitor; + GDBusObjectManagerServer *manager; + + struct spa_hook_list hooks; + + uint32_t id; +}; + +struct _MidiEnumCharacteristicProxy +{ + Bluez5GattCharacteristic1Proxy parent_instance; + + struct impl *impl; + + gchar *description; + uint32_t id; + GCancellable *read_call; + GCancellable *dsc_call; + unsigned int node_emitted:1; + unsigned int read_probed:1; + unsigned int read_done:1; + unsigned int dsc_probed:1; + unsigned int dsc_done:1; +}; + +G_DECLARE_FINAL_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, MIDI_ENUM, + CHARACTERISTIC_PROXY, Bluez5GattCharacteristic1Proxy) +G_DEFINE_TYPE(MidiEnumCharacteristicProxy, midi_enum_characteristic_proxy, BLUEZ5_TYPE_GATT_CHARACTERISTIC1_PROXY) +#define MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY (midi_enum_characteristic_proxy_get_type()) + +struct _MidiEnumManagerProxy +{ + Bluez5GattManager1Proxy parent_instance; + + GCancellable *register_call; + unsigned int registered:1; +}; + +G_DECLARE_FINAL_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, MIDI_ENUM, + MANAGER_PROXY, Bluez5GattManager1Proxy) +G_DEFINE_TYPE(MidiEnumManagerProxy, midi_enum_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY) +#define MIDI_ENUM_TYPE_MANAGER_PROXY (midi_enum_manager_proxy_get_type()) + + +static void emit_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, Bluez5Device1 *device) +{ + struct spa_device_object_info info; + char nick[512], class[16]; + struct spa_dict_item items[23]; + uint32_t n_items = 0; + const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)); + const char *alias = bluez5_device1_get_alias(device); + + spa_log_debug(impl->log, "emit node for path=%s", path); + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + info.factory_name = SPA_NAME_API_BLUEZ5_MIDI_NODE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Midi/Bridge"); + items[n_items++] = SPA_DICT_ITEM_INIT("node.description", + alias ? alias : bluez5_device1_get_name(device)); + if (chr->description && chr->description[0] != '\0') { + spa_scnprintf(nick, sizeof(nick), "%s (%s)", alias, chr->description); + items[n_items++] = SPA_DICT_ITEM_INIT("node.nick", nick); + } + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, bluez5_device1_get_icon(device)); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, path); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ADDRESS, bluez5_device1_get_address(device)); + snprintf(class, sizeof(class), "0x%06x", bluez5_device1_get_class(device)); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_CLASS, class); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ROLE, "client"); + + info.props = &SPA_DICT_INIT(items, n_items); + spa_device_emit_object_info(&impl->hooks, chr->id, &info); +} + +static void remove_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + spa_log_debug(impl->log, "remove node for path=%s", g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr))); + + spa_device_emit_object_info(&impl->hooks, chr->id, NULL); +} + +static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr); + +static void read_probe_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(source_object); + struct impl *impl = user_data; + gchar *value = NULL; + GError *err = NULL; + + bluez5_gatt_characteristic1_call_read_value_finish( + BLUEZ5_GATT_CHARACTERISTIC1(source_object), &value, res, &err); + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Operation canceled: user_data may be invalid by now */ + g_error_free(err); + goto done; + } + if (err) { + spa_log_error(impl->log, "%s.ReadValue() failed: %s", + BLUEZ_GATT_CHR_INTERFACE, + err->message); + g_error_free(err); + goto done; + } + + g_free(value); + + spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s", + g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr))); + + chr->read_done = true; + + check_chr_node(impl, chr); + +done: + g_clear_object(&chr->read_call); +} + +static int read_probe(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + GVariantBuilder builder; + GVariant *options; + + /* + * BLE MIDI-1.0 §5: The Central shall read the MIDI I/O characteristic + * of the Peripheral after establishing a connection with the accessory. + */ + + if (chr->read_probed) + return 0; + if (chr->read_call) + return -EBUSY; + + chr->read_probed = true; + + spa_log_debug(impl->log, "MIDI GATT read probe for path=%s", + g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr))); + + chr->read_call = g_cancellable_new(); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + options = g_variant_builder_end(&builder); + + bluez5_gatt_characteristic1_call_read_value(BLUEZ5_GATT_CHARACTERISTIC1(chr), + options, + chr->read_call, + read_probe_reply, + impl); + + return 0; +} + +Bluez5GattDescriptor1 *find_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + const char *path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)); + Bluez5GattDescriptor1 *found = NULL;; + GList *objects; + + objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor)); + + for (GList *llo = g_list_first(objects); llo; llo = llo->next) { + GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(llo->data)); + + for (GList *lli = g_list_first(interfaces); lli; lli = lli->next) { + Bluez5GattDescriptor1 *dsc; + + if (!BLUEZ5_IS_GATT_DESCRIPTOR1(lli->data)) + continue; + + dsc = BLUEZ5_GATT_DESCRIPTOR1(lli->data); + + if (!spa_streq(bluez5_gatt_descriptor1_get_uuid(dsc), + BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID)) + continue; + + if (spa_streq(bluez5_gatt_descriptor1_get_characteristic(dsc), path)) { + found = dsc; + break; + } + } + g_list_free_full(interfaces, g_object_unref); + + if (found) + break; + } + g_list_free_full(objects, g_object_unref); + + return found; +} + +static void read_dsc_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(user_data); + struct impl *impl = chr->impl; + gchar *value = NULL; + GError *err = NULL; + + chr->dsc_done = true; + + bluez5_gatt_descriptor1_call_read_value_finish( + BLUEZ5_GATT_DESCRIPTOR1(source_object), &value, res, &err); + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Operation canceled: user_data may be invalid by now */ + g_error_free(err); + goto done; + } + if (err) { + spa_log_error(impl->log, "%s.ReadValue() failed: %s", + BLUEZ_GATT_DSC_INTERFACE, + err->message); + g_error_free(err); + goto done; + } + + spa_log_debug(impl->log, "MIDI GATT read probe done for path=%s", + g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr))); + + g_free(chr->description); + chr->description = value; + + spa_log_debug(impl->log, "MIDI GATT user descriptor value: '%s'", + chr->description); + + check_chr_node(impl, chr); + +done: + g_clear_object(&chr->dsc_call); +} + +static int read_dsc(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + Bluez5GattDescriptor1 *dsc; + GVariant *options; + GVariantBuilder builder; + + if (chr->dsc_probed) + return 0; + if (chr->dsc_call) + return -EBUSY; + + chr->dsc_probed = true; + + dsc = find_dsc(impl, chr); + if (dsc == NULL) { + chr->dsc_done = true; + return -ENOENT; + } + + spa_log_debug(impl->log, "MIDI GATT user descriptor read, path=%s", + g_dbus_proxy_get_object_path(G_DBUS_PROXY(dsc))); + + chr->dsc_call = g_cancellable_new(); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + options = g_variant_builder_end(&builder); + + bluez5_gatt_descriptor1_call_read_value(BLUEZ5_GATT_DESCRIPTOR1(dsc), + options, + chr->dsc_call, + read_dsc_reply, + chr); + + return 0; +} + +static int read_probe_reset(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + g_cancellable_cancel(chr->read_call); + g_clear_object(&chr->read_call); + + g_cancellable_cancel(chr->dsc_call); + g_clear_object(&chr->dsc_call); + + chr->read_probed = false; + chr->read_done = false; + chr->dsc_probed = false; + chr->dsc_done = false; + return 0; +} + +static void lookup_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr, + Bluez5GattService1 **service, Bluez5Device1 **device) +{ + GDBusObject *object; + const char *service_path; + const char *device_path; + + *service = NULL; + *device = NULL; + + service_path = bluez5_gatt_characteristic1_get_service(BLUEZ5_GATT_CHARACTERISTIC1(chr)); + if (!service_path) + return; + + object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), service_path); + if (object) { + GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_GATT_SERVICE_INTERFACE); + *service = BLUEZ5_GATT_SERVICE1(iface); + } + + if (!*service) + return; + + device_path = bluez5_gatt_service1_get_device(*service); + if (!device_path) + return; + + object = g_dbus_object_manager_get_object(dbus_monitor_manager(&impl->monitor), device_path); + if (object) { + GDBusInterface *iface = g_dbus_object_get_interface(object, BLUEZ_DEVICE_INTERFACE); + *device = BLUEZ5_DEVICE1(iface); + } +} + +static void check_chr_node(struct impl *impl, MidiEnumCharacteristicProxy *chr) +{ + Bluez5GattService1 *service; + Bluez5Device1 *device; + bool available; + + lookup_chr_node(impl, chr, &service, &device); + + if (!device || !bluez5_device1_get_connected(device)) { + /* Retry read probe on each connection */ + read_probe_reset(impl, chr); + } + + spa_log_debug(impl->log, + "At %s, connected:%d resolved:%d", + g_dbus_proxy_get_object_path(G_DBUS_PROXY(chr)), + bluez5_device1_get_connected(device), + bluez5_device1_get_services_resolved(device)); + + available = service && device && + bluez5_device1_get_connected(device) && + bluez5_device1_get_services_resolved(device) && + spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID) && + spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)), + BT_MIDI_CHR_UUID); + + if (available && !chr->read_done) { + read_probe(impl, chr); + available = false; + } + + if (available && !chr->dsc_done) { + read_dsc(impl, chr); + available = chr->dsc_done; + } + + if (chr->node_emitted && !available) { + remove_chr_node(impl, chr); + chr->node_emitted = false; + } else if (!chr->node_emitted && available) { + emit_chr_node(impl, chr, device); + chr->node_emitted = true; + } +} + +static GList *get_all_valid_chr(struct impl *impl) +{ + GList *lst = NULL; + GList *objects; + + if (!dbus_monitor_manager(&impl->monitor)) { + /* Still initializing (or it failed) */ + return NULL; + } + + objects = g_dbus_object_manager_get_objects(dbus_monitor_manager(&impl->monitor)); + for (GList *p = g_list_first(objects); p; p = p->next) { + GList *interfaces = g_dbus_object_get_interfaces(G_DBUS_OBJECT(p->data)); + + for (GList *p2 = g_list_first(interfaces); p2; p2 = p2->next) { + MidiEnumCharacteristicProxy *chr; + + if (!MIDI_ENUM_IS_CHARACTERISTIC_PROXY(p2->data)) + continue; + + chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p2->data); + if (chr->impl == NULL) + continue; + + lst = g_list_append(lst, g_object_ref(chr)); + } + g_list_free_full(interfaces, g_object_unref); + } + g_list_free_full(objects, g_object_unref); + + return lst; +} + +static void check_all_nodes(struct impl *impl) +{ + /* + * Check if the nodes we have emitted are in sync with connected devices. + */ + + GList *chrs = get_all_valid_chr(impl); + + for (GList *p = chrs; p; p = p->next) + check_chr_node(impl, MIDI_ENUM_CHARACTERISTIC_PROXY(p->data)); + + g_list_free_full(chrs, g_object_unref); +} + +static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(source_object); + struct impl *impl = user_data; + GError *err = NULL; + + bluez5_gatt_manager1_call_register_application_finish( + BLUEZ5_GATT_MANAGER1(source_object), res, &err); + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Operation canceled: user_data may be invalid by now */ + g_error_free(err); + goto done; + } + if (err) { + spa_log_error(impl->log, "%s.RegisterApplication() failed: %s", + BLUEZ_GATT_MANAGER_INTERFACE, + err->message); + g_error_free(err); + goto done; + } + + manager->registered = true; + +done: + g_clear_object(&manager->register_call); +} + +static int manager_register_application(struct impl *impl, MidiEnumManagerProxy *manager) +{ + GVariantBuilder builder; + GVariant *options; + + if (manager->registered) + return 0; + if (manager->register_call) + return -EBUSY; + + spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s", + BLUEZ_GATT_MANAGER_INTERFACE, + g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)), + g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager))); + + manager->register_call = g_cancellable_new(); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + options = g_variant_builder_end(&builder); + + bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager), + g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)), + options, + manager->register_call, + manager_register_application_reply, + impl); + + return 0; +} + +/* + * DBus monitoring (Glib) + */ + +static void midi_enum_characteristic_proxy_init(MidiEnumCharacteristicProxy *chr) +{ +} + +static void midi_enum_characteristic_proxy_finalize(GObject *object) +{ + MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(object); + + g_cancellable_cancel(chr->read_call); + g_clear_object(&chr->read_call); + + g_cancellable_cancel(chr->dsc_call); + g_clear_object(&chr->dsc_call); + + if (chr->impl && chr->node_emitted) + remove_chr_node(chr->impl, chr); + + chr->impl = NULL; + + g_free(chr->description); + chr->description = NULL; +} + +static void midi_enum_characteristic_proxy_class_init(MidiEnumCharacteristicProxyClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->finalize = midi_enum_characteristic_proxy_finalize; +} + +static void midi_enum_manager_proxy_init(MidiEnumManagerProxy *manager) +{ +} + +static void midi_enum_manager_proxy_finalize(GObject *object) +{ + MidiEnumManagerProxy *manager = MIDI_ENUM_MANAGER_PROXY(object); + + g_cancellable_cancel(manager->register_call); + g_clear_object(&manager->register_call); +} + +static void midi_enum_manager_proxy_class_init(MidiEnumManagerProxyClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->finalize = midi_enum_manager_proxy_finalize; +} + +static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + + manager_register_application(impl, MIDI_ENUM_MANAGER_PROXY(iface)); +} + +static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + midi_enum_manager_proxy_finalize(G_OBJECT(iface)); +} + +static void device_update(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + + check_all_nodes(impl); +} + +static void service_update(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + Bluez5GattService1 *service = BLUEZ5_GATT_SERVICE1(iface); + + if (!spa_streq(bluez5_gatt_service1_get_uuid(service), BT_MIDI_SERVICE_UUID)) + return; + + check_all_nodes(impl); +} + +static void chr_update(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(iface); + + if (!spa_streq(bluez5_gatt_characteristic1_get_uuid(BLUEZ5_GATT_CHARACTERISTIC1(chr)), + BT_MIDI_CHR_UUID)) + return; + + if (chr->impl == NULL) { + chr->impl = impl; + chr->id = ++impl->id; + } + + check_chr_node(impl, chr); +} + +static void chr_clear(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + midi_enum_characteristic_proxy_finalize(G_OBJECT(iface)); +} + +static void monitor_start(struct impl *impl) +{ + struct dbus_monitor_proxy_type proxy_types[] = { + { BLUEZ_DEVICE_INTERFACE, BLUEZ5_TYPE_DEVICE1_PROXY, device_update, NULL }, + { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_ENUM_TYPE_MANAGER_PROXY, manager_update, manager_clear }, + { BLUEZ_GATT_SERVICE_INTERFACE, BLUEZ5_TYPE_GATT_SERVICE1_PROXY, service_update, NULL }, + { BLUEZ_GATT_CHR_INTERFACE, MIDI_ENUM_TYPE_CHARACTERISTIC_PROXY, chr_update, chr_clear }, + { BLUEZ_GATT_DSC_INTERFACE, BLUEZ5_TYPE_GATT_DESCRIPTOR1_PROXY, NULL, NULL }, + { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL }, + { NULL, G_TYPE_INVALID, NULL, NULL } + }; + + SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES); + + dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT, + impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, NULL); +} + +/* + * DBus GATT profile, to enable BlueZ autoconnect + */ + +static gboolean profile_handle_release(Bluez5GattProfile1 *iface, GDBusMethodInvocation *invocation) +{ + bluez5_gatt_profile1_complete_release(iface, invocation); + return TRUE; +} + +static int export_profile(struct impl *impl) +{ + static const char *uuids[] = { BT_MIDI_SERVICE_UUID, NULL }; + GDBusObjectSkeleton *skeleton = NULL; + Bluez5GattProfile1 *iface = NULL; + int res = -ENOMEM; + + iface = bluez5_gatt_profile1_skeleton_new(); + if (!iface) + goto done; + + skeleton = g_dbus_object_skeleton_new(MIDI_PROFILE_PATH); + if (!skeleton) + goto done; + g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface)); + + bluez5_gatt_profile1_set_uuids(iface, uuids); + g_signal_connect(iface, "handle-release", G_CALLBACK(profile_handle_release), NULL); + + g_dbus_object_manager_server_export(impl->manager, skeleton); + + spa_log_debug(impl->log, "MIDI GATT Profile exported, path=%s", + g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton))); + + res = 0; + +done: + g_clear_object(&iface); + g_clear_object(&skeleton); + return res; +} + +/* + * Monitor impl + */ + +static int impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + GList *chrs; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + chrs = get_all_valid_chr(this); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + for (GList *p = g_list_first(chrs); p; p = p->next) { + MidiEnumCharacteristicProxy *chr = MIDI_ENUM_CHARACTERISTIC_PROXY(p->data); + Bluez5Device1 *device; + Bluez5GattService1 *service; + + if (!chr->node_emitted) + continue; + + lookup_chr_node(this, chr, &service, &device); + if (device) + emit_chr_node(this, chr, device); + } + g_list_free_full(chrs, g_object_unref); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + this = (struct impl *) handle; + + dbus_monitor_clear(&this->monitor); + g_clear_object(&this->manager); + g_clear_object(&this->conn); + + spa_zero(*this); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + GError *error = NULL; + int res = 0; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + if (this->log == NULL) + return -EINVAL; + + spa_log_topic_init(this->log, &log_topic); + + if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) { + spa_log_error(this->log, "Glib mainloop is not usable: %s not set", + SPA_KEY_API_GLIB_MAINLOOP); + return -EINVAL; + } + + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + if (!this->conn) { + spa_log_error(this->log, "Creating GDBus connection failed: %s", + error->message); + g_error_free(error); + goto fail; + } + + this->manager = g_dbus_object_manager_server_new(MIDI_OBJECT_PATH); + if (!this->manager){ + spa_log_error(this->log, "Creating GDBus object manager failed"); + goto fail; + } + + if ((res = export_profile(this)) < 0) + goto fail; + + g_dbus_object_manager_server_set_connection(this->manager, this->conn); + + monitor_start(this); + + return 0; + +fail: + res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO); + impl_clear(handle); + return res; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_bluez5_midi_enum_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_BLUEZ5_MIDI_ENUM, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c new file mode 100644 index 0000000..c6a7e46 --- /dev/null +++ b/spa/plugins/bluez5/midi-node.c @@ -0,0 +1,2151 @@ +/* Spa MIDI node + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.node"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +#define DLL_BW 0.05 + +#define DEFAULT_LATENCY_OFFSET (0 * SPA_NSEC_PER_MSEC) + +#define MAX_BUFFERS 32 + +#define MIDI_RINGBUF_SIZE (8192*4) + +enum node_role { + NODE_SERVER, + NODE_CLIENT, +}; + +struct props { + char clock_name[64]; + char device_name[512]; + int64_t latency_offset; +}; + +struct midi_event_ringbuffer_entry { + uint64_t time; + unsigned int size; +}; + +struct midi_event_ringbuffer { + struct spa_ringbuffer rbuf; + uint8_t buf[MIDI_RINGBUF_SIZE]; +}; + +struct buffer { + uint32_t id; + unsigned int outgoing:1; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct time_sync { + uint64_t prev_recv_time; + uint64_t recv_time; + + uint16_t prev_device_timestamp; + uint16_t device_timestamp; + + uint64_t device_time; + + struct spa_dll dll; +}; + +struct port { + uint32_t id; + enum spa_direction direction; + + struct spa_audio_info current_format; + unsigned int have_format:1; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_latency_info latency; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list free; + struct spa_list ready; + + int fd; + uint16_t mtu; + + struct buffer *buffer; + struct spa_pod_builder builder; + struct spa_pod_frame frame; + + struct time_sync sync; + + unsigned int acquired:1; + GCancellable *acquire_call; + + struct spa_source source; + + struct impl *impl; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *main_loop; + struct spa_loop *data_loop; + struct spa_system *data_system; + + GDBusConnection *conn; + Bluez5GattCharacteristic1 *proxy; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define IDX_NODE_IO 2 +#define N_NODE_PARAMS 3 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + +#define PORT_IN 0 +#define PORT_OUT 1 +#define N_PORTS 2 + struct port ports[N_PORTS]; + + char *chr_path; + + unsigned int started:1; + unsigned int following:1; + + struct spa_source timer_source; + + int timerfd; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint32_t duration; + uint32_t rate; + + uint64_t current_time; + uint64_t next_time; + + struct midi_event_ringbuffer event_rbuf; + + struct spa_bt_midi_parser parser; + struct spa_bt_midi_parser tmp_parser; + uint8_t read_buffer[MIDI_MAX_MTU]; + + struct spa_bt_midi_writer writer; + + enum node_role role; + + struct spa_bt_midi_server *server; +}; + +#define CHECK_PORT(this,d,p) ((p) == 0 && ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT)) +#define GET_PORT(this,d,p) (&(this)->ports[(d) == SPA_DIRECTION_OUTPUT ? PORT_OUT : PORT_IN]) + +static void midi_event_ringbuffer_init(struct midi_event_ringbuffer *mbuf) +{ + spa_ringbuffer_init(&mbuf->rbuf); +} + +static int midi_event_ringbuffer_push(struct midi_event_ringbuffer *mbuf, + uint64_t time, uint8_t *event, unsigned int size) +{ + const unsigned int bufsize = sizeof(mbuf->buf); + int32_t avail; + uint32_t index; + struct midi_event_ringbuffer_entry evt = { + .time = time, + .size = size + }; + + avail = spa_ringbuffer_get_write_index(&mbuf->rbuf, &index); + if (avail < 0 || avail + sizeof(evt) + size > bufsize) + return -ENOSPC; + + spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize, + &evt, sizeof(evt)); + index += sizeof(evt); + spa_ringbuffer_write_update(&mbuf->rbuf, index); + spa_ringbuffer_write_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize, + event, size); + index += size; + spa_ringbuffer_write_update(&mbuf->rbuf, index); + + return 0; +} + +static int midi_event_ringbuffer_peek(struct midi_event_ringbuffer *mbuf, uint64_t *time, unsigned int *size) +{ + const unsigned bufsize = sizeof(mbuf->buf); + int32_t avail; + uint32_t index; + struct midi_event_ringbuffer_entry evt; + + avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index); + if (avail < (int)sizeof(evt)) + return -ENOENT; + + spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize, + &evt, sizeof(evt)); + + *time = evt.time; + *size = evt.size; + return 0; +} + +static int midi_event_ringbuffer_pop(struct midi_event_ringbuffer *mbuf, uint8_t *data, size_t max_size) +{ + const unsigned bufsize = sizeof(mbuf->buf); + int32_t avail; + uint32_t index; + struct midi_event_ringbuffer_entry evt; + + avail = spa_ringbuffer_get_read_index(&mbuf->rbuf, &index); + if (avail < (int)sizeof(evt)) + return -ENOENT; + + spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize, + &evt, sizeof(evt)); + index += sizeof(evt); + avail -= sizeof(evt); + spa_ringbuffer_read_update(&mbuf->rbuf, index); + + if ((uint32_t)avail < evt.size) { + /* corrupted ringbuffer: should never happen */ + spa_assert_not_reached(); + return -EINVAL; + } + + if (evt.size <= max_size) + spa_ringbuffer_read_data(&mbuf->rbuf, mbuf->buf, bufsize, index % bufsize, + data, SPA_MIN(max_size, evt.size)); + index += evt.size; + spa_ringbuffer_read_update(&mbuf->rbuf, index); + + if (evt.size > max_size) + return -ENOSPC; + + return 0; +} + +static void reset_props(struct props *props) +{ + props->latency_offset = DEFAULT_LATENCY_OFFSET; + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->device_name[0] = '\0'; +} + +static bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int set_timeout(struct impl *this, uint64_t time) +{ + struct itimerspec ts; + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + return spa_system_timerfd_settime(this->data_system, + this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static int set_timers(struct impl *this) +{ + struct timespec now; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + return set_timeout(this, this->following ? 0 : this->next_time); +} + +static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + + if (b->outgoing) { + spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&port->free, &b->link); + b->outgoing = false; + } +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_list_init(&port->free); + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static void reset_buffers(struct port *port) +{ + uint32_t i; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + + if (port->direction == SPA_DIRECTION_OUTPUT) { + spa_list_append(&port->free, &b->link); + b->outgoing = false; + } else { + b->outgoing = true; + } + } +} + +static struct buffer *peek_buffer(struct impl *this, struct port *port) +{ + if (spa_list_is_empty(&port->free)) + return NULL; + return spa_list_first(&port->free, struct buffer, link); +} + +static int prepare_buffer(struct impl *this, struct port *port) +{ + if (port->buffer != NULL) + return 0; + if ((port->buffer = peek_buffer(this, port)) == NULL) + return -EPIPE; + + spa_pod_builder_init(&port->builder, + port->buffer->buf->datas[0].data, + port->buffer->buf->datas[0].maxsize); + spa_pod_builder_push_sequence(&port->builder, &port->frame, 0); + + return 0; +} + +static int finish_buffer(struct impl *this, struct port *port) +{ + if (port->buffer == NULL) + return 0; + + spa_pod_builder_pop(&port->builder, &port->frame); + + port->buffer->buf->datas[0].chunk->offset = 0; + port->buffer->buf->datas[0].chunk->size = port->builder.state.offset; + + /* move buffer to ready queue */ + spa_list_remove(&port->buffer->link); + spa_list_append(&port->ready, &port->buffer->link); + port->buffer = NULL; + + return 0; +} + +/* Replace value -> value + n*period, to minimize |value - target| */ +static int64_t unwrap_to_closest(int64_t value, int64_t target, int64_t period) +{ + if (value > target) + value -= SPA_ROUND_DOWN(value - target + period/2, period); + if (value < target) + value += SPA_ROUND_DOWN(target - value + period/2, period); + return value; +} + +static int64_t time_diff(uint64_t a, uint64_t b) +{ + if (a >= b) + return a - b; + else + return -(int64_t)(b - a); +} + +static void midi_event_get_last_timestamp(void *user_data, uint16_t timestamp, uint8_t *data, size_t size) +{ + int *last_timestamp = user_data; + *last_timestamp = timestamp; +} + +static uint64_t midi_convert_time(struct time_sync *sync, uint16_t timestamp) +{ + int offset; + + /* + * sync->device_timestamp is a device timestamp that corresponds to system + * clock time sync->device_time. + * + * It is the timestamp of the last MIDI event in the current packet, so we can + * assume here no event here has timestamp after it. + */ + if (timestamp > sync->device_timestamp) + offset = sync->device_timestamp + MIDI_CLOCK_PERIOD_MSEC - timestamp; + else + offset = sync->device_timestamp - timestamp; + + return sync->device_time - offset * SPA_NSEC_PER_MSEC; +} + +static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data, size_t size) +{ + struct impl *this = user_data; + struct port *port = &this->ports[PORT_OUT]; + struct time_sync *sync = &port->sync; + uint64_t time; + int res; + + spa_assert(size > 0); + + time = midi_convert_time(sync, timestamp); + + spa_log_trace(this->log, "%p: event:0x%x size:%d timestamp:%d time:%"PRIu64"", + this, (int)data[0], (int)size, (int)timestamp, (uint64_t)time); + + res = midi_event_ringbuffer_push(&this->event_rbuf, time, data, size); + if (res < 0) { + midi_event_ringbuffer_init(&this->event_rbuf); + spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s", + this, spa_strerror(res)); + } +} + +static int unacquire_port(struct port *port) +{ + struct impl *this = port->impl; + + if (!port->acquired) + return 0; + + spa_log_debug(this->log, "%p: unacquire port:%d", this, port->direction); + + shutdown(port->fd, SHUT_RDWR); + close(port->fd); + port->fd = -1; + port->acquired = false; + + if (this->server) + spa_bt_midi_server_released(this->server, + (port->direction == SPA_DIRECTION_OUTPUT)); + + return 0; +} + +static int do_unacquire_port(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct port *port = user_data; + + /* in main thread */ + unacquire_port(port); + return 0; +} + +static void on_ready_read(struct spa_source *source) +{ + struct port *port = source->data; + struct impl *this = port->impl; + struct timespec now; + int res, size, last_timestamp; + + if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) || + SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) { + spa_log_debug(this->log, "%p: port:%d ERR/HUP", this, port->direction); + goto stop; + } + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + + /* read data from socket */ +again: + size = recv(port->fd, this->read_buffer, sizeof(this->read_buffer), MSG_DONTWAIT | MSG_NOSIGNAL); + if (size == 0) { + return; + } else if (size < 0) { + if (errno == EINTR) + goto again; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + goto stop; + } + + spa_log_trace(this->log, "%p: port:%d recv data size:%d", this, port->direction, size); + spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->read_buffer, size); + + if (port->direction != SPA_DIRECTION_OUTPUT) { + /* Just monitor errors for the input port */ + spa_log_debug(this->log, "%p: port:%d is not RX port; ignoring data", + this, port->direction); + return; + } + + /* prepare for producing events */ + if (port->io == NULL || port->n_buffers == 0 || !this->started) + return; + + /* + * Remote clock synchronization: + * + * Assume: Last timestamp in packet on average corresponds to packet send time. + * There is some unknown latency in between, but on average it is constant. + * + * The `device_time` computed below is the estimated wall-clock time + * corresponding to the timestamp `device_timestamp` of the last event + * in the packet. This timestamp is late by the average transmission latency, + * which is unknown. + * + * Packet reception jitter and any clock drift is smoothed over with DLL. + * The estimated timestamps are stable and preserve event intervals. + * + * To allow latency_offset to work better, we don't write the events + * to the output buffer here, but instead put them to a ringbuffer. + * This is because if the offset shifts events to later buffers, + * this is simpler to handle with the rbuf. + */ + last_timestamp = -1; + spa_bt_midi_parser_dup(&this->parser, &this->tmp_parser, true); + res = spa_bt_midi_parser_parse(&this->tmp_parser, this->read_buffer, size, true, + midi_event_get_last_timestamp, &last_timestamp); + if (res >= 0 && last_timestamp >= 0) { + struct time_sync *sync = &port->sync; + int64_t clock_elapsed; + int64_t device_elapsed; + int64_t err_nsec; + double corr, tcorr; + + sync->prev_recv_time = sync->recv_time; + sync->recv_time = SPA_TIMESPEC_TO_NSEC(&now); + + sync->prev_device_timestamp = sync->device_timestamp; + sync->device_timestamp = last_timestamp; + + if (port->sync.prev_recv_time == 0) { + sync->prev_recv_time = sync->recv_time; + sync->prev_device_timestamp = sync->device_timestamp; + spa_dll_init(&sync->dll); + } + if (SPA_UNLIKELY(sync->dll.bw == 0)) + spa_dll_set_bw(&sync->dll, DLL_BW, 1024, 48000); + + /* move device clock forward */ + clock_elapsed = sync->recv_time - sync->prev_recv_time; + + device_elapsed = (int)sync->device_timestamp - (int)sync->prev_device_timestamp; + device_elapsed *= SPA_NSEC_PER_MSEC; + device_elapsed = unwrap_to_closest(device_elapsed, clock_elapsed, MIDI_CLOCK_PERIOD_NSEC); + sync->device_time += device_elapsed; + + /* smooth clock sync */ + err_nsec = time_diff(sync->recv_time, sync->device_time); + corr = spa_dll_update(&sync->dll, + -SPA_CLAMP(err_nsec, -20*SPA_NSEC_PER_MSEC, 20*SPA_NSEC_PER_MSEC) + * this->rate / SPA_NSEC_PER_SEC); + tcorr = SPA_MIN(device_elapsed, SPA_NSEC_PER_SEC) * (corr - 1); + sync->device_time += tcorr; + + /* reset if too much off */ + if (err_nsec < -50 * SPA_NSEC_PER_MSEC || + err_nsec > 200 * SPA_NSEC_PER_MSEC || + SPA_ABS(tcorr) > 20*SPA_NSEC_PER_MSEC || + device_elapsed < 0) { + spa_log_debug(this->log, "%p: device clock sync off too much: resync", this); + spa_dll_init(&sync->dll); + sync->device_time = sync->recv_time; + } + + spa_log_debug(this->log, + "timestamp:%d dt:%d dt2:%d err:%.1f tcorr:%.2f (ms) corr:%f", + (int)sync->device_timestamp, + (int)(clock_elapsed/SPA_NSEC_PER_MSEC), + (int)(device_elapsed/SPA_NSEC_PER_MSEC), + (double)err_nsec / SPA_NSEC_PER_MSEC, + tcorr/SPA_NSEC_PER_MSEC, + corr); + } + + /* put midi event data to the buffer */ + res = spa_bt_midi_parser_parse(&this->parser, this->read_buffer, size, false, + midi_event_recv, this); + if (res < 0) { + /* bad data */ + spa_bt_midi_parser_init(&this->parser); + + spa_log_info(this->log, "BLE MIDI data packet parsing failed: %d", res); + spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 4, this->read_buffer, size); + } + + return; + +stop: + spa_log_debug(this->log, "%p: port:%d stopping port", this, port->direction); + + if (port->source.loop) + spa_loop_remove_source(this->data_loop, &port->source); + + /* port->acquired is updated only from the main thread */ + spa_loop_invoke(this->main_loop, do_unacquire_port, 0, NULL, 0, false, port); +} + +static int process_output(struct impl *this) +{ + struct port *port = &this->ports[PORT_OUT]; + struct buffer *buffer; + struct spa_io_buffers *io = port->io; + + /* Check if we are able to process */ + if (io == NULL || !port->acquired) + return SPA_STATUS_OK; + + /* Return if we already have a buffer */ + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* Recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + /* Produce buffer */ + if (prepare_buffer(this, port) >= 0) { + /* + * this->current_time is at the end time of the buffer, and offsets + * are recorded vs. the start of the buffer. + */ + const uint64_t start_time = this->current_time + - this->duration * SPA_NSEC_PER_SEC / this->rate; + const uint64_t end_time = this->current_time; + uint64_t time; + uint32_t offset; + void *buf; + unsigned int size; + int res; + + while (true) { + res = midi_event_ringbuffer_peek(&this->event_rbuf, &time, &size); + if (res < 0) + break; + + time -= this->props.latency_offset; + + if (time > end_time) { + break; + } else if (time + SPA_NSEC_PER_MSEC < start_time) { + /* Log events in the past by more than 1 ms, but don't + * do anything about them. The user can change the latency + * offset to choose whether to tradeoff latency for more + * accurate timestamps. + * + * TODO: maybe this information should be available in + * a more visible place, some latency property? + */ + spa_log_debug(this->log, "%p: event in the past by %d ms", + this, (int)((start_time - time) / SPA_NSEC_PER_MSEC)); + } + + time = SPA_MAX(time, start_time) - start_time; + offset = time * this->rate / SPA_NSEC_PER_SEC; + offset = SPA_CLAMP(offset, 0u, this->duration - 1); + + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi); + buf = spa_pod_builder_reserve_bytes(&port->builder, size); + if (buf) { + midi_event_ringbuffer_pop(&this->event_rbuf, buf, size); + + spa_log_trace(this->log, "%p: produce event:0x%x offset:%d time:%"PRIu64"", + this, (int)*(uint8_t*)buf, (int)offset, + (uint64_t)(start_time + offset * SPA_NSEC_PER_SEC / this->rate)); + } + } + + finish_buffer(this, port); + } + + /* Return if there are no buffers ready to be processed */ + if (spa_list_is_empty(&port->ready)) + return SPA_STATUS_OK; + + /* Get the new buffer from the ready list */ + buffer = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&buffer->link); + buffer->outgoing = true; + + /* Set the new buffer in IO */ + io->buffer_id = buffer->id; + io->status = SPA_STATUS_HAVE_DATA; + + /* Notify we have a buffer ready to be processed */ + return SPA_STATUS_HAVE_DATA; +} + +static int flush_packet(struct impl *this) +{ + struct port *port = &this->ports[PORT_IN]; + int res; + + if (this->writer.size == 0) + return 0; + + res = send(port->fd, this->writer.buf, this->writer.size, + MSG_DONTWAIT | MSG_NOSIGNAL); + if (res < 0) + return -errno; + + spa_log_trace(this->log, "%p: send packet size:%d", this, this->writer.size); + spa_debug_log_mem(this->log, SPA_LOG_LEVEL_TRACE, 4, this->writer.buf, this->writer.size); + + return 0; +} + +static int write_data(struct impl *this, struct spa_data *d) +{ + struct port *port = &this->ports[PORT_IN]; + struct spa_pod_sequence *pod; + struct spa_pod_control *c; + uint64_t time; + int res; + + pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size); + if (pod == NULL) { + spa_log_warn(this->log, "%p: invalid sequence in buffer max:%u offset:%u size:%u", + this, d->maxsize, d->chunk->offset, d->chunk->size); + return -EINVAL; + } + + spa_bt_midi_writer_init(&this->writer, port->mtu); + time = 0; + + SPA_POD_SEQUENCE_FOREACH(pod, c) { + uint8_t *event; + size_t size; + + if (c->type != SPA_CONTROL_Midi) + continue; + + time = SPA_MAX(time, this->current_time + c->offset * SPA_NSEC_PER_SEC / this->rate); + event = SPA_POD_BODY(&c->value); + size = SPA_POD_BODY_SIZE(&c->value); + + spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this, + (size > 0) ? event[0] : 0, time); + + do { + res = spa_bt_midi_writer_write(&this->writer, + time, event, size); + if (res < 0) { + return res; + } else if (res) { + int res2; + if ((res2 = flush_packet(this)) < 0) + return res2; + } + } while (res); + } + + if ((res = flush_packet(this)) < 0) + return res; + + return 0; +} + +static int process_input(struct impl *this) +{ + struct port *port = &this->ports[PORT_IN]; + struct buffer *b; + struct spa_io_buffers *io = port->io; + int res; + + /* Check if we are able to process */ + if (io == NULL || !port->acquired) + return SPA_STATUS_OK; + + if (io->status != SPA_STATUS_HAVE_DATA || io->buffer_id >= port->n_buffers) + return SPA_STATUS_OK; + + b = &port->buffers[io->buffer_id]; + if (!b->outgoing) { + spa_log_warn(this->log, "%p: buffer %u not outgoing", this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + + if ((res = write_data(this, &b->buf->datas[0])) < 0) { + spa_log_info(this->log, "%p: writing data failed: %s", + this, spa_strerror(res)); + } + + port->io->buffer_id = b->id; + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_reuse_buffer(&this->callbacks, 0, io->buffer_id); + + return SPA_STATUS_HAVE_DATA; +} + +static void update_position(struct impl *this) +{ + if (SPA_LIKELY(this->position)) { + this->duration = this->position->clock.duration; + this->rate = this->position->clock.rate.denom; + } else { + this->duration = 1024; + this->rate = 48000; + } +} + +static void on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t exp; + uint64_t prev_time, now_time; + int status; + + if (!this->started) + return; + + if (spa_system_timerfd_read(this->data_system, this->timerfd, &exp) < 0) + spa_log_warn(this->log, "%p: error reading timerfd: %s", this, strerror(errno)); + + prev_time = this->current_time; + now_time = this->current_time = this->next_time; + + spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, + now_time, now_time - prev_time); + + update_position(this); + + this->next_time = now_time + this->duration * SPA_NSEC_PER_SEC / this->rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->position += this->duration; + this->clock->duration = this->duration; + this->clock->rate_diff = 1.0f; + this->clock->next_nsec = this->next_time; + } + + status = process_output(this); + spa_log_trace(this->log, "%p: status:%d", this, status); + + spa_node_call_ready(&this->callbacks, status | SPA_STATUS_NEED_DATA); + + set_timeout(this, this->next_time); +} + +static int do_start(struct impl *this); + +static int do_release(struct impl *this); + +static int do_stop(struct impl *this); + +static void acquire_reply(GObject *source_object, GAsyncResult *res, gpointer user_data, bool notify) +{ + struct port *port; + struct impl *this; + const char *method; + GError *err = NULL; + GUnixFDList *fd_list = NULL; + GVariant *fd_handle = NULL; + int fd; + guint16 mtu; + + if (notify) { + bluez5_gatt_characteristic1_call_acquire_notify_finish( + BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err); + } else { + bluez5_gatt_characteristic1_call_acquire_write_finish( + BLUEZ5_GATT_CHARACTERISTIC1(source_object), &fd_handle, &mtu, &fd_list, res, &err); + } + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Operation canceled: user_data may be invalid by now. */ + g_error_free(err); + return; + } + + port = user_data; + this = port->impl; + method = notify ? "AcquireNotify" : "AcquireWrite"; + if (err) { + spa_log_error(this->log, "%s.%s() for %s failed: %s", + BLUEZ_GATT_CHR_INTERFACE, method, + this->chr_path, err->message); + goto fail; + } + + fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(fd_handle), &err); + if (fd < 0) { + spa_log_error(this->log, "%s.%s() for %s failed to get fd: %s", + BLUEZ_GATT_CHR_INTERFACE, method, + this->chr_path, err->message); + goto fail; + } + + spa_log_info(this->log, "%p: BLE MIDI %s %s success mtu:%d", + this, this->chr_path, method, mtu); + port->fd = fd; + port->mtu = mtu; + port->acquired = true; + + if (port->direction == SPA_DIRECTION_OUTPUT) { + spa_bt_midi_parser_init(&this->parser); + + /* Start source */ + port->source.data = port; + port->source.fd = port->fd; + port->source.func = on_ready_read; + port->source.mask = SPA_IO_IN | SPA_IO_HUP | SPA_IO_ERR; + port->source.rmask = 0; + spa_loop_add_source(this->data_loop, &port->source); + } + return; + +fail: + g_error_free(err); + g_clear_object(&fd_list); + g_clear_object(&fd_handle); + do_stop(this); + do_release(this); +} + +static void acquire_notify_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + acquire_reply(source_object, res, user_data, true); +} + +static void acquire_write_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + acquire_reply(source_object, res, user_data, false); +} + +static int do_acquire(struct port *port) +{ + struct impl *this = port->impl; + const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ? + "AcquireNotify" : "AcquireWrite"; + GVariant *options; + GVariantBuilder builder; + + if (port->acquired) + return 0; + if (port->acquire_call) + return 0; + + spa_log_info(this->log, + "%p: port %d: client %s for BLE MIDI device characteristic %s", + this, port->direction, method, this->chr_path); + + port->acquire_call = g_cancellable_new(); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + options = g_variant_builder_end(&builder); + + if (port->direction == SPA_DIRECTION_OUTPUT) { + bluez5_gatt_characteristic1_call_acquire_notify( + BLUEZ5_GATT_CHARACTERISTIC1(this->proxy), + options, + NULL, + port->acquire_call, + acquire_notify_reply, + port); + } else { + bluez5_gatt_characteristic1_call_acquire_write( + BLUEZ5_GATT_CHARACTERISTIC1(this->proxy), + options, + NULL, + port->acquire_call, + acquire_write_reply, + port); + } + + return 0; +} + +static int server_do_acquire(struct port *port, int fd, uint16_t mtu) +{ + struct impl *this = port->impl; + const char *method = (port->direction == SPA_DIRECTION_OUTPUT) ? + "AcquireWrite" : "AcquireNotify"; + + spa_log_info(this->log, + "%p: port %d: server %s for BLE MIDI device characteristic %s", + this, port->direction, method, this->server->chr_path); + + if (port->acquired) { + spa_log_info(this->log, + "%p: port %d: %s failed: already acquired", + this, port->direction, method); + return -EBUSY; + } + + port->fd = fd; + port->mtu = mtu; + + if (port->direction == SPA_DIRECTION_OUTPUT) + spa_bt_midi_parser_init(&this->parser); + + /* Start source */ + port->source.data = port; + port->source.fd = port->fd; + port->source.func = on_ready_read; + port->source.mask = SPA_IO_HUP | SPA_IO_ERR; + if (port->direction == SPA_DIRECTION_OUTPUT) + port->source.mask |= SPA_IO_IN; + port->source.rmask = 0; + spa_loop_add_source(this->data_loop, &port->source); + + port->acquired = true; + return 0; +} + +static int server_acquire_write(void *user_data, int fd, uint16_t mtu) +{ + struct impl *this = user_data; + return server_do_acquire(&this->ports[PORT_OUT], fd, mtu); +} + +static int server_acquire_notify(void *user_data, int fd, uint16_t mtu) +{ + struct impl *this = user_data; + return server_do_acquire(&this->ports[PORT_IN], fd, mtu); +} + +static int server_release(void *user_data) +{ + struct impl *this = user_data; + do_release(this); + return 0; +} + +static const char *server_description(void *user_data) +{ + struct impl *this = user_data; + return this->props.device_name; +} + +static int do_remove_port_source(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + int i; + + for (i = 0; i < N_PORTS; ++i) { + struct port *port = &this->ports[i]; + + if (port->source.loop) + spa_loop_remove_source(this->data_loop, &port->source); + } + + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct itimerspec ts; + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL); + + return 0; +} + +static int do_stop(struct impl *this) +{ + int res = 0; + + spa_log_debug(this->log, "%p: stop", this); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + this->started = false; + + return res; +} + +static int do_release(struct impl *this) +{ + int res = 0; + size_t i; + + spa_log_debug(this->log, "%p: release", this); + + spa_loop_invoke(this->data_loop, do_remove_port_source, 0, NULL, 0, true, this); + + for (i = 0; i < N_PORTS; ++i) { + struct port *port = &this->ports[i]; + + g_cancellable_cancel(port->acquire_call); + g_clear_object(&port->acquire_call); + + unacquire_port(port); + } + + return res; +} + +static int do_start(struct impl *this) +{ + int res; + size_t i; + + if (this->started) + return 0; + + this->following = is_following(this); + + update_position(this); + + spa_log_debug(this->log, "%p: start following:%d", + this, this->following); + + for (i = 0; i < N_PORTS; ++i) { + struct port *port = &this->ports[i]; + + switch (this->role) { + case NODE_CLIENT: + /* Acquire Bluetooth I/O */ + if ((res = do_acquire(port)) < 0) { + do_stop(this); + do_release(this); + return res; + } + break; + case NODE_SERVER: + /* + * In MIDI server role, the device/BlueZ invokes + * the acquire asynchronously as available/needed. + */ + break; + default: + spa_assert_not_reached(); + } + + reset_buffers(port); + } + + midi_event_ringbuffer_init(&this->event_rbuf); + + this->started = true; + + /* Start timer */ + this->timer_source.data = this; + this->timer_source.fd = this->timerfd; + this->timer_source.func = on_timeout; + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->timer_source); + + set_timers(this); + + return 0; +} + + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + + set_timers(this); + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + bool following; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + + following = is_following(this); + if (this->started && following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + + return 0; +} + +static void emit_node_info(struct impl *this, bool full); + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec), + SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("Device name"), + SPA_PROP_INFO_type, SPA_POD_String(p->device_name)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset), + SPA_PROP_deviceName, SPA_POD_String(p->device_name)); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static void emit_port_info(struct impl *this, struct port *port, bool full); + +static void set_latency(struct impl *this, bool emit_latency) +{ + struct port *port = &this->ports[PORT_OUT]; + + port->latency.min_ns = port->latency.max_ns = this->props.latency_offset; + + if (emit_latency) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + } +} + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct props new_props = this->props; + int changed = 0; + + if (param == NULL) { + reset_props(&new_props); + } else { + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset), + SPA_PROP_deviceName, SPA_POD_OPT_Stringn(new_props.device_name, + sizeof(new_props.device_name))); + } + + changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); + this->props = new_props; + + if (changed) + set_latency(this, true); + + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + if (apply_props(this, param) > 0) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, false); + } + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + int res, res2; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if ((res = do_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + if ((res = do_stop(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + res = do_stop(this); + if (this->role == NODE_CLIENT) + res2 = do_release(this); + else + res2 = 0; + if (res < 0) + return res; + if (res2 < 0) + return res2; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + const struct spa_dict_item node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" }, + }; + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + size_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + + for (i = 0; i < N_PORTS; ++i) + emit_port_info(this, &this->ports[i], true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_Format, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + 4096, 4096, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: + param = spa_latency_build(&b, id, &port->latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int err; + + if (format == NULL) { + if (!port->have_format) + return 0; + + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_application || + info.media_subtype != SPA_MEDIA_SUBTYPE_control) + return -EINVAL; + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, 1); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + res = 0; + break; + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, "%p: use buffers %d", this, n_buffers); + + if (!port->have_format) + return -EIO; + + clear_buffers(this, port); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + reset_buffers(port); + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + + port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id); + + if (port->n_buffers == 0) + return -EIO; + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + recycle_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + int status = SPA_STATUS_OK; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (!this->started) + return SPA_STATUS_OK; + + if (this->following) { + if (this->position) { + this->current_time = this->position->clock.nsec; + } else { + struct timespec now = { 0 }; + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->current_time = SPA_TIMESPEC_TO_NSEC(&now); + } + } + + update_position(this); + + if (this->following) + status |= process_output(this); + + status |= process_input(this); + + return status; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static const struct spa_bt_midi_server_cb impl_server = { + .acquire_write = server_acquire_write, + .acquire_notify = server_acquire_notify, + .release = server_release, + .get_description = server_description, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + + do_stop(this); + do_release(this); + + free(this->chr_path); + if (this->timerfd > 0) + spa_system_close(this->data_system, this->timerfd); + if (this->server) + spa_bt_midi_server_destroy(this->server); + g_clear_object(&this->proxy); + g_clear_object(&this->conn); + + spa_zero(*this); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *device_name = ""; + int res = 0; + GError *err = NULL; + size_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + if (this->log == NULL) + return -EINVAL; + + spa_log_topic_init(this->log, &log_topic); + + if (!(info && spa_atob(spa_dict_lookup(info, SPA_KEY_API_GLIB_MAINLOOP)))) { + spa_log_error(this->log, "Glib mainloop is not usable: %s not set", + SPA_KEY_API_GLIB_MAINLOOP); + return -EINVAL; + } + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->role = NODE_CLIENT; + + if (info) { + const char *str; + + if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_PATH)) != NULL) + this->chr_path = strdup(str); + + if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_ROLE)) != NULL) { + if (spa_streq(str, "server")) + this->role = NODE_SERVER; + } + + if ((str = spa_dict_lookup(info, "node.nick")) != NULL) + device_name = str; + else if ((str = spa_dict_lookup(info, "node.description")) != NULL) + device_name = str; + } + + if (this->role == NODE_CLIENT && this->chr_path == NULL) { + spa_log_error(this->log, "missing MIDI service characteristic path"); + res = -EINVAL; + goto fail; + } + + this->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err); + if (this->conn == NULL) { + spa_log_error(this->log, "failed to get dbus connection: %s", + err->message); + g_error_free(err); + res = -EIO; + goto fail; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + spa_scnprintf(this->props.device_name, sizeof(this->props.device_name), + "%s", device_name); + + /* set the node info */ + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + /* set the port info */ + for (i = 0; i < N_PORTS; ++i) { + struct port *port = &this->ports[i]; + static const struct spa_dict_item in_port_items[] = { + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"), + }; + static const struct spa_dict_item out_port_items[] = { + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"), + SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"), + }; + static const struct spa_dict in_port_props = SPA_DICT_INIT_ARRAY(in_port_items); + static const struct spa_dict out_port_props = SPA_DICT_INIT_ARRAY(out_port_items); + + spa_zero(*port); + + port->impl = this; + + port->id = 0; + port->direction = (i == PORT_OUT) ? SPA_DIRECTION_OUTPUT : + SPA_DIRECTION_INPUT; + + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + port->info.props = (i == PORT_OUT) ? &out_port_props : &in_port_props; + + port->latency = SPA_LATENCY_INFO(port->direction); + port->latency.min_quantum = 1.0f; + port->latency.max_quantum = 1.0f; + + /* Init the buffer lists */ + spa_list_init(&port->ready); + spa_list_init(&port->free); + } + + this->duration = 1024; + this->rate = 48000; + + set_latency(this, false); + + if (this->role == NODE_SERVER) { + this->server = spa_bt_midi_server_new(&impl_server, this->conn, this->log, this); + if (this->server == NULL) + goto fail; + } else { + this->proxy = bluez5_gatt_characteristic1_proxy_new_sync(this->conn, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + BLUEZ_SERVICE, + this->chr_path, + NULL, + &err); + if (this->proxy == NULL) { + spa_log_error(this->log, + "Failed to create BLE MIDI GATT proxy %s: %s", + this->chr_path, err->message); + g_error_free(err); + res = -EIO; + goto fail; + } + } + + this->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + return 0; + +fail: + res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO); + impl_clear(handle); + return res; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Pauli Virtanen " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Bluez5 MIDI connection" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_bluez5_midi_node_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_BLUEZ5_MIDI_NODE, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/bluez5/midi-parser.c b/spa/plugins/bluez5/midi-parser.c new file mode 100644 index 0000000..ba3cd32 --- /dev/null +++ b/spa/plugins/bluez5/midi-parser.c @@ -0,0 +1,295 @@ +/* BLE MIDI parser + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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..a1b2682 --- /dev/null +++ b/spa/plugins/bluez5/midi-server.c @@ -0,0 +1,581 @@ +/* Spa Bluez5 midi + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include + +#include "midi.h" + +#include "bluez5-interface-gen.h" +#include "dbus-monitor.h" + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT (&impl->log_topic) + +#define MIDI_SERVER_PATH "/midiserver%u" +#define MIDI_SERVICE_PATH "/midiserver%u/service" +#define MIDI_CHR_PATH "/midiserver%u/service/chr" +#define MIDI_DSC_PATH "/midiserver%u/service/chr/dsc" + +#define BLE_DEFAULT_MTU 23 + +struct impl +{ + struct spa_bt_midi_server this; + + struct spa_log_topic log_topic; + struct spa_log *log; + + const struct spa_bt_midi_server_cb *cb; + + GDBusConnection *conn; + struct dbus_monitor monitor; + GDBusObjectManagerServer *manager; + + Bluez5GattCharacteristic1 *chr; + + void *user_data; + + uint32_t server_id; + + unsigned int write_acquired:1; + unsigned int notify_acquired:1; +}; + +struct _MidiServerManagerProxy +{ + Bluez5GattManager1Proxy parent_instance; + + GCancellable *register_call; + unsigned int registered:1; +}; + +G_DECLARE_FINAL_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, MIDI_SERVER, + MANAGER_PROXY, Bluez5GattManager1Proxy) +G_DEFINE_TYPE(MidiServerManagerProxy, midi_server_manager_proxy, BLUEZ5_TYPE_GATT_MANAGER1_PROXY) +#define MIDI_SERVER_TYPE_MANAGER_PROXY (midi_server_manager_proxy_get_type()) + + +/* + * Characteristic user descriptor: not in BLE MIDI standard, but we + * put a device name here in case we have multiple MIDI endpoints. + */ + +static gboolean dsc_handle_read_value(Bluez5GattDescriptor1 *iface, GDBusMethodInvocation *invocation, + GVariant *arg_options, gpointer user_data) +{ + struct impl *impl = user_data; + const char *description = NULL; + uint16_t offset = 0; + int len; + + g_variant_lookup(arg_options, "offset", "q", &offset); + + if (impl->cb->get_description) + description = impl->cb->get_description(impl->user_data); + if (!description) + description = ""; + + len = strlen(description); + if (offset > len) { + g_dbus_method_invocation_return_dbus_error(invocation, + "org.freedesktop.DBus.Error.InvalidArgs", + "Invalid arguments"); + return TRUE; + } + + bluez5_gatt_descriptor1_complete_read_value(iface, + invocation, description + offset); + return TRUE; +} + +static int export_dsc(struct impl *impl) +{ + static const char * const flags[] = { "encrypt-read", NULL }; + GDBusObjectSkeleton *skeleton = NULL; + Bluez5GattDescriptor1 *iface = NULL; + int res = -ENOMEM; + char path[128]; + + iface = bluez5_gatt_descriptor1_skeleton_new(); + if (!iface) + goto done; + + spa_scnprintf(path, sizeof(path), MIDI_DSC_PATH, impl->server_id); + skeleton = g_dbus_object_skeleton_new(path); + if (!skeleton) + goto done; + g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface)); + + bluez5_gatt_descriptor1_set_uuid(iface, BT_GATT_CHARACTERISTIC_USER_DESCRIPTION_UUID); + spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id); + bluez5_gatt_descriptor1_set_characteristic(iface, path); + bluez5_gatt_descriptor1_set_flags(iface, flags); + + g_signal_connect(iface, "handle-read-value", G_CALLBACK(dsc_handle_read_value), impl); + + g_dbus_object_manager_server_export(impl->manager, skeleton); + + spa_log_debug(impl->log, "MIDI GATT Descriptor exported, path=%s", + g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton))); + + res = 0; + +done: + g_clear_object(&iface); + g_clear_object(&skeleton); + return res; +} + + +/* + * MIDI characteristic + */ + +static gboolean chr_handle_read_value(Bluez5GattCharacteristic1 *iface, + GDBusMethodInvocation *invocation, GVariant *arg_options, + gpointer user_data) +{ + /* BLE MIDI-1.0: returns empty value */ + bluez5_gatt_characteristic1_complete_read_value(iface, invocation, ""); + return TRUE; +} + +static void chr_change_acquired(struct impl *impl, bool write, bool enabled) +{ + if (write) { + impl->write_acquired = enabled; + bluez5_gatt_characteristic1_set_write_acquired(impl->chr, enabled); + } else { + impl->notify_acquired = enabled; + bluez5_gatt_characteristic1_set_notify_acquired(impl->chr, enabled); + } +} + +static int create_socketpair(int fds[2]) +{ + if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, fds) < 0) + return -errno; + return 0; +} + +static gboolean chr_handle_acquire(Bluez5GattCharacteristic1 *object, + GDBusMethodInvocation *invocation, + GUnixFDList *dummy, GVariant *arg_options, + bool write, gpointer user_data) +{ + struct impl *impl = user_data; + const char *err_msg = "Failed"; + uint16_t mtu = BLE_DEFAULT_MTU; + gint fds[2] = {-1, -1}; + int res; + GUnixFDList *fd_list = NULL; + GVariant *fd_handle = NULL; + GError *err = NULL; + + if ((write && (impl->cb->acquire_write == NULL)) || + (!write && (impl->cb->acquire_notify == NULL))) { + err_msg = "Not supported"; + goto fail; + } + if ((write && impl->write_acquired) || + (!write && impl->notify_acquired)) { + err_msg = "Already acquired"; + goto fail; + } + + g_variant_lookup(arg_options, "mtu", "q", &mtu); + + if (create_socketpair(fds) < 0) { + err_msg = "Socketpair creation failed"; + goto fail; + } + + if (write) + res = impl->cb->acquire_write(impl->user_data, fds[0], mtu); + else + res = impl->cb->acquire_notify(impl->user_data, fds[0], mtu); + if (res < 0) { + err_msg = "Acquiring failed"; + goto fail; + } + fds[0] = -1; + + fd_handle = g_variant_new_handle(0); + fd_list = g_unix_fd_list_new_from_array(&fds[1], 1); + fds[1] = -1; + + chr_change_acquired(impl, write, true); + + if (write) { + bluez5_gatt_characteristic1_complete_acquire_write( + object, invocation, fd_list, fd_handle, mtu); + } else { + bluez5_gatt_characteristic1_complete_acquire_notify( + object, invocation, fd_list, fd_handle, mtu); + } + + g_clear_object(&fd_list); + return TRUE; + +fail: + if (fds[0] >= 0) + close(fds[0]); + if (fds[1] >= 0) + close(fds[1]); + + if (err) + g_error_free(err); + g_clear_pointer(&fd_handle, g_variant_unref); + g_clear_object(&fd_list); + g_dbus_method_invocation_return_dbus_error(invocation, + "org.freedesktop.DBus.Error.Failed", err_msg); + return TRUE; + +} + +static gboolean chr_handle_acquire_write(Bluez5GattCharacteristic1 *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, GVariant *arg_options, + gpointer user_data) +{ + return chr_handle_acquire(object, invocation, fd_list, arg_options, true, user_data); +} + +static gboolean chr_handle_acquire_notify(Bluez5GattCharacteristic1 *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, GVariant *arg_options, + gpointer user_data) +{ + return chr_handle_acquire(object, invocation, fd_list, arg_options, false, user_data); +} + +static int export_chr(struct impl *impl) +{ + static const char * const flags[] = { "encrypt-read", "write-without-response", + "encrypt-write", "encrypt-notify", NULL }; + GDBusObjectSkeleton *skeleton = NULL; + Bluez5GattCharacteristic1 *iface = NULL; + int res = -ENOMEM; + char path[128]; + + iface = bluez5_gatt_characteristic1_skeleton_new(); + if (!iface) + goto done; + + spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id); + skeleton = g_dbus_object_skeleton_new(path); + if (!skeleton) + goto done; + g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface)); + + bluez5_gatt_characteristic1_set_uuid(iface, BT_MIDI_CHR_UUID); + spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id); + bluez5_gatt_characteristic1_set_service(iface, path); + bluez5_gatt_characteristic1_set_write_acquired(iface, FALSE); + bluez5_gatt_characteristic1_set_notify_acquired(iface, FALSE); + bluez5_gatt_characteristic1_set_flags(iface, flags); + + g_signal_connect(iface, "handle-read-value", G_CALLBACK(chr_handle_read_value), impl); + g_signal_connect(iface, "handle-acquire-write", G_CALLBACK(chr_handle_acquire_write), impl); + g_signal_connect(iface, "handle-acquire-notify", G_CALLBACK(chr_handle_acquire_notify), impl); + + g_dbus_object_manager_server_export(impl->manager, skeleton); + + impl->chr = g_object_ref(iface); + + spa_log_debug(impl->log, "MIDI GATT Characteristic exported, path=%s", + g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton))); + + res = 0; + +done: + g_clear_object(&iface); + g_clear_object(&skeleton); + return res; +} + + +/* + * MIDI service + */ + +static int export_service(struct impl *impl) +{ + GDBusObjectSkeleton *skeleton = NULL; + Bluez5GattService1 *iface = NULL; + int res = -ENOMEM; + char path[128]; + + iface = bluez5_gatt_service1_skeleton_new(); + if (!iface) + goto done; + + spa_scnprintf(path, sizeof(path), MIDI_SERVICE_PATH, impl->server_id); + skeleton = g_dbus_object_skeleton_new(path); + if (!skeleton) + goto done; + g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(iface)); + + bluez5_gatt_service1_set_uuid(iface, BT_MIDI_SERVICE_UUID); + bluez5_gatt_service1_set_primary(iface, TRUE); + + g_dbus_object_manager_server_export(impl->manager, skeleton); + + spa_log_debug(impl->log, "MIDI GATT Service exported, path=%s", + g_dbus_object_get_object_path(G_DBUS_OBJECT(skeleton))); + + res = 0; + +done: + g_clear_object(&iface); + g_clear_object(&skeleton); + return res; +} + + +/* + * Registration on all GattManagers + */ + +static void manager_register_application_reply(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(source_object); + struct impl *impl = user_data; + GError *err = NULL; + + bluez5_gatt_manager1_call_register_application_finish( + BLUEZ5_GATT_MANAGER1(source_object), res, &err); + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* Operation canceled: user_data may be invalid by now */ + g_error_free(err); + goto done; + } + if (err) { + spa_log_error(impl->log, "%s.RegisterApplication() failed: %s", + BLUEZ_GATT_MANAGER_INTERFACE, + err->message); + g_error_free(err); + goto done; + } + + manager->registered = true; + +done: + g_clear_object(&manager->register_call); +} + +static int manager_register_application(struct impl *impl, MidiServerManagerProxy *manager) +{ + GVariantBuilder builder; + GVariant *options; + + if (manager->registered) + return 0; + if (manager->register_call) + return -EBUSY; + + spa_log_debug(impl->log, "%s.RegisterApplication(%s) on %s", + BLUEZ_GATT_MANAGER_INTERFACE, + g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)), + g_dbus_proxy_get_object_path(G_DBUS_PROXY(manager))); + + manager->register_call = g_cancellable_new(); + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + options = g_variant_builder_end(&builder); + bluez5_gatt_manager1_call_register_application(BLUEZ5_GATT_MANAGER1(manager), + g_dbus_object_manager_get_object_path(G_DBUS_OBJECT_MANAGER(impl->manager)), + options, + manager->register_call, + manager_register_application_reply, + impl); + + return 0; +} + +static void midi_server_manager_proxy_init(MidiServerManagerProxy *manager) +{ +} + +static void midi_server_manager_proxy_finalize(GObject *object) +{ + MidiServerManagerProxy *manager = MIDI_SERVER_MANAGER_PROXY(object); + + g_cancellable_cancel(manager->register_call); + g_clear_object(&manager->register_call); +} + +static void midi_server_manager_proxy_class_init(MidiServerManagerProxyClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->finalize = midi_server_manager_proxy_finalize; +} + +static void manager_update(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + + manager_register_application(impl, MIDI_SERVER_MANAGER_PROXY(iface)); +} + +static void manager_clear(struct dbus_monitor *monitor, GDBusInterface *iface) +{ + midi_server_manager_proxy_finalize(G_OBJECT(iface)); +} + +static void on_name_owner_change(struct dbus_monitor *monitor) +{ + struct impl *impl = SPA_CONTAINER_OF(monitor, struct impl, monitor); + + /* + * BlueZ disappeared/appeared. It does not appear to close the sockets + * it quits, so we should force the chr release now. + */ + if (impl->cb->release) + impl->cb->release(impl->user_data); + chr_change_acquired(impl, true, false); + chr_change_acquired(impl, false, false); +} + +static void monitor_start(struct impl *impl) +{ + struct dbus_monitor_proxy_type proxy_types[] = { + { BLUEZ_GATT_MANAGER_INTERFACE, MIDI_SERVER_TYPE_MANAGER_PROXY, manager_update, manager_clear }, + { NULL, BLUEZ5_TYPE_OBJECT_PROXY, NULL, NULL }, + { NULL, G_TYPE_INVALID, NULL, NULL }, + }; + + SPA_STATIC_ASSERT(SPA_N_ELEMENTS(proxy_types) <= DBUS_MONITOR_MAX_TYPES); + + dbus_monitor_init(&impl->monitor, BLUEZ5_TYPE_OBJECT_MANAGER_CLIENT, + impl->log, impl->conn, BLUEZ_SERVICE, "/", proxy_types, + on_name_owner_change); +} + + +/* + * Object registration + */ + +static int export_objects(struct impl *impl) +{ + int res = 0; + char path[128]; + + spa_scnprintf(path, sizeof(path), MIDI_SERVER_PATH, impl->server_id); + impl->manager = g_dbus_object_manager_server_new(path); + if (!impl->manager){ + spa_log_error(impl->log, "Creating GDBus object manager failed"); + goto fail; + } + + if ((res = export_service(impl)) < 0) + goto fail; + + if ((res = export_chr(impl)) < 0) + goto fail; + + if ((res = export_dsc(impl)) < 0) + goto fail; + + g_dbus_object_manager_server_set_connection(impl->manager, impl->conn); + + return 0; + +fail: + res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO); + + spa_log_error(impl->log, "Failed to register BLE MIDI services in DBus: %s", + spa_strerror(res)); + + g_clear_object(&impl->manager); + + return res; +} + +struct spa_bt_midi_server *spa_bt_midi_server_new(const struct spa_bt_midi_server_cb *cb, + GDBusConnection *conn, struct spa_log *log, void *user_data) +{ + static unsigned int server_id = 0; + struct impl *impl; + char path[128]; + int res = 0; + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + goto fail; + + impl->server_id = server_id++; + impl->user_data = user_data; + impl->cb = cb; + impl->log = log; + impl->log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.midi.server"); + impl->conn = conn; + spa_log_topic_init(impl->log, &impl->log_topic); + + if ((res = export_objects(impl)) < 0) + goto fail; + + monitor_start(impl); + + g_object_ref(impl->conn); + + spa_scnprintf(path, sizeof(path), MIDI_CHR_PATH, impl->server_id); + impl->this.chr_path = strdup(path); + + return &impl->this; + +fail: + res = (res < 0) ? res : ((errno > 0) ? -errno : -EIO); + free(impl); + errno = res; + return NULL; +} + +void spa_bt_midi_server_destroy(struct spa_bt_midi_server *server) +{ + struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this); + + free((void *)impl->this.chr_path); + g_clear_object(&impl->chr); + dbus_monitor_clear(&impl->monitor); + g_clear_object(&impl->manager); + g_clear_object(&impl->conn); + + free(impl); +} + +void spa_bt_midi_server_released(struct spa_bt_midi_server *server, bool write) +{ + struct impl *impl = SPA_CONTAINER_OF(server, struct impl, this); + + chr_change_acquired(impl, write, false); +} diff --git a/spa/plugins/bluez5/midi.h b/spa/plugins/bluez5/midi.h new file mode 100644 index 0000000..fbf2702 --- /dev/null +++ b/spa/plugins/bluez5/midi.h @@ -0,0 +1,142 @@ +/* Spa V4l2 dbus + * + * Copyright © 2022 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#ifndef SPA_BT_MIDI_H_ +#define SPA_BT_MIDI_H_ + +#include +#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..d9df95a --- /dev/null +++ b/spa/plugins/bluez5/modemmanager.c @@ -0,0 +1,1249 @@ +/* Spa Bluez5 ModemManager proxy + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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_bt_monitor *monitor; + + struct spa_log *log; + DBusConnection *conn; + + char *allowed_modem_device; + bool filters_added; + DBusPendingCall *pending; + DBusPendingCall *voice_pending; + + const struct mm_ops *ops; + void *user_data; + + struct modem modem; + struct spa_list call_list; +}; + +struct dbus_cmd_data { + struct impl *this; + struct call *call; + void *user_data; +}; + +static bool mm_dbus_connection_send_with_reply(struct impl *this, DBusMessage *m, DBusPendingCall **pending_return, + DBusPendingCallNotifyFunction function, void *user_data) +{ + dbus_bool_t dbus_ret; + + spa_assert(*pending_return == NULL); + + dbus_ret = dbus_connection_send_with_reply(this->conn, m, pending_return, -1); + if (!dbus_ret || *pending_return == NULL) { + spa_log_debug(this->log, "dbus call failure"); + return false; + } + + dbus_ret = dbus_pending_call_set_notify(*pending_return, function, user_data, NULL); + if (!dbus_ret) { + spa_log_debug(this->log, "dbus set notify failure"); + dbus_pending_call_cancel(*pending_return); + dbus_pending_call_unref(*pending_return); + *pending_return = NULL; + return false; + } + + return true; +} + +static int mm_state_to_clcc(struct impl *this, MMCallState state) +{ + switch (state) { + case MM_CALL_STATE_DIALING: + return CLCC_DIALING; + case MM_CALL_STATE_RINGING_OUT: + return CLCC_ALERTING; + case MM_CALL_STATE_RINGING_IN: + return CLCC_INCOMING; + case MM_CALL_STATE_ACTIVE: + return CLCC_ACTIVE; + case MM_CALL_STATE_HELD: + return CLCC_HELD; + case MM_CALL_STATE_WAITING: + return CLCC_WAITING; + case MM_CALL_STATE_TERMINATED: + case MM_CALL_STATE_UNKNOWN: + default: + return -1; + } +} + +static void mm_call_state_changed(struct impl *this) +{ + struct call *call; + bool call_indicator = false; + enum call_setup call_setup_indicator = CIND_CALLSETUP_NONE; + + spa_list_for_each(call, &this->call_list, link) { + call_indicator |= (call->state == CLCC_ACTIVE); + + if (call->state == CLCC_INCOMING && call_setup_indicator < CIND_CALLSETUP_INCOMING) + call_setup_indicator = CIND_CALLSETUP_INCOMING; + else if (call->state == CLCC_DIALING && call_setup_indicator < CIND_CALLSETUP_DIALING) + call_setup_indicator = CIND_CALLSETUP_DIALING; + else if (call->state == CLCC_ALERTING && call_setup_indicator < CIND_CALLSETUP_ALERTING) + call_setup_indicator = CIND_CALLSETUP_ALERTING; + } + + if (this->ops->set_call_active) + this->ops->set_call_active(call_indicator, this->user_data); + + if (this->ops->set_call_setup) + this->ops->set_call_setup(call_setup_indicator, this->user_data); +} + +static void mm_get_call_properties_reply(DBusPendingCall *pending, void *user_data) +{ + struct call *call = user_data; + struct impl *this = call->this; + DBusMessage *r; + DBusMessageIter arg_i, element_i; + MMCallDirection direction; + MMCallState state; + + spa_assert(call->pending == pending); + dbus_pending_call_unref(pending); + call->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus Call not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "GetAll() failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &arg_i) || !spa_streq(dbus_message_get_signature(r), "a{sv}")) { + spa_log_error(this->log, "Invalid arguments in GetAll() reply"); + goto finish; + } + + spa_log_debug(this->log, "Call path: %s", call->path); + + dbus_message_iter_recurse(&arg_i, &element_i); + while (dbus_message_iter_get_arg_type(&element_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(&element_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_CALL_PROPERTY_DIRECTION)) { + dbus_message_iter_get_basic(&value_i, &direction); + spa_log_debug(this->log, "Call direction: %u", direction); + call->direction = (direction == MM_CALL_DIRECTION_INCOMING) ? CALL_INCOMING : CALL_OUTGOING; + } else if (spa_streq(key, MM_CALL_PROPERTY_NUMBER)) { + char *number; + + dbus_message_iter_get_basic(&value_i, &number); + spa_log_debug(this->log, "Call number: %s", number); + if (call->number) + free(call->number); + call->number = strdup(number); + } else if (spa_streq(key, MM_CALL_PROPERTY_STATE)) { + int clcc_state; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Call state: %u", state); + clcc_state = mm_state_to_clcc(this, state); + if (clcc_state < 0) { + spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state); + } else { + call->state = clcc_state; + mm_call_state_changed(this); + } + } + + dbus_message_iter_next(&element_i); + } + +finish: + dbus_message_unref(r); +} + +static DBusHandlerResult mm_parse_voice_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i, element_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_MODEM_VOICE_PROPERTY_CALLS)) { + spa_log_debug(this->log, "Voice properties"); + dbus_message_iter_recurse(&value_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_OBJECT_PATH) { + const char *call_object; + + dbus_message_iter_get_basic(&element_i, &call_object); + spa_log_debug(this->log, " Call: %s", call_object); + + dbus_message_iter_next(&element_i); + } + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_modem3gpp_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_OPERATORNAME)) { + char *operator_name; + + dbus_message_iter_get_basic(&value_i, &operator_name); + spa_log_debug(this->log, "Network operator code: %s", operator_name); + if (this->ops->set_modem_operator_name) + this->ops->set_modem_operator_name(operator_name, this->user_data); + } else if (spa_streq(key, MM_MODEM_MODEM3GPP_PROPERTY_REGISTRATIONSTATE)) { + MMModem3gppRegistrationState state; + bool is_roaming; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Registration state: %d", state); + + if (state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING || + state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED || + state == MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY) + is_roaming = true; + else + is_roaming = false; + + if (this->ops->set_modem_roaming) + this->ops->set_modem_roaming(is_roaming, this->user_data); + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_modem_properties(struct impl *this, DBusMessageIter *props_i) +{ + while (dbus_message_iter_get_arg_type(props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if(spa_streq(key, MM_MODEM_PROPERTY_EQUIPMENTIDENTIFIER)) { + char *imei; + + dbus_message_iter_get_basic(&value_i, &imei); + spa_log_debug(this->log, "Modem IMEI: %s", imei); + } else if(spa_streq(key, MM_MODEM_PROPERTY_MANUFACTURER)) { + char *manufacturer; + + dbus_message_iter_get_basic(&value_i, &manufacturer); + spa_log_debug(this->log, "Modem manufacturer: %s", manufacturer); + } else if(spa_streq(key, MM_MODEM_PROPERTY_MODEL)) { + char *model; + + dbus_message_iter_get_basic(&value_i, &model); + spa_log_debug(this->log, "Modem model: %s", model); + } else if (spa_streq(key, MM_MODEM_PROPERTY_OWNNUMBERS)) { + char *number; + DBusMessageIter array_i; + + dbus_message_iter_recurse(&value_i, &array_i); + if (dbus_message_iter_get_arg_type(&array_i) == DBUS_TYPE_STRING) { + dbus_message_iter_get_basic(&array_i, &number); + spa_log_debug(this->log, "Modem own number: %s", number); + if (this->ops->set_modem_own_number) + this->ops->set_modem_own_number(number, this->user_data); + } + } else if(spa_streq(key, MM_MODEM_PROPERTY_REVISION)) { + char *revision; + + dbus_message_iter_get_basic(&value_i, &revision); + spa_log_debug(this->log, "Modem revision: %s", revision); + } else if(spa_streq(key, MM_MODEM_PROPERTY_SIGNALQUALITY)) { + unsigned int percentage, signal_strength; + DBusMessageIter struct_i; + + dbus_message_iter_recurse(&value_i, &struct_i); + if (dbus_message_iter_get_arg_type(&struct_i) == DBUS_TYPE_UINT32) { + dbus_message_iter_get_basic(&struct_i, &percentage); + signal_strength = (unsigned int) round(percentage / 20.0); + spa_log_debug(this->log, "Network signal strength: %d (%d)", percentage, signal_strength); + if(this->ops->set_modem_signal_strength) + this->ops->set_modem_signal_strength(signal_strength, this->user_data); + } + } else if(spa_streq(key, MM_MODEM_PROPERTY_STATE)) { + MMModemState state; + bool has_service; + + dbus_message_iter_get_basic(&value_i, &state); + spa_log_debug(this->log, "Network state: %d", state); + + has_service = (state >= MM_MODEM_STATE_REGISTERED); + if (this->ops->set_modem_service) + this->ops->set_modem_service(has_service, this->user_data); + } + + dbus_message_iter_next(props_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult mm_parse_interfaces(struct impl *this, DBusMessageIter *dict_i) +{ + DBusMessageIter element_i, props_i; + const char *path; + + spa_assert(this); + spa_assert(dict_i); + + dbus_message_iter_get_basic(dict_i, &path); + dbus_message_iter_next(dict_i); + dbus_message_iter_recurse(dict_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter iface_i; + const char *interface; + + dbus_message_iter_recurse(&element_i, &iface_i); + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&iface_i, &props_i); + + if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Found Modem interface %s, path %s", interface, path); + if (this->modem.path == NULL) { + if (this->allowed_modem_device) { + DBusMessageIter i; + + dbus_message_iter_recurse(&iface_i, &i); + while (dbus_message_iter_get_arg_type(&i) != DBUS_TYPE_INVALID) { + DBusMessageIter key_i, value_i; + const char *key; + + dbus_message_iter_recurse(&i, &key_i); + + dbus_message_iter_get_basic(&key_i, &key); + dbus_message_iter_next(&key_i); + dbus_message_iter_recurse(&key_i, &value_i); + + if (spa_streq(key, MM_MODEM_PROPERTY_DEVICE)) { + char *device; + + dbus_message_iter_get_basic(&value_i, &device); + if (!spa_streq(this->allowed_modem_device, device)) { + spa_log_debug(this->log, "Modem not allowed: %s", device); + goto next; + } + } + dbus_message_iter_next(&i); + } + } + this->modem.path = strdup(path); + } else if (!spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "A modem is already registered"); + goto next; + } + mm_parse_modem_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + if (spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "Found Modem3GPP interface %s, path %s", interface, path); + mm_parse_modem3gpp_properties(this, &props_i); + } + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + if (spa_streq(this->modem.path, path)) { + spa_log_debug(this->log, "Found Voice interface %s, path %s", interface, path); + mm_parse_voice_properties(this, &props_i); + } + } + +next: + dbus_message_iter_next(&element_i); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void mm_get_managed_objects_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *this = user_data; + DBusMessage *r; + DBusMessageIter i, array_i; + + spa_assert(this->pending == pending); + dbus_pending_call_unref(pending); + this->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "Failed to get a list of endpoints from ModemManager: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "a{oa{sa{sv}}}")) { + spa_log_error(this->log, "Invalid arguments in GetManagedObjects() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &array_i); + while (dbus_message_iter_get_arg_type(&array_i) != DBUS_TYPE_INVALID) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&array_i, &dict_i); + mm_parse_interfaces(this, &dict_i); + dbus_message_iter_next(&array_i); + } + +finish: + dbus_message_unref(r); +} + +static void call_free(struct call *call) { + spa_list_remove(&call->link); + + if (call->pending != NULL) { + dbus_pending_call_cancel(call->pending); + dbus_pending_call_unref(call->pending); + } + + if (call->number) + free(call->number); + if (call->path) + free(call->path); + free(call); +} + +static void mm_clean_voice(struct impl *this) +{ + struct call *call; + + spa_list_consume(call, &this->call_list, link) + call_free(call); + + if (this->voice_pending != NULL) { + dbus_pending_call_cancel(this->voice_pending); + dbus_pending_call_unref(this->voice_pending); + } + + if (this->ops->set_call_setup) + this->ops->set_call_setup(CIND_CALLSETUP_NONE, this->user_data); + if (this->ops->set_call_active) + this->ops->set_call_active(false, this->user_data); +} + +static void mm_clean_modem3gpp(struct impl *this) +{ + if (this->ops->set_modem_operator_name) + this->ops->set_modem_operator_name(NULL, this->user_data); + if (this->ops->set_modem_roaming) + this->ops->set_modem_roaming(false, this->user_data); +} + +static void mm_clean_modem(struct impl *this) +{ + if (this->modem.path) { + free(this->modem.path); + this->modem.path = NULL; + } + if(this->ops->set_modem_signal_strength) + this->ops->set_modem_signal_strength(0, this->user_data); + if (this->ops->set_modem_service) + this->ops->set_modem_service(false, this->user_data); + this->modem.network_has_service = false; +} + +static DBusHandlerResult mm_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct impl *this = user_data; + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m)); + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto finish; + } + + if (spa_streq(name, MM_DBUS_SERVICE)) { + if (old_owner && *old_owner) { + spa_log_debug(this->log, "ModemManager daemon disappeared (%s)", old_owner); + mm_clean_voice(this); + mm_clean_modem3gpp(this); + mm_clean_modem(this); + } + + if (new_owner && *new_owner) + spa_log_debug(this->log, "ModemManager daemon appeared (%s)", new_owner); + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_ADDED)) { + DBusMessageIter arg_i; + + spa_log_warn(this->log, "sender: %s", dbus_message_get_sender(m)); + + if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oa{sa{sv}}")) { + spa_log_error(this->log, "Invalid signature found in InterfacesAdded"); + goto finish; + } + + mm_parse_interfaces(this, &arg_i); + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_OBJECTMANAGER, DBUS_SIGNAL_INTERFACES_REMOVED)) { + const char *path; + DBusMessageIter arg_i, element_i; + + if (!dbus_message_iter_init(m, &arg_i) || !spa_streq(dbus_message_get_signature(m), "oas")) { + spa_log_error(this->log, "Invalid signature found in InterfacesRemoved"); + goto finish; + } + + dbus_message_iter_get_basic(&arg_i, &path); + if (!spa_streq(this->modem.path, path)) + goto finish; + + dbus_message_iter_next(&arg_i); + dbus_message_iter_recurse(&arg_i, &element_i); + + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(&element_i, &iface); + + spa_log_debug(this->log, "Interface removed %s", path); + if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Modem interface %s removed, path %s", iface, path); + mm_clean_modem(this); + } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + spa_log_debug(this->log, "Modem3GPP interface %s removed, path %s", iface, path); + mm_clean_modem3gpp(this); + } else if (spa_streq(iface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + spa_log_debug(this->log, "Voice interface %s removed, path %s", iface, path); + mm_clean_voice(this); + } + + dbus_message_iter_next(&element_i); + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) { + const char *path; + DBusMessageIter iface_i, props_i; + const char *interface; + + path = dbus_message_get_path(m); + if (!spa_streq(this->modem.path, path)) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + spa_log_error(this->log, "Invalid signature found in PropertiesChanged"); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&iface_i, &props_i); + + if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_modem_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_MODEM3GPP)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_modem3gpp_properties(this, &props_i); + } else if (spa_streq(interface, MM_DBUS_INTERFACE_MODEM_VOICE)) { + spa_log_debug(this->log, "Properties changed on %s", path); + mm_parse_voice_properties(this, &props_i); + } + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLADDED)) { + DBusMessageIter iface_i; + const char *path; + struct call *call_object; + const char *mm_call_interface = MM_DBUS_INTERFACE_CALL; + + if (!spa_streq(this->modem.path, dbus_message_get_path(m))) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLADDED); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &path); + spa_log_debug(this->log, "New call: %s", path); + + call_object = calloc(1, sizeof(struct call)); + if (call_object == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + call_object->this = this; + call_object->path = strdup(path); + spa_list_append(&this->call_list, &call_object->link); + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, path, DBUS_INTERFACE_PROPERTIES, "GetAll"); + if (m == NULL) + goto finish; + dbus_message_append_args(m, DBUS_TYPE_STRING, &mm_call_interface, DBUS_TYPE_INVALID); + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_properties_reply, call_object)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + goto finish; + } + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_SIGNAL_CALLDELETED)) { + const char *path; + DBusMessageIter iface_i; + struct call *call, *call_tmp; + + if (!spa_streq(this->modem.path, dbus_message_get_path(m))) + goto finish; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "o")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_MODEM_VOICE_SIGNAL_CALLDELETED); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &path); + spa_log_debug(this->log, "Call ended: %s", path); + + spa_list_for_each_safe(call, call_tmp, &this->call_list, link) { + if (spa_streq(call->path, path)) + call_free(call); + } + mm_call_state_changed(this); + } else if (dbus_message_is_signal(m, MM_DBUS_INTERFACE_CALL, MM_CALL_SIGNAL_STATECHANGED)) { + const char *path; + DBusMessageIter iface_i; + MMCallState old, new; + MMCallStateReason reason; + struct call *call = NULL, *call_tmp; + int clcc_state; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "iiu")) { + spa_log_error(this->log, "Invalid signature found in %s", MM_CALL_SIGNAL_STATECHANGED); + goto finish; + } + + path = dbus_message_get_path(m); + + dbus_message_iter_get_basic(&iface_i, &old); + dbus_message_iter_next(&iface_i); + dbus_message_iter_get_basic(&iface_i, &new); + dbus_message_iter_next(&iface_i); + dbus_message_iter_get_basic(&iface_i, &reason); + + spa_log_debug(this->log, "Call state %s changed to %d (old = %d, reason = %u)", path, new, old, reason); + + spa_list_for_each(call_tmp, &this->call_list, link) { + if (spa_streq(call_tmp->path, path)) { + call = call_tmp; + break; + } + } + + if (call == NULL) { + spa_log_warn(this->log, "No call reference for %s", path); + goto finish; + } + + clcc_state = mm_state_to_clcc(this, new); + if (clcc_state < 0) { + spa_log_debug(this->log, "Unsupported modem state: %s, state=%d", call->path, call->state); + } else { + call->state = clcc_state; + mm_call_state_changed(this); + } + } + +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int add_filters(struct impl *this) +{ + DBusError err; + + if (this->filters_added) + return 0; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(this->conn, mm_filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" MM_DBUS_SERVICE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_ADDED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_OBJECTMANAGER "',member='" DBUS_SIGNAL_INTERFACES_REMOVED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLADDED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_MODEM_VOICE "',member='" MM_MODEM_VOICE_SIGNAL_CALLDELETED "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" MM_DBUS_SERVICE "'," + "interface='" MM_DBUS_INTERFACE_CALL "',member='" MM_CALL_SIGNAL_STATECHANGED "'", &err); + + this->filters_added = true; + + return 0; + +fail: + dbus_error_free(&err); + return -EIO; +} + +static bool is_dbus_service_available(struct impl *this, const char *service) +{ + DBusMessage *m, *r; + DBusError err; + bool success = false; + + m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "NameHasOwner"); + if (m == NULL) + return false; + dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_info(this->log, "NameHasOwner failed for %s", service); + dbus_error_free(&err); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_get_args(r, &err, + DBUS_TYPE_BOOLEAN, &success, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message); + dbus_error_free(&err); + goto finish; + } + +finish: + if (r) + dbus_message_unref(r); + + return success; +} + +bool mm_is_available(void *modemmanager) +{ + struct impl *this = modemmanager; + + if (this == NULL) + return false; + + return this->modem.path != NULL; +} + +unsigned int mm_supported_features() +{ + return SPA_BT_HFP_AG_FEATURE_REJECT_CALL | SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS; +} + +static void mm_get_call_simple_reply(DBusPendingCall *pending, void *data) +{ + struct dbus_cmd_data *dbus_cmd_data = data; + struct impl *this = dbus_cmd_data->this; + struct call *call = dbus_cmd_data->call; + void *user_data = dbus_cmd_data->user_data; + DBusMessage *r; + + free(data); + + spa_assert(call->pending == pending); + dbus_pending_call_unref(pending); + call->pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus method not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + this->ops->send_cmd_result(true, 0, user_data); + return; + +finish: + this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data); +} + +static void mm_get_call_create_reply(DBusPendingCall *pending, void *data) +{ + struct dbus_cmd_data *dbus_cmd_data = data; + struct impl *this = dbus_cmd_data->this; + void *user_data = dbus_cmd_data->user_data; + DBusMessage *r; + + free(data); + + spa_assert(this->voice_pending == pending); + dbus_pending_call_unref(pending); + this->voice_pending = NULL; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_is_error(r, DBUS_ERROR_UNKNOWN_METHOD)) { + spa_log_warn(this->log, "ModemManager D-Bus method not available"); + goto finish; + } + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "ModemManager method failed: %s", dbus_message_get_error_name(r)); + goto finish; + } + + this->ops->send_cmd_result(true, 0, user_data); + return; + +finish: + this->ops->send_cmd_result(false, CMEE_AG_FAILURE, user_data); +} + +bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No ringing in call"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_ACCEPT); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_ACTIVE) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_DIALING || + call_tmp->state == CLCC_ALERTING || + call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + } + if (!call_object) { + spa_log_debug(this->log, "No call to reject or hang up"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_HANGUP); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +static void append_basic_variant_dict_entry(DBusMessageIter *dict, const char* key, int variant_type_int, const char* variant_type_str, void* variant) { + DBusMessageIter dict_entry_it, variant_it; + dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &dict_entry_it); + dbus_message_iter_append_basic(&dict_entry_it, DBUS_TYPE_STRING, &key); + + dbus_message_iter_open_container(&dict_entry_it, DBUS_TYPE_VARIANT, variant_type_str, &variant_it); + dbus_message_iter_append_basic(&variant_it, variant_type_int, variant); + dbus_message_iter_close_container(&dict_entry_it, &variant_it); + dbus_message_iter_close_container(dict, &dict_entry_it); +} + +static inline bool is_valid_dial_string_char(char c) +{ + return ('0' <= c && c <= '9') + || ('A' <= c && c <= 'C') + || c == '*' + || c == '#' + || c == '+'; +} + +bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct dbus_cmd_data *data; + DBusMessage *m; + DBusMessageIter iter, dict; + + for (size_t i = 0; number[i]; i++) { + if (!is_valid_dial_string_char(number[i])) { + spa_log_warn(this->log, "Call creation canceled, invalid character found in dial string: %c", number[i]); + if (error) + *error = CMEE_INVALID_CHARACTERS_DIAL_STRING; + return false; + } + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, this->modem.path, MM_DBUS_INTERFACE_MODEM_VOICE, MM_MODEM_VOICE_METHOD_CREATECALL); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + append_basic_variant_dict_entry(&dict, "number", DBUS_TYPE_STRING, "s", &number); + dbus_message_iter_close_container(&iter, &dict); + if (!mm_dbus_connection_send_with_reply(this, m, &this->voice_pending, mm_get_call_create_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + struct dbus_cmd_data *data; + DBusMessage *m; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_ACTIVE) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No active call"); + if (error) + *error = CMEE_OPERATION_NOT_ALLOWED; + return false; + } + + /* Allowed dtmf characters: 0-9, *, #, A-D */ + if (!((dtmf[0] >= '0' && dtmf[0] <= '9') + || (dtmf[0] == '*') + || (dtmf[0] == '#') + || (dtmf[0] >= 'A' && dtmf[0] <= 'D'))) { + spa_log_debug(this->log, "Invalid DTMF character: %s", dtmf); + if (error) + *error = CMEE_INVALID_CHARACTERS_TEXT_STRING; + return false; + } + + data = malloc(sizeof(struct dbus_cmd_data)); + if (!data) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + data->this = this; + data->call = call_object; + data->user_data = user_data; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, call_object->path, MM_DBUS_INTERFACE_CALL, MM_CALL_METHOD_SENDDTMF); + if (m == NULL) { + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + dbus_message_append_args(m, DBUS_TYPE_STRING, &dtmf, DBUS_TYPE_INVALID); + if (!mm_dbus_connection_send_with_reply(this, m, &call_object->pending, mm_get_call_simple_reply, data)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + if (error) + *error = CMEE_AG_FAILURE; + return false; + } + + return true; +} + +const char *mm_get_incoming_call_number(void *modemmanager) +{ + struct impl *this = modemmanager; + struct call *call_object, *call_tmp; + + call_object = NULL; + spa_list_for_each(call_tmp, &this->call_list, link) { + if (call_tmp->state == CLCC_INCOMING) { + call_object = call_tmp; + break; + } + } + if (!call_object) { + spa_log_debug(this->log, "No ringing in call"); + return NULL; + } + + return call_object->number; +} + +struct spa_list *mm_get_calls(void *modemmanager) +{ + struct impl *this = modemmanager; + + return &this->call_list; +} + +void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info, + const struct mm_ops *ops, void *user_data) +{ + struct impl *this; + const char *modem_device_str = NULL; + bool modem_device_found = false; + + spa_assert(log); + spa_assert(dbus_connection); + + if (info) { + if ((modem_device_str = spa_dict_lookup(info, "bluez5.hfphsp-backend-native-modem")) != NULL) { + if (!spa_streq(modem_device_str, "none")) + modem_device_found = true; + } + } + if (!modem_device_found) { + spa_log_info(log, "No modem allowed, doesn't link to ModemManager"); + return NULL; + } + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) + return NULL; + + this->log = log; + this->conn = dbus_connection; + this->ops = ops; + this->user_data = user_data; + if (modem_device_str && !spa_streq(modem_device_str, "any")) + this->allowed_modem_device = strdup(modem_device_str); + spa_list_init(&this->call_list); + + if (add_filters(this) < 0) { + goto fail; + } + + if (is_dbus_service_available(this, MM_DBUS_SERVICE)) { + DBusMessage *m; + + m = dbus_message_new_method_call(MM_DBUS_SERVICE, "/org/freedesktop/ModemManager1", + DBUS_INTERFACE_OBJECTMANAGER, "GetManagedObjects"); + if (m == NULL) + goto fail; + + if (!mm_dbus_connection_send_with_reply(this, m, &this->pending, mm_get_managed_objects_reply, this)) { + spa_log_error(this->log, "dbus call failure"); + dbus_message_unref(m); + goto fail; + } + } + + return this; + +fail: + free(this); + return NULL; +} + +void mm_unregister(void *data) +{ + struct impl *this = data; + + if (this->pending != NULL) { + dbus_pending_call_cancel(this->pending); + dbus_pending_call_unref(this->pending); + } + + mm_clean_voice(this); + mm_clean_modem3gpp(this); + mm_clean_modem(this); + + if (this->filters_added) { + dbus_connection_remove_filter(this->conn, mm_filter_cb, this); + this->filters_added = false; + } + + if (this->allowed_modem_device) + free(this->allowed_modem_device); + + free(this); +} diff --git a/spa/plugins/bluez5/modemmanager.h b/spa/plugins/bluez5/modemmanager.h new file mode 100644 index 0000000..a239b2a --- /dev/null +++ b/spa/plugins/bluez5/modemmanager.h @@ -0,0 +1,161 @@ +/* Spa Bluez5 ModemManager proxy + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUEZ5_MODEMMANAGER_H_ +#define SPA_BLUEZ5_MODEMMANAGER_H_ + +#include + +#include "defs.h" + +enum cmee_error { + CMEE_AG_FAILURE = 0, + CMEE_NO_CONNECTION_TO_PHONE = 1, + CMEE_OPERATION_NOT_ALLOWED = 3, + CMEE_OPERATION_NOT_SUPPORTED = 4, + CMEE_INVALID_CHARACTERS_TEXT_STRING = 25, + CMEE_INVALID_CHARACTERS_DIAL_STRING = 27, + CMEE_NO_NETWORK_SERVICE = 30 +}; + +enum call_setup { + CIND_CALLSETUP_NONE = 0, + CIND_CALLSETUP_INCOMING, + CIND_CALLSETUP_DIALING, + CIND_CALLSETUP_ALERTING +}; + +enum call_direction { + CALL_OUTGOING, + CALL_INCOMING +}; + +enum call_state { + CLCC_ACTIVE, + CLCC_HELD, + CLCC_DIALING, + CLCC_ALERTING, + CLCC_INCOMING, + CLCC_WAITING, + CLCC_RESPONSE_AND_HOLD +}; + +struct call { + struct spa_list link; + unsigned int index; + struct impl *this; + DBusPendingCall *pending; + + char *path; + char *number; + bool call_indicator; + enum call_direction direction; + enum call_state state; + bool multiparty; +}; + +struct mm_ops { + void (*send_cmd_result)(bool success, enum cmee_error error, void *user_data); + void (*set_modem_service)(bool available, void *user_data); + void (*set_modem_signal_strength)(unsigned int strength, void *user_data); + void (*set_modem_operator_name)(const char *name, void *user_data); + void (*set_modem_own_number)(const char *number, void *user_data); + void (*set_modem_roaming)(bool is_roaming, void *user_data); + void (*set_call_active)(bool active, void *user_data); + void (*set_call_setup)(enum call_setup value, void *user_data); +}; + +#ifdef HAVE_BLUEZ_5_BACKEND_NATIVE_MM +void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info, + const struct mm_ops *ops, void *user_data); +void mm_unregister(void *data); +bool mm_is_available(void *modemmanager); +unsigned int mm_supported_features(); +bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error); +bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error); +bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error); +bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error); +const char *mm_get_incoming_call_number(void *modemmanager); +struct spa_list *mm_get_calls(void *modemmanager); +#else +void *mm_register(struct spa_log *log, void *dbus_connection, const struct spa_dict *info, + const struct mm_ops *ops, void *user_data) +{ + return NULL; +} + +void mm_unregister(void *data) +{ +} + +bool mm_is_available(void *modemmanager) +{ + return false; +} + +unsigned int mm_supported_features(void) +{ + return 0; +} + +bool mm_answer_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +bool mm_hangup_call(void *modemmanager, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +bool mm_do_call(void *modemmanager, const char* number, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +bool mm_send_dtmf(void *modemmanager, const char *dtmf, void *user_data, enum cmee_error *error) +{ + if (error) + *error = CMEE_OPERATION_NOT_SUPPORTED; + return false; +} + +const char *mm_get_incoming_call_number(void *modemmanager) +{ + return NULL; +} + +struct spa_list *mm_get_calls(void *modemmanager) +{ + return NULL; +} +#endif + +#endif diff --git a/spa/plugins/bluez5/org.bluez.xml b/spa/plugins/bluez5/org.bluez.xml new file mode 100644 index 0000000..dee131e --- /dev/null +++ b/spa/plugins/bluez5/org.bluez.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spa/plugins/bluez5/player.c b/spa/plugins/bluez5/player.c new file mode 100644 index 0000000..a77ca25 --- /dev/null +++ b/spa/plugins/bluez5/player.c @@ -0,0 +1,428 @@ +/* Spa Bluez5 AVRCP Player + * + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 \ + "" \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + " " \ + "" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define MAX_PROPERTIES 1 + +struct impl { + struct spa_bt_player this; + DBusConnection *conn; + char *path; + struct spa_log *log; + struct spa_dict_item properties_items[MAX_PROPERTIES]; + struct spa_dict properties; + unsigned int playing_count; +}; + +static size_t instance_counter = 0; + +static DBusMessage *properties_get(struct impl *impl, DBusMessage *m) +{ + const char *iface, *name; + size_t j; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) + return NULL; + + if (!spa_streq(iface, PLAYER_INTERFACE)) + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "No such interface"); + + for (j = 0; j < impl->properties.n_items; ++j) { + const struct spa_dict_item *item = &impl->properties.items[j]; + if (spa_streq(item->key, name)) { + DBusMessage *r; + DBusMessageIter i, v; + + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + "s", &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, + &item->value); + dbus_message_iter_close_container(&i, &v); + return r; + } + } + + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "No such property"); +} + +static void append_properties(struct impl *impl, DBusMessageIter *i) +{ + DBusMessageIter d, e, v; + size_t j; + + dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d); + + for (j = 0; j < impl->properties.n_items; ++j) { + const struct spa_dict_item *item = &impl->properties.items[j]; + + spa_log_debug(impl->log, "player %s: %s=%s", impl->path, + item->key, item->value); + + dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e); + dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key); + dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value); + dbus_message_iter_close_container(&e, &v); + dbus_message_iter_close_container(&d, &e); + } + + dbus_message_iter_close_container(i, &d); +} + +static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m) +{ + const char *iface, *name; + DBusMessage *r; + DBusMessageIter i; + + if (!dbus_message_get_args(m, NULL, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) + return NULL; + + if (!spa_streq(iface, PLAYER_INTERFACE)) + return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS, + "No such interface"); + + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + + dbus_message_iter_init_append(r, &i); + append_properties(impl, &i); + return r; +} + +static DBusMessage *properties_set(struct impl *impl, DBusMessage *m) +{ + return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY, + "Property not writable"); +} + +static DBusMessage *introspect(struct impl *impl, DBusMessage *m) +{ + const char *xml = PLAYER_INTROSPECT_XML; + DBusMessage *r; + if ((r = dbus_message_new_method_return(m)) == NULL) + return NULL; + if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)) + return NULL; + return r; +} + +static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata) +{ + struct impl *impl = userdata; + DBusMessage *r; + + if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { + r = introspect(impl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) { + r = properties_get(impl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) { + r = properties_get_all(impl, m); + } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) { + r = properties_set(impl, m); + } else { + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + if (r == NULL) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + if (!dbus_connection_send(impl->conn, r, NULL)) { + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + +static int send_update_signal(struct impl *impl) +{ + DBusMessage *m; + const char *iface = PLAYER_INTERFACE; + DBusMessageIter i, a; + int res = 0; + + m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged"); + if (m == NULL) + return -ENOMEM; + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface); + + append_properties(impl, &i); + + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &a); + dbus_message_iter_close_container(&i, &a); + + if (!dbus_connection_send(impl->conn, m, NULL)) + res = -EIO; + + dbus_message_unref(m); + + return res; +} + +static void update_properties(struct impl *impl, bool send_signal) +{ + int nitems = 0; + + switch (impl->this.state) { + case SPA_BT_PLAYER_PLAYING: + impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing"); + break; + case SPA_BT_PLAYER_STOPPED: + impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped"); + break; + } + impl->properties = SPA_DICT_INIT(impl->properties_items, nitems); + + if (!send_signal) + return; + + send_update_signal(impl); +} + +struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log) +{ + struct impl *impl; + const DBusObjectPathVTable vtable = { + .message_function = player_handler, + }; + + spa_log_topic_init(log, &log_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return NULL; + + impl->this.state = SPA_BT_PLAYER_STOPPED; + impl->conn = dbus_connection; + impl->log = log; + impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++); + if (impl->path == NULL) { + free(impl); + return NULL; + } + + dbus_connection_ref(impl->conn); + + update_properties(impl, false); + + if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) { + spa_bt_player_destroy(&impl->this); + errno = EIO; + return NULL; + } + + return &impl->this; +} + +void spa_bt_player_destroy(struct spa_bt_player *player) +{ + struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this); + + /* + * We unregister only the object path, but don't unregister it from + * BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is + * that the DBus connection is terminated immediately after. + */ + dbus_connection_unregister_object_path(impl->conn, impl->path); + + dbus_connection_unref(impl->conn); + free(impl->path); + free(impl); +} + +int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state) +{ + struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this); + + switch (state) { + case SPA_BT_PLAYER_PLAYING: + if (impl->playing_count++ > 0) + return 0; + break; + case SPA_BT_PLAYER_STOPPED: + if (impl->playing_count == 0) + return -EINVAL; + if (--impl->playing_count > 0) + return 0; + break; + default: + return -EINVAL; + } + + impl->this.state = state; + update_properties(impl, true); + + return 0; +} + +int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path) +{ + struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this); + + DBusError err; + DBusMessageIter i; + DBusMessage *m, *r; + int res = 0; + + spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s", + impl->path, adapter_path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path, + BLUEZ_MEDIA_INTERFACE, "RegisterPlayer"); + if (m == NULL) + return -EIO; + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path); + append_properties(impl, &i); + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err); + dbus_message_unref(m); + + if (r == NULL) { + spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message); + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(impl->log, "RegisterPlayer() failed"); + res = -EIO; + } + + dbus_message_unref(r); + + return res; +} + +int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path) +{ + struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this); + + DBusError err; + DBusMessageIter i; + DBusMessage *m, *r; + int res = 0; + + spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s", + impl->path, adapter_path); + + m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path, + BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer"); + if (m == NULL) + return -EIO; + + dbus_message_iter_init_append(m, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path); + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err); + dbus_message_unref(m); + + if (r == NULL) { + spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message); + dbus_error_free(&err); + return -EIO; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(impl->log, "UnregisterPlayer() failed"); + res = -EIO; + } + + dbus_message_unref(r); + + return res; +} diff --git a/spa/plugins/bluez5/player.h b/spa/plugins/bluez5/player.h new file mode 100644 index 0000000..b50eb6b --- /dev/null +++ b/spa/plugins/bluez5/player.h @@ -0,0 +1,51 @@ +/* Spa Bluez5 AVRCP Player + * + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUEZ5_PLAYER_H_ +#define SPA_BLUEZ5_PLAYER_H_ + +enum spa_bt_player_state { + SPA_BT_PLAYER_STOPPED, + SPA_BT_PLAYER_PLAYING, +}; + +/** + * Dummy AVRCP player. + * + * Some headsets require an AVRCP player to be present, before their + * AVRCP volume synchronization works. To work around this, we + * register a dummy player that does nothing. + */ +struct spa_bt_player { + enum spa_bt_player_state state; +}; + +struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log); +void spa_bt_player_destroy(struct spa_bt_player *player); +int spa_bt_player_set_state(struct spa_bt_player *player, + enum spa_bt_player_state state); +int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path); +int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path); + +#endif diff --git a/spa/plugins/bluez5/plugin.c b/spa/plugins/bluez5/plugin.c new file mode 100644 index 0000000..7b7f862 --- /dev/null +++ b/spa/plugins/bluez5/plugin.c @@ -0,0 +1,83 @@ +/* Spa Volume plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_bluez5_dbus_factory; + break; + case 1: + *factory = &spa_bluez5_device_factory; + break; + case 2: + *factory = &spa_media_sink_factory; + break; + case 3: + *factory = &spa_media_source_factory; + break; + case 4: + *factory = &spa_sco_sink_factory; + break; + case 5: + *factory = &spa_sco_source_factory; + break; + case 6: + *factory = &spa_a2dp_sink_factory; + break; + case 7: + *factory = &spa_a2dp_source_factory; + break; + case 8: + *factory = &spa_bluez5_midi_enum_factory; + break; + case 9: + *factory = &spa_bluez5_midi_node_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/bluez5/quirks.c b/spa/plugins/bluez5/quirks.c new file mode 100644 index 0000000..8a7f926 --- /dev/null +++ b/spa/plugins/bluez5/quirks.c @@ -0,0 +1,406 @@ +/* Device/adapter/kernel quirk table + * + * Copyright © 2021 Pauli Virtanen + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.quirks"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct spa_bt_quirks { + struct spa_log *log; + + int force_msbc; + int force_hw_volume; + int force_sbc_xq; + int force_faststream; + int force_a2dp_duplex; + + char *device_rules; + char *adapter_rules; + char *kernel_rules; +}; + +static enum spa_bt_feature parse_feature(const char *str) +{ + static const struct { const char *key; enum spa_bt_feature value; } feature_keys[] = { + { "msbc", SPA_BT_FEATURE_MSBC }, + { "msbc-alt1", SPA_BT_FEATURE_MSBC_ALT1 }, + { "msbc-alt1-rtl", SPA_BT_FEATURE_MSBC_ALT1_RTL }, + { "hw-volume", SPA_BT_FEATURE_HW_VOLUME }, + { "hw-volume-mic", SPA_BT_FEATURE_HW_VOLUME_MIC }, + { "sbc-xq", SPA_BT_FEATURE_SBC_XQ }, + { "faststream", SPA_BT_FEATURE_FASTSTREAM }, + { "a2dp-duplex", SPA_BT_FEATURE_A2DP_DUPLEX }, + }; + SPA_FOR_EACH_ELEMENT_VAR(feature_keys, f) { + if (spa_streq(str, f->key)) + return f->value; + } + return 0; +} + +static int do_match(const char *rules, struct spa_dict *dict, uint32_t *no_features) +{ + struct spa_json rules_json = SPA_JSON_INIT(rules, strlen(rules)); + struct spa_json rules_arr, it[2]; + + if (spa_json_enter_array(&rules_json, &rules_arr) <= 0) + return 1; + + while (spa_json_enter_object(&rules_arr, &it[0]) > 0) { + char key[256]; + int match = true; + uint32_t no_features_cur = 0; + + while (spa_json_get_string(&it[0], key, sizeof(key)) > 0) { + char val[4096]; + const char *str, *value; + int len; + bool success = false; + + if (spa_streq(key, "no-features")) { + if (spa_json_enter_array(&it[0], &it[1]) > 0) { + while (spa_json_get_string(&it[1], val, sizeof(val)) > 0) + no_features_cur |= parse_feature(val); + } + continue; + } + + if ((len = spa_json_next(&it[0], &value)) <= 0) + break; + + if (spa_json_is_null(value, len)) { + value = NULL; + } else { + if (spa_json_parse_stringn(value, len, val, sizeof(val)) < 0) + continue; + value = val; + } + + str = spa_dict_lookup(dict, key); + if (value == NULL) { + success = str == NULL; + } else if (str != NULL) { + if (value[0] == '~') { + regex_t r; + if (regcomp(&r, value+1, REG_EXTENDED | REG_NOSUB) == 0) { + if (regexec(&r, str, 0, NULL, 0) == 0) + success = true; + regfree(&r); + } + } else if (spa_streq(str, value)) { + success = true; + } + } + + if (!success) { + match = false; + break; + } + } + + if (match) { + *no_features = no_features_cur; + return 0; + } + } + return 0; +} + +static int parse_force_flag(const struct spa_dict *info, const char *key) +{ + const char *str; + str = spa_dict_lookup(info, key); + if (str == NULL) + return -1; + else + return (strcmp(str, "true") == 0 || atoi(str)) ? 1 : 0; +} + +static void load_quirks(struct spa_bt_quirks *this, const char *str, size_t len) +{ + struct spa_json data = SPA_JSON_INIT(str, len); + struct spa_json rules; + char key[1024]; + + if (spa_json_enter_object(&data, &rules) <= 0) + spa_json_init(&rules, str, len); + + while (spa_json_get_string(&rules, key, sizeof(key)) > 0) { + int sz; + const char *value; + + if ((sz = spa_json_next(&rules, &value)) <= 0) + break; + + if (!spa_json_is_container(value, sz)) + continue; + + sz = spa_json_container_len(&rules, value, sz); + + if (spa_streq(key, "bluez5.features.kernel") && !this->kernel_rules) + this->kernel_rules = strndup(value, sz); + else if (spa_streq(key, "bluez5.features.adapter") && !this->adapter_rules) + this->adapter_rules = strndup(value, sz); + else if (spa_streq(key, "bluez5.features.device") && !this->device_rules) + this->device_rules = strndup(value, sz); + } +} + +static int load_conf(struct spa_bt_quirks *this, const char *path) +{ + char *data; + struct stat sbuf; + int fd = -1; + + spa_log_debug(this->log, "loading %s", path); + + if ((fd = open(path, O_CLOEXEC | O_RDONLY)) < 0) + goto fail; + if (fstat(fd, &sbuf) < 0) + goto fail; + if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) + goto fail; + close(fd); + + load_quirks(this, data, sbuf.st_size); + munmap(data, sbuf.st_size); + + return 0; + +fail: + if (fd >= 0) + close(fd); + return -errno; +} + +struct spa_bt_quirks *spa_bt_quirks_create(const struct spa_dict *info, struct spa_log *log) +{ + struct spa_bt_quirks *this; + const char *str; + + if (!info) { + errno = -EINVAL; + return NULL; + } + + this = calloc(1, sizeof(struct spa_bt_quirks)); + if (this == NULL) + return NULL; + + this->log = log; + + spa_log_topic_init(this->log, &log_topic); + + this->force_sbc_xq = parse_force_flag(info, "bluez5.enable-sbc-xq"); + this->force_msbc = parse_force_flag(info, "bluez5.enable-msbc"); + this->force_hw_volume = parse_force_flag(info, "bluez5.enable-hw-volume"); + this->force_faststream = parse_force_flag(info, "bluez5.enable-faststream"); + this->force_a2dp_duplex = parse_force_flag(info, "bluez5.enable-a2dp-duplex"); + + if ((str = spa_dict_lookup(info, "bluez5.hardware-database")) != NULL) { + spa_log_debug(this->log, "loading session manager provided data"); + load_quirks(this, str, strlen(str)); + } else { + char path[PATH_MAX]; + const char *dir = getenv("SPA_DATA_DIR"); + int res; + + if (dir == NULL) + dir = SPADATADIR; + + if (spa_scnprintf(path, sizeof(path), "%s/bluez5/bluez-hardware.conf", dir) >= 0) + if ((res = load_conf(this, path)) < 0) + spa_log_warn(this->log, "failed to load '%s': %s", path, + spa_strerror(res)); + } + if (!(this->kernel_rules && this->adapter_rules && this->device_rules)) + spa_log_warn(this->log, "failed to load bluez-hardware.conf"); + + return this; +} + +void spa_bt_quirks_destroy(struct spa_bt_quirks *this) +{ + free(this->kernel_rules); + free(this->adapter_rules); + free(this->device_rules); + free(this); +} + +static void log_props(struct spa_log *log, const struct spa_dict *dict) +{ + const struct spa_dict_item *item; + spa_dict_for_each(item, dict) + spa_log_debug(log, "quirk property %s=%s", item->key, item->value); +} + +static void strtolower(char *src, char *dst, int maxsize) +{ + while (maxsize > 1 && *src != '\0') { + *dst = (*src >= 'A' && *src <= 'Z') ? ('a' + (*src - 'A')) : *src; + ++src; + ++dst; + --maxsize; + } + if (maxsize > 0) + *dst = '\0'; +} + +int spa_bt_quirks_get_features(const struct spa_bt_quirks *this, + const struct spa_bt_adapter *adapter, + const struct spa_bt_device *device, + uint32_t *features) +{ + struct spa_dict props; + struct spa_dict_item items[5]; + int res; + + *features = ~(uint32_t)0; + + /* Kernel */ + if (this->kernel_rules) { + uint32_t no_features = 0; + int nitems = 0; + struct utsname name; + if ((res = uname(&name)) < 0) + return res; + items[nitems++] = SPA_DICT_ITEM_INIT("sysname", name.sysname); + items[nitems++] = SPA_DICT_ITEM_INIT("release", name.release); + items[nitems++] = SPA_DICT_ITEM_INIT("version", name.version); + props = SPA_DICT_INIT(items, nitems); + log_props(this->log, &props); + do_match(this->kernel_rules, &props, &no_features); + spa_log_debug(this->log, "kernel quirks:%08x", no_features); + *features &= ~no_features; + } + + /* Adapter */ + if (this->adapter_rules && adapter) { + uint32_t no_features = 0; + int nitems = 0; + char vendor_id[64], product_id[64], address[64]; + + if (spa_bt_format_vendor_product_id( + adapter->source_id, adapter->vendor_id, adapter->product_id, + vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) { + items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id); + items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id); + } + items[nitems++] = SPA_DICT_ITEM_INIT("bus-type", + (adapter->bus_type == BUS_TYPE_USB) ? "usb" : "other"); + if (adapter->address) { + strtolower(adapter->address, address, sizeof(address)); + items[nitems++] = SPA_DICT_ITEM_INIT("address", address); + } + props = SPA_DICT_INIT(items, nitems); + log_props(this->log, &props); + do_match(this->adapter_rules, &props, &no_features); + spa_log_debug(this->log, "adapter quirks:%08x", no_features); + *features &= ~no_features; + } + + /* Device */ + if (this->device_rules && device) { + uint32_t no_features = 0; + int nitems = 0; + char vendor_id[64], product_id[64], version_id[64], address[64]; + if (spa_bt_format_vendor_product_id( + device->source_id, device->vendor_id, device->product_id, + vendor_id, sizeof(vendor_id), product_id, sizeof(product_id)) == 0) { + snprintf(version_id, sizeof(version_id), "%04x", + (unsigned int)device->version_id); + items[nitems++] = SPA_DICT_ITEM_INIT("vendor-id", vendor_id); + items[nitems++] = SPA_DICT_ITEM_INIT("product-id", product_id); + items[nitems++] = SPA_DICT_ITEM_INIT("version-id", version_id); + } + if (device->name) + items[nitems++] = SPA_DICT_ITEM_INIT("name", device->name); + if (device->address) { + strtolower(device->address, address, sizeof(address)); + items[nitems++] = SPA_DICT_ITEM_INIT("address", address); + } + props = SPA_DICT_INIT(items, nitems); + log_props(this->log, &props); + do_match(this->device_rules, &props, &no_features); + spa_log_debug(this->log, "device quirks:%08x", no_features); + *features &= ~no_features; + } + + /* Force flags */ + if (this->force_msbc != -1) { + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC, this->force_msbc); + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1, this->force_msbc); + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_MSBC_ALT1_RTL, this->force_msbc); + } + + if (this->force_hw_volume != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_HW_VOLUME, this->force_hw_volume); + + if (this->force_sbc_xq != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_SBC_XQ, this->force_sbc_xq); + + if (this->force_faststream != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_FASTSTREAM, this->force_faststream); + + if (this->force_a2dp_duplex != -1) + SPA_FLAG_UPDATE(*features, SPA_BT_FEATURE_A2DP_DUPLEX, this->force_a2dp_duplex); + + return 0; +} diff --git a/spa/plugins/bluez5/rtp.h b/spa/plugins/bluez5/rtp.h new file mode 100644 index 0000000..20694c1 --- /dev/null +++ b/spa/plugins/bluez5/rtp.h @@ -0,0 +1,74 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * 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..0657750 --- /dev/null +++ b/spa/plugins/bluez5/sco-io.c @@ -0,0 +1,289 @@ +/* Spa SCO I/O + * + * Copyright © 2019 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defs.h" + + +/* We'll use the read rx data size to find the correct packet size for writing, + * since kernel might not report it as the socket MTU, see + * https://lore.kernel.org/linux-bluetooth/20201210003528.3pmaxvubiwegxmhl@pali/T/ + * + * We continue reading also when there's no source connected, to keep socket + * flushed. + * + * XXX: when the kernel/backends start giving the right values, the heuristic + * XXX: can be removed + */ +#define MAX_MTU 1024 + + +struct spa_bt_sco_io { + bool started; + + uint8_t read_buffer[MAX_MTU]; + uint32_t read_size; + + int fd; + uint16_t read_mtu; + uint16_t write_mtu; + + struct spa_loop *data_loop; + struct spa_source source; + + int (*source_cb)(void *userdata, uint8_t *data, int size); + void *source_userdata; + + int (*sink_cb)(void *userdata); + void *sink_userdata; +}; + + +static void update_source(struct spa_bt_sco_io *io) +{ + int enabled; + int changed = 0; + + enabled = io->sink_cb != NULL; + if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_OUT) != enabled) { + SPA_FLAG_UPDATE(io->source.mask, SPA_IO_OUT, enabled); + changed = 1; + } + + if (changed) { + spa_loop_update_source(io->data_loop, &io->source); + } +} + +static void sco_io_on_ready(struct spa_source *source) +{ + struct spa_bt_sco_io *io = source->data; + + if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_IN)) { + int res; + + read_again: + res = read(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU)); + if (res <= 0) { + if (errno == EINTR) { + /* retry if interrupted */ + goto read_again; + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* no data: try it next time */ + goto read_done; + } + + /* error */ + goto stop; + } + + io->read_size = res; + + if (io->source_cb) { + int res; + res = io->source_cb(io->source_userdata, io->read_buffer, io->read_size); + if (res) { + io->source_cb = NULL; + } + } + } + +read_done: + if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_OUT)) { + if (io->sink_cb) { + int res; + res = io->sink_cb(io->sink_userdata); + if (res) { + io->sink_cb = NULL; + } + } + } + + if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) || SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) { + goto stop; + } + + /* Poll socket in/out only if necessary */ + update_source(io); + + return; + +stop: + if (io->source.loop) { + spa_loop_remove_source(io->data_loop, &io->source); + io->started = false; + } +} + +/* + * Write data to socket in correctly sized blocks. + * Returns the number of bytes written, 0 when data cannot be written now or + * there is too little of it to write, and <0 on write error. + */ +int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *buf, int size) +{ + uint16_t packet_size; + uint8_t *buf_start = buf; + + if (io->read_size == 0) { + /* The proper write packet size is not known yet */ + return 0; + } + + packet_size = SPA_MIN(io->write_mtu, io->read_size); + + if (size < packet_size) { + return 0; + } + + do { + int written; + + written = write(io->fd, buf, packet_size); + if (written < 0) { + if (errno == EINTR) { + /* retry if interrupted */ + continue; + } else if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* Don't continue writing */ + break; + } + return -errno; + } + + buf += written; + size -= written; + } while (size >= packet_size); + + return buf - buf_start; +} + + +struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_loop *data_loop, + int fd, + uint16_t read_mtu, + uint16_t write_mtu) +{ + struct spa_bt_sco_io *io; + + io = calloc(1, sizeof(struct spa_bt_sco_io)); + if (io == NULL) + return io; + + io->fd = fd; + io->read_mtu = read_mtu; + io->write_mtu = write_mtu; + io->data_loop = data_loop; + + io->read_size = 0; + + /* Add the ready callback */ + io->source.data = io; + io->source.fd = io->fd; + io->source.func = sco_io_on_ready; + io->source.mask = SPA_IO_IN | SPA_IO_OUT | SPA_IO_ERR | SPA_IO_HUP; + io->source.rmask = 0; + spa_loop_add_source(io->data_loop, &io->source); + + io->started = true; + + return io; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct spa_bt_sco_io *io = user_data; + + if (io->source.loop) + spa_loop_remove_source(io->data_loop, &io->source); + + return 0; +} + +void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io) +{ + if (io->started) + spa_loop_invoke(io->data_loop, do_remove_source, 0, NULL, 0, true, io); + + io->started = false; + free(io); +} + +/* Set source callback. + * This function should only be called from the data thread. + * Callback is called (in data loop) with data just read from the socket. + */ +void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *, uint8_t *, int), void *userdata) +{ + io->source_cb = source_cb; + io->source_userdata = userdata; + + if (io->started) { + update_source(io); + } +} + +/* Set sink callback. + * This function should only be called from the data thread. + * Callback is called (in data loop) when socket can be written to. + */ +void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *), void *userdata) +{ + io->sink_cb = sink_cb; + io->sink_userdata = userdata; + + if (io->started) { + update_source(io); + } +} diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c new file mode 100644 index 0000000..18a34f6 --- /dev/null +++ b/spa/plugins/bluez5/sco-sink.c @@ -0,0 +1,1517 @@ +/* Spa SCO Sink + * + * Copyright © 2019 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.sink.sco"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + char clock_name[64]; +}; + +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; + unsigned int outstanding:1; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + struct spa_audio_info current_format; + int frame_size; + unsigned int have_format:1; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_io_rate_match *rate_match; + struct spa_latency_info latency; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list ready; + + struct buffer *current_buffer; + uint32_t ready_offset; + uint8_t write_buffer[4096]; + uint32_t write_buffer_size; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + /* Support */ + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + /* Hooks and callbacks */ + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + /* Info */ + uint64_t info_all; + struct spa_node_info info; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + uint32_t quantum_limit; + + /* Transport */ + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + + /* Port */ + struct port port; + + /* Flags */ + unsigned int started:1; + unsigned int following:1; + unsigned int flush_pending:1; + + /* Sources */ + struct spa_source source; + struct spa_source flush_timer_source; + + /* Timer */ + int timerfd; + int flush_timerfd; + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint64_t current_time; + uint64_t next_time; + uint64_t process_time; + uint64_t prev_flush_time; + uint64_t next_flush_time; + + /* mSBC */ + sbc_t msbc; + uint8_t *buffer; + uint8_t *buffer_head; + uint8_t *buffer_next; + int buffer_size; + int msbc_seq; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0) + +static const char sntable[4] = { 0x08, 0x38, 0xC8, 0xF8 }; + +static void reset_props(struct props *props) +{ + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + switch (result.index) { + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int set_timeout(struct impl *this, uint64_t time) +{ + struct itimerspec ts; + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + return spa_system_timerfd_settime(this->data_system, + this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static int set_timers(struct impl *this) +{ + struct timespec now; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + return set_timeout(this, this->following ? 0 : this->next_time); +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + set_timers(this); + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + bool following; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + + following = is_following(this); + if (this->started && following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full); + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct props new_props = this->props; + int changed = 0; + + if (param == NULL) { + reset_props(&new_props); + } else { + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL); + } + + changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); + this->props = new_props; + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + if (apply_props(this, param) > 0) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, false); + } + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static void enable_flush_timer(struct impl *this, bool enabled) +{ + struct itimerspec ts; + + if (!enabled) + this->next_flush_time = 0; + + ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, + this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); + + this->flush_pending = enabled; +} + +static uint32_t get_queued_frames(struct impl *this) +{ + struct port *port = &this->port; + uint32_t bytes = 0; + struct buffer *b; + + spa_list_for_each(b, &port->ready, link) { + struct spa_data *d = b->buf->datas; + + bytes += d[0].chunk->size; + } + + if (bytes > port->ready_offset) + bytes -= port->ready_offset; + else + bytes = 0; + + return bytes / port->frame_size; +} + +static void flush_data(struct impl *this) +{ + struct port *port = &this->port; + const uint32_t min_in_size = + (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? + MSBC_DECODED_SIZE : this->transport->write_mtu; + uint8_t * const packet = + (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? + this->buffer_head : port->write_buffer; + const uint32_t packet_samples = min_in_size / port->frame_size; + const uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC + / port->current_format.info.raw.rate; + int processed = 0; + int written; + + if (this->transport == NULL || this->transport->sco_io == NULL) + return; + + while (!spa_list_is_empty(&port->ready) && port->write_buffer_size < min_in_size) { + struct spa_data *datas; + + /* get buffer */ + if (!port->current_buffer) { + spa_return_if_fail(!spa_list_is_empty(&port->ready)); + port->current_buffer = spa_list_first(&port->ready, struct buffer, link); + port->ready_offset = 0; + } + datas = port->current_buffer->buf->datas; + + /* if buffer has data, copy it into the write buffer */ + if (datas[0].chunk->size - port->ready_offset > 0) { + const uint32_t avail = + SPA_MIN(min_in_size, datas[0].chunk->size - port->ready_offset); + const uint32_t size = + (avail + port->write_buffer_size) > min_in_size ? + min_in_size - port->write_buffer_size : avail; + memcpy(port->write_buffer + port->write_buffer_size, + (uint8_t *)datas[0].data + port->ready_offset, + size); + port->write_buffer_size += size; + port->ready_offset += size; + } else { + struct buffer *b; + + b = port->current_buffer; + port->current_buffer = NULL; + + /* reuse buffer */ + spa_list_remove(&b->link); + b->outstanding = true; + spa_log_trace(this->log, "sco-sink %p: reuse buffer %u", this, b->id); + port->io->buffer_id = b->id; + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } + } + + if (this->flush_pending) { + spa_log_trace(this->log, "%p: wait for flush timer", this); + return; + } + + if (port->write_buffer_size < min_in_size) { + /* wait for more data */ + spa_log_trace(this->log, "%p: skip flush", this); + enable_flush_timer(this, false); + return; + } + + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + ssize_t out_encoded; + + /* Encode */ + if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) { + /* Buffer overrun; shouldn't usually happen. Drop data and reset. */ + this->buffer_head = this->buffer_next = this->buffer; + spa_log_warn(this->log, "sco-sink: mSBC buffer overrun, dropping data"); + } + this->buffer_next[0] = 0x01; + this->buffer_next[1] = sntable[this->msbc_seq % 4]; + this->buffer_next[59] = 0x00; + this->msbc_seq = (this->msbc_seq + 1) % 4; + processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size, + this->buffer_next + 2, MSBC_ENCODED_SIZE - 3, &out_encoded); + if (processed < 0) { + spa_log_warn(this->log, "sbc_encode failed: %d", processed); + return; + } + this->buffer_next += out_encoded + 3; + port->write_buffer_size = 0; + + /* Write */ + written = spa_bt_sco_io_write(this->transport->sco_io, packet, + this->buffer_next - this->buffer_head); + if (written < 0) { + spa_log_warn(this->log, "failed to write data: %d (%s)", + written, spa_strerror(written)); + goto stop; + } + + this->buffer_head += written; + + if (this->buffer_head == this->buffer_next) + this->buffer_head = this->buffer_next = this->buffer; + else if (this->buffer_next + MSBC_ENCODED_SIZE > this->buffer + this->buffer_size) { + /* Written bytes is not necessarily commensurate + * with MSBC_ENCODED_SIZE. If this occurs, copy data. + */ + int size = this->buffer_next - this->buffer_head; + spa_memmove(this->buffer, this->buffer_head, size); + this->buffer_next = this->buffer + size; + this->buffer_head = this->buffer; + } + } else { + written = spa_bt_sco_io_write(this->transport->sco_io, packet, + port->write_buffer_size); + if (written < 0) { + spa_log_warn(this->log, "sco-sink: write failure: %d (%s)", + written, spa_strerror(written)); + goto stop; + } else if (written == 0) { + /* EAGAIN or similar, just skip ahead */ + written = SPA_MIN(port->write_buffer_size, (uint32_t)48); + } + + processed = written; + port->write_buffer_size -= written; + + if (port->write_buffer_size > 0 && written > 0) { + spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size); + } + } + + if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { + struct timespec ts; + uint64_t now; + uint64_t dt; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts); + now = SPA_TIMESPEC_TO_NSEC(&ts); + dt = now - this->prev_flush_time; + this->prev_flush_time = now; + + spa_log_trace(this->log, + "%p: send wrote:%d dt:%"PRIu64, + this, written, dt); + } + + spa_log_trace(this->log, "write socket data %d", written); + + if (SPA_LIKELY(this->position)) { + uint32_t frames = get_queued_frames(this); + uint64_t duration_ns; + + /* + * Flush at the time position of the next buffered sample. + */ + duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC + / this->position->clock.rate.denom); + this->next_flush_time = this->process_time + duration_ns + - ((uint64_t)frames * SPA_NSEC_PER_SEC + / port->current_format.info.raw.rate); + + /* + * We could delay the output by one packet to avoid waiting + * for the next buffer and so make send intervals more regular. + * However, this appears not needed in practice, and it's better + * to not add latency if not needed. + */ +#if 0 + this->next_flush_time += SPA_MIN(packet_time, + duration_ns * (port->n_buffers - 1)); +#endif + } else { + if (this->next_flush_time == 0) + this->next_flush_time = this->process_time; + this->next_flush_time += packet_time; + } + + enable_flush_timer(this, true); + return; + +stop: + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + enable_flush_timer(this, false); +} + +static void sco_on_flush_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t exp; + int res; + + spa_log_trace(this->log, "%p: flush on timeout", this); + + if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); + return; + } + + if (this->transport == NULL) { + enable_flush_timer(this, false); + return; + } + + while (exp-- > 0) { + this->flush_pending = false; + flush_data(this); + } +} + +static void sco_on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + uint64_t exp, duration; + uint32_t rate; + struct spa_io_buffers *io = port->io; + uint64_t prev_time, now_time; + int res; + + if (this->transport == NULL) + return; + + if (this->started) { + if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); + return; + } + } + + prev_time = this->current_time; + now_time = this->current_time = this->next_time; + + spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, + now_time, now_time - prev_time); + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->rate_diff = 1.0f; + this->clock->next_nsec = this->next_time; + this->clock->delay = 0; + } + + spa_log_trace(this->log, "%p: %d", this, io->status); + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); + + set_timeout(this, this->next_time); +} + +/* greater common divider */ +static int gcd(int a, int b) { + while(b) { + int c = b; + b = a % b; + a = c; + } + return a; +} +/* least common multiple */ +static int lcm(int a, int b) { + return (a*b)/gcd(a,b); +} + +static int do_start(struct impl *this) +{ + bool do_accept; + int res; + + /* Don't do anything if the node has already started */ + if (this->started) + return 0; + + /* Make sure the transport is valid */ + spa_return_val_if_fail(this->transport != NULL, -EIO); + + this->following = is_following(this); + + spa_log_debug(this->log, "%p: start following:%d", this, this->following); + + /* Do accept if Gateway; otherwise do connect for Head Unit */ + do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; + + /* acquire the socket fd (false -> connect | true -> accept) */ + if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) + return res; + + /* Init mSBC if needed */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + sbc_init_msbc(&this->msbc, 0); + /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ + this->msbc.endian = SBC_LE; + + /* write_mtu might not be correct at this point, so we'll throw + * in some common ones, at the cost of a potentially larger + * allocation (size <= 120 * write_mtu). If it still fails to be + * commensurate, we may end up doing memmoves, but nothing worse + * is going to happen. + */ + this->buffer_size = lcm(24, lcm(60, lcm(this->transport->write_mtu, 2 * MSBC_ENCODED_SIZE))); + this->buffer = calloc(this->buffer_size, sizeof(uint8_t)); + this->buffer_head = this->buffer_next = this->buffer; + if (this->buffer == NULL) { + res = -errno; + goto fail; + } + } + + spa_return_val_if_fail(this->transport->write_mtu <= sizeof(this->port.write_buffer), -EINVAL); + + /* start socket i/o */ + if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0) + goto fail; + + /* Add the timeout callback */ + this->source.data = this; + this->source.fd = this->timerfd; + this->source.func = sco_on_timeout; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->source); + + this->flush_timer_source.data = this; + this->flush_timer_source.fd = this->flush_timerfd; + this->flush_timer_source.func = sco_on_flush_timeout; + this->flush_timer_source.mask = SPA_IO_IN; + this->flush_timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->flush_timer_source); + + /* start processing */ + this->flush_pending = false; + set_timers(this); + + /* Set the started flag */ + this->started = true; + + return 0; + +fail: + free(this->buffer); + this->buffer = NULL; + spa_bt_transport_release(this->transport); + return res; +} + +/* Drop any buffered data remaining in the port */ +static void drop_port_output(struct impl *this) +{ + struct port *port = &this->port; + + port->write_buffer_size = 0; + port->current_buffer = NULL; + port->ready_offset = 0; + + while (!spa_list_is_empty(&port->ready)) { + struct buffer *b; + b = spa_list_first(&port->ready, struct buffer, link); + + spa_list_remove(&b->link); + b->outstanding = true; + port->io->buffer_id = b->id; + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct itimerspec ts; + + set_timeout(this, 0); + if (this->source.loop) + spa_loop_remove_source(this->data_loop, &this->source); + + if (this->flush_timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->flush_timerfd, 0, &ts, NULL); + + /* Drop buffered data in the ready queue. Ideally there shouldn't be any. */ + drop_port_output(this); + + return 0; +} + +static int do_stop(struct impl *this) +{ + int res = 0; + + if (!this->started) + return 0; + + spa_log_trace(this->log, "sco-sink %p: stop", this); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + this->started = false; + + if (this->buffer) { + free(this->buffer); + this->buffer = NULL; + this->buffer_head = this->buffer_next = this->buffer; + } + + if (this->transport) { + /* Release the transport; it is responsible for closing the fd */ + res = spa_bt_transport_release(this->transport); + } + + return res; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + if ((res = do_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = do_stop(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + static const struct spa_dict_item hu_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, "Audio/Sink" }, + { SPA_KEY_NODE_DRIVER, "true" }, + }; + + const struct spa_dict_item ag_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" }, + { "media.name", ((this->transport && this->transport->device->name) ? + this->transport->device->name : "HSP/HFP") }, + { SPA_KEY_MEDIA_ROLE, "Communication" }, + }; + bool is_ag = this->transport && + (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = is_ag ? + &SPA_DICT_INIT_ARRAY(ag_node_info_items) : + &SPA_DICT_INIT_ARRAY(hu_node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (result.index > 0) + return 0; + if (this->transport == NULL) + return -EIO; + + /* set the info structure */ + struct spa_audio_info_raw info = { 0, }; + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.channels = 1; + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + + /* CVSD format has a rate of 8kHz + * MSBC format has a rate of 16kHz */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) + info.rate = 16000; + else + info.rate = 8000; + + /* build the param */ + param = spa_format_audio_raw_build(&b, id, &info); + + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * port->frame_size, + 16 * port->frame_size, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: + param = spa_latency_build(&b, id, &port->latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + do_stop(this); + if (port->n_buffers > 0) { + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int err; + + if (format == NULL) { + spa_log_debug(this->log, "clear format"); + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE || + info.info.raw.rate == 0 || + info.info.raw.channels != 1) + return -EINVAL; + + port->frame_size = info.info.raw.channels * 2; + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + res = 0; + break; + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + spa_log_debug(this->log, "use buffers %d", n_buffers); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + + b->buf = buffers[i]; + b->id = i; + b->outstanding = true; + + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (buffers[i]->datas[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + port->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + + if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) { + struct buffer *b = &port->buffers[io->buffer_id]; + + if (!b->outstanding) { + spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + + spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id); + + spa_list_append(&port->ready, &b->link); + b->outstanding = false; + io->buffer_id = SPA_ID_INVALID; + io->status = SPA_STATUS_OK; + } + + if (this->following) { + if (this->position) { + this->current_time = this->position->clock.nsec; + } else { + struct timespec now; + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->current_time = SPA_TIMESPEC_TO_NSEC(&now); + } + } + + this->process_time = this->current_time; + + if (!spa_list_is_empty(&port->ready)) { + spa_log_trace(this->log, "%p: flush on process", this); + flush_data(this); + } + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int do_transport_destroy(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + this->transport = NULL; + return 0; +} + +static void transport_destroy(void *data) +{ + struct impl *this = data; + spa_log_debug(this->log, "transport %p destroy", this->transport); + spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + + do_stop(this); + if (this->transport) + spa_hook_remove(&this->transport_listener); + spa_system_close(this->data_system, this->timerfd); + spa_system_close(this->data_system, this->flush_timerfd); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_log_topic_init(this->log, &log_topic); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS | + SPA_NODE_CHANGE_MASK_PROPS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = 0; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + port->latency.min_quantum = 1.0f; + port->latency.max_quantum = 1.0f; + + spa_list_init(&port->ready); + + this->quantum_limit = 8192; + + if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) + spa_atou32(str, &this->quantum_limit, 0); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT))) + sscanf(str, "pointer:%p", &this->transport); + + if (this->transport == NULL) { + spa_log_error(this->log, "a transport is needed"); + return -EINVAL; + } + spa_bt_transport_add_listener(this->transport, + &this->transport_listener, &transport_events, this); + + this->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + this->flush_timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. " }, + { 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..6f5fe08 --- /dev/null +++ b/spa/plugins/bluez5/sco-source.c @@ -0,0 +1,1592 @@ +/* Spa SCO Source + * + * Copyright © 2019 Collabora Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.source.sco"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#include "decode-buffer.h" + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + char clock_name[64]; +}; + +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; + unsigned int outstanding:1; + struct spa_buffer *buf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + struct spa_audio_info current_format; + int frame_size; + unsigned int have_format:1; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_io_rate_match *rate_match; + struct spa_latency_info latency; +#define IDX_EnumFormat 0 +#define IDX_Meta 1 +#define IDX_IO 2 +#define IDX_Format 3 +#define IDX_Buffers 4 +#define IDX_Latency 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list free; + struct spa_list ready; + + struct spa_bt_decode_buffer buffer; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + uint32_t quantum_limit; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_PropInfo 0 +#define IDX_Props 1 +#define IDX_NODE_IO 2 +#define N_NODE_PARAMS 3 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_bt_transport *transport; + struct spa_hook transport_listener; + + struct port port; + + unsigned int started:1; + unsigned int following:1; + unsigned int matching:1; + unsigned int resampling:1; + + struct spa_source timer_source; + int timerfd; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint64_t current_time; + uint64_t next_time; + + /* mSBC */ + sbc_t msbc; + bool msbc_seq_initialized; + uint8_t msbc_seq; + + /* mSBC frame parsing */ + uint8_t msbc_buffer[MSBC_ENCODED_SIZE]; + uint8_t msbc_buffer_pos; + + struct timespec now; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) + +static void reset_props(struct props *props) +{ + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + switch (result.index) { + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + switch (result.index) { + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int set_timeout(struct impl *this, uint64_t time) +{ + struct itimerspec ts; + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + return spa_system_timerfd_settime(this->data_system, + this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static int set_timers(struct impl *this) +{ + struct timespec now; + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + return set_timeout(this, this->following ? 0 : this->next_time); +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct port *port = &this->port; + + set_timers(this); + spa_bt_decode_buffer_recover(&port->buffer); + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + bool following; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + + following = is_following(this); + if (this->started && following != this->following) { + spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + + return 0; +} + +static void emit_node_info(struct impl *this, bool full); + +static int apply_props(struct impl *this, const struct spa_pod *param) +{ + struct props new_props = this->props; + int changed = 0; + + if (param == NULL) { + reset_props(&new_props); + } else { + /* noop */ + } + + changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); + this->props = new_props; + return changed; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + if (apply_props(this, param) > 0) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; + emit_node_info(this, false); + } + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static void reset_buffers(struct port *port) +{ + uint32_t i; + + spa_list_init(&port->free); + spa_list_init(&port->ready); + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } +} + +static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + + if (b->outstanding) { + spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } +} + +/* Append data to msbc buffer, syncing buffer start to frame headers */ +static void msbc_buffer_append_byte(struct impl *this, uint8_t byte) +{ + /* Parse mSBC frame header */ + if (this->msbc_buffer_pos == 0) { + if (byte != 0x01) { + this->msbc_buffer_pos = 0; + return; + } + } + else if (this->msbc_buffer_pos == 1) { + if (!((byte & 0x0F) == 0x08 && + ((byte >> 4) & 1) == ((byte >> 5) & 1) && + ((byte >> 6) & 1) == ((byte >> 7) & 1))) { + this->msbc_buffer_pos = 0; + return; + } + } + else if (this->msbc_buffer_pos == 2) { + /* .. and beginning of MSBC frame: SYNCWORD + 2 nul bytes */ + if (byte != 0xAD) { + this->msbc_buffer_pos = 0; + return; + } + } + else if (this->msbc_buffer_pos == 3) { + if (byte != 0x00) { + this->msbc_buffer_pos = 0; + return; + } + } + else if (this->msbc_buffer_pos == 4) { + if (byte != 0x00) { + this->msbc_buffer_pos = 0; + return; + } + } + else if (this->msbc_buffer_pos >= MSBC_ENCODED_SIZE) { + /* Packet completed. Reset. */ + this->msbc_buffer_pos = 0; + msbc_buffer_append_byte(this, byte); + return; + } + this->msbc_buffer[this->msbc_buffer_pos] = byte; + ++this->msbc_buffer_pos; +} + +/* Helper function for debugging */ +static SPA_UNUSED void hexdump_to_log(struct impl *this, uint8_t *data, size_t size) +{ + char buf[2048]; + size_t i, col = 0, pos = 0; + buf[0] = '\0'; + for (i = 0; i < size; ++i) { + int res; + res = spa_scnprintf(buf + pos, sizeof(buf) - pos, "%s%02x", + (col == 0) ? "\n\t" : " ", data[i]); + if (res < 0) + break; + pos += res; + col = (col + 1) % 16; + } + spa_log_trace(this->log, "hexdump (%d bytes):%s", (int)size, buf); +} + +/* helper function to detect if a packet consists only of zeros */ +static bool is_zero_packet(uint8_t *data, int size) +{ + for (int i = 0; i < size; ++i) { + if (data[i] != 0) { + return false; + } + } + return true; +} + +static uint32_t preprocess_and_decode_msbc_data(void *userdata, uint8_t *read_data, int size_read) +{ + struct impl *this = userdata; + struct port *port = &this->port; + uint32_t decoded = 0; + int i; + + spa_log_trace(this->log, "handling mSBC data"); + + /* + * Check if the packet contains only zeros - if so ignore the packet. + * This is necessary, because some kernels insert bogus "all-zero" packets + * into the datastream. + * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549 + */ + if (is_zero_packet(read_data, size_read)) + return 0; + + for (i = 0; i < size_read; ++i) { + void *buf; + uint32_t avail; + int seq, processed; + size_t written; + + msbc_buffer_append_byte(this, read_data[i]); + + if (this->msbc_buffer_pos != MSBC_ENCODED_SIZE) + continue; + + /* + * Handle found mSBC packet + */ + + buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + + /* Check sequence number */ + seq = ((this->msbc_buffer[1] >> 4) & 1) | + ((this->msbc_buffer[1] >> 6) & 2); + + spa_log_trace(this->log, "mSBC packet seq=%u", seq); + if (!this->msbc_seq_initialized) { + this->msbc_seq_initialized = true; + this->msbc_seq = seq; + } else if (seq != this->msbc_seq) { + /* TODO: PLC (too late to insert data now) */ + spa_log_info(this->log, + "missing mSBC packet: %u != %u", seq, this->msbc_seq); + this->msbc_seq = seq; + } + + this->msbc_seq = (this->msbc_seq + 1) % 4; + + if (avail < MSBC_DECODED_SIZE) + spa_log_warn(this->log, "Output buffer full, dropping msbc data"); + + /* decode frame */ + processed = sbc_decode( + &this->msbc, this->msbc_buffer + 2, MSBC_ENCODED_SIZE - 3, + buf, avail, &written); + + if (processed < 0) { + spa_log_warn(this->log, "sbc_decode failed: %d", processed); + /* TODO: manage errors */ + continue; + } + + spa_bt_decode_buffer_write_packet(&port->buffer, written); + decoded += written; + } + + return decoded; +} + +static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read) +{ + struct impl *this = userdata; + struct port *port = &this->port; + uint32_t decoded; + uint64_t dt; + + if (this->transport == NULL) { + spa_log_debug(this->log, "no transport, stop reading"); + goto stop; + } + + /* update the current pts */ + dt = SPA_TIMESPEC_TO_NSEC(&this->now); + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now); + dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt; + + /* handle data read from socket */ +#if 0 + hexdump_to_log(this, read_data, size_read); +#endif + + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + decoded = preprocess_and_decode_msbc_data(userdata, read_data, size_read); + } else { + uint32_t avail; + uint8_t *packet; + + if (size_read != 48 && is_zero_packet(read_data, size_read)) { + /* Adapter is returning non-standard CVSD stream. For example + * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021 + * on kernel 5.13.19 produces such data. + */ + return 0; + } + + if (size_read % port->frame_size != 0) { + /* Unaligned data: reception or adapter problem. + * Consider the whole packet lost and report. + */ + spa_log_debug(this->log, + "received bad Bluetooth SCO CVSD packet"); + return 0; + } + + packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + avail = SPA_MIN(avail, (uint32_t)size_read); + spa_memmove(packet, read_data, avail); + spa_bt_decode_buffer_write_packet(&port->buffer, avail); + + decoded = avail; + } + + spa_log_trace(this->log, "read socket data size:%d decoded frames:%d dt:%d dms", + size_read, decoded / port->frame_size, + (int)(dt / 100000)); + + return 0; + +stop: + return 1; +} + +static int setup_matching(struct impl *this) +{ + struct port *port = &this->port; + + if (this->position && port->rate_match) { + port->rate_match->rate = 1 / port->buffer.corr; + + this->matching = this->following; + this->resampling = this->matching || + (port->current_format.info.raw.rate != this->position->clock.rate.denom); + } else { + this->matching = false; + this->resampling = false; + } + + if (port->rate_match) + SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching); + + return 0; +} + +static int produce_buffer(struct impl *this); + +static void sco_on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + uint64_t exp, duration; + uint32_t rate; + uint64_t prev_time, now_time; + int res; + + if (this->transport == NULL) + return; + + if (this->started) { + if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading timerfd: %s", + spa_strerror(res)); + return; + } + } + + prev_time = this->current_time; + now_time = this->current_time = this->next_time; + + spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, + now_time, now_time - prev_time); + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + setup_matching(this); + + this->next_time = now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = now_time; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->rate_diff = port->buffer.corr; + this->clock->next_nsec = this->next_time; + } + + if (port->io) { + int status = produce_buffer(this); + spa_log_trace(this->log, "%p: io:%d status:%d", this, port->io->status, status); + } + + spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); + + set_timeout(this, this->next_time); +} + +static int do_add_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + + spa_bt_sco_io_set_source_cb(this->transport->sco_io, sco_source_cb, this); + + return 0; +} + +static int do_start(struct impl *this) +{ + struct port *port = &this->port; + bool do_accept; + int res; + + /* Don't do anything if the node has already started */ + if (this->started) + return 0; + + this->following = is_following(this); + + spa_log_debug(this->log, "%p: start following:%d", + this, this->following); + + /* Make sure the transport is valid */ + spa_return_val_if_fail (this->transport != NULL, -EIO); + + /* Do accept if Gateway; otherwise do connect for Head Unit */ + do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; + + /* acquire the socket fd (false -> connect | true -> accept) */ + if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) + return res; + + /* Reset the buffers and sample count */ + reset_buffers(port); + + spa_bt_decode_buffer_clear(&port->buffer); + if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log, + port->frame_size, port->current_format.info.raw.rate, + this->quantum_limit, this->quantum_limit)) < 0) + return res; + + /* Init mSBC if needed */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { + sbc_init_msbc(&this->msbc, 0); + /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ + this->msbc.endian = SBC_LE; + this->msbc_seq_initialized = false; + + this->msbc_buffer_pos = 0; + } + + /* Start socket i/o */ + if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0) + goto fail; + spa_loop_invoke(this->data_loop, do_add_source, 0, NULL, 0, true, this); + + /* Start timer */ + this->timer_source.data = this; + this->timer_source.fd = this->timerfd; + this->timer_source.func = sco_on_timeout; + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->timer_source); + + setup_matching(this); + set_timers(this); + + /* Set the started flag */ + this->started = true; + + return 0; + +fail: + spa_bt_transport_release(this->transport); + return res; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + struct itimerspec ts; + + if (this->transport && this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); + + if (this->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->timer_source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(this->data_system, this->timerfd, 0, &ts, NULL); + + return 0; +} + +static int do_stop(struct impl *this) +{ + struct port *port = &this->port; + int res = 0; + + if (!this->started) + return 0; + + spa_log_debug(this->log, "sco-source %p: stop", this); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + + this->started = false; + + if (this->transport) { + /* Release the transport; it is responsible for closing the fd */ + res = spa_bt_transport_release(this->transport); + } + + spa_bt_decode_buffer_clear(&port->buffer); + + return res; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if ((res = do_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = do_stop(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + static const struct spa_dict_item hu_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, "Audio/Source" }, + { SPA_KEY_NODE_DRIVER, "true" }, + }; + const struct spa_dict_item ag_node_info_items[] = { + { SPA_KEY_DEVICE_API, "bluez5" }, + { SPA_KEY_MEDIA_CLASS, "Stream/Output/Audio" }, + { "media.name", ((this->transport && this->transport->device->name) ? + this->transport->device->name : "HSP/HFP") }, + { SPA_KEY_MEDIA_ROLE, "Communication" }, + }; + bool is_ag = this->transport && (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + uint64_t old = full ? this->info.change_mask : 0; + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = is_ag ? + &SPA_DICT_INIT_ARRAY(ag_node_info_items) : + &SPA_DICT_INIT_ARRAY(hu_node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if (result.index > 0) + return 0; + if (this->transport == NULL) + return -EIO; + + /* set the info structure */ + struct spa_audio_info_raw info = { 0, }; + info.format = SPA_AUDIO_FORMAT_S16_LE; + info.channels = 1; + info.position[0] = SPA_AUDIO_CHANNEL_MONO; + + /* CVSD format has a rate of 8kHz + * MSBC format has a rate of 16kHz */ + if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) + info.rate = 16000; + else + info.rate = 8000; + + /* build the param */ + param = spa_format_audio_raw_build(&b, id, &info); + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * port->frame_size, + 16 * port->frame_size, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: + param = spa_latency_build(&b, id, &port->latency); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + do_stop(this); + if (port->n_buffers > 0) { + spa_list_init(&port->free); + spa_list_init(&port->ready); + port->n_buffers = 0; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int err; + + if (format == NULL) { + spa_log_debug(this->log, "clear format"); + clear_buffers(this, port); + port->have_format = false; + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format != SPA_AUDIO_FORMAT_S16_LE || + info.info.raw.rate == 0 || + info.info.raw.channels != 1) + return -EINVAL; + + port->frame_size = info.info.raw.channels * 2; + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + } else { + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + case SPA_PARAM_Latency: + res = 0; + break; + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + spa_log_debug(this->log, "use buffers %d", n_buffers); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_list_append(&port->free, &b->link); + b->outstanding = false; + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_RateMatch: + port->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + + if (port->n_buffers == 0) + return -EIO; + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + recycle_buffer(this, port, buffer_id); + + return 0; +} + +static uint32_t get_samples(struct impl *this, uint32_t *duration) +{ + struct port *port = &this->port; + uint32_t samples; + + if (SPA_LIKELY(port->rate_match) && this->resampling) { + samples = port->rate_match->size; + } else { + if (SPA_LIKELY(this->position)) + samples = this->position->clock.duration * port->current_format.info.raw.rate + / this->position->clock.rate.denom; + else + samples = 1024; + } + + if (SPA_LIKELY(this->position)) + *duration = this->position->clock.duration * port->current_format.info.raw.rate + / this->position->clock.rate.denom; + else if (SPA_LIKELY(this->clock)) + *duration = this->clock->duration * port->current_format.info.raw.rate + / this->clock->rate.denom; + else + *duration = 1024 * port->current_format.info.raw.rate / 48000; + + return samples; +} + +static void process_buffering(struct impl *this) +{ + struct port *port = &this->port; + uint32_t duration; + const uint32_t samples = get_samples(this, &duration); + void *buf; + uint32_t avail; + + spa_bt_decode_buffer_process(&port->buffer, samples, duration); + + setup_matching(this); + + buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); + + /* copy data to buffers */ + if (!spa_list_is_empty(&port->free) && avail > 0) { + struct buffer *buffer; + struct spa_data *datas; + uint32_t data_size; + + data_size = samples * port->frame_size; + + avail = SPA_MIN(avail, data_size); + + spa_bt_decode_buffer_read(&port->buffer, avail); + + buffer = spa_list_first(&port->free, struct buffer, link); + spa_list_remove(&buffer->link); + + spa_log_trace(this->log, "dequeue %d", buffer->id); + + datas = buffer->buf->datas; + + spa_assert(datas[0].maxsize >= data_size); + + datas[0].chunk->offset = 0; + datas[0].chunk->size = avail; + datas[0].chunk->stride = port->frame_size; + memcpy(datas[0].data, buf, avail); + + /* ready buffer if full */ + spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)avail / port->frame_size); + spa_list_append(&port->ready, &buffer->link); + } +} + +static int produce_buffer(struct impl *this) +{ + struct buffer *buffer; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + + if (io == NULL) + return -EIO; + + /* Return if we already have a buffer */ + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* Recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + /* Handle buffering */ + process_buffering(this); + + /* Return if there are no buffers ready to be processed */ + if (spa_list_is_empty(&port->ready)) + return SPA_STATUS_OK; + + /* Get the new buffer from the ready list */ + buffer = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&buffer->link); + buffer->outstanding = true; + + /* Set the new buffer in IO */ + io->buffer_id = buffer->id; + io->status = SPA_STATUS_HAVE_DATA; + + /* Notify we have a buffer ready to be processed */ + return SPA_STATUS_HAVE_DATA; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + /* Return if we already have a buffer */ + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* Recycle */ + if (io->buffer_id < port->n_buffers) { + recycle_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + /* Follower produces buffers here, driver in timeout */ + if (this->following) + return produce_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int do_transport_destroy(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + this->transport = NULL; + return 0; +} + +static void transport_destroy(void *data) +{ + struct impl *this = data; + spa_log_debug(this->log, "transport %p destroy", this->transport); + spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); +} + +static const struct spa_bt_transport_events transport_events = { + SPA_VERSION_BT_TRANSPORT_EVENTS, + .destroy = transport_destroy, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + + do_stop(this); + if (this->transport) + spa_hook_remove(&this->transport_listener); + spa_system_close(this->data_system, this->timerfd); + spa_bt_decode_buffer_clear(&this->port.buffer); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_log_topic_init(this->log, &log_topic); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + /* set the node info */ + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.flags = SPA_NODE_FLAG_RT; + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + /* set the port info */ + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_TERMINAL; + port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + port->latency.min_quantum = 1.0f; + port->latency.max_quantum = 1.0f; + + /* Init the buffer lists */ + spa_list_init(&port->ready); + spa_list_init(&port->free); + + this->quantum_limit = 8192; + if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) + spa_atou32(str, &this->quantum_limit, 0); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT))) + sscanf(str, "pointer:%p", &this->transport); + + if (this->transport == NULL) { + spa_log_error(this->log, "a transport is needed"); + return -EINVAL; + } + spa_bt_transport_add_listener(this->transport, + &this->transport_listener, &transport_events, this); + + this->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. " }, + { 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/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..23a637a --- /dev/null +++ b/spa/plugins/bluez5/upower.c @@ -0,0 +1,311 @@ +/* Spa Bluez5 UPower proxy + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; + + bool filters_added; + + void *user_data; + void (*set_battery_level)(unsigned int level, void *user_data); +}; + +static DBusHandlerResult upower_parse_percentage(struct impl *this, DBusMessageIter *variant_i) +{ + double percentage; + unsigned int battery_level; + + dbus_message_iter_get_basic(variant_i, &percentage); + spa_log_debug(this->log, "Battery level: %f %%", percentage); + + battery_level = (unsigned int) round(percentage / 20.0); + this->set_battery_level(battery_level, this->user_data); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void upower_get_percentage_properties_reply(DBusPendingCall *pending, void *user_data) +{ + struct impl *backend = user_data; + DBusMessage *r; + DBusMessageIter i, variant_i; + + r = dbus_pending_call_steal_reply(pending); + if (r == NULL) + return; + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(backend->log, "Failed to get percentage from UPower: %s", + dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_iter_init(r, &i) || !spa_streq(dbus_message_get_signature(r), "v")) { + spa_log_error(backend->log, "Invalid arguments in Get() reply"); + goto finish; + } + + dbus_message_iter_recurse(&i, &variant_i); + upower_parse_percentage(backend, &variant_i); + +finish: + dbus_message_unref(r); +} + +static void upower_clean(struct impl *this) +{ + this->set_battery_level(0, this->user_data); +} + +static DBusHandlerResult upower_filter_cb(DBusConnection *bus, DBusMessage *m, void *user_data) +{ + struct impl *this = user_data; + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) { + const char *name, *old_owner, *new_owner; + + spa_log_debug(this->log, "Name owner changed %s", dbus_message_get_path(m)); + + if (!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, + DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse org.freedesktop.DBus.NameOwnerChanged: %s", err.message); + goto finish; + } + + if (spa_streq(name, UPOWER_SERVICE)) { + if (old_owner && *old_owner) { + spa_log_debug(this->log, "UPower daemon disappeared (%s)", old_owner); + upower_clean(this); + } + + if (new_owner && *new_owner) { + DBusPendingCall *call; + static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE; + static const char* percentage_property = "Percentage"; + + spa_log_debug(this->log, "UPower daemon appeared (%s)", new_owner); + + m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get"); + if (m == NULL) + goto finish; + dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface, + DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID); + dbus_connection_send_with_reply(this->conn, m, &call, -1); + dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL); + dbus_message_unref(m); + } + } + } else if (dbus_message_is_signal(m, DBUS_INTERFACE_PROPERTIES, DBUS_SIGNAL_PROPERTIES_CHANGED)) { + const char *path; + DBusMessageIter iface_i, props_i; + const char *interface; + + if (!dbus_message_iter_init(m, &iface_i) || !spa_streq(dbus_message_get_signature(m), "sa{sv}as")) { + spa_log_error(this->log, "Invalid signature found in PropertiesChanged"); + goto finish; + } + + dbus_message_iter_get_basic(&iface_i, &interface); + dbus_message_iter_next(&iface_i); + spa_assert(dbus_message_iter_get_arg_type(&iface_i) == DBUS_TYPE_ARRAY); + + dbus_message_iter_recurse(&iface_i, &props_i); + + path = dbus_message_get_path(m); + + if (spa_streq(interface, UPOWER_DEVICE_INTERFACE)) { + spa_log_debug(this->log, "Properties changed on %s", path); + + while (dbus_message_iter_get_arg_type(&props_i) != DBUS_TYPE_INVALID) { + DBusMessageIter i, value_i; + const char *key; + + dbus_message_iter_recurse(&props_i, &i); + + dbus_message_iter_get_basic(&i, &key); + dbus_message_iter_next(&i); + dbus_message_iter_recurse(&i, &value_i); + + if(spa_streq(key, "Percentage")) + upower_parse_percentage(this, &value_i); + + dbus_message_iter_next(&props_i); + } + } + } + +finish: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int add_filters(struct impl *this) +{ + DBusError err; + + if (this->filters_added) + return 0; + + dbus_error_init(&err); + + if (!dbus_connection_add_filter(this->conn, upower_filter_cb, this, NULL)) { + spa_log_error(this->log, "failed to add filter function"); + goto fail; + } + + dbus_bus_add_match(this->conn, + "type='signal',sender='org.freedesktop.DBus'," + "interface='org.freedesktop.DBus',member='NameOwnerChanged'," "arg0='" UPOWER_SERVICE "'", &err); + dbus_bus_add_match(this->conn, + "type='signal',sender='" UPOWER_SERVICE "'," + "interface='" DBUS_INTERFACE_PROPERTIES "',member='" DBUS_SIGNAL_PROPERTIES_CHANGED "'," + "path='" UPOWER_DISPLAY_DEVICE_OBJECT "',arg0='" UPOWER_DEVICE_INTERFACE "'", &err); + + this->filters_added = true; + + return 0; + +fail: + dbus_error_free(&err); + return -EIO; +} + +static bool is_dbus_service_available(struct impl *this, const char *service) +{ + DBusMessage *m, *r; + DBusError err; + bool success = false; + + m = dbus_message_new_method_call("org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "NameHasOwner"); + if (m == NULL) + return false; + dbus_message_append_args(m, DBUS_TYPE_STRING, &service, DBUS_TYPE_INVALID); + + dbus_error_init(&err); + r = dbus_connection_send_with_reply_and_block(this->conn, m, -1, &err); + dbus_message_unref(m); + m = NULL; + + if (r == NULL) { + spa_log_info(this->log, "NameHasOwner failed for %s", service); + dbus_error_free(&err); + goto finish; + } + + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(this->log, "NameHasOwner() returned error: %s", dbus_message_get_error_name(r)); + goto finish; + } + + if (!dbus_message_get_args(r, &err, + DBUS_TYPE_BOOLEAN, &success, + DBUS_TYPE_INVALID)) { + spa_log_error(this->log, "Failed to parse NameHasOwner() reply: %s", err.message); + dbus_error_free(&err); + goto finish; + } + +finish: + if (r) + dbus_message_unref(r); + + return success; +} + +void *upower_register(struct spa_log *log, + void *dbus_connection, + void (*set_battery_level)(unsigned int level, void *user_data), + void *user_data) +{ + struct impl *this; + + spa_assert(log); + spa_assert(dbus_connection); + spa_assert(set_battery_level); + spa_assert(user_data); + + this = calloc(1, sizeof(struct impl)); + if (this == NULL) + return NULL; + + this->log = log; + this->conn = dbus_connection; + this->set_battery_level = set_battery_level; + this->user_data = user_data; + + if (add_filters(this) < 0) { + goto fail4; + } + + if (is_dbus_service_available(this, UPOWER_SERVICE)) { + DBusMessage *m; + DBusPendingCall *call; + static const char* upower_device_interface = UPOWER_DEVICE_INTERFACE; + static const char* percentage_property = "Percentage"; + + m = dbus_message_new_method_call(UPOWER_SERVICE, UPOWER_DISPLAY_DEVICE_OBJECT, DBUS_INTERFACE_PROPERTIES, "Get"); + if (m == NULL) + goto fail4; + dbus_message_append_args(m, DBUS_TYPE_STRING, &upower_device_interface, + DBUS_TYPE_STRING, &percentage_property, DBUS_TYPE_INVALID); + dbus_connection_send_with_reply(this->conn, m, &call, -1); + dbus_pending_call_set_notify(call, upower_get_percentage_properties_reply, this, NULL); + dbus_message_unref(m); + } + + return this; + +fail4: + free(this); + return NULL; +} + +void upower_unregister(void *data) +{ + struct impl *this = data; + + if (this->filters_added) { + dbus_connection_remove_filter(this->conn, upower_filter_cb, this); + this->filters_added = false; + } + free(this); +} diff --git a/spa/plugins/bluez5/upower.h b/spa/plugins/bluez5/upower.h new file mode 100644 index 0000000..9ebd751 --- /dev/null +++ b/spa/plugins/bluez5/upower.h @@ -0,0 +1,36 @@ +/* Spa Bluez5 UPower proxy + * + * Copyright © 2022 Collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_BLUEZ5_UPOWER_H_ +#define SPA_BLUEZ5_UPOWER_H_ + +#include "defs.h" + +void *upower_register(struct spa_log *log, + void *dbus_connection, + void (*set_battery_level)(unsigned int level, void *user_data), + void *user_data); +void upower_unregister(void *data); + +#endif \ No newline at end of file diff --git a/spa/plugins/control/meson.build b/spa/plugins/control/meson.build new file mode 100644 index 0000000..adabdfa --- /dev/null +++ b/spa/plugins/control/meson.build @@ -0,0 +1,10 @@ +control_sources = [ + 'mixer.c', + 'plugin.c' +] + +controllib = shared_library('spa-control', + control_sources, + dependencies : [ spa_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'control') diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c new file mode 100644 index 0000000..9d28bef --- /dev/null +++ b/spa/plugins/control/mixer.c @@ -0,0 +1,869 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "control-mixer" + +#define MAX_BUFFERS 64 +#define MAX_PORTS 128 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_QUEUED (1 << 0) + uint32_t flags; + + struct spa_list link; + struct spa_buffer *buffer; +}; + +struct port { + uint32_t direction; + uint32_t id; + + struct spa_io_buffers *io; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + unsigned int valid:1; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list queue; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[8]; + + struct spa_hook_list hooks; + + uint32_t port_count; + uint32_t last_port; + struct port *in_ports[MAX_PORTS]; + struct port out_ports[1]; + + int n_formats; + + unsigned int have_format:1; + unsigned int started:1; +}; + +#define PORT_VALID(p) ((p) != NULL && (p)->valid) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) +#define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) +#define GET_IN_PORT(this,p) (this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + for (i = 0; i < this->last_port; i++) { + if (PORT_VALID(this->in_ports[i])) + emit_port_info(this, GET_IN_PORT(this, i), true); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT (this, port_id); + if (port == NULL) { + port = calloc(1, sizeof(struct port)); + if (port == NULL) + return -errno; + this->in_ports[port_id] = port; + } + + port->direction = direction; + port->id = port_id; + + spa_list_init(&port->queue); + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_DYNAMIC_DATA | + SPA_PORT_FLAG_REMOVABLE | + SPA_PORT_FLAG_OPTIONAL; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + this->port_count++; + if (this->last_port <= port_id) + this->last_port = port_id + 1; + port->valid = true; + + spa_log_debug(this->log, NAME " %p: add port %d %d", this, port_id, this->last_port); + emit_port_info(this, port, true); + + return 0; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); + + port = GET_IN_PORT (this, port_id); + + port->valid = false; + this->port_count--; + if (port->have_format && this->have_format) { + if (--this->n_formats == 0) + this->have_format = false; + } + spa_memzero(port, sizeof(struct port)); + + if (port_id + 1 == this->last_port) { + int i; + + for (i = this->last_port - 1; i >= 0; i--) + if (GET_IN_PORT (this, i)->valid) + break; + + this->last_port = i + 1; + } + spa_log_debug(this->log, NAME " %p: remove port %d %d", this, port_id, this->last_port); + + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + + return 0; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if ((res = port_enum_formats(this, direction, port_id, result.index, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(4096, 512, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)); + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, NAME " %p: clear buffers %p", this, port); + port->n_buffers = 0; + spa_list_init(&port->queue); + } + return 0; +} + +static int queue_buffer(struct impl *this, struct port *port, struct buffer *b) +{ + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_QUEUED)) + return -EINVAL; + + spa_list_append(&port->queue, &b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, NAME " %p: queue buffer %d", this, b->id); + return 0; +} + +static struct buffer *dequeue_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->queue)) + return NULL; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_QUEUED); + spa_log_trace_fp(this->log, NAME " %p: dequeue buffer %d", this, b->id); + return b; +} + +static int port_set_format(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + if (format == NULL) { + if (port->have_format) { + port->have_format = false; + if (--this->n_formats == 0) + this->have_format = false; + clear_buffers(this, port); + } + } else { + uint32_t media_type, media_subtype; + if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0) + return res; + + if (media_type != SPA_MEDIA_TYPE_application || + media_subtype != SPA_MEDIA_SUBTYPE_control) + return -EINVAL; + + this->have_format = true; + + if (!port->have_format) { + this->n_formats++; + port->have_format = true; + spa_log_debug(this->log, NAME " %p: set format on port %d:%d", + this, direction, port_id); + } + } + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + if (id == SPA_PARAM_Format) { + return port_set_format(this, direction, port_id, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, NAME " %p: use buffers %d on port %d:%d", + this, n_buffers, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->buffer = buffers[i]; + b->flags = 0; + b->id = i; + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %d", this, i); + return -EINVAL; + } + if (direction == SPA_DIRECTION_OUTPUT) + queue_buffer(this, port, b); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + spa_log_debug(this->log, NAME " %p: port %d:%d io %d %p/%zd", this, + direction, port_id, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); + port = GET_OUT_PORT(this, 0); + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + return queue_buffer(this, port, &port->buffers[buffer_id]); +} + +static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b) +{ + if (a->offset < b->offset) + return -1; + if (a->offset > b->offset) + return 1; + if (a->type != b->type) + return 0; + switch(a->type) { + case SPA_CONTROL_Midi: + { + /* 11 (controller) > 12 (program change) > + * 8 (note off) > 9 (note on) > 10 (aftertouch) > + * 13 (channel pressure) > 14 (pitch bend) */ + static int priotab[] = { 5,4,3,7,6,2,1,0 }; + uint8_t *da, *db; + + if (SPA_POD_BODY_SIZE(&a->value) < 1 || + SPA_POD_BODY_SIZE(&b->value) < 1) + return 0; + + da = SPA_POD_BODY(&a->value); + db = SPA_POD_BODY(&b->value); + if ((da[0] & 0xf) != (db[0] & 0xf)) + return 0; + return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7]; + } + default: + return 0; + } +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *outport; + struct spa_io_buffers *outio; + uint32_t n_seq, i; + struct spa_pod_sequence **seq; + struct spa_pod_control **ctrl; + struct spa_pod_builder builder; + struct spa_pod_frame f; + struct buffer *outb; + struct spa_data *d; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + outport = GET_OUT_PORT(this, 0); + if ((outio = outport->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, NAME " %p: status %p %d %d", + this, outio, outio->status, outio->buffer_id); + + if (outio->status == SPA_STATUS_HAVE_DATA) + return outio->status; + + /* recycle */ + if (outio->buffer_id < outport->n_buffers) { + queue_buffer(this, outport, &outport->buffers[outio->buffer_id]); + outio->buffer_id = SPA_ID_INVALID; + } + + /* get output buffer */ + if ((outb = dequeue_buffer(this, outport)) == NULL) { + spa_log_trace(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + + ctrl = alloca(MAX_PORTS * sizeof(struct spa_pod_control *)); + seq = alloca(MAX_PORTS * sizeof(struct spa_pod_sequence *)); + n_seq = 0; + + /* collect all sequence pod on input ports */ + for (i = 0; i < this->last_port; i++) { + struct port *inport = GET_IN_PORT(this, i); + struct spa_io_buffers *inio = NULL; + void *pod; + + if (!inport->valid || + (inio = inport->io) == NULL || + inio->buffer_id >= inport->n_buffers || + inio->status != SPA_STATUS_HAVE_DATA) { + spa_log_trace_fp(this->log, NAME " %p: skip input idx:%d valid:%d " + "io:%p status:%d buf_id:%d n_buffers:%d", this, + i, inport->valid, inio, + inio ? inio->status : -1, + inio ? inio->buffer_id : SPA_ID_INVALID, + inport->n_buffers); + continue; + } + + spa_log_trace_fp(this->log, NAME " %p: mix input %d %p->%p %d %d", this, + i, inio, outio, inio->status, inio->buffer_id); + + d = inport->buffers[inio->buffer_id].buffer->datas; + + if ((pod = spa_pod_from_data(d->data, d->maxsize, + d->chunk->offset, d->chunk->size)) == NULL) + continue; + if (!spa_pod_is_sequence(pod)) + continue; + + seq[n_seq] = pod; + ctrl[n_seq] = spa_pod_control_first(&seq[n_seq]->body); + inio->status = SPA_STATUS_NEED_DATA; + n_seq++; + } + + d = outb->buffer->datas; + + /* prepare to write into output */ + spa_pod_builder_init(&builder, d->data, d->maxsize); + spa_pod_builder_push_sequence(&builder, &f, 0); + + /* merge sort all sequences into output buffer */ + while (true) { + struct spa_pod_control *next = NULL; + uint32_t next_index = 0; + + for (i = 0; i < n_seq; i++) { + if (!spa_pod_control_is_inside(&seq[i]->body, + SPA_POD_BODY_SIZE(seq[i]), ctrl[i])) + continue; + + if (next == NULL || event_sort(ctrl[i], next) <= 0) { + next = ctrl[i]; + next_index = i; + } + } + if (next == NULL) + break; + + spa_pod_builder_control(&builder, next->offset, next->type); + spa_pod_builder_primitive(&builder, &next->value); + + ctrl[next_index] = spa_pod_control_next(ctrl[next_index]); + } + spa_pod_builder_pop(&builder, &f); + + d->chunk->offset = 0; + d->chunk->size = builder.state.offset; + d->chunk->stride = 1; + d->chunk->flags = 0; + + outio->buffer_id = outb->id; + outio->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + for (i = 0; i < MAX_PORTS; i++) + free(this->in_ports[i]); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = 1; + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; + + port = GET_OUT_PORT(this, 0); + port->valid = true; + port->direction = SPA_DIRECTION_OUTPUT; + port->id = 0; + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; + port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + spa_list_init(&port->queue); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_control_mixer_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_CONTROL_MIXER, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/control/plugin.c b/spa/plugins/control/plugin.c new file mode 100644 index 0000000..ed53382 --- /dev/null +++ b/spa/plugins/control/plugin.c @@ -0,0 +1,46 @@ +/* Spa Control plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_control_mixer_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_control_mixer_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/ffmpeg/ffmpeg-dec.c b/spa/plugins/ffmpeg/ffmpeg-dec.c new file mode 100644 index 0000000..e9c4c1e --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg-dec.c @@ -0,0 +1,529 @@ +/* Spa FFmpeg decoder + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffmpeg.h" + +#define IS_VALID_PORT(this,d,id) ((id) == 0) +#define GET_IN_PORT(this,p) (&this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_list link; +}; + +struct port { + enum spa_direction direction; + uint32_t id; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + struct spa_video_info current_format; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_io_buffers *io; + + struct spa_list free; + struct spa_list ready; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + + struct spa_hook_list hooks; + + struct port in_ports[1]; + struct port out_ports[1]; + + bool started; +}; + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + if (this == NULL || command == NULL) + return -EINVAL; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_IN_PORT(this, 0), true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int +impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, + enum spa_direction direction, + uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + if (!IS_VALID_PORT(object, direction, port_id)) + return -EINVAL; + + switch (index) { + case 0: + *param = NULL; + break; + default: + return 0; + } + return 1; +} + +static int port_get_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + struct port *port; + + port = GET_PORT(this, direction, port_id); + + if (!port->have_format) + return -EIO; + + if (index > 0) + return 0; + + *param = NULL; + + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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..6218183 --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg-enc.c @@ -0,0 +1,507 @@ +/* Spa FFmpeg encoder + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ffmpeg.h" + +#define IS_VALID_PORT(this,d,id) ((id) == 0) +#define GET_IN_PORT(this,p) (&this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +#define MAX_BUFFERS 32 + +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_list link; +}; + +struct port { + enum spa_direction direction; + uint32_t id; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[8]; + + struct spa_video_info current_format; + unsigned int have_format:1; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_io_buffers *io; + + struct spa_list free; + struct spa_list ready; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + + struct spa_hook_list hooks; + + struct port in_ports[1]; + struct port out_ports[1]; + + bool started; +}; + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + if (this == NULL || command == NULL) + return -EINVAL; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_IN_PORT(this, 0), true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *user_data) +{ + return 0; +} + +static int +impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, + enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + return -ENOTSUP; +} + +static int port_get_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct impl *this = object; + struct port *port; + + port = GET_PORT(this, direction, port_id); + + if (!port->have_format) + return -EIO; + + if (index > 0) + return 0; + + *param = NULL; + + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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..9546dae --- /dev/null +++ b/spa/plugins/ffmpeg/ffmpeg.c @@ -0,0 +1,160 @@ +/* Spa FFmpeg support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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_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/jack/jack-client.c b/spa/plugins/jack/jack-client.c new file mode 100644 index 0000000..b3cdcc7 --- /dev/null +++ b/spa/plugins/jack/jack-client.c @@ -0,0 +1,113 @@ +/* Spa JACK Client + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include "jack-client.h" + +static int jack_process(jack_nframes_t nframes, void *arg) +{ + struct spa_jack_client *client = arg; + + jack_get_cycle_times(client->client, + &client->current_frames, &client->current_usecs, + &client->next_usecs, &client->period_usecs); + + jack_transport_query (client->client, &client->pos); + + client->buffer_size = nframes; + + spa_jack_client_emit_process(client); + + return 0; +} + +static void jack_shutdown(void* arg) +{ + struct spa_jack_client *client = arg; + + spa_jack_client_emit_shutdown(client); + + spa_hook_list_init(&client->listener_list); + client->client = NULL; +} + +static int status_to_result(jack_status_t status) +{ + int res; + + if (status & JackInvalidOption) + res = -EINVAL; + else if (status & JackServerFailed) + res = -ECONNREFUSED; + else if (status & JackVersionError) + res = -EPROTO; + else if (status & JackInitFailure) + res = -EIO; + else + res = -EFAULT; + + return res; +} + +int spa_jack_client_open(struct spa_jack_client *client, + const char *client_name, const char *server_name) +{ + jack_status_t status; + + if (client->client) + return 0; + + client->client = jack_client_open(client_name, + JackNoStartServer, &status, NULL); + + if (client->client == NULL) + return status_to_result(status); + + spa_hook_list_init(&client->listener_list); + + jack_set_process_callback(client->client, jack_process, client); + jack_on_shutdown(client->client, jack_shutdown, client); + client->frame_rate = jack_get_sample_rate(client->client); + client->buffer_size = jack_get_buffer_size(client->client); + + return 0; +} + + +int spa_jack_client_close(struct spa_jack_client *client) +{ + if (client->client == NULL) + return 0; + + spa_jack_client_emit_destroy(client); + + if (jack_client_close(client->client) != 0) + return -EIO; + + spa_hook_list_init(&client->listener_list); + client->client = NULL; + + return 0; +} diff --git a/spa/plugins/jack/jack-client.h b/spa/plugins/jack/jack-client.h new file mode 100644 index 0000000..b758ba0 --- /dev/null +++ b/spa/plugins/jack/jack-client.h @@ -0,0 +1,84 @@ +/* Spa JACK Client + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_JACK_CLIENT_H +#define SPA_JACK_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#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..0ae67d0 --- /dev/null +++ b/spa/plugins/jack/jack-device.c @@ -0,0 +1,457 @@ +/* Spa JACK Device + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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" + +#define NAME "jack-device" + +#define MAX_DEVICES 64 + +#define DEFAULT_SERVER "default" + +struct props { + char server[128]; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->server, DEFAULT_SERVER, 64); +} + +struct node { + enum spa_direction direction; +}; + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_hook_list hooks; + + struct props props; + + struct node nodes[2]; + uint32_t n_nodes; + + uint32_t profile; + + struct spa_jack_client client; +}; + +static int emit_node(struct impl *this, uint32_t id) +{ + struct spa_dict_item items[6]; + struct spa_device_object_info info; + char jack_client[64]; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + info.type = SPA_TYPE_INTERFACE_Node; + if (this->nodes[id].direction == SPA_DIRECTION_INPUT) + info.factory_name = SPA_NAME_API_JACK_SINK; + else + info.factory_name = SPA_NAME_API_JACK_SOURCE; + + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + snprintf(jack_client, sizeof(jack_client), "pointer:%p", &this->client); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_API_JACK_CLIENT, jack_client); + info.props = &SPA_DICT_INIT(items, 1); + + spa_device_emit_object_info(&this->hooks, id, &info); + + return 0; +} + +static int activate_profile(struct impl *this, uint32_t id) +{ + int res = 0; + uint32_t i, n; + const char ** ports; + + spa_log_debug(this->log, "profile %d", id); + if (this->profile == id) + return 0; + + for (i = 0; i < this->n_nodes; i++) + spa_device_emit_object_info(&this->hooks, i, NULL); + this->n_nodes = 0; + + spa_jack_client_close(&this->client); + + if (id == 0) + goto done; + + res = spa_jack_client_open(&this->client, "PipeWire", NULL); + if (res < 0) { + spa_log_error(this->log, NAME" %p: can't open client: %s", + this, spa_strerror(res)); + return res; + } + n = 0; + ports = jack_get_ports(this->client.client, + NULL, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsPhysical|JackPortIsOutput); + if (ports) { + jack_free(ports); + this->nodes[n].direction = SPA_DIRECTION_OUTPUT; + emit_node(this, n++); + } + + ports = jack_get_ports(this->client.client, + NULL, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsPhysical|JackPortIsInput); + if (ports) { + jack_free(ports); + this->nodes[n].direction = SPA_DIRECTION_INPUT; + emit_node(this, n++); + } + this->n_nodes = n; +done: + this->profile = id; + + return res; +} + +static int emit_info(struct impl *this, bool full) +{ + int err = 0; + struct spa_dict_item items[10]; + struct spa_device_info dinfo; + struct spa_param_info params[2]; + char name[200]; + + dinfo = SPA_DEVICE_INFO_INIT(); + + dinfo.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "jack"); + items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NICK, "jack"); + if (spa_streq(this->props.server, "default")) + snprintf(name, sizeof(name), "JACK Client"); + else + snprintf(name, sizeof(name), "JACK Client (%s)", this->props.server); + items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, name); + items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_DESCRIPTION, name); + items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_API_JACK_SERVER, this->props.server); + items[5] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + dinfo.props = &SPA_DICT_INIT(items, 6); + + dinfo.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE); + dinfo.n_params = SPA_N_ELEMENTS(params); + dinfo.params = params; + + spa_device_emit_info(&this->hooks, &dinfo); + + return err; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info) + emit_info(this, true); + + if (events->object_info) { + for (i = 0; i < this->n_nodes; i++) + emit_node(this, i); + } + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b, + uint32_t id, uint32_t index) +{ + struct spa_pod_frame f[2]; + const char *name, *desc; + + switch (index) { + case 0: + name = "off"; + desc = "Off"; + break; + case 1: + name = "on"; + desc = "On"; + break; + default: + errno = EINVAL; + return NULL; + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id); + spa_pod_builder_add(b, + SPA_PARAM_PROFILE_index, SPA_POD_Int(index), + SPA_PARAM_PROFILE_name, SPA_POD_String(name), + SPA_PARAM_PROFILE_description, SPA_POD_String(desc), + 0); + return spa_pod_builder_pop(b, &f[0]); +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_device_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumProfile: + { + switch (result.index) { + case 0: + case 1: + param = build_profile(this, &b, id, result.index); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Profile: + { + switch (result.index) { + case 0: + param = build_profile(this, &b, id, this->profile); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_device_emit_result(&this->hooks, seq, 0, + SPA_RESULT_TYPE_DEVICE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Profile: + { + uint32_t idx; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamProfile, NULL, + SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx))) < 0) { + spa_log_warn(this->log, "can't parse profile"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + return res; + } + activate_profile(this, idx); + break; + } + default: + return -ENOENT; + } + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + activate_profile(this, 0); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + spa_hook_list_init(&this->hooks); + + reset_props(&this->props); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_SERVER))) + snprintf(this->props.server, 64, "%s", str); + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_jack_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_JACK_DEVICE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/jack/jack-sink.c b/spa/plugins/jack/jack-sink.c new file mode 100644 index 0000000..e9ca5a8 --- /dev/null +++ b/spa/plugins/jack/jack-sink.c @@ -0,0 +1,924 @@ +/* Spa jack client + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jack-client.h" + +#define NAME "jack-sink" + +#define MAX_PORTS 128 +#define MAX_BUFFERS 8 +#define MAX_SAMPLES 8192 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_list link; +}; + +struct port { + uint32_t id; + + uint64_t info_all; + struct spa_port_info info; + struct spa_dict_item items[4]; + struct spa_dict props; + struct spa_param_info params[5]; + + unsigned int have_format:1; + struct spa_audio_info current_format; + int stride; + + struct spa_io_buffers *io; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + jack_port_t *jack_port; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[5]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + struct port in_ports[MAX_PORTS]; + uint32_t n_in_ports; + + struct spa_audio_info current_format; + + struct spa_jack_client *client; + struct spa_hook client_listener; + + unsigned int started:1; +}; + +#define CHECK_IN_PORT(this,p) ((p) < this->n_in_ports) +#define CHECK_PORT(this,d,p) (d == SPA_DIRECTION_INPUT && CHECK_IN_PORT(this,p)) +#define GET_IN_PORT(this,p) (&this->in_ports[p]) +#define GET_PORT(this,d,p) GET_IN_PORT(this,p) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + return 0; + + case SPA_PARAM_Props: + return 0; + + case SPA_PARAM_EnumFormat: + case SPA_PARAM_Format: + switch (result.index) { + case 0: + param = spa_format_audio_dsp_build(&b, + id, &this->current_format.info.dsp); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (this->started) + return 0; + + this->started = true; + break; + + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + struct spa_dict_item items[5]; + char latency[64]; + snprintf(latency, sizeof(latency), "%d/%d", + this->client->buffer_size, this->client->frame_rate); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink"); + items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_NAME, "JACK System"); + items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_PAUSE_ON_IDLE, "false"); + items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_LATENCY, latency); + this->info.props = &SPA_DICT_INIT_ARRAY(items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + char* aliases[2]; + int n_aliases, n_items; + + aliases[0] = alloca(jack_port_name_size()); + aliases[1] = alloca(jack_port_name_size()); + n_aliases = jack_port_get_aliases(port->jack_port, aliases); + n_items = 1; + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, + jack_port_short_name(port->jack_port)); + if (n_aliases > 0) + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, aliases[0]); + if (n_aliases > 1) + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, aliases[1]); + port->props = SPA_DICT_INIT(port->items, n_items); + + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, port->id, &port->info); + + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + for (i = 0; i < this->n_in_ports; i++) + emit_port_info(this, GET_IN_PORT(this, i), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static void client_process(void *data) +{ + struct impl *this = data; + + if (this->clock) { + struct spa_io_clock *c = this->clock; + c->nsec = this->client->current_usecs * SPA_NSEC_PER_USEC; + c->rate = SPA_FRACTION(1, this->client->frame_rate); + c->position = this->client->current_frames; + c->duration = this->client->buffer_size; + c->delay = 0; + c->rate_diff = 1.0; + c->next_nsec = this->client->next_usecs * SPA_NSEC_PER_USEC; + } + if (this->position) { + jack_position_t *jp = &this->client->pos; + struct spa_io_position *p = this->position; + struct spa_io_segment *s; + + p->n_segments = 1; + s = &p->segments[0]; + + s->flags = 0; + s->position = jp->frame; + s->rate = 1.0; + if (jp->valid & JackPositionBBT) { + s->bar.flags = SPA_IO_SEGMENT_BAR_FLAG_VALID; + if (jp->valid & JackBBTFrameOffset) + s->bar.offset = jp->bbt_offset; + else + s->bar.offset = 0; + s->bar.signature_num = jp->beats_per_bar; + s->bar.signature_denom = jp->beat_type; + s->bar.bpm = jp->beats_per_minute; + s->bar.beat = jp->bar * jp->beats_per_bar + jp->beat; + } + } + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); +} + +static const struct spa_jack_client_events client_events = { + SPA_VERSION_JACK_CLIENT_EVENTS, + .process = client_process, +}; + +static int init_port(struct impl *this, struct port *port) +{ + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + + port->items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio"); + port->props = SPA_DICT_INIT(port->items, 1); + port->info.props = &port->props; + + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + return 0; +} + +static int init_ports(struct impl *this) +{ + const char **ports; + uint32_t i; + jack_client_t *client = this->client->client; + int res; + + ports = jack_get_ports(client, + NULL, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsPhysical|JackPortIsInput); + if (ports == NULL) { + spa_log_error(this->log, NAME" %p: can't enumerate ports", this); + res = -ENODEV; + goto exit; + } + + for (i = 0; ports[i]; i++) { + struct port *port = GET_IN_PORT(this, i); + jack_port_t *p = jack_port_by_name(client, ports[i]); + char *aliases[2]; + int n_aliases; + + port->id = i; + port->jack_port = jack_port_register(client, + jack_port_short_name(p), + jack_port_type(p), + JackPortIsOutput, 0); + if (port->jack_port == NULL) { + spa_log_error(this->log, NAME" %p: jack_port_register() %d (%s) failed", + this, i, ports[i]); + res = -EFAULT; + goto exit_free; + } + + aliases[0] = alloca(jack_port_name_size()); + aliases[1] = alloca(jack_port_name_size()); + + n_aliases = jack_port_get_aliases(p, aliases); + if (n_aliases > 0) + jack_port_set_alias(port->jack_port, aliases[0]); + if (n_aliases > 1) + jack_port_set_alias(port->jack_port, aliases[1]); + + init_port(this, port); + } + this->n_in_ports = i; + + this->current_format.info.dsp = SPA_AUDIO_INFO_DSP_INIT( + .format = SPA_AUDIO_FORMAT_DSP_F32); + + spa_jack_client_add_listener(this->client, + &this->client_listener, + &client_events, this); + + jack_activate(client); + + for (i = 0; ports[i]; i++) { + struct port *port = GET_IN_PORT(this, i); + if (jack_connect(client, jack_port_name(port->jack_port), ports[i])) { + spa_log_warn(this->log, NAME" %p: Failed to connect %s to %s", + this, jack_port_name(port->jack_port), ports[i]); + } + } + + res = 0; +exit_free: + jack_free(ports); +exit: + return res; +} + + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + MAX_SAMPLES * port->stride, + 16 * port->stride, + MAX_SAMPLES * port->stride), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, NAME " %p: clear buffers", this); + port->n_buffers = 0; + this->started = false; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio && + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) + return -EINVAL; + + if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0) + return -EINVAL; + + if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) + return -EINVAL; + + port->stride = 4; + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + default: + return -ENOENT; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = 0; + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + uint32_t i; + int res = 0; + + spa_log_trace(this->log, NAME" %p: process %d", this, this->n_in_ports); + + for (i = 0; i < this->n_in_ports; i++) { + struct port *port = GET_IN_PORT(this, i); + struct spa_io_buffers *io = port->io; + struct buffer *b; + struct spa_data *src; + uint32_t n_frames = this->client->buffer_size; + void *dst; + + dst = jack_port_get_buffer(port->jack_port, n_frames); + + if (io == NULL || + io->status != SPA_STATUS_HAVE_DATA || + io->buffer_id >= port->n_buffers) { + memset(dst, 0, n_frames * sizeof(float)); + continue; + } + + spa_log_trace(this->log, NAME" %p: port %d: buffer %d", this, i, io->buffer_id); + b = &port->buffers[io->buffer_id]; + src = &b->outbuf->datas[0]; + + spa_memcpy(dst, src->data, n_frames * port->stride); + + io->status = SPA_STATUS_NEED_DATA; + + res |= SPA_STATUS_NEED_DATA; + } + return res; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_CLIENT))) + sscanf(str, "pointer:%p", &this->client); + + if (this->client == NULL) { + spa_log_error(this->log, NAME" %p: missing "SPA_KEY_API_JACK_CLIENT + " property", this); + return -EINVAL; + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ); + this->params[3] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[4] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = 5; + + init_ports(this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the JACK API" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_jack_sink_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_JACK_SINK, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/jack/jack-source.c b/spa/plugins/jack/jack-source.c new file mode 100644 index 0000000..7004eed --- /dev/null +++ b/spa/plugins/jack/jack-source.c @@ -0,0 +1,946 @@ +/* Spa jack client + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jack-client.h" + +#define NAME "jack-source" + +#define MAX_PORTS 128 +#define MAX_BUFFERS 8 +#define MAX_SAMPLES 8192 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_list link; +}; + +struct port { + uint32_t id; + + uint64_t info_all; + struct spa_port_info info; + struct spa_dict_item items[4]; + struct spa_dict props; + struct spa_param_info params[5]; + + unsigned int have_format:1; + struct spa_audio_info current_format; + int stride; + + struct spa_io_buffers *io; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; + + jack_port_t *jack_port; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[5]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + struct port out_ports[MAX_PORTS]; + uint32_t n_out_ports; + + struct spa_audio_info current_format; + + struct spa_jack_client *client; + struct spa_hook client_listener; + + unsigned int started:1; +}; + +#define CHECK_OUT_PORT(this,p) ((p) < this->n_out_ports) +#define CHECK_PORT(this,d,p) (d == SPA_DIRECTION_OUTPUT && CHECK_OUT_PORT(this,p)) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) GET_OUT_PORT(this,p) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + return 0; + + case SPA_PARAM_Props: + return 0; + + case SPA_PARAM_EnumFormat: + case SPA_PARAM_Format: + switch (result.index) { + case 0: + param = spa_format_audio_dsp_build(&b, + id, &this->current_format.info.dsp); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + default: + return -ENOENT; + } + return 0; +} + + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + spa_list_append(&port->empty, &b->link); + } +} + +static struct buffer *dequeue_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->empty)) + return NULL; + + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + return b; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (this->started) + return 0; + + this->started = true; + break; + + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + struct spa_dict_item items[5]; + char latency[64]; + snprintf(latency, sizeof(latency), "%d/%d", + this->client->buffer_size, this->client->frame_rate); + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source"); + items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_NAME, "JACK System"); + items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); + items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_PAUSE_ON_IDLE, "false"); + items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_LATENCY, latency); + this->info.props = &SPA_DICT_INIT_ARRAY(items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + char* aliases[2]; + int n_aliases, n_items; + + aliases[0] = alloca(jack_port_name_size()); + aliases[1] = alloca(jack_port_name_size()); + n_aliases = jack_port_get_aliases(port->jack_port, aliases); + n_items = 1; + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, + jack_port_short_name(port->jack_port)); + if (n_aliases > 0) + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, aliases[0]); + if (n_aliases > 1) + port->items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, aliases[1]); + port->props = SPA_DICT_INIT(port->items, n_items); + + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + for (i = 0; i < this->n_out_ports; i++) + emit_port_info(this, GET_OUT_PORT(this, i), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static void client_process(void *data) +{ + struct impl *this = data; + int res; + + res = spa_node_process(&this->node); + + if (res != SPA_STATUS_OK) + spa_node_call_ready(&this->callbacks, res); +} + +static const struct spa_jack_client_events client_events = { + SPA_VERSION_JACK_CLIENT_EVENTS, + .process = client_process, +}; + +static int init_port(struct impl *this, struct port *port) +{ + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PROPS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + + port->items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float mono audio"); + port->props = SPA_DICT_INIT(port->items, 1); + port->info.props = &port->props; + + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + + spa_list_init(&port->empty); + return 0; +} + +static int init_ports(struct impl *this) +{ + const char **ports; + jack_client_t *client = this->client->client; + uint32_t i; + int res; + + ports = jack_get_ports(client, + NULL, JACK_DEFAULT_AUDIO_TYPE, + JackPortIsPhysical|JackPortIsOutput); + if (ports == NULL) { + spa_log_error(this->log, NAME" %p: can't enumerate ports", this); + res = -ENODEV; + goto exit; + } + + for (i = 0; ports[i]; i++) { + struct port *port = GET_OUT_PORT(this, i); + jack_port_t *p = jack_port_by_name(client, ports[i]); + char *aliases[2]; + int n_aliases; + + port->id = i; + port->jack_port = jack_port_register(client, + jack_port_short_name(p), + jack_port_type(p), + JackPortIsInput, 0); + if (port->jack_port == NULL) { + spa_log_error(this->log, NAME" %p: jack_port_register() %d (%s) failed", + this, i, ports[i]); + res = -EFAULT; + goto exit_free; + } + + aliases[0] = alloca(jack_port_name_size()); + aliases[1] = alloca(jack_port_name_size()); + + n_aliases = jack_port_get_aliases(p, aliases); + if (n_aliases > 0) + jack_port_set_alias(port->jack_port, aliases[0]); + if (n_aliases > 1) + jack_port_set_alias(port->jack_port, aliases[1]); + + init_port(this, port); + } + this->n_out_ports = i; + + this->current_format.info.dsp = SPA_AUDIO_INFO_DSP_INIT( + .format = SPA_AUDIO_FORMAT_DSP_F32); + + spa_jack_client_add_listener(this->client, + &this->client_listener, + &client_events, this); + + jack_activate(client); + + for (i = 0; ports[i]; i++) { + struct port *port = GET_OUT_PORT(this, i); + if (jack_connect(client, ports[i], jack_port_name(port->jack_port))) { + spa_log_warn(this->log, NAME" %p: Failed to connect %s to %s", + this, jack_port_name(port->jack_port), ports[i]); + } + } + + res = 0; +exit_free: + jack_free(ports); +exit: + return res; +} + + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + MAX_SAMPLES * port->stride, + 16 * port->stride, + MAX_SAMPLES * port->stride), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + break; + } + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->empty); + this->started = false; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio && + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) + return -EINVAL; + + if (spa_format_audio_dsp_parse(format, &info.info.dsp) < 0) + return -EINVAL; + + if (info.info.dsp.format != SPA_AUDIO_FORMAT_DSP_F32) + return -EINVAL; + + port->stride = 4; + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + default: + return -ENOENT; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = 0; + spa_list_append(&port->empty, &b->link); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_OUT_PORT(this, port_id), -EINVAL); + + port = GET_OUT_PORT(this, port_id); + + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + uint32_t i; + int res = 0; + + spa_log_trace(this->log, NAME" %p: process %d", this, this->n_out_ports); + + for (i = 0; i < this->n_out_ports; i++) { + struct port *port = GET_OUT_PORT(this, i); + struct spa_io_buffers *io = port->io; + struct buffer *b; + struct spa_data *d; + const void *src; + uint32_t n_frames = this->client->buffer_size; + + if (io == NULL || io->status == SPA_STATUS_HAVE_DATA) + continue; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if ((b = dequeue_buffer(this, port)) == NULL) { + spa_log_trace(this->log, NAME" %p: out of buffers", this); + io->status = -EPIPE; + continue; + } + + src = jack_port_get_buffer(port->jack_port, n_frames); + + d = &b->outbuf->datas[0]; + spa_memcpy(d->data, src, n_frames * port->stride); + d->chunk->offset = 0; + d->chunk->size = n_frames * port->stride; + d->chunk->stride = port->stride; + d->chunk->flags = 0; + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + res |= SPA_STATUS_HAVE_DATA; + } + return res; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_JACK_CLIENT))) + sscanf(str, "pointer:%p", &this->client); + + if (this->client == NULL) { + spa_log_error(this->log, NAME" %p: missing "SPA_KEY_API_JACK_CLIENT + " property", this); + return -EINVAL; + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = MAX_PORTS; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ); + this->params[3] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[4] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = 5; + + init_ports(this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Record audio with the JACK API" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_jack_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_JACK_SOURCE, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/jack/meson.build b/spa/plugins/jack/meson.build new file mode 100644 index 0000000..312a540 --- /dev/null +++ b/spa/plugins/jack/meson.build @@ -0,0 +1,12 @@ +spa_jack_sources = [ +'plugin.c', + 'jack-client.c', + 'jack-device.c', + 'jack-sink.c', + 'jack-source.c'] + +spa_jack = shared_library('spa-jack', + spa_jack_sources, + dependencies : [ spa_dep, jack_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'jack') diff --git a/spa/plugins/jack/plugin.c b/spa/plugins/jack/plugin.c new file mode 100644 index 0000000..4aa9cbd --- /dev/null +++ b/spa/plugins/jack/plugin.c @@ -0,0 +1,54 @@ +/* Spa jack plugin + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_jack_device_factory; + break; + case 1: + *factory = &spa_jack_source_factory; + break; + case 2: + *factory = &spa_jack_sink_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/libcamera/libcamera-client.c b/spa/plugins/libcamera/libcamera-client.c new file mode 100644 index 0000000..a31a41f --- /dev/null +++ b/spa/plugins/libcamera/libcamera-client.c @@ -0,0 +1,247 @@ +/* Spa libcamera client + * + * Copyright (C) 2020, Collabora Ltd. + * Author: Raghavendra Rao Sidlagatta + * + * libcamera-client.c + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libcamera.h" + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + + struct spa_hook_list hooks; + + uint64_t info_all; + struct spa_device_info info; + + struct spa_source source; +}; + +static int emit_object_info(struct impl *this, uint32_t id) +{ + struct spa_device_object_info info; + struct spa_dict_item items[20]; + uint32_t n_items = 0; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_LIBCAMERA_DEVICE; + info.change_mask = (SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS); + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API,"libcamera-client"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "libcamera"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device"); + + info.props = &SPA_DICT_INIT(items, n_items); + spa_device_emit_object_info(&this->hooks, id, &info); + + return 1; +} + +static const struct spa_dict_item device_info_items[] = { + { SPA_KEY_DEVICE_API, "libcamera" }, + { SPA_KEY_DEVICE_NICK, "libcamera-client" }, + { SPA_KEY_API_UDEV_MATCH, "libcamera" }, +}; + + +static void emit_device_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(device_info_items); + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void impl_hook_removed(struct spa_hook *hook) +{ + return; +} + +static int +impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_device_info(this, true); + + emit_object_info(this, 0); + + spa_hook_list_join(&this->hooks, &save); + + listener->removed = impl_hook_removed; + listener->priv = this; + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + + if(this->dev.camera) { + deleteLibCamera(this->dev.camera); + this->dev.camera = NULL; + } + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + libcamera_log_topic_init(this->log); + + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main-loop is needed"); + return -EINVAL; + } + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | + SPA_DEVICE_CHANGE_MASK_PROPS; + this->info.flags = 0; + + if(this->dev.camera == NULL) + this->dev.camera = (LibCamera*)newLibCamera(); + if(this->dev.camera != NULL) + libcamera_set_log(this->dev.camera, this->dev.log); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_libcamera_client_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_LIBCAMERA_ENUM_CLIENT, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp new file mode 100644 index 0000000..ff206a4 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-device.cpp @@ -0,0 +1,332 @@ +/* Spa libcamera Device + * + * Copyright (C) 2020, Collabora Ltd. + * Author: Raghavendra Rao Sidlagatta + * Copyright (C) 2021 Wim Taymans + * + * libcamera-device.cpp + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libcamera.h" +#include "libcamera-manager.hpp" + +#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 std::string cameraModel(const Camera *camera) +{ + const ControlList &props = camera->properties(); + + if (auto model = props.get(properties::Model)) + return std::move(model.value()); + + return camera->id(); +} + +static const char *cameraLoc(const Camera *camera) +{ + const ControlList &props = camera->properties(); + + if (auto location = props.get(properties::Location)) { + switch (location.value()) { + case properties::CameraLocationFront: + return "front"; + case properties::CameraLocationBack: + return "back"; + case properties::CameraLocationExternal: + return "external"; + } + } + + return nullptr; +} + +static int emit_info(struct impl *impl, bool full) +{ + struct spa_dict_item items[10]; + struct spa_dict dict; + uint32_t n_items = 0; + struct spa_device_info info; + struct spa_param_info params[2]; + char path[256], model[256], name[256]; + + info = SPA_DEVICE_INFO_INIT(); + + info.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + snprintf(path, sizeof(path), "libcamera:%s", impl->device_id.c_str()); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device"); + ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, impl->device_id.c_str()); + + if (auto location = cameraLoc(impl->camera.get())) + ADD_ITEM(SPA_KEY_API_LIBCAMERA_LOCATION, location); + + snprintf(model, sizeof(model), "%s", cameraModel(impl->camera.get()).c_str()); + ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_NAME, model); + ADD_ITEM(SPA_KEY_DEVICE_DESCRIPTION, model); + snprintf(name, sizeof(name), "libcamera_device.%s", impl->device_id.c_str()); + ADD_ITEM(SPA_KEY_DEVICE_NAME, name); +#undef ADD_ITEM + + dict = SPA_DICT_INIT(items, n_items); + info.props = &dict; + + info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_WRITE); + info.n_params = SPA_N_ELEMENTS(params); + info.params = params; + + spa_device_emit_info(&impl->hooks, &info); + + if (true) { + struct spa_device_object_info oinfo; + + oinfo = SPA_DEVICE_OBJECT_INFO_INIT(); + oinfo.type = SPA_TYPE_INTERFACE_Node; + oinfo.factory_name = SPA_NAME_API_LIBCAMERA_SOURCE; + oinfo.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + oinfo.props = &dict; + + spa_device_emit_object_info(&impl->hooks, 0, &oinfo); + } + return 0; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *impl = (struct impl*)object; + struct spa_hook_list save; + int res = 0; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); + + if (events->info || events->object_info) + res = emit_info(impl, true); + + spa_hook_list_join(&impl->hooks, &save); + + return res; +} + +static int impl_sync(void *object, int seq) +{ + struct impl *impl = (struct impl*) object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + spa_device_emit_result(&impl->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &impl->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + std::destroy_at(reinterpret_cast(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..afba403 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-manager.cpp @@ -0,0 +1,488 @@ +/* Spa libcamera manager + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 = get_free_id(impl);; + device->camera = std::move(camera); + impl->n_devices++; + return device; +} + +static struct device *find_device(struct impl *impl, const Camera *camera) +{ + uint32_t i; + for (i = 0; i < impl->n_devices; i++) { + if (impl->devices[i].camera.get() == camera) + return &impl->devices[i]; + } + return NULL; +} + +static void remove_device(struct impl *impl, struct device *device) +{ + uint32_t old = --impl->n_devices; + device->camera.reset(); + *device = std::move(impl->devices[old]); + impl->devices[old].camera = nullptr; +} + +static void clear_devices(struct impl *impl) +{ + while (impl->n_devices > 0) + impl->devices[--impl->n_devices].camera.reset(); +} + +static int emit_object_info(struct impl *impl, struct device *device) +{ + struct spa_device_object_info info; + uint32_t id = device->id; + struct spa_dict_item items[20]; + struct spa_dict dict; + uint32_t n_items = 0; + char path[256]; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_LIBCAMERA_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + ADD_ITEM(SPA_KEY_DEVICE_ENUM_API,"libcamera.manager"); + ADD_ITEM(SPA_KEY_DEVICE_API, "libcamera"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device"); + snprintf(path, sizeof(path), "%s", device->camera->id().c_str()); + ADD_ITEM(SPA_KEY_API_LIBCAMERA_PATH, path); +#undef ADD_ITEM + + dict = SPA_DICT_INIT(items, n_items); + info.props = &dict; + spa_device_emit_object_info(&impl->hooks, id, &info); + + return 1; +} + +static void try_add_camera(struct impl *impl, std::shared_ptr camera) +{ + 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 = { + 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..4336a39 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-manager.hpp @@ -0,0 +1,29 @@ +/* Spa libcamera support + * + * Copyright © 2021 wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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..d2ca670 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-source.cpp @@ -0,0 +1,1073 @@ +/* Spa libcamera Source + * + * Copyright (C) 2020, Collabora Ltd. + * Author: Raghavendra Rao Sidlagatta + * Copyright (C) 2021 Wim Taymans + * + * libcamera-source.cpp + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; +}; + +#define MAX_CONTROLS 64 + +struct control { + uint32_t id; + uint32_t ctrl_id; + double value; +}; + +struct port { + struct impl *impl; + + std::optional current_format; + + struct spa_fraction rate = {}; + StreamConfiguration streamConfig; + + uint32_t memtype = 0; + + struct control controls[MAX_CONTROLS]; + uint32_t n_controls = 0; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers = 0; + struct spa_list queue; + struct spa_ringbuffer ring = SPA_RINGBUFFER_INIT(); + uint32_t ring_ids[MAX_BUFFERS]; + + static constexpr uint64_t info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PARAMS; + struct spa_port_info info = SPA_PORT_INFO_INIT(); + struct spa_io_buffers *io = nullptr; + struct spa_io_sequence *control = nullptr; +#define PORT_PropInfo 0 +#define PORT_EnumFormat 1 +#define PORT_Meta 2 +#define PORT_IO 3 +#define PORT_Format 4 +#define PORT_Buffers 5 +#define N_PORT_PARAMS 6 + struct spa_param_info params[N_PORT_PARAMS]; + + uint32_t fmt_index = 0; + PixelFormat enum_fmt; + uint32_t size_index = 0; + + port(struct impl *impl) + : impl(impl) + { + spa_list_init(&queue); + + params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + + info.flags = SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL; + info.params = params; + info.n_params = N_PORT_PARAMS; + } +}; + +struct impl { + struct spa_handle handle; + struct spa_node node = {}; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *system; + + static constexpr uint64_t info_all = + SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + struct spa_node_info info = SPA_NODE_INFO_INIT(); +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_EnumFormat 2 +#define NODE_Format 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + + std::string device_id; + std::string device_name; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks = {}; + + std::array out_ports; + + struct spa_io_position *position = nullptr; + struct spa_io_clock *clock = nullptr; + + 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); +}; + +} + +#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; + break; + case SPA_IO_Position: + impl->position = (struct spa_io_position*)data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *impl = (struct impl*)object; + int res; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct port *port = GET_OUT_PORT(impl, 0); + + if (!port->current_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if ((res = spa_libcamera_stream_on(impl)) < 0) + return res; + break; + } + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_libcamera_stream_off(impl)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + + return 0; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_DEVICE_API, "libcamera" }, + { SPA_KEY_MEDIA_CLASS, "Video/Source" }, + { SPA_KEY_MEDIA_ROLE, "Camera" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *impl, bool full) +{ + uint64_t old = full ? impl->info.change_mask : 0; + if (full) + impl->info.change_mask = impl->info_all; + if (impl->info.change_mask) { + struct spa_dict dict = SPA_DICT_INIT_ARRAY(info_items); + impl->info.props = &dict; + spa_node_emit_info(&impl->hooks, &impl->info); + impl->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *impl, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&impl->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *impl = (struct impl*)object; + struct spa_hook_list save; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); + + emit_node_info(impl, true); + emit_port_info(impl, GET_OUT_PORT(impl, 0), true); + + spa_hook_list_join(&impl->hooks, &save); + + return 0; +} + +static int impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *impl = (struct impl*)object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + impl->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *impl = (struct impl*)object; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + spa_node_emit_result(&impl->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, + enum spa_direction direction, + uint32_t port_id, const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, + enum spa_direction direction, + uint32_t port_id) +{ + return -ENOTSUP; +} + +static int impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *impl = (struct impl*)object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); + + port = GET_PORT(impl, direction, port_id); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + return spa_libcamera_enum_controls(impl, port, seq, start, num, filter); + + case SPA_PARAM_EnumFormat: + return spa_libcamera_enum_format(impl, port, seq, start, num, filter); + + case SPA_PARAM_Format: + if((res = port_get_format(impl, port, result.index, filter, ¶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(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(port->streamConfig.frameSize), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->streamConfig.stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = (struct spa_pod*)spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + case 1: + param = (struct spa_pod*)spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = (struct spa_pod*)spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = (struct spa_pod*)spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 2: + param = (struct spa_pod*)spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int port_set_format(struct impl *impl, struct port *port, + uint32_t flags, const struct spa_pod *format) +{ + struct spa_video_info info; + int res; + + if (format == NULL) { + if (!port->current_format) + return 0; + + spa_libcamera_stream_off(impl); + spa_libcamera_clear_buffers(impl, port); + port->current_format.reset(); + + spa_libcamera_close(impl); + goto done; + } else { + spa_zero(info); + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video) { + spa_log_error(impl->log, "media type must be video"); + return -EINVAL; + } + + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + if (spa_format_video_raw_parse(format, &info.info.raw) < 0) { + spa_log_error(impl->log, "can't parse video raw"); + return -EINVAL; + } + + if (port->current_format && info.media_type == port->current_format->media_type && + info.media_subtype == port->current_format->media_subtype && + info.info.raw.format == port->current_format->info.raw.format && + info.info.raw.size.width == port->current_format->info.raw.size.width && + info.info.raw.size.height == port->current_format->info.raw.size.height && + info.info.raw.flags == port->current_format->info.raw.flags && + (!(info.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER) || + (info.info.raw.modifier == port->current_format->info.raw.modifier))) + return 0; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0) + return -EINVAL; + + if (port->current_format && info.media_type == port->current_format->media_type && + info.media_subtype == port->current_format->media_subtype && + info.info.mjpg.size.width == port->current_format->info.mjpg.size.width && + info.info.mjpg.size.height == port->current_format->info.mjpg.size.height) + return 0; + break; + case SPA_MEDIA_SUBTYPE_h264: + if (spa_format_video_h264_parse(format, &info.info.h264) < 0) + return -EINVAL; + + if (port->current_format && info.media_type == port->current_format->media_type && + info.media_subtype == port->current_format->media_subtype && + info.info.h264.size.width == port->current_format->info.h264.size.width && + info.info.h264.size.height == port->current_format->info.h264.size.height) + return 0; + break; + default: + return -EINVAL; + } + } + + if (port->current_format && !(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + spa_libcamera_use_buffers(impl, port, NULL, 0); + port->current_format.reset(); + } + + if (spa_libcamera_set_format(impl, port, &info, flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) < 0) + return -EINVAL; + + if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + port->current_format = info; + } + + done: + impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->current_format) { + impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + impl->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(impl, port, false); + emit_node_info(impl, false); + + return 0; +} + +static int impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *impl = (struct impl*)object; + struct port *port; + int res; + + spa_return_val_if_fail(object != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); + + port = GET_PORT(impl, direction, port_id); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(impl, port, flags, param); + break; + default: + res = -ENOENT; + } + return res; +} + +static int impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *impl = (struct impl*)object; + struct port *port; + int res; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); + + port = GET_PORT(impl, direction, port_id); + + if (port->n_buffers) { + spa_libcamera_stream_off(impl); + if ((res = spa_libcamera_clear_buffers(impl, port)) < 0) + return res; + } + if (n_buffers > 0 && !port->current_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + if (buffers == NULL) + return 0; + + if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { + res = spa_libcamera_alloc_buffers(impl, port, buffers, n_buffers); + } else { + res = spa_libcamera_use_buffers(impl, port, buffers, n_buffers); + } + return res; +} + +static int impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *impl = (struct impl*)object; + struct port *port; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); + + port = GET_PORT(impl, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = (struct spa_io_buffers*)data; + break; + case SPA_IO_Control: + port->control = (struct spa_io_sequence*)data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, + uint32_t port_id, + uint32_t buffer_id) +{ + struct impl *impl = (struct impl*)object; + struct port *port; + int res; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + + port = GET_OUT_PORT(impl, port_id); + + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + res = spa_libcamera_buffer_recycle(impl, port, buffer_id); + + return res; +} + +static int process_control(struct impl *impl, struct spa_pod_sequence *control) +{ + struct spa_pod_control *c; + + SPA_POD_SEQUENCE_FOREACH(control, c) { + switch (c->type) { + case SPA_CONTROL_Properties: + { + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) &c->value; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + spa_libcamera_set_control(impl, prop); + } + break; + } + default: + break; + } + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *impl = (struct impl*)object; + int res; + struct spa_io_buffers *io; + struct port *port; + struct buffer *b; + + spa_return_val_if_fail(impl != NULL, -EINVAL); + + port = GET_OUT_PORT(impl, 0); + if ((io = port->io) == NULL) + return -EIO; + + if (port->control) + process_control(impl, &port->control->sequence); + + spa_log_trace(impl->log, "%p; status %d", impl, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) { + return SPA_STATUS_HAVE_DATA; + } + + if (io->buffer_id < port->n_buffers) { + if ((res = spa_libcamera_buffer_recycle(impl, port, io->buffer_id)) < 0) + return res; + + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&port->queue)) { + return SPA_STATUS_OK; + } + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + spa_log_trace(impl->log, "%p: dequeue buffer %d", impl, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &impl->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + std::destroy_at(reinterpret_cast(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; +} + +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..af07484 --- /dev/null +++ b/spa/plugins/libcamera/libcamera-utils.cpp @@ -0,0 +1,990 @@ +/* Spa + * + * Copyright (C) 2020, Collabora Ltd. + * Author: Raghavendra Rao Sidlagatta + * Copyright (C) 2021 Wim Taymans + * + * libcamera-utils.cpp + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include +#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; + + StreamRoles roles; + roles.push_back(StreamRole::VideoRecording); + impl->config = impl->camera->generateConfiguration(roles); +} + +static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + int res; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) + return 0; + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING); + + if (buffer_id >= impl->requestPool.size()) { + spa_log_warn(impl->log, "invalid buffer_id %u >= %zu", + buffer_id, impl->requestPool.size()); + return -EINVAL; + } + Request *request = impl->requestPool[buffer_id].get(); + Stream *stream = port->streamConfig.stream(); + FrameBuffer *buffer = impl->allocator->buffers(stream)[buffer_id].get(); + if ((res = request->addBuffer(stream, buffer)) < 0) { + spa_log_warn(impl->log, "can't add buffer %u for request: %s", + buffer_id, spa_strerror(res)); + return -ENOMEM; + } + if (!impl->active) { + impl->pendingRequests.push_back(request); + return 0; + } else { + request->controls().merge(impl->ctrls); + impl->ctrls.clear(); + if ((res = impl->camera->queueRequest(request)) < 0) { + spa_log_warn(impl->log, "can't queue buffer %u: %s", + buffer_id, spa_strerror(res)); + return res == -EACCES ? -EBUSY : res; + } + } + return 0; +} + +static int allocBuffers(struct impl *impl, struct port *port, unsigned int count) +{ + int res; + + if ((res = impl->allocator->allocate(port->streamConfig.stream())) < 0) + return res; + + for (unsigned int i = 0; i < count; i++) { + std::unique_ptr request = impl->camera->createRequest(i); + if (!request) { + impl->requestPool.clear(); + return -ENOMEM; + } + impl->requestPool.push_back(std::move(request)); + } + return res; +} + +static void freeBuffers(struct impl *impl, struct port *port) +{ + impl->pendingRequests.clear(); + impl->requestPool.clear(); + impl->allocator->free(port->streamConfig.stream()); +} + +static int spa_libcamera_clear_buffers(struct impl *impl, struct port *port) +{ + uint32_t i; + + if (port->n_buffers == 0) + return 0; + + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b; + struct spa_data *d; + + b = &port->buffers[i]; + d = b->outbuf->datas; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) { + spa_log_debug(impl->log, "queueing outstanding buffer %p", b); + spa_libcamera_buffer_recycle(impl, port, i); + } + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { + munmap(SPA_PTROFF(b->ptr, -d[0].mapoffset, void), + d[0].maxsize - d[0].mapoffset); + } + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) { + close(d[0].fd); + } + d[0].type = SPA_ID_INVALID; + } + + freeBuffers(impl, port); + port->n_buffers = 0; + + return 0; +} + +struct format_info { + PixelFormat pix; + uint32_t format; + uint32_t media_type; + uint32_t media_subtype; +}; + +#define MAKE_FMT(pix,fmt,mt,mst) { pix, SPA_VIDEO_FORMAT_ ##fmt, SPA_MEDIA_TYPE_ ##mt, SPA_MEDIA_SUBTYPE_ ##mst } +static const struct format_info format_info[] = { + /* RGB formats */ + MAKE_FMT(formats::RGB565, RGB16, video, raw), + MAKE_FMT(formats::RGB565_BE, RGB16, video, raw), + MAKE_FMT(formats::RGB888, RGB, video, raw), + MAKE_FMT(formats::BGR888, BGR, video, raw), + MAKE_FMT(formats::XRGB8888, xRGB, video, raw), + MAKE_FMT(formats::XBGR8888, xBGR, video, raw), + MAKE_FMT(formats::RGBX8888, RGBx, video, raw), + MAKE_FMT(formats::BGRX8888, BGRx, video, raw), + MAKE_FMT(formats::ARGB8888, ARGB, video, raw), + MAKE_FMT(formats::ABGR8888, ABGR, video, raw), + MAKE_FMT(formats::RGBA8888, RGBA, video, raw), + MAKE_FMT(formats::BGRA8888, BGRA, video, raw), + + MAKE_FMT(formats::YUYV, YUY2, video, raw), + MAKE_FMT(formats::YVYU, YVYU, video, raw), + MAKE_FMT(formats::UYVY, UYVY, video, raw), + MAKE_FMT(formats::VYUY, VYUY, video, raw), + + MAKE_FMT(formats::NV12, NV12, video, raw), + MAKE_FMT(formats::NV21, NV21, video, raw), + MAKE_FMT(formats::NV16, NV16, video, raw), + MAKE_FMT(formats::NV61, NV61, video, raw), + MAKE_FMT(formats::NV24, NV24, video, raw), + + MAKE_FMT(formats::YUV420, I420, video, raw), + MAKE_FMT(formats::YVU420, YV12, video, raw), + MAKE_FMT(formats::YUV422, Y42B, video, raw), + + MAKE_FMT(formats::MJPEG, ENCODED, video, mjpg), +#undef MAKE_FMT +}; + +static const struct format_info *video_format_to_info(const PixelFormat &pix) { + size_t i; + + for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) { + if (format_info[i].pix == pix) + return &format_info[i]; + } + return NULL; +} + +static const struct format_info *find_format_info_by_media_type(uint32_t type, + uint32_t subtype, uint32_t format, int startidx) +{ + size_t i; + + for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) { + if ((format_info[i].media_type == type) && + (format_info[i].media_subtype == subtype) && + (format == 0 || format_info[i].format == format)) + return &format_info[i]; + } + return NULL; +} + +static int score_size(Size &a, Size &b) +{ + int x, y; + x = (int)a.width - (int)b.width; + y = (int)a.height - (int)b.height; + return x * x + y * y; +} + +static int +spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq, + uint32_t start, uint32_t num, const struct spa_pod *filter) +{ + int res; + const struct format_info *info; + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + struct spa_pod *fmt; + uint32_t i, count = 0, num_sizes; + PixelFormat format; + Size frameSize; + SizeRange sizeRange = SizeRange(); + + spa_libcamera_get_config(impl); + + const StreamConfiguration& streamConfig = impl->config->at(0); + const StreamFormats &formats = streamConfig.formats(); + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + if (result.next == 0) { + port->fmt_index = 0; + port->size_index = 0; + } +next: + result.index = result.next++; + +next_fmt: + if (port->fmt_index >= formats.pixelformats().size()) + goto enum_end; + + format = formats.pixelformats()[port->fmt_index]; + + spa_log_debug(impl->log, "format: %s", format.toString().c_str()); + + info = video_format_to_info(format); + if (info == NULL) { + spa_log_debug(impl->log, "unknown format"); + port->fmt_index++; + goto next_fmt; + } + + num_sizes = formats.sizes(format).size(); + if (num_sizes > 0 && port->size_index <= num_sizes) { + if (port->size_index == 0) { + Size wanted = Size(640, 480), test; + int score, best = INT_MAX; + for (i = 0; i < num_sizes; i++) { + test = formats.sizes(format)[i]; + score = score_size(wanted, test); + if (score < best) { + best = score; + frameSize = test; + } + } + } + else { + frameSize = formats.sizes(format)[port->size_index - 1]; + } + } else if (port->size_index < 1) { + sizeRange = formats.range(format); + if (sizeRange.hStep == 0 || sizeRange.vStep == 0) { + port->size_index = 0; + port->fmt_index++; + goto next_fmt; + } + } else { + port->size_index = 0; + port->fmt_index++; + goto next_fmt; + } + port->size_index++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b, + SPA_FORMAT_mediaType, SPA_POD_Id(info->media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype), + 0); + + if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id(&b, info->format); + } + if (info->pix.modifier()) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_modifier, 0); + spa_pod_builder_long(&b, info->pix.modifier()); + } + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0); + + if (sizeRange.hStep != 0 && sizeRange.vStep != 0) { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Step, 0); + spa_pod_builder_frame(&b, &f[1]); + spa_pod_builder_rectangle(&b, + sizeRange.min.width, + sizeRange.min.height); + spa_pod_builder_rectangle(&b, + sizeRange.min.width, + sizeRange.min.height); + spa_pod_builder_rectangle(&b, + sizeRange.max.width, + sizeRange.max.height); + spa_pod_builder_rectangle(&b, + sizeRange.hStep, + sizeRange.vStep); + spa_pod_builder_pop(&b, &f[1]); + + } else { + spa_pod_builder_rectangle(&b, frameSize.width, frameSize.height); + } + + fmt = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]); + + if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) + goto next; + + spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + return res; +} + +static int spa_libcamera_set_format(struct impl *impl, struct port *port, + struct spa_video_info *format, bool try_only) +{ + const struct format_info *info = NULL; + uint32_t video_format; + struct spa_rectangle *size = NULL; + struct spa_fraction *framerate = NULL; + CameraConfiguration::Status validation; + int res; + + switch (format->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + video_format = format->info.raw.format; + size = &format->info.raw.size; + framerate = &format->info.raw.framerate; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.mjpg.size; + framerate = &format->info.mjpg.framerate; + break; + case SPA_MEDIA_SUBTYPE_h264: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.h264.size; + framerate = &format->info.h264.framerate; + break; + default: + video_format = SPA_VIDEO_FORMAT_ENCODED; + break; + } + + info = find_format_info_by_media_type(format->media_type, + format->media_subtype, video_format, 0); + if (info == NULL || size == NULL || framerate == NULL) { + spa_log_error(impl->log, "unknown media type %d %d %d", format->media_type, + format->media_subtype, video_format); + return -EINVAL; + } + StreamConfiguration& streamConfig = impl->config->at(0); + + streamConfig.pixelFormat = info->pix; + streamConfig.size.width = size->width; + streamConfig.size.height = size->height; + streamConfig.bufferCount = 8; + + validation = impl->config->validate(); + if (validation == CameraConfiguration::Invalid) + return -EINVAL; + + if (try_only) + return 0; + + if ((res = spa_libcamera_open(impl)) < 0) + return res; + + res = impl->camera->configure(impl->config.get()); + if (res != 0) + goto error; + + port->streamConfig = impl->config->at(0); + + if ((res = allocBuffers(impl, port, port->streamConfig.bufferCount)) < 0) + goto error; + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE; + port->info.flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom); + + return 0; +error: + spa_libcamera_close(impl); + return res; + +} + +static int +spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + const ControlInfoMap &info = impl->camera->controls(); + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + struct spa_pod *ctrl; + uint32_t count = 0, skip; + int res; + const ControlId *ctrl_id; + ControlInfo ctrl_info; + + result.id = SPA_PARAM_PropInfo; + result.next = start; + + auto it = info.begin(); + for (skip = result.next; skip; skip--) + it++; + + if (false) { +next: + it++; + } + result.index = result.next++; + if (it == info.end()) + goto enum_end; + + ctrl_id = it->first; + ctrl_info = it->second; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(ctrl_id->id()), + SPA_PROP_INFO_description, SPA_POD_String(ctrl_id->name().c_str()), + 0); + + switch (ctrl_id->type()) { + case ControlTypeBool: { + bool def; + if (ctrl_info.def().isNone()) + def = ctrl_info.min().get(); + 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; + + auto v = info.idmap().find(prop->key); + if (v == info.idmap().end()) + return -ENOENT; + + ctrl_id = v->second; + + d.type = ctrl_id->type(); + d.id = ctrl_id->id(); + + switch (d.type) { + case ControlTypeBool: + if ((res = spa_pod_get_bool(&prop->value, &d.b_val)) < 0) + goto done; + break; + case ControlTypeFloat: + if ((res = spa_pod_get_float(&prop->value, &d.f_val)) < 0) + goto done; + break; + case ControlTypeInteger32: + if ((res = spa_pod_get_int(&prop->value, &d.i_val)) < 0) + goto done; + break; + default: + res = -EINVAL; + goto done; + } + spa_loop_invoke(impl->data_loop, do_update_ctrls, 0, &d, sizeof(d), true, impl); + res = 0; +done: + return res; +} + + +static void libcamera_on_fd_events(struct spa_source *source) +{ + struct impl *impl = (struct impl*) source->data; + struct spa_io_buffers *io; + struct port *port = &impl->out_ports[0]; + uint32_t index, buffer_id; + struct buffer *b; + uint64_t cnt; + + if (source->rmask & SPA_IO_ERR) { + spa_log_error(impl->log, "libcamera %p: error %08x", impl, source->rmask); + if (impl->source.loop) + spa_loop_remove_source(impl->data_loop, &impl->source); + return; + } + + if (!(source->rmask & SPA_IO_IN)) { + spa_log_warn(impl->log, "libcamera %p: spurious wakeup %d", impl, source->rmask); + return; + } + + if (spa_system_eventfd_read(impl->system, impl->source.fd, &cnt) < 0) { + spa_log_error(impl->log, "Failed to read on event fd"); + return; + } + + if (spa_ringbuffer_get_read_index(&port->ring, &index) < 1) { + spa_log_error(impl->log, "nothing is queued"); + return; + } + buffer_id = port->ring_ids[index & MASK_BUFFERS]; + spa_ringbuffer_read_update(&port->ring, index + 1); + + b = &port->buffers[buffer_id]; + spa_list_append(&port->queue, &b->link); + + io = port->io; + if (io == NULL) { + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_libcamera_buffer_recycle(impl, port, b->id); + } else if (io->status != SPA_STATUS_HAVE_DATA) { + if (io->buffer_id < port->n_buffers) + spa_libcamera_buffer_recycle(impl, port, io->buffer_id); + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace(impl->log, "libcamera %p: now queued %d", impl, b->id); + } + spa_node_call_ready(&impl->callbacks, SPA_STATUS_HAVE_DATA); +} + +static int spa_libcamera_use_buffers(struct impl *impl, struct port *port, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + return -ENOTSUP; +} + +static const struct { + Transform libcamera_transform; + uint32_t spa_transform_value; +} transform_map[] = { + { Transform::Identity, SPA_META_TRANSFORMATION_None }, + { Transform::Rot0, SPA_META_TRANSFORMATION_None }, + { Transform::HFlip, SPA_META_TRANSFORMATION_Flipped }, + { Transform::VFlip, SPA_META_TRANSFORMATION_Flipped180 }, + { Transform::HVFlip, SPA_META_TRANSFORMATION_180 }, + { Transform::Rot180, SPA_META_TRANSFORMATION_180 }, + { Transform::Transpose, SPA_META_TRANSFORMATION_Flipped90 }, + { Transform::Rot90, SPA_META_TRANSFORMATION_90 }, + { Transform::Rot270, SPA_META_TRANSFORMATION_270 }, + { Transform::Rot180Transpose, SPA_META_TRANSFORMATION_Flipped270 }, +}; + +static uint32_t libcamera_transform_to_spa_transform_value(Transform transform) +{ + for (const auto& t : transform_map) { + if (t.libcamera_transform == transform) + return t.spa_transform_value; + } + return SPA_META_TRANSFORMATION_None; +} + +static int +mmap_init(struct impl *impl, struct port *port, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + unsigned int i, j; + struct spa_data *d; + Stream *stream = impl->config->at(0).stream(); + const std::vector> &bufs = + impl->allocator->buffers(stream); + + if (n_buffers > 0) { + if (bufs.size() != n_buffers) + return -EINVAL; + + d = buffers[0]->datas; + + if (d[0].type != SPA_ID_INVALID && + d[0].type & (1u << SPA_DATA_DmaBuf)) { + port->memtype = SPA_DATA_DmaBuf; + } else if (d[0].type != SPA_ID_INVALID && + d[0].type & (1u << SPA_DATA_MemFd)) { + port->memtype = SPA_DATA_MemFd; + } else if (d[0].type & (1u << SPA_DATA_MemPtr)) { + port->memtype = SPA_DATA_MemPtr; + } else { + spa_log_error(impl->log, "v4l2: can't use buffers of type %d", d[0].type); + return -EINVAL; + } + } + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + + if (buffers[i]->n_datas < 1) { + spa_log_error(impl->log, "invalid buffer data"); + return -EINVAL; + } + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_OUTSTANDING; + b->h = (struct spa_meta_header*)spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + b->videotransform = (struct spa_meta_videotransform*)spa_buffer_find_meta_data( + buffers[i], SPA_META_VideoTransform, sizeof(*b->videotransform)); + if (b->videotransform) { + b->videotransform->transform = + libcamera_transform_to_spa_transform_value(impl->config->transform); + spa_log_debug(impl->log, "Setting videotransform for buffer %d to %u (from %s)", + i, b->videotransform->transform, transformToString(impl->config->transform)); + + } + + d = buffers[i]->datas; + for(j = 0; j < buffers[i]->n_datas; ++j) { + d[j].type = port->memtype; + d[j].flags = SPA_DATA_FLAG_READABLE; + d[j].mapoffset = 0; + d[j].maxsize = port->streamConfig.frameSize; + d[j].chunk->offset = 0; + d[j].chunk->size = port->streamConfig.frameSize; + d[j].chunk->stride = port->streamConfig.stride; + d[j].chunk->flags = 0; + + if (port->memtype == SPA_DATA_DmaBuf || + port->memtype == SPA_DATA_MemFd) { + d[j].fd = bufs[i]->planes()[j].fd.get(); + spa_log_debug(impl->log, "Got fd = %ld for buffer: #%d", d[j].fd, i); + d[j].data = NULL; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED); + } + else if(port->memtype == SPA_DATA_MemPtr) { + d[j].fd = -1; + d[j].data = mmap(NULL, + d[j].maxsize + d[j].mapoffset, + PROT_READ, MAP_SHARED, + bufs[i]->planes()[j].fd.get(), + 0); + if (d[j].data == MAP_FAILED) { + spa_log_error(impl->log, "mmap: %m"); + continue; + } + b->ptr = d[j].data; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(impl->log, "mmap ptr:%p", d[j].data); + } else { + spa_log_error(impl->log, "invalid buffer type"); + return -EIO; + } + } + spa_libcamera_buffer_recycle(impl, port, i); + } + port->n_buffers = n_buffers; + spa_log_debug(impl->log, "we have %d buffers", n_buffers); + + return 0; +} + +static int +spa_libcamera_alloc_buffers(struct impl *impl, struct port *port, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + int res; + + if (port->n_buffers > 0) + return -EIO; + + if ((res = mmap_init(impl, port, buffers, n_buffers)) < 0) + return res; + + return 0; +} + + +void impl::requestComplete(libcamera::Request *request) +{ + struct impl *impl = this; + struct port *port = &impl->out_ports[0]; + Stream *stream = port->streamConfig.stream(); + uint32_t index, buffer_id; + struct buffer *b; + + spa_log_debug(impl->log, "request complete"); + + buffer_id = request->cookie(); + b = &port->buffers[buffer_id]; + + if ((request->status() == Request::RequestCancelled)) { + spa_log_debug(impl->log, "Request was cancelled"); + request->reuse(); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_libcamera_buffer_recycle(impl, port, b->id); + return; + } + FrameBuffer *buffer = request->findBuffer(stream); + if (buffer == nullptr) { + spa_log_warn(impl->log, "unknown buffer"); + return; + } + const FrameMetadata &fmd = buffer->metadata(); + + + + if (impl->clock) { + impl->clock->nsec = fmd.timestamp; + impl->clock->rate = port->rate; + impl->clock->position = fmd.sequence; + impl->clock->duration = 1; + impl->clock->delay = 0; + impl->clock->rate_diff = 1.0; + impl->clock->next_nsec = fmd.timestamp; + } + if (b->h) { + b->h->flags = 0; + b->h->offset = 0; + b->h->seq = fmd.sequence; + b->h->pts = fmd.timestamp; + b->h->dts_offset = 0; + } + request->reuse(); + + spa_ringbuffer_get_write_index(&port->ring, &index); + port->ring_ids[index & MASK_BUFFERS] = buffer_id; + spa_ringbuffer_write_update(&port->ring, index + 1); + + if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0) + spa_log_error(impl->log, "Failed to write on event fd"); + +} + +static int spa_libcamera_stream_on(struct impl *impl) +{ + struct port *port = &impl->out_ports[0]; + int res; + + if (!port->current_format) { + spa_log_error(impl->log, "Exting %s with -EIO", __FUNCTION__); + return -EIO; + } + + if (impl->active) + return 0; + + impl->camera->requestCompleted.connect(impl, &impl::requestComplete); + + spa_log_info(impl->log, "starting camera %s", impl->device_id.c_str()); + if ((res = impl->camera->start()) < 0) + goto error; + + for (Request *req : impl->pendingRequests) { + if ((res = impl->camera->queueRequest(req)) < 0) + goto error_stop; + } + impl->pendingRequests.clear(); + + impl->source.func = libcamera_on_fd_events; + impl->source.data = impl; + impl->source.fd = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + impl->source.mask = SPA_IO_IN | SPA_IO_ERR; + impl->source.rmask = 0; + if (impl->source.fd < 0) { + spa_log_error(impl->log, "Failed to create eventfd: %s", spa_strerror(impl->source.fd)); + res = impl->source.fd; + goto error_stop; + } + spa_loop_add_source(impl->data_loop, &impl->source); + + impl->active = true; + + return 0; + +error_stop: + impl->camera->stop(); +error: + impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete); + return res == -EACCES ? -EBUSY : res; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *impl = (struct impl *)user_data; + if (impl->source.loop) + spa_loop_remove_source(loop, &impl->source); + return 0; +} + +static int spa_libcamera_stream_off(struct impl *impl) +{ + struct port *port = &impl->out_ports[0]; + int res; + + if (!impl->active) { + for (std::unique_ptr &req : impl->requestPool) + req->reuse(); + return 0; + } + + impl->active = false; + spa_log_info(impl->log, "stopping camera %s", impl->device_id.c_str()); + impl->pendingRequests.clear(); + + if ((res = impl->camera->stop()) < 0) { + spa_log_warn(impl->log, "error stopping camera %s: %s", + impl->device_id.c_str(), spa_strerror(res)); + } + + impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete); + + spa_loop_invoke(impl->data_loop, do_remove_source, 0, NULL, 0, true, impl); + if (impl->source.fd >= 0) { + spa_system_close(impl->system, impl->source.fd); + impl->source.fd = -1; + } + + spa_list_init(&port->queue); + + return 0; +} diff --git a/spa/plugins/libcamera/libcamera.c b/spa/plugins/libcamera/libcamera.c new file mode 100644 index 0000000..bcf24d2 --- /dev/null +++ b/spa/plugins/libcamera/libcamera.c @@ -0,0 +1,57 @@ +/* Spa libcamera support + * + * Copyright © 2020 collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include + +#include "libcamera.h" + +struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.libcamera"); +struct spa_log_topic *libcamera_log_topic = &log_topic; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_libcamera_manager_factory; + break; + case 1: + *factory = &spa_libcamera_device_factory; + break; + case 2: + *factory = &spa_libcamera_source_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/libcamera/libcamera.h b/spa/plugins/libcamera/libcamera.h new file mode 100644 index 0000000..1a4d618 --- /dev/null +++ b/spa/plugins/libcamera/libcamera.h @@ -0,0 +1,51 @@ +/* Spa libcamera support + * + * Copyright © 2020 collabora + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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..abb1a42 --- /dev/null +++ b/spa/plugins/libcamera/meson.build @@ -0,0 +1,17 @@ +libcamera_sources = [ + 'libcamera.c', + 'libcamera-manager.cpp', + 'libcamera-device.cpp', + 'libcamera-source.cpp' +] + +libdrm_dep = dependency('libdrm', version : '>= 2.4.98', + required : get_option('libcamera')) +summary({'libdrm': libdrm_dep.found()}, bool_yn: true, section: 'Backend') +if libdrm_dep.found() + libcameralib = shared_library('spa-libcamera', + libcamera_sources, + dependencies : [ spa_dep, libudev_dep, libcamera_dep, pthread_lib, libdrm_dep ], + install : true, + install_dir : spa_plugindir / 'libcamera') +endif diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build new file mode 100644 index 0000000..d641dd8 --- /dev/null +++ b/spa/plugins/meson.build @@ -0,0 +1,58 @@ +if alsa_dep.found() + subdir('alsa') +endif +if get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed() + subdir('avb') +endif +if get_option('audioconvert').allowed() + subdir('audioconvert') +endif +if get_option('audiomixer').allowed() + subdir('audiomixer') +endif +if get_option('control').allowed() + subdir('control') +endif +if get_option('audiotestsrc').allowed() + subdir('audiotestsrc') +endif +if bluez_deps_found + subdir('bluez5') +endif +if avcodec_dep.found() + subdir('ffmpeg') +endif +if jack_dep.found() + subdir('jack') +endif +if get_option('support').allowed() + subdir('support') +endif +if get_option('test').allowed() + subdir('test') +endif +if get_option('videoconvert').allowed() + subdir('videoconvert') +endif +if get_option('videotestsrc').allowed() + subdir('videotestsrc') +endif +if get_option('volume').allowed() + subdir('volume') +endif +if vulkan_headers + subdir('vulkan') +endif + +v4l2_header_found = cc.has_header('linux/videodev2.h', required: get_option('v4l2')) +v4l2_supported = libudev_dep.found() and v4l2_header_found +summary({'V4L2 kernel header': v4l2_header_found}, bool_yn: true, section: 'Backend') +summary({'V4L2 enabled': v4l2_supported}, bool_yn: true, section: 'Backend') +if v4l2_supported + subdir('v4l2') +endif +if libcamera_dep.found() + subdir('libcamera') +endif + +subdir('aec') diff --git a/spa/plugins/support/cpu-arm.c b/spa/plugins/support/cpu-arm.c new file mode 100644 index 0000000..6cd68d8 --- /dev/null +++ b/spa/plugins/support/cpu-arm.c @@ -0,0 +1,137 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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-x86.c b/spa/plugins/support/cpu-x86.c new file mode 100644 index 0000000..722f7c9 --- /dev/null +++ b/spa/plugins/support/cpu-x86.c @@ -0,0 +1,204 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +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..fc13c16 --- /dev/null +++ b/spa/plugins/support/cpu.c @@ -0,0 +1,313 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.cpu"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + struct spa_handle handle; + struct spa_cpu cpu; + + struct spa_log *log; + + uint32_t flags; + uint32_t force; + uint32_t count; + uint32_t max_align; + uint32_t vm_type; +}; + +char *spa_cpu_read_file(const char *name, char *buffer, size_t len) +{ + int n, fd; + + if ((fd = open(name, O_RDONLY | O_CLOEXEC, 0)) < 0) + return NULL; + + if ((n = read(fd, buffer, len-1)) < 0) { + close(fd); + return NULL; + } + buffer[n] = '\0'; + close(fd); + return buffer; +} + +# if defined (__i386__) || defined (__x86_64__) +#include "cpu-x86.c" +#define init(t) x86_init(t) +#define impl_cpu_zero_denormals x86_zero_denormals +# elif defined (__arm__) || defined (__aarch64__) +#include "cpu-arm.c" +#define init(t) arm_init(t) +#define impl_cpu_zero_denormals arm_zero_denormals +# else +#define init(t) +#define impl_cpu_zero_denormals NULL +#endif + +static uint32_t +impl_cpu_get_flags(void *object) +{ + struct impl *impl = object; + if (impl->force != SPA_CPU_FORCE_AUTODETECT) + return impl->force; + return impl->flags; +} + +static int +impl_cpu_force_flags(void *object, uint32_t flags) +{ + struct impl *impl = object; + impl->force = flags; + return 0; +} + +#ifndef __FreeBSD__ +static uint32_t get_count(struct impl *this) +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + if (sched_getaffinity(0, sizeof(cpuset), &cpuset) == 0) + return CPU_COUNT(&cpuset); + return 1; +} +#else +static uint32_t get_count(struct impl *this) +{ + static const int mib[] = {CTL_HW, HW_NCPU}; + int r; + size_t rSize = sizeof(r); + if(-1 == sysctl(mib, 2, &r, &rSize, 0, 0)) + return 1; + return r; +} +#endif + +static uint32_t +impl_cpu_get_count(void *object) +{ + struct impl *impl = object; + return impl->count; +} + +static uint32_t +impl_cpu_get_max_align(void *object) +{ + struct impl *impl = object; + return impl->max_align; +} + +static uint32_t +impl_cpu_get_vm_type(void *object) +{ + struct impl *impl = object; + + if (impl->vm_type != 0) + return impl->vm_type; + +#if defined(__i386__) || defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) + static const char *const dmi_vendors[] = { + "/sys/class/dmi/id/product_name", /* Test this before sys_vendor to detect KVM over QEMU */ + "/sys/class/dmi/id/sys_vendor", + "/sys/class/dmi/id/board_vendor", + "/sys/class/dmi/id/bios_vendor" + }; + static const struct { + const char *vendor; + int id; + } dmi_vendor_table[] = { + { "KVM", SPA_CPU_VM_KVM }, + { "QEMU", SPA_CPU_VM_QEMU }, + { "VMware", SPA_CPU_VM_VMWARE }, /* https://kb.vmware.com/s/article/1009458 */ + { "VMW", SPA_CPU_VM_VMWARE }, + { "innotek GmbH", SPA_CPU_VM_ORACLE }, + { "Oracle Corporation", SPA_CPU_VM_ORACLE }, + { "Xen", SPA_CPU_VM_XEN }, + { "Bochs", SPA_CPU_VM_BOCHS }, + { "Parallels", SPA_CPU_VM_PARALLELS }, + /* https://wiki.freebsd.org/bhyve */ + { "BHYVE", SPA_CPU_VM_BHYVE }, + }; + + SPA_FOR_EACH_ELEMENT_VAR(dmi_vendors, dv) { + char buffer[256], *s; + + if ((s = spa_cpu_read_file(*dv, buffer, sizeof(buffer))) == NULL) + continue; + + SPA_FOR_EACH_ELEMENT_VAR(dmi_vendor_table, t) { + if (spa_strstartswith(s, t->vendor)) { + spa_log_debug(impl->log, "Virtualization %s found in DMI (%s)", + s, *dv); + impl->vm_type = t->id; + goto done; + } + } + } +done: +#endif + return impl->vm_type; +} + +static const struct spa_cpu_methods impl_cpu = { + SPA_VERSION_CPU_METHODS, + .get_flags = impl_cpu_get_flags, + .force_flags = impl_cpu_force_flags, + .get_count = impl_cpu_get_count, + .get_max_align = impl_cpu_get_max_align, + .get_vm_type = impl_cpu_get_vm_type, + .zero_denormals = impl_cpu_zero_denormals, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_CPU)) + *interface = &this->cpu; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->cpu.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_CPU, + SPA_VERSION_CPU, + &impl_cpu, this); + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + this->flags = 0; + this->force = SPA_CPU_FORCE_AUTODETECT; + this->max_align = 16; + this->count = get_count(this); + init(this); + + if (info) { + if ((str = spa_dict_lookup(info, SPA_KEY_CPU_FORCE)) != NULL) + this->flags = atoi(str); + if ((str = spa_dict_lookup(info, SPA_KEY_CPU_VM_TYPE)) != NULL) + this->vm_type = atoi(str); + if ((str = spa_dict_lookup(info, SPA_KEY_CPU_ZERO_DENORMALS)) != NULL) + spa_cpu_zero_denormals(&this->cpu, spa_atob(str)); + } + + spa_log_debug(this->log, "%p: count:%d align:%d flags:%08x", + this, this->count, this->max_align, this->flags); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_CPU,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +const struct spa_handle_factory spa_support_cpu_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_CPU, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/support/dbus.c b/spa/plugins/support/dbus.c new file mode 100644 index 0000000..a6b5e58 --- /dev/null +++ b/spa/plugins/support/dbus.c @@ -0,0 +1,592 @@ +/* PipeWire + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.dbus"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +struct impl { + struct spa_handle handle; + struct spa_dbus dbus; + + struct spa_log *log; + struct spa_loop_utils *utils; + + struct spa_list connection_list; +}; + +struct source_data { + struct spa_list link; + struct spa_source *source; + struct connection *conn; +}; + +#define connection_emit(c,m,v,...) spa_hook_list_call(&c->listener_list, struct spa_dbus_connection_events, m, v, ##__VA_ARGS__) +#define connection_emit_destroy(c) connection_emit(c, destroy, 0) +#define connection_emit_disconnected(c) connection_emit(c, disconnected, 0) + +struct connection { + struct spa_list link; + + struct spa_dbus_connection this; + struct impl *impl; + enum spa_dbus_type type; + DBusConnection *conn; + struct spa_source *dispatch_event; + struct spa_list source_list; + + struct spa_hook_list listener_list; +}; + +static void source_data_free(void *data) +{ + struct source_data *d = data; + struct connection *conn = d->conn; + struct impl *impl = conn->impl; + + spa_list_remove(&d->link); + spa_loop_utils_destroy_source(impl->utils, d->source); + free(d); +} + +static void dispatch_cb(void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + + if (dbus_connection_dispatch(conn->conn) == DBUS_DISPATCH_COMPLETE) + spa_loop_utils_enable_idle(impl->utils, conn->dispatch_event, false); +} + +static void dispatch_status(DBusConnection *conn, DBusDispatchStatus status, void *userdata) +{ + struct connection *c = userdata; + struct impl *impl = c->impl; + + spa_loop_utils_enable_idle(impl->utils, c->dispatch_event, + status == DBUS_DISPATCH_COMPLETE ? false : true); +} + +static inline uint32_t dbus_to_io(DBusWatch *watch) +{ + uint32_t mask; + unsigned int flags; + + /* no watch flags for disabled watches */ + if (!dbus_watch_get_enabled(watch)) + return 0; + + flags = dbus_watch_get_flags(watch); + mask = SPA_IO_HUP | SPA_IO_ERR; + + if (flags & DBUS_WATCH_READABLE) + mask |= SPA_IO_IN; + if (flags & DBUS_WATCH_WRITABLE) + mask |= SPA_IO_OUT; + + return mask; +} + +static inline unsigned int io_to_dbus(uint32_t mask) +{ + unsigned int flags = 0; + + if (mask & SPA_IO_IN) + flags |= DBUS_WATCH_READABLE; + if (mask & SPA_IO_OUT) + flags |= DBUS_WATCH_WRITABLE; + if (mask & SPA_IO_HUP) + flags |= DBUS_WATCH_HANGUP; + if (mask & SPA_IO_ERR) + flags |= DBUS_WATCH_ERROR; + return flags; +} + +static void +handle_io_event(void *userdata, int fd, uint32_t mask) +{ + DBusWatch *watch = userdata; + + if (!dbus_watch_get_enabled(watch)) { + fprintf(stderr, "Asked to handle disabled watch: %p %i", (void *) watch, fd); + return; + } + dbus_watch_handle(watch, io_to_dbus(mask)); +} + +static dbus_bool_t add_watch(DBusWatch *watch, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + struct source_data *data; + + spa_log_debug(impl->log, "add watch %p %d", watch, dbus_watch_get_unix_fd(watch)); + + data = calloc(1, sizeof(struct source_data)); + data->conn = conn; + /* we dup because dbus tends to add the same fd multiple times and our epoll + * implementation does not like that */ + data->source = spa_loop_utils_add_io(impl->utils, + dup(dbus_watch_get_unix_fd(watch)), + dbus_to_io(watch), true, handle_io_event, watch); + spa_list_append(&conn->source_list, &data->link); + + dbus_watch_set_data(watch, data, source_data_free); + return TRUE; +} + +static void remove_watch(DBusWatch *watch, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + spa_log_debug(impl->log, "remove watch %p", watch); + dbus_watch_set_data(watch, NULL, NULL); +} + +static void toggle_watch(DBusWatch *watch, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + struct source_data *data; + + spa_log_debug(impl->log, "toggle watch %p", watch); + + if ((data = dbus_watch_get_data(watch)) == NULL) + return; + + spa_loop_utils_update_io(impl->utils, data->source, dbus_to_io(watch)); +} + +static void +handle_timer_event(void *userdata, uint64_t expirations) +{ + DBusTimeout *timeout = userdata; + uint64_t t; + struct timespec ts; + struct source_data *data; + struct connection *conn; + struct impl *impl; + + if ((data = dbus_timeout_get_data(timeout)) == NULL) + return; + + conn = data->conn; + impl = conn->impl; + + spa_log_debug(impl->log, "timeout %p conn:%p impl:%p", timeout, conn, impl); + + if (dbus_timeout_get_enabled(timeout)) { + t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC; + ts.tv_sec = t / SPA_NSEC_PER_SEC; + ts.tv_nsec = t % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(impl->utils, + data->source, &ts, NULL, false); + dbus_timeout_handle(timeout); + } +} + +static dbus_bool_t add_timeout(DBusTimeout *timeout, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + struct timespec ts; + struct source_data *data; + uint64_t t; + + if (!dbus_timeout_get_enabled(timeout)) + return FALSE; + + spa_log_debug(impl->log, "add timeout %p conn:%p impl:%p", timeout, conn, impl); + + data = calloc(1, sizeof(struct source_data)); + data->conn = conn; + data->source = spa_loop_utils_add_timer(impl->utils, handle_timer_event, timeout); + spa_list_append(&conn->source_list, &data->link); + + dbus_timeout_set_data(timeout, data, source_data_free); + + t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC; + ts.tv_sec = t / SPA_NSEC_PER_SEC; + ts.tv_nsec = t % SPA_NSEC_PER_SEC; + spa_loop_utils_update_timer(impl->utils, data->source, &ts, NULL, false); + + return TRUE; +} + +static void remove_timeout(DBusTimeout *timeout, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + spa_log_debug(impl->log, "remove timeout %p conn:%p impl:%p", timeout, conn, impl); + dbus_timeout_set_data(timeout, NULL, NULL); +} + +static void toggle_timeout(DBusTimeout *timeout, void *userdata) +{ + struct connection *conn = userdata; + struct impl *impl = conn->impl; + struct source_data *data; + struct timespec ts, *tsp; + + if ((data = dbus_timeout_get_data(timeout)) == NULL) + return; + + spa_log_debug(impl->log, "toggle timeout %p conn:%p impl:%p", timeout, conn, impl); + + if (dbus_timeout_get_enabled(timeout)) { + uint64_t t = dbus_timeout_get_interval(timeout) * SPA_NSEC_PER_MSEC; + ts.tv_sec = t / SPA_NSEC_PER_SEC; + ts.tv_nsec = t % SPA_NSEC_PER_SEC; + tsp = &ts; + } else { + tsp = NULL; + } + spa_loop_utils_update_timer(impl->utils, data->source, tsp, NULL, false); +} + +static void wakeup_main(void *userdata) +{ + struct connection *this = userdata; + struct impl *impl = this->impl; + + spa_loop_utils_enable_idle(impl->utils, this->dispatch_event, true); +} + +static void connection_close(struct connection *this); + +static DBusHandlerResult filter_message (DBusConnection *connection, + DBusMessage *message, void *user_data) +{ + struct connection *this = user_data; + struct impl *impl = this->impl; + + if (dbus_message_is_signal(message, DBUS_INTERFACE_LOCAL, "Disconnected")) { + spa_log_debug(impl->log, "dbus connection %p disconnected", this); + connection_close(this); + connection_emit_disconnected(this); + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static const char *type_to_string(enum spa_dbus_type type) { + switch (type) { + case SPA_DBUS_TYPE_SESSION: return "session"; + case SPA_DBUS_TYPE_SYSTEM: return "system"; + case SPA_DBUS_TYPE_STARTER: return "starter"; + default: return "unknown"; + } +} + +static void * +impl_connection_get(struct spa_dbus_connection *conn) +{ + struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this); + struct impl *impl = this->impl; + DBusError error; + + if (this->conn != NULL) + return this->conn; + + dbus_error_init(&error); + + this->conn = dbus_bus_get_private((DBusBusType)this->type, &error); + if (this->conn == NULL) + goto error; + + dbus_connection_set_exit_on_disconnect(this->conn, false); + if (!dbus_connection_add_filter(this->conn, filter_message, this, NULL)) + goto error_filter; + + dbus_connection_set_dispatch_status_function(this->conn, dispatch_status, this, NULL); + dbus_connection_set_watch_functions(this->conn, add_watch, remove_watch, toggle_watch, this, + NULL); + dbus_connection_set_timeout_functions(this->conn, add_timeout, remove_timeout, + toggle_timeout, this, NULL); + dbus_connection_set_wakeup_main_function(this->conn, wakeup_main, this, NULL); + + return this->conn; + +error: + spa_log_error(impl->log, "Failed to connect to %s bus: %s", type_to_string(this->type), error.message); + dbus_error_free(&error); + errno = ECONNREFUSED; + return NULL; +error_filter: + spa_log_error(impl->log, "Failed to create filter"); + dbus_connection_close(this->conn); + dbus_connection_unref(this->conn); + this->conn = NULL; + errno = ENOMEM; + return NULL; +} + + +static void connection_close(struct connection *this) +{ + if (this->conn) { + dbus_connection_remove_filter(this->conn, filter_message, this); + dbus_connection_close(this->conn); + + /* Someone may still hold a ref to the handle from get(), so the + * unref below may not be the final one. For that case, reset + * all callbacks we defined to be sure they are not called. */ + dbus_connection_set_dispatch_status_function(this->conn, NULL, NULL, NULL); + dbus_connection_set_watch_functions(this->conn, NULL, NULL, NULL, NULL, NULL); + dbus_connection_set_timeout_functions(this->conn, NULL, NULL, NULL, NULL, NULL); + dbus_connection_set_wakeup_main_function(this->conn, NULL, NULL, NULL); + + dbus_connection_unref(this->conn); + } + this->conn = NULL; +} + +static void connection_free(struct connection *conn) +{ + struct impl *impl = conn->impl; + struct source_data *data; + + spa_list_remove(&conn->link); + + connection_close(conn); + + spa_list_consume(data, &conn->source_list, link) + source_data_free(data); + + spa_loop_utils_destroy_source(impl->utils, conn->dispatch_event); + + spa_hook_list_clean(&conn->listener_list); + + free(conn); +} + +static void +impl_connection_destroy(struct spa_dbus_connection *conn) +{ + struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this); + struct impl *impl = this->impl; + + connection_emit_destroy(this); + + spa_log_debug(impl->log, "destroy conn %p", this); + connection_free(this); +} + +static void +impl_connection_add_listener(struct spa_dbus_connection *conn, + struct spa_hook *listener, + const struct spa_dbus_connection_events *events, + void *data) +{ + struct connection *this = SPA_CONTAINER_OF(conn, struct connection, this); + spa_hook_list_append(&this->listener_list, listener, events, data); +} + +static const struct spa_dbus_connection impl_connection = { + SPA_VERSION_DBUS_CONNECTION, + impl_connection_get, + impl_connection_destroy, + impl_connection_add_listener, +}; + +static struct spa_dbus_connection * +impl_get_connection(void *object, + enum spa_dbus_type type) +{ + struct impl *impl = object; + struct connection *conn; + int res; + + conn = calloc(1, sizeof(struct connection)); + conn->this = impl_connection; + conn->impl = impl; + conn->type = type; + conn->dispatch_event = spa_loop_utils_add_idle(impl->utils, + false, dispatch_cb, conn); + if (conn->dispatch_event == NULL) + goto no_event; + + spa_list_init(&conn->source_list); + spa_hook_list_init(&conn->listener_list); + + spa_list_append(&impl->connection_list, &conn->link); + + spa_log_debug(impl->log, "new conn %p", conn); + + return &conn->this; + +no_event: + res = -errno; + spa_log_error(impl->log, "Failed to create idle event: %m"); + free(conn); + errno = -res; + return NULL; +} + +static const struct spa_dbus_methods impl_dbus = { + SPA_VERSION_DBUS_METHODS, + .get_connection = impl_get_connection, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_DBus)) + *interface = &this->dbus; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *impl = (struct impl *) handle; + struct connection *conn; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + spa_list_consume(conn, &impl->connection_list, link) + connection_free(conn); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + spa_list_init(&this->connection_list); + + this->dbus.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_DBus, + SPA_VERSION_DBUS, + &impl_dbus, this); + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, &log_topic); + + this->utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); + + if (this->utils == NULL) { + spa_log_error(this->log, "a LoopUtils is needed"); + return -EINVAL; + } + + spa_log_debug(this->log, "%p: initialized", this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_DBus,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_handle_factory dbus_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_DBUS, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &dbus_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/support/evl-plugin.c b/spa/plugins/support/evl-plugin.c new file mode 100644 index 0000000..e140cf7 --- /dev/null +++ b/spa/plugins/support/evl-plugin.c @@ -0,0 +1,47 @@ +/* Spa Support plugin + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include + +extern const struct spa_handle_factory spa_support_evl_system_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_support_evl_system_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/support/evl-system.c b/spa/plugins/support/evl-system.c new file mode 100644 index 0000000..58130a0 --- /dev/null +++ b/spa/plugins/support/evl-system.c @@ -0,0 +1,460 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#define NAME "evl-system" + +#define MAX_POLL 512 + +struct poll_entry { + int pfd; + int fd; + uint32_t events; + void *data; +}; + +struct impl { + struct spa_handle handle; + struct spa_system system; + + struct spa_log *log; + + struct poll_entry entries[MAX_POLL]; + uint32_t n_entries; + + uint32_t n_xbuf; + int attached; + int pid; +}; + +static ssize_t impl_read(void *object, int fd, void *buf, size_t count) +{ + return oob_read(fd, buf, count); +} + +static ssize_t impl_write(void *object, int fd, const void *buf, size_t count) +{ + return oob_write(fd, buf, count); +} + +static int impl_ioctl(void *object, int fd, unsigned long request, ...) +{ + int res; + va_list ap; + long arg; + + va_start(ap, request); + arg = va_arg(ap, long); + res = oob_ioctl(fd, request, arg); + va_end(ap); + + return res; +} + +static int impl_close(void *object, int fd) +{ + return close(fd); +} + +static inline int clock_id_to_evl(int clockid) +{ + switch(clockid) { + case CLOCK_MONOTONIC: + return EVL_CLOCK_MONOTONIC; + case CLOCK_REALTIME: + return EVL_CLOCK_REALTIME; + default: + return -clockid; + } +} + +/* clock */ +static int impl_clock_gettime(void *object, + int clockid, struct timespec *value) +{ + return evl_read_clock(clock_id_to_evl(clockid), value); +} + +static int impl_clock_getres(void *object, + int clockid, struct timespec *res) +{ + return evl_get_clock_resolution(clock_id_to_evl(clockid), res); +} + +/* poll */ +static int impl_pollfd_create(void *object, int flags) +{ + int retval; + retval = evl_new_poll(); + return retval; +} + +static inline struct poll_entry *find_entry(struct impl *impl, int pfd, int fd) +{ + uint32_t i; + for (i = 0; i < impl->n_entries; i++) { + struct poll_entry *e = &impl->entries[i]; + if (e->pfd == pfd && e->fd == fd) + return e; + } + return NULL; +} + +static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data) +{ + struct impl *impl = object; + struct poll_entry *e; + + if (impl->n_entries == MAX_POLL) + return -ENOSPC; + + e = &impl->entries[impl->n_entries++]; + e->pfd = pfd; + e->fd = fd; + e->events = events; + e->data = data; + return evl_add_pollfd(pfd, fd, e->events); +} + +static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data) +{ + struct impl *impl = object; + struct poll_entry *e; + + e = find_entry(impl, pfd, fd); + if (e == NULL) + return -ENOENT; + + e->events = events; + e->data = data; + return evl_mod_pollfd(pfd, fd, e->events); +} + +static int impl_pollfd_del(void *object, int pfd, int fd) +{ + struct impl *impl = object; + struct poll_entry *e; + + e = find_entry(impl, pfd, fd); + if (e == NULL) + return -ENOENT; + + e->pfd = -1; + e->fd = -1; + return evl_del_pollfd(pfd, fd); +} + +static int impl_pollfd_wait(void *object, int pfd, + struct spa_poll_event *ev, int n_ev, int timeout) +{ + struct impl *impl = object; + struct evl_poll_event pollset[n_ev]; + struct timespec tv; + int i, j, res; + + if (impl->attached == 0) { + res = evl_attach_self("evl-thread-%d-%p", impl->pid, impl); + if (res < 0) + return res; + impl->attached = res; + } + + if (timeout == -1) { + tv.tv_sec = 0; + tv.tv_nsec = 0; + } else { + tv.tv_sec = timeout / SPA_MSEC_PER_SEC; + tv.tv_nsec = (timeout % SPA_MSEC_PER_SEC) * SPA_NSEC_PER_MSEC; + } + res = evl_timedpoll(pfd, pollset, n_ev, &tv); + if (SPA_UNLIKELY(res < 0)) + return res; + + for (i = 0, j = 0; i < res; i++) { + struct poll_entry *e; + + e = find_entry(impl, pfd, pollset[i].fd); + if (e == NULL) + continue; + + ev[j].events = pollset[i].events; + ev[j].data = e->data; + j++; + } + return j; +} + +/* timers */ +static int impl_timerfd_create(void *object, int clockid, int flags) +{ + int cid; + + switch (clockid) { + case CLOCK_MONOTONIC: + cid = EVL_CLOCK_MONOTONIC; + break; + default: + return -ENOTSUP; + } + return evl_new_timer(cid); +} + +static int impl_timerfd_settime(void *object, + int fd, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value) +{ + struct itimerspec val = *new_value; + + if (!(flags & SPA_FD_TIMER_ABSTIME)) { + struct timespec now; + + evl_read_clock(EVL_CLOCK_MONOTONIC, &now); + val.it_value.tv_sec += now.tv_sec; + val.it_value.tv_nsec += now.tv_nsec; + if (val.it_value.tv_nsec >= 1000000000) { + val.it_value.tv_sec++; + val.it_value.tv_nsec -= 1000000000; + } + } + return evl_set_timer(fd, &val, old_value); +} + +static int impl_timerfd_gettime(void *object, + int fd, struct itimerspec *curr_value) +{ + return evl_get_timer(fd, curr_value); + +} +static int impl_timerfd_read(void *object, int fd, uint64_t *expirations) +{ + uint32_t ticks; + if (oob_read(fd, &ticks, sizeof(ticks)) != sizeof(ticks)) + return -errno; + *expirations = ticks; + return 0; +} + +/* events */ +static int impl_eventfd_create(void *object, int flags) +{ + struct impl *impl = object; + int res; + + res = evl_new_xbuf(1024, 1024, "xbuf-%d-%p-%d", impl->pid, impl, impl->n_xbuf); + if (res < 0) + return res; + + impl->n_xbuf++; + + if (flags & SPA_FD_NONBLOCK) + fcntl(res, F_SETFL, fcntl(res, F_GETFL) | O_NONBLOCK); + + return res; +} + +static int impl_eventfd_write(void *object, int fd, uint64_t count) +{ + if (write(fd, &count, sizeof(uint64_t)) != sizeof(uint64_t)) + return -errno; + return 0; +} + +static int impl_eventfd_read(void *object, int fd, uint64_t *count) +{ + if (oob_read(fd, count, sizeof(uint64_t)) != sizeof(uint64_t)) + return -errno; + return 0; +} + +/* signals */ +static int impl_signalfd_create(void *object, int signal, int flags) +{ + sigset_t mask; + int res, fl = 0; + + if (flags & SPA_FD_CLOEXEC) + fl |= SFD_CLOEXEC; + if (flags & SPA_FD_NONBLOCK) + fl |= SFD_NONBLOCK; + + sigemptyset(&mask); + sigaddset(&mask, signal); + res = signalfd(-1, &mask, fl); + sigprocmask(SIG_BLOCK, &mask, NULL); + + return res; +} + +static int impl_signalfd_read(void *object, int fd, int *signal) +{ + struct signalfd_siginfo signal_info; + int len; + + len = read(fd, &signal_info, sizeof signal_info); + if (!(len == -1 && errno == EAGAIN) && len != sizeof signal_info) + return -errno; + + *signal = signal_info.ssi_signo; + + return 0; +} + +static const struct spa_system_methods impl_system = { + SPA_VERSION_SYSTEM_METHODS, + .read = impl_read, + .write = impl_write, + .ioctl = impl_ioctl, + .close = impl_close, + .clock_gettime = impl_clock_gettime, + .clock_getres = impl_clock_getres, + .pollfd_create = impl_pollfd_create, + .pollfd_add = impl_pollfd_add, + .pollfd_mod = impl_pollfd_mod, + .pollfd_del = impl_pollfd_del, + .pollfd_wait = impl_pollfd_wait, + .timerfd_create = impl_timerfd_create, + .timerfd_settime = impl_timerfd_settime, + .timerfd_gettime = impl_timerfd_gettime, + .timerfd_read = impl_timerfd_read, + .eventfd_create = impl_eventfd_create, + .eventfd_write = impl_eventfd_write, + .eventfd_read = impl_eventfd_read, + .signalfd_create = impl_signalfd_create, + .signalfd_read = impl_signalfd_read, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_System)) + *interface = &impl->system; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + spa_return_val_if_fail(handle != NULL, -EINVAL); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *impl; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct impl *) handle; + impl->system.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_System, + SPA_VERSION_SYSTEM, + &impl_system, impl); + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + impl->pid = getpid(); + + if ((res = evl_attach_self("evl-system-%d-%p", impl->pid, impl)) < 0) { + spa_log_error(impl->log, NAME " %p: init failed: %s", impl, spa_strerror(res)); + return res; + } + + spa_log_debug(impl->log, NAME " %p: initialized", impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_System,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_support_evl_system_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_SYSTEM, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info +}; diff --git a/spa/plugins/support/journal.c b/spa/plugins/support/journal.c new file mode 100644 index 0000000..e0e58eb --- /dev/null +++ b/spa/plugins/support/journal.c @@ -0,0 +1,341 @@ +/* Spa + * + * Copyright © 2020 Sergey Bugaev + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "log-patterns.h" + +#define NAME "journal" + +#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_INFO + +struct impl { + struct spa_handle handle; + struct spa_log log; + + /* if non-null, we'll additionally forward all logging to there */ + struct spa_log *chain_log; + + struct spa_list patterns; +}; + +static SPA_PRINTF_FUNC(7,0) void +impl_log_logtv(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ + struct impl *impl = object; + char line_buffer[32]; + char file_buffer[strlen("CODE_FILE=") + strlen(file) + 1]; + char message_buffer[LINE_MAX]; + int priority; + size_t sz = 0; + + if (impl->chain_log != NULL) { + va_list args_copy; + va_copy(args_copy, args); + spa_log_logtv(impl->chain_log, + level, topic, + file, line, func, fmt, args_copy); + va_end(args_copy); + } + + /* convert SPA log level to syslog priority */ + switch (level) { + case SPA_LOG_LEVEL_ERROR: + priority = LOG_ERR; + break; + case SPA_LOG_LEVEL_WARN: + priority = LOG_WARNING; + break; + case SPA_LOG_LEVEL_INFO: + priority = LOG_INFO; + break; + case SPA_LOG_LEVEL_DEBUG: + case SPA_LOG_LEVEL_TRACE: + default: + priority = LOG_DEBUG; + break; + } + + if (topic) + sz = spa_scnprintf(message_buffer, sizeof(message_buffer), + "%s: ", topic->topic); + + /* we'll be using the low-level journal API, which expects us to provide + * the location explicitly. line and file are to be passed as preformatted + * entries, whereas the function name is passed as-is, and converted into + * a field inside sd_journal_send_with_location(). */ + snprintf(line_buffer, sizeof(line_buffer), "CODE_LINE=%d", line); + snprintf(file_buffer, sizeof(file_buffer), "CODE_FILE=%s", file); + vsnprintf(message_buffer + sz, sizeof(message_buffer) - sz, fmt, args); + + sd_journal_send_with_location(file_buffer, line_buffer, func, + "MESSAGE=%s", message_buffer, + "PRIORITY=%i", priority, + NULL); +} + +static SPA_PRINTF_FUNC(6,7) void +impl_log_log(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + impl_log_logtv(object, level, NULL, file, line, func, fmt, args); + va_end(args); +} + +static SPA_PRINTF_FUNC(6,0) void +impl_log_logv(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ + impl_log_logtv(object, level, NULL, file, line, func, fmt, args); +} + +static SPA_PRINTF_FUNC(7,8) void +impl_log_logt(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + impl_log_logtv(object, level, topic, file, line, func, fmt, args); + va_end(args); +} + +static void +impl_log_topic_init(void *object, struct spa_log_topic *t) +{ + struct impl *impl = object; + enum spa_log_level level = impl->log.level; + + support_log_topic_init(&impl->patterns, level, t); +} + +static const struct spa_log_methods impl_log = { + SPA_VERSION_LOG_METHODS, + .log = impl_log_log, + .logv = impl_log_logv, + .logt = impl_log_logt, + .logtv = impl_log_logtv, + .topic_init = impl_log_topic_init, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Log)) + *interface = &this->log; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + support_log_free_patterns(&this->patterns); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +/** Determine if our stderr goes straight to the journal */ +static int +stderr_is_connected_to_journal(void) +{ + const char *journal_stream; + unsigned long long journal_device, journal_inode; + struct stat stderr_stat; + + /* when a service's stderr is connected to the journal, systemd sets + * JOURNAL_STREAM in the environment of that service to device:inode + * of its stderr. if the variable is not set, clearly our stderr is + * not connected to the journal */ + journal_stream = getenv("JOURNAL_STREAM"); + if (journal_stream == NULL) + return 0; + + /* if it *is* set, that doesn't immediately mean that *our* stderr + * is (still) connected to the journal. to know for sure, we have to + * compare our actual stderr to the stream systemd has created for + * the service we're a part of */ + + if (sscanf(journal_stream, "%llu:%llu", &journal_device, &journal_inode) != 2) + return 0; + + if (fstat(STDERR_FILENO, &stderr_stat) < 0) + return 0; + + return stderr_stat.st_dev == journal_device && stderr_stat.st_ino == journal_inode; +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *impl; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct impl *) handle; + + impl->log.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Log, + SPA_VERSION_LOG, + &impl_log, impl); + impl->log.level = DEFAULT_LOG_LEVEL; + + spa_list_init(&impl->patterns); + + if (info) { + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL) + impl->log.level = atoi(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_PATTERNS)) != NULL) + support_log_parse_patterns(&impl->patterns, str); + } + + /* if our stderr goes to the journal, there's no point in logging both + * via the native journal API and by printing to stderr, that would just + * result in message duplication */ + if (stderr_is_connected_to_journal()) + impl->chain_log = NULL; + else + impl->chain_log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_log_debug(&impl->log, NAME " %p: initialized", impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Log,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_handle_factory journal_factory = { + SPA_VERSION_HANDLE_FACTORY, + .name = SPA_NAME_SUPPORT_LOG, + .info = NULL, + .get_size = impl_get_size, + .init = impl_init, + .enum_interface_info = impl_enum_interface_info, +}; + + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &journal_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/support/log-patterns.c b/spa/plugins/support/log-patterns.c new file mode 100644 index 0000000..4a35b1e --- /dev/null +++ b/spa/plugins/support/log-patterns.c @@ -0,0 +1,108 @@ +/* Spa + * + * Copyright © 2021 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include +#include +#include + +#include "log-patterns.h" + +struct support_log_pattern { + struct spa_list link; + enum spa_log_level level; + char pattern[]; +}; + +void +support_log_topic_init(struct spa_list *patterns, enum spa_log_level default_level, + struct spa_log_topic *t) +{ + enum spa_log_level level = default_level; + const char *topic = t->topic; + struct support_log_pattern *pattern; + + spa_list_for_each(pattern, patterns, link) { + if (fnmatch(pattern->pattern, topic, 0) != 0) + continue; + level = pattern->level; + t->has_custom_level = true; + } + + t->level = level; +} + +int +support_log_parse_patterns(struct spa_list *patterns, const char *jsonstr) +{ + struct spa_json iter, array, elem; + int res = 0; + + spa_json_init(&iter, jsonstr, strlen(jsonstr)); + + if (spa_json_enter_array(&iter, &array) < 0) + return -EINVAL; + + while (spa_json_enter_object(&array, &elem) > 0) { + char pattern[512] = {0}; + + while (spa_json_get_string(&elem, pattern, sizeof(pattern)) > 0) { + struct support_log_pattern *p; + const char *val; + int len; + int lvl; + + if ((len = spa_json_next(&elem, &val)) <= 0) + break; + + if (!spa_json_is_int(val, len)) + break; + + if ((res = spa_json_parse_int(val, len, &lvl)) < 0) + break; + + SPA_CLAMP(lvl, SPA_LOG_LEVEL_NONE, SPA_LOG_LEVEL_TRACE); + + p = calloc(1, sizeof(*p) + strlen(pattern) + 1); + p->level = lvl; + strcpy(p->pattern, pattern); + spa_list_append(patterns, &p->link); + } + } + + return res; +} + +void +support_log_free_patterns(struct spa_list *patterns) +{ + struct support_log_pattern *p; + + spa_list_consume(p, patterns, link) { + spa_list_remove(&p->link); + free(p); + } +} diff --git a/spa/plugins/support/log-patterns.h b/spa/plugins/support/log-patterns.h new file mode 100644 index 0000000..a10b445 --- /dev/null +++ b/spa/plugins/support/log-patterns.h @@ -0,0 +1,13 @@ +#ifndef LOG_PATTERNS_H +#define LOG_PATTERNS_H + +#include + +struct spa_list; + +void support_log_topic_init(struct spa_list *patterns, enum spa_log_level default_level, + struct spa_log_topic *t); +int support_log_parse_patterns(struct spa_list *patterns, const char *jsonstr); +void support_log_free_patterns(struct spa_list *patterns); + +#endif /* LOG_PATTERNS_H */ diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c new file mode 100644 index 0000000..615a49b --- /dev/null +++ b/spa/plugins/support/logger.c @@ -0,0 +1,415 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log-patterns.h" + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC +#endif + +#define NAME "logger" + +#define DEFAULT_LOG_LEVEL SPA_LOG_LEVEL_INFO + +#define TRACE_BUFFER (16*1024) + +struct impl { + struct spa_handle handle; + struct spa_log log; + + FILE *file; + bool close_file; + + struct spa_system *system; + struct spa_source source; + struct spa_ringbuffer trace_rb; + uint8_t trace_data[TRACE_BUFFER]; + + unsigned int have_source:1; + unsigned int colors:1; + unsigned int timestamp:1; + unsigned int line:1; + + struct spa_list patterns; +}; + +static SPA_PRINTF_FUNC(7,0) void +impl_log_logtv(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ +#define RESERVED_LENGTH 24 + + struct impl *impl = object; + char timestamp[15] = {0}; + char topicstr[32] = {0}; + char filename[64] = {0}; + char location[1000 + RESERVED_LENGTH], *p, *s; + static const char * const levels[] = { "-", "E", "W", "I", "D", "T", "*T*" }; + const char *prefix = "", *suffix = ""; + int size, len; + bool do_trace; + + if ((do_trace = (level == SPA_LOG_LEVEL_TRACE && impl->have_source))) + level++; + + if (impl->colors) { + if (level <= SPA_LOG_LEVEL_ERROR) + prefix = SPA_ANSI_BOLD_RED; + else if (level <= SPA_LOG_LEVEL_WARN) + prefix = SPA_ANSI_BOLD_YELLOW; + else if (level <= SPA_LOG_LEVEL_INFO) + prefix = SPA_ANSI_BOLD_GREEN; + if (prefix[0]) + suffix = SPA_ANSI_RESET; + } + + p = location; + len = sizeof(location) - RESERVED_LENGTH; + + if (impl->timestamp) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC_RAW, &now); + spa_scnprintf(timestamp, sizeof(timestamp), "[%05lu.%06lu]", + (now.tv_sec & 0x1FFFFFFF) % 100000, now.tv_nsec / 1000); + } + + if (topic && topic->topic) + spa_scnprintf(topicstr, sizeof(topicstr), " %-12s | ", topic->topic); + + + if (impl->line && line != 0) { + s = strrchr(file, '/'); + spa_scnprintf(filename, sizeof(filename), "[%16.16s:%5i %s()]", + s ? s + 1 : file, line, func); + } + + size = spa_scnprintf(p, len, "%s[%s]%s%s%s ", prefix, levels[level], + timestamp, topicstr, filename); + /* + * it is assumed that at this point `size` <= `len`, + * which is reasonable as long as file names and function names + * don't become very long + */ + size += spa_vscnprintf(p + size, len - size, fmt, args); + + /* + * `RESERVED_LENGTH` bytes are reserved for printing the suffix + * (at the moment it's "... (truncated)\x1B[0m\n" at its longest - 21 bytes), + * its length must be less than `RESERVED_LENGTH` (including the null byte), + * otherwise a stack buffer overrun could ensue + */ + + /* if the message could not fit entirely... */ + if (size >= len - 1) { + size = len - 1; /* index of the null byte */ + len = sizeof(location); + size += spa_scnprintf(p + size, len - size, "... (truncated)"); + } + else { + len = sizeof(location); + } + + size += spa_scnprintf(p + size, len - size, "%s\n", suffix); + + if (SPA_UNLIKELY(do_trace)) { + uint32_t index; + + spa_ringbuffer_get_write_index(&impl->trace_rb, &index); + spa_ringbuffer_write_data(&impl->trace_rb, impl->trace_data, TRACE_BUFFER, + index & (TRACE_BUFFER - 1), location, size); + spa_ringbuffer_write_update(&impl->trace_rb, index + size); + + if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0) + fprintf(impl->file, "error signaling eventfd: %s\n", strerror(errno)); + } else + fputs(location, impl->file); + +#undef RESERVED_LENGTH +} + +static SPA_PRINTF_FUNC(6,0) void +impl_log_logv(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, + va_list args) +{ + impl_log_logtv(object, level, NULL, file, line, func, fmt, args); +} + +static SPA_PRINTF_FUNC(7,8) void +impl_log_logt(void *object, + enum spa_log_level level, + const struct spa_log_topic *topic, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + impl_log_logtv(object, level, topic, file, line, func, fmt, args); + va_end(args); +} + +static SPA_PRINTF_FUNC(6,7) void +impl_log_log(void *object, + enum spa_log_level level, + const char *file, + int line, + const char *func, + const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + impl_log_logtv(object, level, NULL, file, line, func, fmt, args); + va_end(args); +} + +static void on_trace_event(struct spa_source *source) +{ + struct impl *impl = source->data; + int32_t avail; + uint32_t index; + uint64_t count; + + if (spa_system_eventfd_read(impl->system, source->fd, &count) < 0) + fprintf(impl->file, "failed to read event fd: %s", strerror(errno)); + + while ((avail = spa_ringbuffer_get_read_index(&impl->trace_rb, &index)) > 0) { + int32_t offset, first; + + if (avail > TRACE_BUFFER) { + index += avail - TRACE_BUFFER; + avail = TRACE_BUFFER; + } + offset = index & (TRACE_BUFFER - 1); + first = SPA_MIN(avail, TRACE_BUFFER - offset); + + fwrite(impl->trace_data + offset, first, 1, impl->file); + if (SPA_UNLIKELY(avail > first)) { + fwrite(impl->trace_data, avail - first, 1, impl->file); + } + spa_ringbuffer_read_update(&impl->trace_rb, index + avail); + } +} + +static void +impl_log_topic_init(void *object, struct spa_log_topic *t) +{ + struct impl *impl = object; + enum spa_log_level level = impl->log.level; + + support_log_topic_init(&impl->patterns, level, t); +} + +static const struct spa_log_methods impl_log = { + SPA_VERSION_LOG_METHODS, + .log = impl_log_log, + .logv = impl_log_logv, + .logt = impl_log_logt, + .logtv = impl_log_logtv, + .topic_init = impl_log_topic_init, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Log)) + *interface = &this->log; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + support_log_free_patterns(&this->patterns); + + if (this->close_file && this->file != NULL) + fclose(this->file); + + if (this->have_source) { + spa_loop_remove_source(this->source.loop, &this->source); + spa_system_close(this->system, this->source.fd); + this->have_source = false; + } + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct spa_loop *loop = NULL; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Log, + SPA_VERSION_LOG, + &impl_log, this); + this->log.level = DEFAULT_LOG_LEVEL; + + loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + this->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + spa_list_init(&this->patterns); + + if (loop != NULL && this->system != NULL) { + this->source.func = on_trace_event; + this->source.data = this; + this->source.fd = spa_system_eventfd_create(this->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + + if (this->source.fd < 0) { + fprintf(stderr, "Warning: failed to create eventfd: %m"); + } else { + spa_loop_add_source(loop, &this->source); + this->have_source = true; + } + } + if (info) { + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_TIMESTAMP)) != NULL) + this->timestamp = spa_atob(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LINE)) != NULL) + this->line = spa_atob(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_COLORS)) != NULL) + this->colors = spa_atob(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_LEVEL)) != NULL) + this->log.level = atoi(str); + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_FILE)) != NULL) { + this->file = fopen(str, "we"); + if (this->file == NULL) + fprintf(stderr, "Warning: failed to open file %s: (%m)", str); + else + this->close_file = true; + } + if ((str = spa_dict_lookup(info, SPA_KEY_LOG_PATTERNS)) != NULL) + support_log_parse_patterns(&this->patterns, str); + } + if (this->file == NULL) + this->file = stderr; + if (!isatty(fileno(this->file))) + this->colors = false; + + spa_ringbuffer_init(&this->trace_rb); + + spa_log_debug(&this->log, NAME " %p: initialized", this); + + setlinebuf(this->file); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Log,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +const struct spa_handle_factory spa_support_logger_factory = { + SPA_VERSION_HANDLE_FACTORY, + .name = SPA_NAME_SUPPORT_LOG, + .info = NULL, + .get_size = impl_get_size, + .init = impl_init, + .enum_interface_info = impl_enum_interface_info, +}; diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c new file mode 100644 index 0000000..3c3e020 --- /dev/null +++ b/spa/plugins/support/loop.c @@ -0,0 +1,1024 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.loop"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#define MAX_ALIGN 8 +#define ITEM_ALIGN 8 +#define DATAS_SIZE (4096*8) +#define MAX_EP 32 + +/** \cond */ + +struct invoke_item { + size_t item_size; + spa_invoke_func_t func; + uint32_t seq; + void *data; + size_t size; + bool block; + void *user_data; + int res; +}; + +static int loop_signal_event(void *object, struct spa_source *source); + +struct impl { + struct spa_handle handle; + struct spa_loop loop; + struct spa_loop_control control; + struct spa_loop_utils utils; + + struct spa_log *log; + struct spa_system *system; + + struct spa_list source_list; + struct spa_list destroy_list; + struct spa_hook_list hooks_list; + + int poll_fd; + pthread_t thread; + int enter_count; + + struct spa_source *wakeup; + int ack_fd; + + struct spa_ringbuffer buffer; + uint8_t *buffer_data; + uint8_t buffer_mem[DATAS_SIZE + MAX_ALIGN]; + + uint32_t flush_count; + unsigned int polling:1; +}; + +struct source_impl { + struct spa_source source; + + struct impl *impl; + struct spa_list link; + + union { + spa_source_io_func_t io; + spa_source_idle_func_t idle; + spa_source_event_func_t event; + spa_source_timer_func_t timer; + spa_source_signal_func_t signal; + } func; + + struct spa_source *fallback; + + bool close; + bool enabled; +}; +/** \endcond */ + +static int loop_add_source(void *object, struct spa_source *source) +{ + struct impl *impl = object; + source->loop = &impl->loop; + source->priv = NULL; + source->rmask = 0; + return spa_system_pollfd_add(impl->system, impl->poll_fd, source->fd, source->mask, source); +} + +static int loop_update_source(void *object, struct spa_source *source) +{ + struct impl *impl = object; + + spa_assert(source->loop == &impl->loop); + + return spa_system_pollfd_mod(impl->system, impl->poll_fd, source->fd, source->mask, source); +} + +static void detach_source(struct spa_source *source) +{ + struct spa_poll_event *e; + + source->loop = NULL; + source->rmask = 0; + + if ((e = source->priv)) { + /* active in an iteration of the loop, remove it from there */ + e->data = NULL; + source->priv = NULL; + } +} + +static int remove_from_poll(struct impl *impl, struct spa_source *source) +{ + spa_assert(source->loop == &impl->loop); + + return spa_system_pollfd_del(impl->system, impl->poll_fd, source->fd); +} + +static int loop_remove_source(void *object, struct spa_source *source) +{ + struct impl *impl = object; + spa_assert(!impl->polling); + + int res = remove_from_poll(impl, source); + detach_source(source); + + return res; +} + +static void flush_items(struct impl *impl) +{ + uint32_t index, flush_count; + int32_t avail; + int res; + + flush_count = ++impl->flush_count; + avail = spa_ringbuffer_get_read_index(&impl->buffer, &index); + while (avail > 0) { + struct invoke_item *item; + bool block; + spa_invoke_func_t func; + + item = SPA_PTROFF(impl->buffer_data, index & (DATAS_SIZE - 1), struct invoke_item); + block = item->block; + func = item->func; + + spa_log_trace_fp(impl->log, "%p: flush item %p", impl, item); + /* first we remove the function from the item so that recursive + * calls don't call the callback again. We can't update the + * read index before we call the function because then the item + * might get overwritten. */ + item->func = NULL; + if (func) + item->res = func(&impl->loop, true, item->seq, item->data, + item->size, item->user_data); + + /* if this function did a recursive invoke, it now flushed the + * ringbuffer and we can exit */ + if (flush_count != impl->flush_count) + break; + + index += item->item_size; + avail -= item->item_size; + spa_ringbuffer_read_update(&impl->buffer, index); + + if (block) { + if ((res = spa_system_eventfd_write(impl->system, impl->ack_fd, 1)) < 0) + spa_log_warn(impl->log, "%p: failed to write event fd:%d: %s", + impl, impl->ack_fd, spa_strerror(res)); + } + } +} + +static int +loop_invoke_inthread(struct impl *impl, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + bool block, + void *user_data) +{ + /* we should probably have a second ringbuffer for the in-thread pending + * callbacks. A recursive callback when flushing will insert itself + * before this one. */ + flush_items(impl); + return func ? func(&impl->loop, true, seq, data, size, user_data) : 0; +} + +static int +loop_invoke(void *object, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + bool block, + void *user_data) +{ + struct impl *impl = object; + struct invoke_item *item; + int res; + int32_t filled; + uint32_t avail, idx, offset, l0; + + /* the ringbuffer can only be written to from one thread, if we are + * in the same thread as the loop, don't write into the ringbuffer + * but try to emit the calback right away after flushing what we have */ + if (impl->thread == 0 || pthread_equal(impl->thread, pthread_self())) + return loop_invoke_inthread(impl, func, seq, data, size, block, user_data); + + filled = spa_ringbuffer_get_write_index(&impl->buffer, &idx); + if (filled < 0 || filled > DATAS_SIZE) { + spa_log_warn(impl->log, "%p: queue xrun %d", impl, filled); + return -EPIPE; + } + avail = DATAS_SIZE - filled; + if (avail < sizeof(struct invoke_item)) { + spa_log_warn(impl->log, "%p: queue full %d", impl, avail); + return -EPIPE; + } + offset = idx & (DATAS_SIZE - 1); + + /* l0 is remaining size in ringbuffer, this should always be larger than + * invoke_item, see below */ + l0 = DATAS_SIZE - offset; + + item = SPA_PTROFF(impl->buffer_data, offset, struct invoke_item); + item->func = func; + item->seq = seq; + item->size = size; + item->block = block; + item->user_data = user_data; + item->res = 0; + item->item_size = SPA_ROUND_UP_N(sizeof(struct invoke_item) + size, ITEM_ALIGN); + + spa_log_trace_fp(impl->log, "%p: add item %p filled:%d", impl, item, filled); + + if (l0 >= item->item_size) { + /* item + size fit in current ringbuffer idx */ + item->data = SPA_PTROFF(item, sizeof(struct invoke_item), void); + if (l0 < sizeof(struct invoke_item) + item->item_size) { + /* not enough space for next invoke_item, fill up till the end + * so that the next item will be at the start */ + item->item_size = l0; + } + } else { + /* item does not fit, place the invoke_item at idx and start the + * data at the start of the ringbuffer */ + item->data = impl->buffer_data; + item->item_size = SPA_ROUND_UP_N(l0 + size, ITEM_ALIGN); + } + if (avail < item->item_size) { + spa_log_warn(impl->log, "%p: queue full %d, need %zd", impl, avail, + item->item_size); + return -EPIPE; + } + if (data && size > 0) + memcpy(item->data, data, size); + + spa_ringbuffer_write_update(&impl->buffer, idx + item->item_size); + + loop_signal_event(impl, impl->wakeup); + + if (block) { + uint64_t count = 1; + + spa_loop_control_hook_before(&impl->hooks_list); + + if ((res = spa_system_eventfd_read(impl->system, impl->ack_fd, &count)) < 0) + spa_log_warn(impl->log, "%p: failed to read event fd:%d: %s", + impl, impl->ack_fd, spa_strerror(res)); + + spa_loop_control_hook_after(&impl->hooks_list); + + res = item->res; + } + else { + if (seq != SPA_ID_INVALID) + res = SPA_RESULT_RETURN_ASYNC(seq); + else + res = 0; + } + return res; +} + +static void wakeup_func(void *data, uint64_t count) +{ + struct impl *impl = data; + flush_items(impl); +} + +static int loop_get_fd(void *object) +{ + struct impl *impl = object; + return impl->poll_fd; +} + +static void +loop_add_hook(void *object, + struct spa_hook *hook, + const struct spa_loop_control_hooks *hooks, + void *data) +{ + struct impl *impl = object; + spa_hook_list_append(&impl->hooks_list, hook, hooks, data); +} + +static void loop_enter(void *object) +{ + struct impl *impl = object; + pthread_t thread_id = pthread_self(); + + if (impl->enter_count == 0) { + spa_return_if_fail(impl->thread == 0); + impl->thread = thread_id; + impl->enter_count = 1; + } else { + spa_return_if_fail(impl->enter_count > 0); + spa_return_if_fail(impl->thread == thread_id); + impl->enter_count++; + } + spa_log_trace(impl->log, "%p: enter %lu", impl, impl->thread); +} + +static void loop_leave(void *object) +{ + struct impl *impl = object; + pthread_t thread_id = pthread_self(); + + spa_return_if_fail(impl->enter_count > 0); + spa_return_if_fail(impl->thread == thread_id); + + spa_log_trace(impl->log, "%p: leave %lu", impl, impl->thread); + + if (--impl->enter_count == 0) { + impl->thread = 0; + flush_items(impl); + impl->polling = false; + } +} + +static inline void free_source(struct source_impl *s) +{ + detach_source(&s->source); + free(s); +} + +static inline void process_destroy(struct impl *impl) +{ + struct source_impl *source, *tmp; + + spa_list_for_each_safe(source, tmp, &impl->destroy_list, link) + free_source(source); + + spa_list_init(&impl->destroy_list); +} + +struct cancellation_handler_data { + struct spa_poll_event *ep; + int ep_count; +}; + +static void cancellation_handler(void *closure) +{ + const struct cancellation_handler_data *data = closure; + + for (int i = 0; i < data->ep_count; i++) { + struct spa_source *s = data->ep[i].data; + if (SPA_LIKELY(s)) { + s->rmask = 0; + s->priv = NULL; + } + } +} + +static int loop_iterate(void *object, int timeout) +{ + struct impl *impl = object; + struct spa_poll_event ep[MAX_EP], *e; + int i, nfds; + + impl->polling = true; + spa_loop_control_hook_before(&impl->hooks_list); + + nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout); + + spa_loop_control_hook_after(&impl->hooks_list); + impl->polling = false; + + struct cancellation_handler_data cdata = { ep, nfds }; + pthread_cleanup_push(cancellation_handler, &cdata); + + /* first we set all the rmasks, then call the callbacks. The reason is that + * some callback might also want to look at other sources it manages and + * can then reset the rmask to suppress the callback */ + for (i = 0; i < nfds; i++) { + struct spa_source *s = ep[i].data; + + spa_assert(s->loop == &impl->loop); + + s->rmask = ep[i].events; + /* already active in another iteration of the loop, + * remove it from that iteration */ + if (SPA_UNLIKELY(e = s->priv)) + e->data = NULL; + s->priv = &ep[i]; + } + + if (SPA_UNLIKELY(!spa_list_is_empty(&impl->destroy_list))) + process_destroy(impl); + + for (i = 0; i < nfds; i++) { + struct spa_source *s = ep[i].data; + if (SPA_LIKELY(s && s->rmask)) + s->func(s); + } + + pthread_cleanup_pop(true); + + return nfds; +} + +static void source_io_func(struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + spa_log_trace_fp(s->impl->log, "%p: io %08x", s, source->rmask); + s->func.io(source->data, source->fd, source->rmask); +} + +static struct spa_source *loop_add_io(void *object, + int fd, + uint32_t mask, + bool close, spa_source_io_func_t func, void *data) +{ + struct impl *impl = object; + struct source_impl *source; + int res; + + source = calloc(1, sizeof(struct source_impl)); + if (source == NULL) + goto error_exit; + + source->source.func = source_io_func; + source->source.data = data; + source->source.fd = fd; + source->source.mask = mask; + source->impl = impl; + source->close = close; + source->func.io = func; + + if ((res = loop_add_source(impl, &source->source)) < 0) { + if (res != -EPERM) + goto error_exit_free; + + /* file fds (stdin/stdout/...) give EPERM in epoll. Those fds always + * return from epoll with the mask set, so we can handle this with + * an idle source */ + source->source.rmask = mask; + source->fallback = spa_loop_utils_add_idle(&impl->utils, + mask & (SPA_IO_IN | SPA_IO_OUT) ? true : false, + (spa_source_idle_func_t) source_io_func, source); + spa_log_trace(impl->log, "%p: adding fallback %p", impl, + source->fallback); + } + + spa_list_insert(&impl->source_list, &source->link); + + return &source->source; + +error_exit_free: + free(source); + errno = -res; +error_exit: + return NULL; +} + +static int loop_update_io(void *object, struct spa_source *source, uint32_t mask) +{ + struct impl *impl = object; + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + int res; + + spa_assert(s->impl == object); + spa_assert(source->func == source_io_func); + + spa_log_trace(impl->log, "%p: update %08x -> %08x", s, source->mask, mask); + source->mask = mask; + + if (s->fallback) + res = spa_loop_utils_enable_idle(&impl->utils, s->fallback, + mask & (SPA_IO_IN | SPA_IO_OUT) ? true : false); + else + res = loop_update_source(object, source); + return res; +} + +static void source_idle_func(struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + s->func.idle(source->data); +} + +static int loop_enable_idle(void *object, struct spa_source *source, bool enabled) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + int res = 0; + + spa_assert(s->impl == object); + spa_assert(source->func == source_idle_func); + + if (enabled && !s->enabled) { + if ((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0) + spa_log_warn(s->impl->log, "%p: failed to write idle fd:%d: %s", + source, source->fd, spa_strerror(res)); + } else if (!enabled && s->enabled) { + uint64_t count; + if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0) + spa_log_warn(s->impl->log, "%p: failed to read idle fd:%d: %s", + source, source->fd, spa_strerror(res)); + } + s->enabled = enabled; + return res; +} + +static struct spa_source *loop_add_idle(void *object, + bool enabled, spa_source_idle_func_t func, void *data) +{ + struct impl *impl = object; + struct source_impl *source; + int res; + + source = calloc(1, sizeof(struct source_impl)); + if (source == NULL) + goto error_exit; + + if ((res = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_exit_free; + + source->source.func = source_idle_func; + source->source.data = data; + source->source.fd = res; + source->impl = impl; + source->close = true; + source->source.mask = SPA_IO_IN; + source->func.idle = func; + + if ((res = loop_add_source(impl, &source->source)) < 0) + goto error_exit_close; + + spa_list_insert(&impl->source_list, &source->link); + + if (enabled) + loop_enable_idle(impl, &source->source, true); + + return &source->source; + +error_exit_close: + spa_system_close(impl->system, source->source.fd); +error_exit_free: + free(source); + errno = -res; +error_exit: + return NULL; +} + +static void source_event_func(struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + uint64_t count = 0; + int res; + + if ((res = spa_system_eventfd_read(s->impl->system, source->fd, &count)) < 0) { + if (res != -EAGAIN) + spa_log_warn(s->impl->log, "%p: failed to read event fd:%d: %s", + source, source->fd, spa_strerror(res)); + return; + } + s->func.event(source->data, count); +} + +static struct spa_source *loop_add_event(void *object, + spa_source_event_func_t func, void *data) +{ + struct impl *impl = object; + struct source_impl *source; + int res; + + source = calloc(1, sizeof(struct source_impl)); + if (source == NULL) + goto error_exit; + + if ((res = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_exit_free; + + source->source.func = source_event_func; + source->source.data = data; + source->source.fd = res; + source->source.mask = SPA_IO_IN; + source->impl = impl; + source->close = true; + source->func.event = func; + + if ((res = loop_add_source(impl, &source->source)) < 0) + goto error_exit_close; + + spa_list_insert(&impl->source_list, &source->link); + + return &source->source; + +error_exit_close: + spa_system_close(impl->system, source->source.fd); +error_exit_free: + free(source); + errno = -res; +error_exit: + return NULL; +} + +static int loop_signal_event(void *object, struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + int res; + + spa_assert(s->impl == object); + spa_assert(source->func == source_event_func); + + if (SPA_UNLIKELY((res = spa_system_eventfd_write(s->impl->system, source->fd, 1)) < 0)) + spa_log_warn(s->impl->log, "%p: failed to write event fd:%d: %s", + source, source->fd, spa_strerror(res)); + return res; +} + +static void source_timer_func(struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + uint64_t expirations = 0; + int res; + + if (SPA_UNLIKELY((res = spa_system_timerfd_read(s->impl->system, + source->fd, &expirations)) < 0)) { + if (res != -EAGAIN) + spa_log_warn(s->impl->log, "%p: failed to read timer fd:%d: %s", + source, source->fd, spa_strerror(res)); + return; + } + s->func.timer(source->data, expirations); +} + +static struct spa_source *loop_add_timer(void *object, + spa_source_timer_func_t func, void *data) +{ + struct impl *impl = object; + struct source_impl *source; + int res; + + source = calloc(1, sizeof(struct source_impl)); + if (source == NULL) + goto error_exit; + + if ((res = spa_system_timerfd_create(impl->system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_exit_free; + + source->source.func = source_timer_func; + source->source.data = data; + source->source.fd = res; + source->source.mask = SPA_IO_IN; + source->impl = impl; + source->close = true; + source->func.timer = func; + + if ((res = loop_add_source(impl, &source->source)) < 0) + goto error_exit_close; + + spa_list_insert(&impl->source_list, &source->link); + + return &source->source; + +error_exit_close: + spa_system_close(impl->system, source->source.fd); +error_exit_free: + free(source); + errno = -res; +error_exit: + return NULL; +} + +static int +loop_update_timer(void *object, struct spa_source *source, + struct timespec *value, struct timespec *interval, bool absolute) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + struct itimerspec its; + int flags = 0, res; + + spa_assert(s->impl == object); + spa_assert(source->func == source_timer_func); + + spa_zero(its); + if (SPA_LIKELY(value)) { + its.it_value = *value; + } else if (interval) { + its.it_value = *interval; + absolute = true; + } + if (SPA_UNLIKELY(interval)) + its.it_interval = *interval; + if (SPA_LIKELY(absolute)) + flags |= SPA_FD_TIMER_ABSTIME; + + if (SPA_UNLIKELY((res = spa_system_timerfd_settime(s->impl->system, source->fd, flags, &its, NULL)) < 0)) + return res; + + return 0; +} + +static void source_signal_func(struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + int res, signal_number = 0; + + if ((res = spa_system_signalfd_read(s->impl->system, source->fd, &signal_number)) < 0) { + if (res != -EAGAIN) + spa_log_warn(s->impl->log, "%p: failed to read signal fd:%d: %s", + source, source->fd, spa_strerror(res)); + return; + } + s->func.signal(source->data, signal_number); +} + +static struct spa_source *loop_add_signal(void *object, + int signal_number, + spa_source_signal_func_t func, void *data) +{ + struct impl *impl = object; + struct source_impl *source; + int res; + + source = calloc(1, sizeof(struct source_impl)); + if (source == NULL) + goto error_exit; + + if ((res = spa_system_signalfd_create(impl->system, + signal_number, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0) + goto error_exit_free; + + source->source.func = source_signal_func; + source->source.data = data; + source->source.fd = res; + source->source.mask = SPA_IO_IN; + source->impl = impl; + source->close = true; + source->func.signal = func; + + if ((res = loop_add_source(impl, &source->source)) < 0) + goto error_exit_close; + + spa_list_insert(&impl->source_list, &source->link); + + return &source->source; + +error_exit_close: + spa_system_close(impl->system, source->source.fd); +error_exit_free: + free(source); + errno = -res; +error_exit: + return NULL; +} + +static void loop_destroy_source(void *object, struct spa_source *source) +{ + struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); + + spa_assert(s->impl == object); + + spa_log_trace(s->impl->log, "%p ", s); + + spa_list_remove(&s->link); + + if (s->fallback) + loop_destroy_source(s->impl, s->fallback); + else + remove_from_poll(s->impl, source); + + if (source->fd != -1 && s->close) { + spa_system_close(s->impl->system, source->fd); + source->fd = -1; + } + + if (!s->impl->polling) + free_source(s); + else + spa_list_insert(&s->impl->destroy_list, &s->link); +} + +static const struct spa_loop_methods impl_loop = { + SPA_VERSION_LOOP_METHODS, + .add_source = loop_add_source, + .update_source = loop_update_source, + .remove_source = loop_remove_source, + .invoke = loop_invoke, +}; + +static const struct spa_loop_control_methods impl_loop_control = { + SPA_VERSION_LOOP_CONTROL_METHODS, + .get_fd = loop_get_fd, + .add_hook = loop_add_hook, + .enter = loop_enter, + .leave = loop_leave, + .iterate = loop_iterate, +}; + +static const struct spa_loop_utils_methods impl_loop_utils = { + SPA_VERSION_LOOP_UTILS_METHODS, + .add_io = loop_add_io, + .update_io = loop_update_io, + .add_idle = loop_add_idle, + .enable_idle = loop_enable_idle, + .add_event = loop_add_event, + .signal_event = loop_signal_event, + .add_timer = loop_add_timer, + .update_timer = loop_update_timer, + .add_signal = loop_add_signal, + .destroy_source = loop_destroy_source, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Loop)) + *interface = &impl->loop; + else if (spa_streq(type, SPA_TYPE_INTERFACE_LoopControl)) + *interface = &impl->control; + else if (spa_streq(type, SPA_TYPE_INTERFACE_LoopUtils)) + *interface = &impl->utils; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *impl; + struct source_impl *source; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (impl->enter_count != 0 || impl->polling) + spa_log_warn(impl->log, "%p: loop is entered %d times polling:%d", + impl, impl->enter_count, impl->polling); + + spa_list_consume(source, &impl->source_list, link) + loop_destroy_source(impl, &source->source); + + spa_system_close(impl->system, impl->ack_fd); + spa_system_close(impl->system, impl->poll_fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *impl; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct impl *) handle; + impl->loop.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Loop, + SPA_VERSION_LOOP, + &impl_loop, impl); + impl->control.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_LoopControl, + SPA_VERSION_LOOP_CONTROL, + &impl_loop_control, impl); + impl->utils.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_LoopUtils, + SPA_VERSION_LOOP_UTILS, + &impl_loop_utils, impl); + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(impl->log, &log_topic); + impl->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + + if (impl->system == NULL) { + spa_log_error(impl->log, "%p: a System is needed", impl); + res = -EINVAL; + goto error_exit; + } + + if ((res = spa_system_pollfd_create(impl->system, SPA_FD_CLOEXEC)) < 0) { + spa_log_error(impl->log, "%p: can't create pollfd: %s", + impl, spa_strerror(res)); + goto error_exit; + } + impl->poll_fd = res; + + spa_list_init(&impl->source_list); + spa_list_init(&impl->destroy_list); + spa_hook_list_init(&impl->hooks_list); + + impl->buffer_data = SPA_PTR_ALIGN(impl->buffer_mem, MAX_ALIGN, uint8_t); + spa_ringbuffer_init(&impl->buffer); + + impl->wakeup = loop_add_event(impl, wakeup_func, impl); + if (impl->wakeup == NULL) { + res = -errno; + spa_log_error(impl->log, "%p: can't create wakeup event: %m", impl); + goto error_exit_free_poll; + } + if ((res = spa_system_eventfd_create(impl->system, + SPA_FD_EVENT_SEMAPHORE | SPA_FD_CLOEXEC)) < 0) { + spa_log_error(impl->log, "%p: can't create ack event: %s", + impl, spa_strerror(res)); + goto error_exit_free_wakeup; + } + impl->ack_fd = res; + + spa_log_debug(impl->log, "%p: initialized", impl); + + return 0; + +error_exit_free_wakeup: + loop_destroy_source(impl, impl->wakeup); +error_exit_free_poll: + spa_system_close(impl->system, impl->poll_fd); +error_exit: + return res; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Loop,}, + {SPA_TYPE_INTERFACE_LoopControl,}, + {SPA_TYPE_INTERFACE_LoopUtils,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_support_loop_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_LOOP, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info +}; diff --git a/spa/plugins/support/meson.build b/spa/plugins/support/meson.build new file mode 100644 index 0000000..1672d38 --- /dev/null +++ b/spa/plugins/support/meson.build @@ -0,0 +1,70 @@ +spa_support_sources = [ + 'cpu.c', + 'logger.c', + 'log-patterns.c', + 'loop.c', + 'node-driver.c', + 'null-audio-sink.c', + 'plugin.c', + 'system.c' +] + +simd_cargs = [] + +if have_sse + simd_cargs += [sse_args, '-DHAVE_SSE'] +endif + +spa_support_lib = shared_library('spa-support', + spa_support_sources, + c_args : [ simd_cargs ], + dependencies : [ spa_dep, pthread_lib, epoll_shim_dep ], + install : true, + install_dir : spa_plugindir / 'support') +spa_support_dep = declare_dependency(link_with: spa_support_lib) + +if get_option('evl').allowed() + evl_inc = include_directories('/usr/evl/include') + evl_lib = cc.find_library('evl', + dirs: ['/usr/evl/lib/'], + required: get_option('evl')) + + spa_evl_sources = ['evl-system.c', 'evl-plugin.c'] + + spa_evl_lib = shared_library('spa-evl', + spa_evl_sources, + include_directories : [ evl_inc], + dependencies : [ spa_dep, pthread_lib, evl_lib ], + install : true, + install_dir : spa_plugindir / 'support') +endif + +if dbus_dep.found() + spa_dbus_sources = ['dbus.c'] + + spa_dbus_lib = shared_library('spa-dbus', + spa_dbus_sources, + dependencies : [ spa_dep, dbus_dep ], + install : true, + install_dir : spa_plugindir / 'support') + spa_dbus_dep = declare_dependency(link_with: spa_dbus_lib) +else + spa_dbus_dep = declare_dependency() +endif + + +if systemd_dep.found() + spa_journal_sources = [ + 'journal.c', + 'log-patterns.c', + ] + + spa_journal_lib = shared_library('spa-journal', + spa_journal_sources, + dependencies : [ spa_dep, systemd_dep ], + install : true, + install_dir : spa_plugindir / 'support') + spa_journal_dep = declare_dependency(link_with: spa_journal_lib) +else + spa_journal_dep = declare_dependency() +endif diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c new file mode 100644 index 0000000..9701a47 --- /dev/null +++ b/spa/plugins/support/node-driver.c @@ -0,0 +1,492 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "driver" + +#define DEFAULT_FREEWHEEL false +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + bool freewheel; + char clock_name[64]; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct props props; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[1]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct spa_io_position *position; + struct spa_io_clock *clock; + + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + bool following; + uint64_t next_time; +}; + +static void reset_props(struct props *props) +{ + props->freewheel = DEFAULT_FREEWHEEL; + spa_scnprintf(props->clock_name, sizeof(props->clock_name), + "%s", DEFAULT_CLOCK_NAME); +} + +static void set_timeout(struct impl *this, uint64_t next_time) +{ + spa_log_trace(this->log, "set timeout %"PRIu64, next_time); + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); +} + +static int set_timers(struct impl *this) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0) + return res; + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (this->following) { + set_timeout(this, 0); + } else { + set_timeout(this, this->next_time); + } + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + set_timers(this); + return 0; +} + +static int reassign_follower(struct impl *this) +{ + bool following; + + if (this->clock) + SPA_FLAG_UPDATE(this->clock->flags, + SPA_IO_CLOCK_FLAG_FREEWHEEL, this->props.freewheel); + + if (!this->started) + return 0; + + following = is_following(this); + if (following != this->following) { + spa_log_debug(this->log, NAME" %p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + if (size > 0 && size < sizeof(struct spa_io_clock)) + return -EINVAL; + this->clock = data; + if (this->clock) + spa_scnprintf(this->clock->name, sizeof(this->clock->name), + "%s", this->props.clock_name); + break; + case SPA_IO_Position: + if (size > 0 && size < sizeof(struct spa_io_position)) + return -EINVAL; + this->position = data; + break; + default: + return -ENOENT; + } + reassign_follower(this); + + return 0; +} + +static void on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t expirations, nsec, duration; + uint32_t rate; + int res; + + spa_log_trace(this->log, "timeout"); + + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + return; + } + + nsec = this->next_time; + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + this->next_time = nsec + duration * SPA_NSEC_PER_SEC / rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = nsec; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->delay = 0; + this->clock->rate_diff = 1.0; + this->clock->next_nsec = this->next_time; + } + + spa_node_call_ready(&this->callbacks, + SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA); + + set_timeout(this, this->next_time); +} + +static int do_start(struct impl *this) +{ + if (this->started) + return 0; + + this->following = is_following(this); + set_timers(this); + this->started = true; + return 0; +} + +static int do_stop(struct impl *this) +{ + if (!this->started) + return 0; + this->started = false; + set_timeout(this, 0); + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + do_start(this); + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + do_stop(this); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct timespec now; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_log_trace(this->log, "process %d", this->props.freewheel); + + if (this->props.freewheel) { + clock_gettime(CLOCK_MONOTONIC, &now); + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + set_timeout(this, this->next_time); + } + return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data_loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data_system is needed"); + return -EINVAL; + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 0; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 0; + + this->timer_source.func = on_timeout; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + reset_props(&this->props); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "node.freewheel")) { + this->props.freewheel = spa_atob(s); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), "%s", s); + } + } + spa_loop_add_source(this->data_loop, &this->timer_source); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_support_node_driver_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_NODE_DRIVER, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c new file mode 100644 index 0000000..e5b9f04 --- /dev/null +++ b/spa/plugins/support/null-audio-sink.c @@ -0,0 +1,1001 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "null-audio-sink" + +#define DEFAULT_CLOCK_NAME "clock.system.monotonic" + +struct props { + uint32_t channels; + uint32_t rate; + uint32_t n_pos; + uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + char clock_name[64]; + unsigned int debug:1; +}; + +static void reset_props(struct props *props) +{ + props->channels = 0; + props->rate = 0; + props->n_pos = 0; + strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + props->debug = false; +} + +#define DEFAULT_CHANNELS 2 +#define DEFAULT_RATE 48000 + +#define MAX_BUFFERS 16 +#define MAX_PORTS 1 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; +}; + +struct impl; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + struct spa_audio_info current_format; + uint32_t blocks; + size_t bpf; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint32_t quantum_limit; + + struct props props; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + struct spa_io_clock *clock; + struct spa_io_position *position; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct port port; + + unsigned int started:1; + unsigned int following:1; + struct spa_source timer_source; + struct itimerspec timerspec; + uint64_t next_time; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_IO: + { + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position))); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static void set_timeout(struct impl *this, uint64_t next_time) +{ + spa_log_trace(this->log, "set timeout %"PRIu64, next_time); + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); +} + +static int set_timers(struct impl *this) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0) + return res; + this->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (this->following) { + set_timeout(this, 0); + } else { + set_timeout(this, this->next_time); + } + return 0; +} + +static inline bool is_following(struct impl *this) +{ + return this->position && this->clock && this->position->clock.id != this->clock->id; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct impl *this = user_data; + set_timers(this); + return 0; +} + +static int reassign_follower(struct impl *this) +{ + bool following; + + if (!this->started) + return 0; + + following = is_following(this); + if (following != this->following) { + spa_log_debug(this->log, NAME" %p: reassign follower %d->%d", this, this->following, following); + this->following = following; + spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + } + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + if (size > 0 && size < sizeof(struct spa_io_clock)) + return -EINVAL; + this->clock = data; + if (this->clock != NULL) { + spa_scnprintf(this->clock->name, + sizeof(this->clock->name), + "%s", this->props.clock_name); + } + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + reassign_follower(this); + + return 0; +} + +static void on_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + uint64_t expirations, nsec, duration = 10; + uint32_t rate; + int res; + + spa_log_trace(this->log, "timeout"); + + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + return; + } + + nsec = this->next_time; + + if (SPA_LIKELY(this->position)) { + duration = this->position->clock.duration; + rate = this->position->clock.rate.denom; + } else { + duration = 1024; + rate = 48000; + } + + this->next_time = nsec + duration * SPA_NSEC_PER_SEC / rate; + + if (SPA_LIKELY(this->clock)) { + this->clock->nsec = nsec; + this->clock->position += duration; + this->clock->duration = duration; + this->clock->delay = 0; + this->clock->rate_diff = 1.0; + this->clock->next_nsec = this->next_time; + } + + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); + + set_timeout(this, this->next_time); +} + +static int do_start(struct impl *this) +{ + if (this->started) + return 0; + + this->following = is_following(this); + set_timers(this); + this->started = true; + return 0; +} + +static int do_stop(struct impl *this) +{ + if (!this->started) + return 0; + this->started = false; + set_timeout(this, 0); + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + do_start(this); + break; + } + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + do_stop(this); + break; + + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +port_enum_formats(struct impl *this, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_pod_frame f[1]; + + switch (index) { + case 0: + spa_pod_builder_push_object(builder, &f[0], + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(3, + SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_F32P, + SPA_AUDIO_FORMAT_F32), + 0); + + if (this->props.rate != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(this->props.rate), + 0); + } else { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int(DEFAULT_RATE, 1, INT32_MAX), + 0); + } + if (this->props.channels != 0) { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(this->props.channels), + 0); + } else { + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(DEFAULT_CHANNELS, 1, INT32_MAX), + 0); + } + if (this->props.n_pos != 0) { + spa_pod_builder_prop(builder, SPA_FORMAT_AUDIO_position, 0); + spa_pod_builder_array(builder, sizeof(uint32_t), SPA_TYPE_Id, + this->props.n_pos, this->props.pos); + } + *param = spa_pod_builder_pop(builder, &f[0]); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + this->started = false; + } + return 0; +} + +static int +port_set_format(struct impl *this, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + struct port *port = &this->port; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.rate == 0 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + if (info.info.raw.format == SPA_AUDIO_FORMAT_F32) { + port->bpf = 4 * info.info.raw.channels; + port->blocks = 1; + } else if (info.info.raw.format == SPA_AUDIO_FORMAT_F32P) { + port->bpf = 4; + port->blocks = info.info.raw.channels; + } else + return -EINVAL; + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } + return 0; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->outbuf = buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (io->status != SPA_STATUS_HAVE_DATA) + return io->status; + if (io->buffer_id >= port->n_buffers) { + io->status = -EINVAL; + return io->status; + } + if (this->props.debug) { + struct buffer *b; + uint32_t i; + + b = &port->buffers[io->buffer_id]; + for (i = 0; i < b->outbuf->n_datas; i++) { + uint32_t offs, size; + struct spa_data *d = b->outbuf->datas; + + offs = SPA_MIN(d->chunk->offset, d->maxsize); + size = SPA_MIN(d->maxsize - offs, d->chunk->size); + spa_debug_mem(i, SPA_PTROFF(d[i].data, offs, void), SPA_MIN(16u, size));; + } + } + io->status = SPA_STATUS_OK; + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static uint32_t channel_from_name(const char *name) +{ + int i; + for (i = 0; spa_type_audio_channel[i].name; i++) { + if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) + return spa_type_audio_channel[i].type; + } + return SPA_AUDIO_CHANNEL_UNKNOWN; +} + +static inline void parse_position(struct impl *this, const char *val, size_t len) +{ + struct spa_json it[2]; + char v[256]; + + spa_json_init(&it[0], val, len); + if (spa_json_enter_array(&it[0], &it[1]) <= 0) + spa_json_init(&it[1], val, len); + + this->props.n_pos = 0; + while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && + this->props.n_pos < SPA_AUDIO_MAX_CHANNELS) { + this->props.pos[this->props.n_pos++] = channel_from_name(v); + } +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data_loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data_system is needed"); + return -EINVAL; + } + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = 1; + reset_props(&this->props); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 4; + + this->timer_source.func = on_timeout; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + spa_loop_add_source(this->data_loop, &this->timer_source); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &this->quantum_limit, 0); + } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { + this->props.channels = atoi(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { + this->props.rate = atoi(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { + parse_position(this, s, strlen(s)); + } else if (spa_streq(k, "clock.name")) { + spa_scnprintf(this->props.clock_name, + sizeof(this->props.clock_name), + "%s", s); + } + } + if (this->props.n_pos > 0) + this->props.channels = this->props.n_pos; + + spa_log_info(this->log, NAME " %p: initialized", this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Consume audio samples" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_support_null_audio_sink_factory = { + SPA_VERSION_HANDLE_FACTORY, + "support.null-audio-sink", + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/support/plugin.c b/spa/plugins/support/plugin.c new file mode 100644 index 0000000..29bd8a9 --- /dev/null +++ b/spa/plugins/support/plugin.c @@ -0,0 +1,67 @@ +/* Spa Support plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_support_logger_factory; + break; + case 1: + *factory = &spa_support_system_factory; + break; + case 2: + *factory = &spa_support_cpu_factory; + break; + case 3: + *factory = &spa_support_loop_factory; + break; + case 4: + *factory = &spa_support_node_driver_factory; + break; + case 5: + *factory = &spa_support_null_audio_sink_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/support/system.c b/spa/plugins/support/system.c new file mode 100644 index 0000000..e7efec9 --- /dev/null +++ b/spa/plugins/support/system.c @@ -0,0 +1,384 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.system"); +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &log_topic + +#ifndef TFD_TIMER_CANCEL_ON_SET +# define TFD_TIMER_CANCEL_ON_SET (1 << 1) +#endif + +struct impl { + struct spa_handle handle; + struct spa_system system; + struct spa_log *log; +}; + +static ssize_t impl_read(void *object, int fd, void *buf, size_t count) +{ + ssize_t res = read(fd, buf, count); + return res < 0 ? -errno : res; +} + +static ssize_t impl_write(void *object, int fd, const void *buf, size_t count) +{ + ssize_t res = write(fd, buf, count); + return res < 0 ? -errno : res; +} + +static int impl_ioctl(void *object, int fd, unsigned long request, ...) +{ + int res; + va_list ap; + long arg; + + va_start(ap, request); + arg = va_arg(ap, long); + res = ioctl(fd, request, arg); + va_end(ap); + + return res < 0 ? -errno : res; +} + +static int impl_close(void *object, int fd) +{ + struct impl *impl = object; + int res = close(fd); + spa_log_debug(impl->log, "%p: close fd:%d", impl, fd); + return res < 0 ? -errno : res; +} + +/* clock */ +static int impl_clock_gettime(void *object, + int clockid, struct timespec *value) +{ + int res = clock_gettime(clockid, value); + return res < 0 ? -errno : res; +} + +static int impl_clock_getres(void *object, + int clockid, struct timespec *res) +{ + int r = clock_getres(clockid, res); + return r < 0 ? -errno : r; +} + +/* poll */ +static int impl_pollfd_create(void *object, int flags) +{ + struct impl *impl = object; + int fl = 0, res; + if (flags & SPA_FD_CLOEXEC) + fl |= EPOLL_CLOEXEC; + res = epoll_create1(fl); + spa_log_debug(impl->log, "%p: new fd:%d", impl, res); + return res < 0 ? -errno : res; +} + +static int impl_pollfd_add(void *object, int pfd, int fd, uint32_t events, void *data) +{ + struct epoll_event ep; + int res; + + spa_zero(ep); + ep.events = events; + ep.data.ptr = data; + + res = epoll_ctl(pfd, EPOLL_CTL_ADD, fd, &ep); + return res < 0 ? -errno : res; +} + +static int impl_pollfd_mod(void *object, int pfd, int fd, uint32_t events, void *data) +{ + struct epoll_event ep; + int res; + + spa_zero(ep); + ep.events = events; + ep.data.ptr = data; + + res = epoll_ctl(pfd, EPOLL_CTL_MOD, fd, &ep); + return res < 0 ? -errno : res; +} + +static int impl_pollfd_del(void *object, int pfd, int fd) +{ + int res = epoll_ctl(pfd, EPOLL_CTL_DEL, fd, NULL); + return res < 0 ? -errno : res; +} + +static int impl_pollfd_wait(void *object, int pfd, + struct spa_poll_event *ev, int n_ev, int timeout) +{ + struct epoll_event ep[n_ev]; + int i, nfds; + + if (SPA_UNLIKELY((nfds = epoll_wait(pfd, ep, n_ev, timeout)) < 0)) + return -errno; + + for (i = 0; i < nfds; i++) { + ev[i].events = ep[i].events; + ev[i].data = ep[i].data.ptr; + } + return nfds; +} + +/* timers */ +static int impl_timerfd_create(void *object, int clockid, int flags) +{ + struct impl *impl = object; + int fl = 0, res; + if (flags & SPA_FD_CLOEXEC) + fl |= TFD_CLOEXEC; + if (flags & SPA_FD_NONBLOCK) + fl |= TFD_NONBLOCK; + res = timerfd_create(clockid, fl); + spa_log_debug(impl->log, "%p: new fd:%d", impl, res); + return res < 0 ? -errno : res; +} + +static int impl_timerfd_settime(void *object, + int fd, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value) +{ + int fl = 0, res; + if (flags & SPA_FD_TIMER_ABSTIME) + fl |= TFD_TIMER_ABSTIME; + if (flags & SPA_FD_TIMER_CANCEL_ON_SET) + fl |= TFD_TIMER_CANCEL_ON_SET; + res = timerfd_settime(fd, fl, new_value, old_value); + return res < 0 ? -errno : res; +} + +static int impl_timerfd_gettime(void *object, + int fd, struct itimerspec *curr_value) +{ + int res = timerfd_gettime(fd, curr_value); + return res < 0 ? -errno : res; + +} +static int impl_timerfd_read(void *object, int fd, uint64_t *expirations) +{ + if (read(fd, expirations, sizeof(uint64_t)) != sizeof(uint64_t)) + return -errno; + return 0; +} + +/* events */ +static int impl_eventfd_create(void *object, int flags) +{ + struct impl *impl = object; + int fl = 0, res; + if (flags & SPA_FD_CLOEXEC) + fl |= EFD_CLOEXEC; + if (flags & SPA_FD_NONBLOCK) + fl |= EFD_NONBLOCK; + if (flags & SPA_FD_EVENT_SEMAPHORE) + fl |= EFD_SEMAPHORE; + res = eventfd(0, fl); + spa_log_debug(impl->log, "%p: new fd:%d", impl, res); + return res < 0 ? -errno : res; +} + +static int impl_eventfd_write(void *object, int fd, uint64_t count) +{ + if (write(fd, &count, sizeof(uint64_t)) != sizeof(uint64_t)) + return -errno; + return 0; +} + +static int impl_eventfd_read(void *object, int fd, uint64_t *count) +{ + if (read(fd, count, sizeof(uint64_t)) != sizeof(uint64_t)) + return -errno; + return 0; +} + +/* signals */ +static int impl_signalfd_create(void *object, int signal, int flags) +{ + struct impl *impl = object; + sigset_t mask; + int res, fl = 0; + + if (flags & SPA_FD_CLOEXEC) + fl |= SFD_CLOEXEC; + if (flags & SPA_FD_NONBLOCK) + fl |= SFD_NONBLOCK; + + sigemptyset(&mask); + sigaddset(&mask, signal); + res = signalfd(-1, &mask, fl); + sigprocmask(SIG_BLOCK, &mask, NULL); + spa_log_debug(impl->log, "%p: new fd:%d", impl, res); + + return res < 0 ? -errno : res; +} + +static int impl_signalfd_read(void *object, int fd, int *signal) +{ + struct signalfd_siginfo signal_info; + int len; + + len = read(fd, &signal_info, sizeof signal_info); + if (!(len == -1 && errno == EAGAIN) && len != sizeof signal_info) + return -errno; + + *signal = signal_info.ssi_signo; + + return 0; +} + +static const struct spa_system_methods impl_system = { + SPA_VERSION_SYSTEM_METHODS, + .read = impl_read, + .write = impl_write, + .ioctl = impl_ioctl, + .close = impl_close, + .clock_gettime = impl_clock_gettime, + .clock_getres = impl_clock_getres, + .pollfd_create = impl_pollfd_create, + .pollfd_add = impl_pollfd_add, + .pollfd_mod = impl_pollfd_mod, + .pollfd_del = impl_pollfd_del, + .pollfd_wait = impl_pollfd_wait, + .timerfd_create = impl_timerfd_create, + .timerfd_settime = impl_timerfd_settime, + .timerfd_gettime = impl_timerfd_gettime, + .timerfd_read = impl_timerfd_read, + .eventfd_create = impl_eventfd_create, + .eventfd_write = impl_eventfd_write, + .eventfd_read = impl_eventfd_read, + .signalfd_create = impl_signalfd_create, + .signalfd_read = impl_signalfd_read, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_System)) + *interface = &impl->system; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + spa_return_val_if_fail(handle != NULL, -EINVAL); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *impl; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct impl *) handle; + impl->system.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_System, + SPA_VERSION_SYSTEM, + &impl_system, impl); + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(impl->log, &log_topic); + + spa_log_debug(impl->log, "%p: initialized", impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_System,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_support_system_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_SUPPORT_SYSTEM, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info +}; diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c new file mode 100644 index 0000000..16bb012 --- /dev/null +++ b/spa/plugins/test/fakesink.c @@ -0,0 +1,846 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "fakesink" + +struct props { + bool live; +}; + +#define MAX_BUFFERS 16 +#define MAX_PORTS 1 + +struct buffer { + uint32_t id; + struct spa_buffer *outbuf; + bool outstanding; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + uint8_t format_buffer[1024]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list ready; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[1]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + uint64_t start_time; + uint64_t elapsed_time; + + uint64_t buffer_count; + + struct port port; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS) + +#define DEFAULT_LIVE false + +static void reset_props(struct impl *this, struct props *props) +{ + props->live = DEFAULT_LIVE; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_Props: + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_live, SPA_POD_Bool(this->props.live)); + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + + switch (id) { + case SPA_PARAM_Props: + { + struct port *port = &this->port; + + if (param == NULL) { + reset_props(this, &this->props); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_live, SPA_POD_OPT_Bool(&this->props.live)); + + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + else + port->info.flags &= ~SPA_PORT_FLAG_LIVE; + break; + } + default: + return -ENOENT; + } + return 0; +} + +static void set_timer(struct impl *this, bool enabled) +{ + if (this->callbacks.funcs || this->props.live) { + if (enabled) { + if (this->props.live) { + uint64_t next_time = this->start_time + this->elapsed_time; + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 1; + } + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + } + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + } +} + +static inline int read_timer(struct impl *this) +{ + uint64_t expirations; + int res = 0; + + if (this->callbacks.funcs || this->props.live) { + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return res; +} + +static void render_buffer(struct impl *this, struct buffer *b) +{ +} + +static int consume_buffer(struct impl *this) +{ + struct port *port = &this->port; + struct buffer *b; + struct spa_io_buffers *io = port->io; + int n_bytes; + + if (read_timer(this) < 0) + return 0; + + if (spa_list_is_empty(&port->ready)) { + io->status = SPA_STATUS_NEED_DATA; + spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); + } + if (spa_list_is_empty(&port->ready)) { + spa_log_error(this->log, NAME " %p: no buffers", this); + return -EPIPE; + } + + b = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&b->link); + + n_bytes = b->outbuf->datas[0].maxsize; + + spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id); + + render_buffer(this, b); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = n_bytes; + b->outbuf->datas[0].chunk->stride = n_bytes; + + if (b->h) { + b->h->seq = this->buffer_count; + b->h->pts = this->start_time + this->elapsed_time; + b->h->dts_offset = 0; + } + + this->buffer_count++; + this->elapsed_time = this->buffer_count; + set_timer(this, true); + + io->buffer_id = b->id; + io->status = SPA_STATUS_NEED_DATA; + b->outstanding = true; + + return SPA_STATUS_NEED_DATA; +} + +static void on_input(struct spa_source *source) +{ + struct impl *this = source->data; + + consume_buffer(this); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct timespec now; + + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (this->started) + return 0; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (this->props.live) + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); + else + this->start_time = 0; + this->buffer_count = 0; + this->elapsed_time = 0; + + this->started = true; + set_timer(this, true); + break; + } + case SPA_NODE_COMMAND_Pause: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (!this->started) + return 0; + + this->started = false; + set_timer(this, false); + break; + default: + return -ENOTSUP; + } + + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_INPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (this->data_loop == NULL && callbacks != NULL) { + spa_log_error(this->log, "a data_loop is needed for async operation"); + return -EINVAL; + } + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, + uint32_t port_id, const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(struct impl *this, int seq, struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + return -ENOTSUP; +} + +static int port_get_format(struct impl *this, struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + if (!port->have_format) + return -EIO; + + if (index > 0) + return 0; + + *param = SPA_PTROFF(port->format_buffer, 0, struct spa_pod); + + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, seq, port, + result.index, filter, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->ready); + this->started = false; + set_timer(this, false); + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, const struct spa_pod *format) +{ + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + if (SPA_POD_SIZE(format) > sizeof(port->format_buffer)) + return -ENOSPC; + memcpy(port->format_buffer, format, SPA_POD_SIZE(format)); + port->have_format = true; + } + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + if (id == SPA_PARAM_Format) { + return port_set_format(this, port, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->outstanding = true; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + if (id == SPA_IO_Buffers) + port->io = data; + else + return -ENOENT; + + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) { + struct buffer *b = &port->buffers[io->buffer_id]; + + if (!b->outstanding) { + spa_log_warn(this->log, NAME " %p: buffer %u in use", this, + io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + + spa_log_trace(this->log, NAME " %p: queue buffer %u", this, io->buffer_id); + + spa_list_append(&port->ready, &b->link); + b->outstanding = false; + + io->buffer_id = SPA_ID_INVALID; + io->status = SPA_STATUS_OK; + } + if (this->callbacks.funcs == NULL) + return consume_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (this->data_loop) + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 1; + reset_props(this, &this->props); + + + this->timer_source.func = on_input; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + if (this->data_loop) + spa_loop_add_source(this->data_loop, &this->timer_source); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 4; + + spa_list_init(&port->ready); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_fakesink_factory = { + SPA_VERSION_HANDLE_FACTORY, + NAME, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c new file mode 100644 index 0000000..f414543 --- /dev/null +++ b/spa/plugins/test/fakesrc.c @@ -0,0 +1,876 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "fakesrc" + +struct props { + bool live; + uint32_t pattern; +}; + +#define MAX_BUFFERS 16 +#define MAX_PORTS 1 + +struct buffer { + uint32_t id; + struct spa_buffer *outbuf; + bool outstanding; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + uint8_t format_buffer[1024]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[1]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + uint64_t start_time; + uint64_t elapsed_time; + + uint64_t buffer_count; + bool underrun; + + struct port port; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS) + +#define DEFAULT_LIVE false +#define DEFAULT_PATTERN 0 + +static void reset_props(struct impl *this, struct props *props) +{ + props->live = DEFAULT_LIVE; + props->pattern = DEFAULT_PATTERN; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_live, SPA_POD_Bool(p->live), + SPA_PROP_patternType, SPA_POD_CHOICE_ENUM_Id(2, p->pattern, p->pattern)); + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct port *port = &this->port; + + if (param == NULL) { + reset_props(this, p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_live, SPA_POD_OPT_Bool(&p->live), + SPA_PROP_patternType, SPA_POD_OPT_Id(&p->pattern)); + + if (p->live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + else + port->info.flags &= ~SPA_PORT_FLAG_LIVE; + break; + } + default: + return -ENOENT; + } + return 0; +} + +static int fill_buffer(struct impl *this, struct buffer *b) +{ + return 0; +} + +static void set_timer(struct impl *this, bool enabled) +{ + if (this->callbacks.funcs || this->props.live) { + if (enabled) { + if (this->props.live) { + uint64_t next_time = this->start_time + this->elapsed_time; + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 1; + } + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + } + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + } +} + +static inline int read_timer(struct impl *this) +{ + uint64_t expirations; + int res = 0; + + if (this->callbacks.funcs || this->props.live) { + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return res; +} + +static int make_buffer(struct impl *this) +{ + struct buffer *b; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + int n_bytes; + + if (read_timer(this) < 0) + return 0; + + if (spa_list_is_empty(&port->empty)) { + set_timer(this, false); + this->underrun = true; + spa_log_error(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + b->outstanding = true; + + n_bytes = b->outbuf->datas[0].maxsize; + + spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id); + + fill_buffer(this, b); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = n_bytes; + b->outbuf->datas[0].chunk->stride = n_bytes; + + if (b->h) { + b->h->seq = this->buffer_count; + b->h->pts = this->start_time + this->elapsed_time; + b->h->dts_offset = 0; + } + + this->buffer_count++; + this->elapsed_time = this->buffer_count; + set_timer(this, true); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static void on_output(struct spa_source *source) +{ + struct impl *this = source->data; + int res; + + res = make_buffer(this); + + if (res == SPA_STATUS_HAVE_DATA && this->callbacks.funcs) + spa_node_call_ready(&this->callbacks, res); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct timespec now; + + if (!port->have_format) + return -EIO; + + if (port->n_buffers == 0) + return -EIO; + + if (this->started) + return 0; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (this->props.live) + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); + else + this->start_time = 0; + this->buffer_count = 0; + this->elapsed_time = 0; + + this->started = true; + set_timer(this, true); + break; + } + case SPA_NODE_COMMAND_Pause: + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (!this->started) + return 0; + + this->started = false; + set_timer(this, false); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (this->data_loop == NULL && callbacks != NULL) { + spa_log_error(this->log, "a data_loop is needed for async operation"); + return -EINVAL; + } + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(struct impl *this, struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + return 0; +} + +static int port_get_format(struct impl *this, struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + if (!port->have_format) + return -EIO; + if (index > 0) + return 0; + + *param = SPA_PTROFF(port->format_buffer, 0, struct spa_pod); + + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, port, + result.index, filter, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->empty); + this->started = false; + set_timer(this, false); + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + if (SPA_POD_SIZE(format) > sizeof(port->format_buffer)) + return -ENOSPC; + memcpy(port->format_buffer, format, SPA_POD_SIZE(format)); + port->have_format = true; + } + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + if (id == SPA_PARAM_Format) { + return port_set_format(this, port, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t flags, + uint32_t port_id, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->outstanding = false; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + } + spa_list_append(&port->empty, &b->link); + } + port->n_buffers = n_buffers; + this->underrun = false; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + if (id == SPA_IO_Buffers) + port->io = data; + else + return -ENOENT; + + return 0; +} + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + spa_return_if_fail(b->outstanding); + + spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id); + + b->outstanding = false; + spa_list_append(&port->empty, &b->link); + + if (this->underrun) { + set_timer(this, true); + this->underrun = false; + } +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (this->callbacks.funcs == NULL) + return make_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (this->data_loop) + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 0; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 1; + reset_props(this, &this->props); + + this->timer_source.func = on_output; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + if (this->data_loop) + spa_loop_add_source(this->data_loop, &this->timer_source); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 4; + + spa_list_init(&port->empty); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_fakesrc_factory = { + SPA_VERSION_HANDLE_FACTORY, + NAME, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/test/meson.build b/spa/plugins/test/meson.build new file mode 100644 index 0000000..950ee7c --- /dev/null +++ b/spa/plugins/test/meson.build @@ -0,0 +1,7 @@ +test_sources = ['fakesrc.c', 'fakesink.c', 'plugin.c'] + +testlib = shared_library('spa-test', + test_sources, + dependencies : [ spa_dep, pthread_lib ], + install : true, + install_dir : spa_plugindir / 'test') diff --git a/spa/plugins/test/plugin.c b/spa/plugins/test/plugin.c new file mode 100644 index 0000000..409230f --- /dev/null +++ b/spa/plugins/test/plugin.c @@ -0,0 +1,50 @@ +/* Spa Test plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_fakesrc_factory; +extern const struct spa_handle_factory spa_fakesink_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fakesrc_factory; + break; + case 1: + *factory = &spa_fakesink_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/v4l2/meson.build b/spa/plugins/v4l2/meson.build new file mode 100644 index 0000000..648583f --- /dev/null +++ b/spa/plugins/v4l2/meson.build @@ -0,0 +1,10 @@ +v4l2_sources = ['v4l2.c', + 'v4l2-device.c', + 'v4l2-udev.c', + 'v4l2-source.c'] + +v4l2lib = shared_library('spa-v4l2', + v4l2_sources, + dependencies : [ spa_dep, libudev_dep, libinotify_dep ], + install : true, + install_dir : spa_plugindir / 'v4l2') diff --git a/spa/plugins/v4l2/v4l2-device.c b/spa/plugins/v4l2/v4l2-device.c new file mode 100644 index 0000000..53fb033 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-device.c @@ -0,0 +1,291 @@ +/* Spa V4l2 Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "v4l2.h" + +#define NAME "v4l2-device" + +static const char default_device[] = "/dev/video0"; + +struct props { + char device[64]; + char product_id[6]; + char vendor_id[6]; + int device_fd; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, 64); +} + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + + struct props props; + + struct spa_hook_list hooks; + + struct spa_v4l2_device dev; +}; + +static int emit_info(struct impl *this, bool full) +{ + int res; + struct spa_dict_item items[12]; + uint32_t n_items = 0; + struct spa_device_info info; + struct spa_param_info params[2]; + char path[128], version[16], capabilities[16], device_caps[16]; + + if ((res = spa_v4l2_open(&this->dev, this->props.device)) < 0) + return res; + + info = SPA_DEVICE_INFO_INIT(); + + info.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS; + +#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value) + snprintf(path, sizeof(path), "v4l2:%s", this->props.device); + ADD_ITEM(SPA_KEY_OBJECT_PATH, path); + ADD_ITEM(SPA_KEY_DEVICE_API, "v4l2"); + ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Video/Device"); + if (this->props.product_id[0]) + ADD_ITEM(SPA_KEY_DEVICE_PRODUCT_ID, this->props.product_id); + if (this->props.vendor_id[0]) + ADD_ITEM(SPA_KEY_DEVICE_VENDOR_ID, this->props.vendor_id); + ADD_ITEM(SPA_KEY_API_V4L2_PATH, (char *)this->props.device); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_DRIVER, (char *)this->dev.cap.driver); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_CARD, (char *)this->dev.cap.card); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_BUS_INFO, (char *)this->dev.cap.bus_info); + snprintf(version, sizeof(version), "%u.%u.%u", + (this->dev.cap.version >> 16) & 0xFF, + (this->dev.cap.version >> 8) & 0xFF, + (this->dev.cap.version) & 0xFF); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_VERSION, version); + snprintf(capabilities, sizeof(capabilities), "%08x", this->dev.cap.capabilities); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_CAPABILITIES, capabilities); + snprintf(device_caps, sizeof(device_caps), "%08x", this->dev.cap.device_caps); + ADD_ITEM(SPA_KEY_API_V4L2_CAP_DEVICE_CAPS, device_caps); +#undef ADD_ITEM + info.props = &SPA_DICT_INIT(items, n_items); + + info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; + params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ); + params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_WRITE); + info.n_params = 0; + info.params = params; + + spa_device_emit_info(&this->hooks, &info); + + if (spa_v4l2_is_capture(&this->dev)) { + struct spa_device_object_info oinfo; + + oinfo = SPA_DEVICE_OBJECT_INFO_INIT(); + oinfo.type = SPA_TYPE_INTERFACE_Node; + oinfo.factory_name = SPA_NAME_API_V4L2_SOURCE; + oinfo.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + oinfo.props = &SPA_DICT_INIT(items, n_items); + + spa_device_emit_object_info(&this->hooks, 0, &oinfo); + } + + spa_v4l2_close(&this->dev); + + return 0; +} + +static int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + int res = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info || events->object_info) + res = emit_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return res; +} + +static int impl_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_device_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + return -ENOTSUP; +} + +static int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + return -ENOTSUP; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_add_listener, + .sync = impl_sync, + .enum_params = impl_enum_params, + .set_param = impl_set_param, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear, this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + this->dev.log = this->log; + this->dev.fd = -1; + + reset_props(&this->props); + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH))) + strncpy(this->props.device, str, 63); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_PRODUCT_ID))) + strncpy(this->props.product_id, str, 5); + if (info && (str = spa_dict_lookup(info, SPA_KEY_DEVICE_VENDOR_ID))) + strncpy(this->props.vendor_id, str, 5); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +const struct spa_handle_factory spa_v4l2_device_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_DEVICE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c new file mode 100644 index 0000000..b6eae49 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-source.c @@ -0,0 +1,1055 @@ +/* Spa V4l2 Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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"; + +struct props { + char device[64]; + char device_name[128]; + int device_fd; +}; + +static void reset_props(struct props *props) +{ + strncpy(props->device, default_device, 64); +} + +#define MAX_BUFFERS 32 + +#define BUFFER_FLAG_OUTSTANDING (1<<0) +#define BUFFER_FLAG_ALLOCATED (1<<1) +#define BUFFER_FLAG_MAPPED (1<<2) + +struct buffer { + uint32_t id; + uint32_t flags; + struct spa_list link; + struct spa_buffer *outbuf; + struct spa_meta_header *h; + struct v4l2_buffer v4l2_buffer; + void *ptr; +}; + +#define MAX_CONTROLS 64 + +struct control { + uint32_t id; + uint32_t ctrl_id; + double value; +}; + +struct port { + struct impl *impl; + + bool alloc_buffers; + bool have_expbuf; + + bool next_fmtdesc; + struct v4l2_fmtdesc fmtdesc; + bool next_frmsize; + struct v4l2_frmsizeenum frmsize; + struct v4l2_frmivalenum frmival; + + bool have_format; + struct spa_video_info current_format; + struct spa_fraction rate; + + struct spa_v4l2_device dev; + + bool have_query_ext_ctrl; + struct v4l2_format fmt; + enum v4l2_buf_type type; + enum v4l2_memory memtype; + + struct control controls[MAX_CONTROLS]; + uint32_t n_controls; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + struct spa_list queue; + + struct spa_source source; + + uint64_t info_all; + struct spa_port_info info; + struct spa_io_buffers *io; + struct spa_io_sequence *control; +#define PORT_PropInfo 0 +#define PORT_EnumFormat 1 +#define PORT_Meta 2 +#define PORT_IO 3 +#define PORT_Format 4 +#define PORT_Buffers 5 +#define PORT_Latency 6 +#define N_PORT_PARAMS 7 + struct spa_param_info params[N_PORT_PARAMS]; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + + uint64_t info_all; + struct spa_node_info info; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define NODE_EnumFormat 2 +#define NODE_Format 3 +#define N_NODE_PARAMS 4 + struct spa_param_info params[N_NODE_PARAMS]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + struct port out_ports[1]; + + struct spa_io_position *position; + struct spa_io_clock *clock; + + struct spa_latency_info latency[2]; +}; + +#define CHECK_PORT(this,direction,port_id) ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0) + +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) GET_OUT_PORT(this,p) + +#include "v4l2-utils.c" + +static int port_get_format(struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + struct spa_pod_frame f; + + if (!port->have_format) + return -EIO; + if (index > 0) + return 0; + + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, SPA_PARAM_Format); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(port->current_format.media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(port->current_format.media_subtype), + 0); + + switch (port->current_format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_format, SPA_POD_Id(port->current_format.info.raw.format), + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.raw.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate), + 0); + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.mjpg.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.mjpg.framerate), + 0); + break; + case SPA_MEDIA_SUBTYPE_h264: + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.h264.size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.h264.framerate), + 0); + break; + default: + return -EIO; + } + + *param = spa_pod_builder_pop(builder, &f); + + return 1; +} + + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device"), + SPA_PROP_INFO_type, SPA_POD_String(p->device)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 device name"), + SPA_PROP_INFO_type, SPA_POD_String(p->device_name)); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceFd), + SPA_PROP_INFO_description, SPA_POD_String("The V4L2 fd"), + SPA_PROP_INFO_type, SPA_POD_Int(p->device_fd)); + break; + default: + return spa_v4l2_enum_controls(this, seq, result.index - 3, num, filter); + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_device, SPA_POD_String(p->device), + SPA_PROP_deviceName, SPA_POD_String(p->device_name), + SPA_PROP_deviceFd, SPA_POD_Int(p->device_fd)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_EnumFormat: + return spa_v4l2_enum_format(this, seq, start, num, filter); + case SPA_PARAM_Format: + if((res = port_get_format(GET_OUT_PORT(this, 0), + result.index, filter, ¶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 impl_node_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + struct spa_pod_prop *prop; + + if (param == NULL) { + reset_props(p); + return 0; + } + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_device: + strncpy(p->device, + (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value), + sizeof(p->device)-1); + break; + default: + spa_v4l2_set_control(this, prop->key, prop); + break; + } + } + + break; + } + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (port->have_format) + return 0; + if ((res = spa_v4l2_close(&port->dev)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + { + if (!port->have_format) { + spa_log_error(this->log, "no format"); + return -EIO; + } + if (port->n_buffers == 0) { + spa_log_error(this->log, "no buffers"); + return -EIO; + } + + if ((res = spa_v4l2_stream_on(this)) < 0) + return res; + break; + } + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_v4l2_stream_off(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + + return 0; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_DEVICE_API, "v4l2" }, + { SPA_KEY_MEDIA_CLASS, "Video/Source" }, + { SPA_KEY_MEDIA_ROLE, "Camera" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, + enum spa_direction direction, + uint32_t port_id, const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, + enum spa_direction direction, + uint32_t port_id) +{ + return -ENOTSUP; +} + +static int impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct impl *this = object; + struct port *port; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + return spa_v4l2_enum_controls(this, seq, start, num, filter); + + case SPA_PARAM_EnumFormat: + return spa_v4l2_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if((res = port_get_format(port, result.index, filter, ¶m, &b)) <= 0) + return res; + break; + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(port->fmt.fmt.pix.sizeimage), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->fmt.fmt.pix.bytesperline)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Control), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_sequence))); + break; + default: + return 0; + } + break; + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + param = spa_latency_build(&b, id, &this->latency[result.index]); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + struct spa_video_info info; + int res; + + if (port->have_format) { + spa_v4l2_stream_off(this); + spa_v4l2_clear_buffers(this); + } + if (format == NULL) { + if (!port->have_format) + return 0; + + port->have_format = false; + port->dev.have_format = false; + spa_v4l2_close(&port->dev); + goto done; + } else { + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video) { + spa_log_error(this->log, "media type must be video"); + return -EINVAL; + } + + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + if (spa_format_video_raw_parse(format, &info.info.raw) < 0) { + spa_log_error(this->log, "can't parse video raw"); + return -EINVAL; + } + break; + case SPA_MEDIA_SUBTYPE_mjpg: + if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_h264: + if (spa_format_video_h264_parse(format, &info.info.h264) < 0) + return -EINVAL; + break; + default: + return -EINVAL; + } + } + + if (port->have_format && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + port->have_format = false; + } + + if ((res = spa_v4l2_set_format(this, &info, flags)) < 0) + return res; + + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY)) { + port->current_format = info; + port->have_format = true; + } + + done: + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READ); + } else { + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0); + } + emit_port_info(this, port, false); + emit_node_info(this, false); + + return 0; +} + +static int impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + int res; + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(object != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL; + emit_port_info(this, port, false); + break; + } + case SPA_PARAM_Format: + res = port_set_format(object, port, flags, param); + break; + default: + res = -ENOENT; + } + return res; +} + +static int impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + if (port->n_buffers) { + spa_v4l2_stream_off(this); + if ((res = spa_v4l2_clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + if (buffers == NULL) + return 0; + + if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { + res = spa_v4l2_alloc_buffers(this, buffers, n_buffers); + } else { + res = spa_v4l2_use_buffers(this, buffers, n_buffers); + } + return res; +} + +static int impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + case SPA_IO_Control: + port->control = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, + uint32_t port_id, + uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + + port = GET_OUT_PORT(this, port_id); + + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + res = spa_v4l2_buffer_recycle(this, buffer_id); + + return res; +} + +static int process_control(struct impl *this, struct spa_pod_sequence *control) +{ + struct spa_pod_control *c; + + SPA_POD_SEQUENCE_FOREACH(control, c) { + switch (c->type) { + case SPA_CONTROL_Properties: + { + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) &c->value; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + spa_v4l2_set_control(this, prop->key, prop); + } + break; + } + default: + break; + } + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + int res; + struct spa_io_buffers *io; + struct port *port; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = GET_OUT_PORT(this, 0); + if ((io = port->io) == NULL) + return -EIO; + + if (port->control) + process_control(this, &port->control->sequence); + + spa_log_trace(this->log, "%p; status %d", this, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + if ((res = spa_v4l2_buffer_recycle(this, io->buffer_id)) < 0) + return res; + + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&port->queue)) + return SPA_STATUS_OK; + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + spa_log_trace(this->log, "%p: dequeue buffer %d", this, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + const char *str; + struct port *port; + int res; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + v4l2_log_topic_init(this->log); + + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data_loop is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + spa_hook_list_init(&this->hooks); + + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, 0); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + reset_props(&this->props); + + port = GET_OUT_PORT(this, 0); + port->impl = this; + spa_list_init(&port->queue); + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[PORT_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READ); + port->info.params = port->params; + port->info.n_params = N_PORT_PARAMS; + + port->alloc_buffers = true; + port->have_expbuf = true; + port->have_query_ext_ctrl = true; + port->dev.log = this->log; + port->dev.fd = -1; + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_V4L2_PATH))) { + strncpy(this->props.device, str, 63); + if ((res = spa_v4l2_open(&port->dev, this->props.device)) < 0) + return res; + spa_v4l2_close(&port->dev); + } + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +const struct spa_handle_factory spa_v4l2_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_SOURCE, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c new file mode 100644 index 0000000..e7f9265 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-udev.c @@ -0,0 +1,754 @@ +/* Spa V4l2 udev monitor + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "v4l2-udev" + +#define MAX_DEVICES 64 + +#define ACTION_ADD 0 +#define ACTION_REMOVE 1 +#define ACTION_DISABLE 2 + +struct device { + uint32_t id; + struct udev_device *dev; + unsigned int accessible:1; + unsigned int ignored:1; + unsigned int emitted:1; +}; + +struct impl { + struct spa_handle handle; + struct spa_device device; + + struct spa_log *log; + struct spa_loop *main_loop; + + struct spa_hook_list hooks; + + uint64_t info_all; + struct spa_device_info info; + + struct udev *udev; + struct udev_monitor *umonitor; + + struct device devices[MAX_DEVICES]; + uint32_t n_devices; + + struct spa_source source; + struct spa_source notify; +}; + +static int impl_udev_open(struct impl *this) +{ + if (this->udev == NULL) { + this->udev = udev_new(); + if (this->udev == NULL) + return -ENOMEM; + } + return 0; +} + +static int impl_udev_close(struct impl *this) +{ + if (this->udev != NULL) + udev_unref(this->udev); + this->udev = NULL; + return 0; +} + +static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev) +{ + struct device *device; + + if (this->n_devices >= MAX_DEVICES) + return NULL; + device = &this->devices[this->n_devices++]; + spa_zero(*device); + device->id = id; + udev_device_ref(dev); + device->dev = dev; + return device; +} + +static struct device *find_device(struct impl *this, uint32_t id) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) { + if (this->devices[i].id == id) + return &this->devices[i]; + } + return NULL; +} + +static void remove_device(struct impl *this, struct device *device) +{ + udev_device_unref(device->dev); + *device = this->devices[--this->n_devices]; +} + +static void clear_devices(struct impl *this) +{ + uint32_t i; + for (i = 0; i < this->n_devices; i++) + udev_device_unref(this->devices[i].dev); + this->n_devices = 0; +} + +static uint32_t get_device_id(struct impl *this, struct udev_device *dev) +{ + const char *str; + + if ((str = udev_device_get_devnode(dev)) == NULL) + return SPA_ID_INVALID; + + if (!(str = strrchr(str, '/'))) + return SPA_ID_INVALID; + + if (strlen(str) <= 6 || strncmp(str, "/video", 6) != 0) + return SPA_ID_INVALID; + + return atoi(str + 6); +} + +static int dehex(char x) +{ + if (x >= '0' && x <= '9') + return x - '0'; + if (x >= 'A' && x <= 'F') + return x - 'A' + 10; + if (x >= 'a' && x <= 'f') + return x - 'a' + 10; + return -1; +} + +static void unescape(const char *src, char *dst) +{ + const char *s; + char *d; + int h1 = 0, h2 = 0; + enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT; + + for (s = src, d = dst; *s; s++) { + switch (state) { + case TEXT: + if (*s == '\\') + state = BACKSLASH; + else + *(d++) = *s; + break; + + case BACKSLASH: + if (*s == 'x') + state = EX; + else { + *(d++) = '\\'; + *(d++) = *s; + state = TEXT; + } + break; + + case EX: + h1 = dehex(*s); + if (h1 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *s; + state = TEXT; + } else + state = FIRST; + break; + + case FIRST: + h2 = dehex(*s); + if (h2 < 0) { + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + *(d++) = *s; + } else + *(d++) = (char) (h1 << 4) | h2; + state = TEXT; + break; + } + } + switch (state) { + case TEXT: + break; + case BACKSLASH: + *(d++) = '\\'; + break; + case EX: + *(d++) = '\\'; + *(d++) = 'x'; + break; + case FIRST: + *(d++) = '\\'; + *(d++) = 'x'; + *(d++) = *(s-1); + break; + } + *d = 0; +} + +static int emit_object_info(struct impl *this, struct device *device) +{ + struct spa_device_object_info info; + uint32_t id = device->id; + struct udev_device *dev = device->dev; + const char *str; + struct spa_dict_item items[20]; + uint32_t n_items = 0; + + info = SPA_DEVICE_OBJECT_INFO_INIT(); + + info.type = SPA_TYPE_INTERFACE_Device; + info.factory_name = SPA_NAME_API_V4L2_DEVICE; + info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS | + SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; + info.flags = 0; + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API,"udev"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "v4l2"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Video/Device"); + + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_V4L2_PATH, udev_device_get_devnode(dev)); + + if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str); + + str = udev_device_get_property_value(dev, "ID_PATH"); + if (!(str && *str)) + str = udev_device_get_syspath(dev); + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str); + } + if ((str = udev_device_get_devpath(dev)) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str); + } + if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str); + } + if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str); + } + if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str); + } + if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec); + } + } + str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_VENDOR"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str); + } + if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) { + int32_t val; + if (spa_atoi32(str, &val, 16)) { + char *dec = alloca(12); /* 0xffff is max */ + snprintf(dec, 12, "0x%04x", val); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec); + } + } + + str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL_ENC"); + if (!(str && *str)) { + str = udev_device_get_property_value(dev, "ID_MODEL"); + if (!(str && *str)) + str = udev_device_get_property_value(dev, "ID_V4L_PRODUCT"); + } else { + char *t = alloca(strlen(str) + 1); + unescape(str, t); + str = t; + } + } + if (str && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str); + + if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str); + } + if ((str = udev_device_get_property_value(dev, "ID_V4L_CAPABILITIES")) && *str) { + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CAPABILITIES, str); + } + info.props = &SPA_DICT_INIT(items, n_items); + spa_device_emit_object_info(&this->hooks, id, &info); + device->emitted = true; + + return 1; +} + +static bool check_access(struct impl *this, struct device *device) +{ + char path[128]; + + snprintf(path, sizeof(path), "/dev/video%u", device->id); + device->accessible = access(path, R_OK|W_OK) >= 0; + spa_log_debug(this->log, "%s accessible:%u", path, device->accessible); + + return device->accessible; +} + +static void process_device(struct impl *this, uint32_t action, struct udev_device *dev) +{ + uint32_t id; + struct device *device; + bool emitted; + + if ((id = get_device_id(this, dev)) == SPA_ID_INVALID) + return; + + device = find_device(this, id); + if (device && device->ignored) + return; + + switch (action) { + case ACTION_ADD: + if (device == NULL) + device = add_device(this, id, dev); + if (device == NULL) + return; + if (!check_access(this, device)) + return; + emit_object_info(this, device); + break; + + case ACTION_REMOVE: + if (device == NULL) + return; + emitted = device->emitted; + remove_device(this, device); + if (emitted) + spa_device_emit_object_info(&this->hooks, id, NULL); + break; + + case ACTION_DISABLE: + if (device == NULL) + return; + if (device->emitted) { + device->emitted = false; + spa_device_emit_object_info(&this->hooks, id, NULL); + } + break; + } +} + +static int stop_inotify(struct impl *this) +{ + if (this->notify.fd == -1) + return 0; + spa_log_info(this->log, "stop inotify"); + spa_loop_remove_source(this->main_loop, &this->notify); + close(this->notify.fd); + this->notify.fd = -1; + return 0; +} + +static void impl_on_notify_events(struct spa_source *source) +{ + bool deleted = false; + struct impl *this = source->data; + union { + unsigned char name[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event e; /* for appropriate alignment */ + } buf; + + while (true) { + ssize_t len; + const struct inotify_event *event; + void *p, *e; + + len = read(source->fd, &buf, sizeof(buf)); + if (len < 0 && errno != EAGAIN) + break; + if (len <= 0) + break; + + e = SPA_PTROFF(&buf, len, void); + + for (p = &buf; p < e; + p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) { + unsigned int id; + struct device *device; + + event = (const struct inotify_event *) p; + + if ((event->mask & IN_ATTRIB)) { + bool access; + if (sscanf(event->name, "video%u", &id) != 1) + continue; + if ((device = find_device(this, id)) == NULL) + continue; + access = check_access(this, device); + if (access && !device->emitted) + process_device(this, ACTION_ADD, device->dev); + else if (!access && device->emitted) + process_device(this, ACTION_DISABLE, device->dev); + } + /* /dev/ might have been removed */ + if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF))) + deleted = true; + } + } + if (deleted) + stop_inotify(this); +} + +static int start_inotify(struct impl *this) +{ + int res, notify_fd; + + if (this->notify.fd != -1) + return 0; + + if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0) + return -errno; + + res = inotify_add_watch(notify_fd, "/dev", + IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF); + if (res < 0) { + res = -errno; + close(notify_fd); + + if (res == -ENOENT) { + spa_log_debug(this->log, "/dev/ does not exist yet"); + return 0; + } + spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res)); + return res; + } + spa_log_info(this->log, "start inotify"); + this->notify.func = impl_on_notify_events; + this->notify.data = this; + this->notify.fd = notify_fd; + this->notify.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->notify); + + return 0; +} + +static void impl_on_fd_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct udev_device *dev; + const char *action; + + dev = udev_monitor_receive_device(this->umonitor); + if (dev == NULL) + return; + + if ((action = udev_device_get_action(dev)) == NULL) + action = "change"; + + spa_log_debug(this->log, "action %s", action); + + start_inotify(this); + + if (spa_streq(action, "add") || + spa_streq(action, "change")) { + process_device(this, ACTION_ADD, dev); + } else if (spa_streq(action, "remove")) { + process_device(this, ACTION_REMOVE, dev); + } + udev_device_unref(dev); +} + +static int start_monitor(struct impl *this) +{ + int res; + + if (this->umonitor != NULL) + return 0; + + this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev"); + if (this->umonitor == NULL) + return -ENOMEM; + + udev_monitor_filter_add_match_subsystem_devtype(this->umonitor, + "video4linux", NULL); + udev_monitor_enable_receiving(this->umonitor); + + this->source.func = impl_on_fd_events; + this->source.data = this; + this->source.fd = udev_monitor_get_fd(this->umonitor); + this->source.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_log_debug(this->log, "monitor %p", this->umonitor); + spa_loop_add_source(this->main_loop, &this->source); + + if ((res = start_inotify(this)) < 0) + return res; + + return 0; +} + +static int stop_monitor(struct impl *this) +{ + if (this->umonitor == NULL) + return 0; + + clear_devices (this); + + spa_loop_remove_source(this->main_loop, &this->source); + udev_monitor_unref(this->umonitor); + this->umonitor = NULL; + + stop_inotify(this); + + return 0; +} + +static int enum_devices(struct impl *this) +{ + struct udev_enumerate *enumerate; + struct udev_list_entry *devices; + + enumerate = udev_enumerate_new(this->udev); + if (enumerate == NULL) + return -ENOMEM; + + udev_enumerate_add_match_subsystem(enumerate, "video4linux"); + udev_enumerate_scan_devices(enumerate); + + for (devices = udev_enumerate_get_list_entry(enumerate); devices; + devices = udev_list_entry_get_next(devices)) { + struct udev_device *dev; + + dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices)); + if (dev == NULL) + continue; + + process_device(this, ACTION_ADD, dev); + + udev_device_unref(dev); + } + udev_enumerate_unref(enumerate); + + return 0; +} + +static const struct spa_dict_item device_info_items[] = { + { SPA_KEY_DEVICE_API, "udev" }, + { SPA_KEY_DEVICE_NICK, "v4l2-udev" }, + { SPA_KEY_API_UDEV_MATCH, "video4linux" }, +}; + +static void emit_device_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(device_info_items); + spa_device_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void impl_hook_removed(struct spa_hook *hook) +{ + struct impl *this = hook->priv; + if (spa_hook_list_is_empty(&this->hooks)) { + stop_monitor(this); + impl_udev_close(this); + } +} + +static int +impl_device_add_listener(void *object, struct spa_hook *listener, + const struct spa_device_events *events, void *data) +{ + int res; + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(events != NULL, -EINVAL); + + if ((res = impl_udev_open(this)) < 0) + return res; + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_device_info(this, true); + + if ((res = enum_devices(this)) < 0) + return res; + + if ((res = start_monitor(this)) < 0) + return res; + + spa_hook_list_join(&this->hooks, &save); + + listener->removed = impl_hook_removed; + listener->priv = this; + + return 0; +} + +static const struct spa_device_methods impl_device = { + SPA_VERSION_DEVICE_METHODS, + .add_listener = impl_device_add_listener, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) + *interface = &this->device; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this = (struct impl *) handle; + stop_monitor(this); + impl_udev_close(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + this->notify.fd = -1; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); + + if (this->main_loop == NULL) { + spa_log_error(this->log, "a main-loop is needed"); + return -EINVAL; + } + spa_hook_list_init(&this->hooks); + + this->device.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Device, + SPA_VERSION_DEVICE, + &impl_device, this); + + this->info = SPA_DEVICE_INFO_INIT(); + this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS | + SPA_DEVICE_CHANGE_MASK_PROPS; + this->info.flags = 0; + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Device,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + return 1; +} + +const struct spa_handle_factory spa_v4l2_udev_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_V4L2_ENUM_UDEV, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c new file mode 100644 index 0000000..8ffda13 --- /dev/null +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -0,0 +1,1743 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include +#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 (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) { + spa_log_debug(this->log, "close %d", (int) d[0].fd); + close(d[0].fd); + } + d[0].type = SPA_ID_INVALID; + } + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = 0; + + if (xioctl(port->dev.fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_warn(this->log, "VIDIOC_REQBUFS: %m"); + } + port->n_buffers = 0; + + return 0; +} + + +struct format_info { + uint32_t fourcc; + uint32_t format; + uint32_t media_type; + uint32_t media_subtype; +}; + +#define VIDEO SPA_MEDIA_TYPE_video +#define IMAGE SPA_MEDIA_TYPE_image + +#define RAW SPA_MEDIA_SUBTYPE_raw + +#define BAYER SPA_MEDIA_SUBTYPE_bayer +#define MJPG SPA_MEDIA_SUBTYPE_mjpg +#define JPEG SPA_MEDIA_SUBTYPE_jpeg +#define DV SPA_MEDIA_SUBTYPE_dv +#define MPEGTS SPA_MEDIA_SUBTYPE_mpegts +#define H264 SPA_MEDIA_SUBTYPE_h264 +#define H263 SPA_MEDIA_SUBTYPE_h263 +#define MPEG1 SPA_MEDIA_SUBTYPE_mpeg1 +#define MPEG2 SPA_MEDIA_SUBTYPE_mpeg2 +#define MPEG4 SPA_MEDIA_SUBTYPE_mpeg4 +#define XVID SPA_MEDIA_SUBTYPE_xvid +#define VC1 SPA_MEDIA_SUBTYPE_vc1 +#define VP8 SPA_MEDIA_SUBTYPE_vp8 + +#define FORMAT_UNKNOWN SPA_VIDEO_FORMAT_UNKNOWN +#define FORMAT_ENCODED SPA_VIDEO_FORMAT_ENCODED +#define FORMAT_RGB15 SPA_VIDEO_FORMAT_RGB15 +#define FORMAT_BGR15 SPA_VIDEO_FORMAT_BGR15 +#define FORMAT_RGB16 SPA_VIDEO_FORMAT_RGB16 +#define FORMAT_BGR SPA_VIDEO_FORMAT_BGR +#define FORMAT_RGB SPA_VIDEO_FORMAT_RGB +#define FORMAT_BGRA SPA_VIDEO_FORMAT_BGRA +#define FORMAT_BGRx SPA_VIDEO_FORMAT_BGRx +#define FORMAT_ARGB SPA_VIDEO_FORMAT_ARGB +#define FORMAT_xRGB SPA_VIDEO_FORMAT_xRGB +#define FORMAT_GRAY8 SPA_VIDEO_FORMAT_GRAY8 +#define FORMAT_GRAY16_LE SPA_VIDEO_FORMAT_GRAY16_LE +#define FORMAT_GRAY16_BE SPA_VIDEO_FORMAT_GRAY16_BE +#define FORMAT_YVU9 SPA_VIDEO_FORMAT_YVU9 +#define FORMAT_YV12 SPA_VIDEO_FORMAT_YV12 +#define FORMAT_YUY2 SPA_VIDEO_FORMAT_YUY2 +#define FORMAT_YVYU SPA_VIDEO_FORMAT_YVYU +#define FORMAT_UYVY SPA_VIDEO_FORMAT_UYVY +#define FORMAT_Y42B SPA_VIDEO_FORMAT_Y42B +#define FORMAT_Y41B SPA_VIDEO_FORMAT_Y41B +#define FORMAT_YUV9 SPA_VIDEO_FORMAT_YUV9 +#define FORMAT_I420 SPA_VIDEO_FORMAT_I420 +#define FORMAT_NV12 SPA_VIDEO_FORMAT_NV12 +#define FORMAT_NV12_64Z32 SPA_VIDEO_FORMAT_NV12_64Z32 +#define FORMAT_NV21 SPA_VIDEO_FORMAT_NV21 +#define FORMAT_NV16 SPA_VIDEO_FORMAT_NV16 +#define FORMAT_NV61 SPA_VIDEO_FORMAT_NV61 +#define FORMAT_NV24 SPA_VIDEO_FORMAT_NV24 + +static const struct format_info format_info[] = { + /* RGB formats */ + {V4L2_PIX_FMT_RGB332, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB555, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB555, FORMAT_RGB15, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB555X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB555X, FORMAT_BGR15, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB565, FORMAT_RGB16, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB565X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR666, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR24, FORMAT_BGR, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB24, FORMAT_RGB, VIDEO, RAW}, + {V4L2_PIX_FMT_ABGR32, FORMAT_BGRA, VIDEO, RAW}, + {V4L2_PIX_FMT_XBGR32, FORMAT_BGRx, VIDEO, RAW}, + {V4L2_PIX_FMT_ARGB32, FORMAT_ARGB, VIDEO, RAW}, + {V4L2_PIX_FMT_XRGB32, FORMAT_xRGB, VIDEO, RAW}, + + /* Deprecated Packed RGB Image Formats (alpha ambiguity) */ + {V4L2_PIX_FMT_RGB444, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB555, FORMAT_RGB15, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB555X, FORMAT_BGR15, VIDEO, RAW}, + {V4L2_PIX_FMT_BGR32, FORMAT_BGRx, VIDEO, RAW}, + {V4L2_PIX_FMT_RGB32, FORMAT_xRGB, VIDEO, RAW}, + + /* Grey formats */ + {V4L2_PIX_FMT_GREY, FORMAT_GRAY8, VIDEO, RAW}, + {V4L2_PIX_FMT_Y4, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y6, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y10, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y12, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_Y16, FORMAT_GRAY16_LE, VIDEO, RAW}, + {V4L2_PIX_FMT_Y16_BE, FORMAT_GRAY16_BE, VIDEO, RAW}, + {V4L2_PIX_FMT_Y10BPACK, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Palette formats */ + {V4L2_PIX_FMT_PAL8, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Chrominance formats */ + {V4L2_PIX_FMT_UV8, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Luminance+Chrominance formats */ + {V4L2_PIX_FMT_YVU410, FORMAT_YVU9, VIDEO, RAW}, + {V4L2_PIX_FMT_YVU420, FORMAT_YV12, VIDEO, RAW}, + {V4L2_PIX_FMT_YVU420M, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUYV, FORMAT_YUY2, VIDEO, RAW}, + {V4L2_PIX_FMT_YYUV, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YVYU, FORMAT_YVYU, VIDEO, RAW}, + {V4L2_PIX_FMT_UYVY, FORMAT_UYVY, VIDEO, RAW}, + {V4L2_PIX_FMT_VYUY, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV422P, FORMAT_Y42B, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV411P, FORMAT_Y41B, VIDEO, RAW}, + {V4L2_PIX_FMT_Y41P, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV444, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV555, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV565, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV32, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV410, FORMAT_YUV9, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV420, FORMAT_I420, VIDEO, RAW}, + {V4L2_PIX_FMT_YUV420M, FORMAT_I420, VIDEO, RAW}, + {V4L2_PIX_FMT_HI240, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_HM12, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_M420, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* two planes -- one Y, one Cr + Cb interleaved */ + {V4L2_PIX_FMT_NV12, FORMAT_NV12, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12M, FORMAT_NV12, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12MT, FORMAT_NV12_64Z32, VIDEO, RAW}, + {V4L2_PIX_FMT_NV12MT_16X16, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_NV21, FORMAT_NV21, VIDEO, RAW}, + {V4L2_PIX_FMT_NV21M, FORMAT_NV21, VIDEO, RAW}, + {V4L2_PIX_FMT_NV16, FORMAT_NV16, VIDEO, RAW}, + {V4L2_PIX_FMT_NV16M, FORMAT_NV16, VIDEO, RAW}, + {V4L2_PIX_FMT_NV61, FORMAT_NV61, VIDEO, RAW}, + {V4L2_PIX_FMT_NV61M, FORMAT_NV61, VIDEO, RAW}, + {V4L2_PIX_FMT_NV24, FORMAT_NV24, VIDEO, RAW}, + {V4L2_PIX_FMT_NV42, FORMAT_UNKNOWN, VIDEO, RAW}, + + /* Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm */ + {V4L2_PIX_FMT_SBGGR8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SGBRG8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SGRBG8, FORMAT_UNKNOWN, VIDEO, BAYER}, + {V4L2_PIX_FMT_SRGGB8, FORMAT_UNKNOWN, VIDEO, BAYER}, + + /* compressed formats */ + {V4L2_PIX_FMT_MJPEG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_JPEG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_PJPG, FORMAT_ENCODED, VIDEO, MJPG}, + {V4L2_PIX_FMT_DV, FORMAT_ENCODED, VIDEO, DV}, + {V4L2_PIX_FMT_MPEG, FORMAT_ENCODED, VIDEO, MPEGTS}, + {V4L2_PIX_FMT_H264, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H264_NO_SC, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H264_MVC, FORMAT_ENCODED, VIDEO, H264}, + {V4L2_PIX_FMT_H263, FORMAT_ENCODED, VIDEO, H263}, + {V4L2_PIX_FMT_MPEG1, FORMAT_ENCODED, VIDEO, MPEG1}, + {V4L2_PIX_FMT_MPEG2, FORMAT_ENCODED, VIDEO, MPEG2}, + {V4L2_PIX_FMT_MPEG4, FORMAT_ENCODED, VIDEO, MPEG4}, + {V4L2_PIX_FMT_XVID, FORMAT_ENCODED, VIDEO, XVID}, + {V4L2_PIX_FMT_VC1_ANNEX_G, FORMAT_ENCODED, VIDEO, VC1}, + {V4L2_PIX_FMT_VC1_ANNEX_L, FORMAT_ENCODED, VIDEO, VC1}, + {V4L2_PIX_FMT_VP8, FORMAT_ENCODED, VIDEO, VP8}, + + /* Vendor-specific formats */ + {V4L2_PIX_FMT_WNVA, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_SN9C10X, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_PWC1, FORMAT_UNKNOWN, VIDEO, RAW}, + {V4L2_PIX_FMT_PWC2, FORMAT_UNKNOWN, VIDEO, RAW}, +}; + +static const struct format_info *fourcc_to_format_info(uint32_t fourcc) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + if (i->fourcc == fourcc) + return i; + } + return NULL; +} + +#if 0 +static const struct format_info *video_format_to_format_info(uint32_t format) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + if (i->format == format) + return i; + } + return NULL; +} +#endif + +static const struct format_info *find_format_info_by_media_type(uint32_t type, + uint32_t subtype, + uint32_t format, + int startidx) +{ + size_t i; + + for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) { + const struct format_info *fi = &format_info[i]; + if ((fi->media_type == type) && + (fi->media_subtype == subtype) && + (format == 0 || fi->format == format)) + return fi; + } + return NULL; +} + +static uint32_t +enum_filter_format(uint32_t media_type, int32_t media_subtype, + const struct spa_pod *filter, uint32_t index) +{ + uint32_t video_format = 0; + + switch (media_type) { + case SPA_MEDIA_TYPE_video: + case SPA_MEDIA_TYPE_image: + if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { + const struct spa_pod_prop *p; + const struct spa_pod *val; + uint32_t n_values, choice; + const uint32_t *values; + + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_format))) + return SPA_VIDEO_FORMAT_UNKNOWN; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + + if (val->type != SPA_TYPE_Id) + return SPA_VIDEO_FORMAT_UNKNOWN; + + values = SPA_POD_BODY(val); + + if (choice == SPA_CHOICE_None) { + if (index == 0) + video_format = values[0]; + } else { + if (index + 1 < n_values) + video_format = values[index + 1]; + } + } else { + if (index == 0) + video_format = SPA_VIDEO_FORMAT_ENCODED; + } + } + return video_format; +} + +static bool +filter_framesize(struct v4l2_frmsizeenum *frmsize, + const struct spa_rectangle *min, + const struct spa_rectangle *max, + const struct spa_rectangle *step) +{ + if (frmsize->type == V4L2_FRMSIZE_TYPE_DISCRETE) { + if (frmsize->discrete.width < min->width || + frmsize->discrete.height < min->height || + frmsize->discrete.width > max->width || + frmsize->discrete.height > max->height) { + return false; + } + } else if (frmsize->type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + frmsize->type == V4L2_FRMSIZE_TYPE_STEPWISE) { + /* FIXME, use LCM */ + frmsize->stepwise.step_width *= step->width; + frmsize->stepwise.step_height *= step->height; + + if (frmsize->stepwise.max_width < min->width || + frmsize->stepwise.max_height < min->height || + frmsize->stepwise.min_width > max->width || + frmsize->stepwise.min_height > max->height) + return false; + + frmsize->stepwise.min_width = SPA_MAX(frmsize->stepwise.min_width, min->width); + frmsize->stepwise.min_height = SPA_MAX(frmsize->stepwise.min_height, min->height); + frmsize->stepwise.max_width = SPA_MIN(frmsize->stepwise.max_width, max->width); + frmsize->stepwise.max_height = SPA_MIN(frmsize->stepwise.max_height, max->height); + } else + return false; + + return true; +} + +static int compare_fraction(struct v4l2_fract *f1, const struct spa_fraction *f2) +{ + uint64_t n1, n2; + + /* fractions are reduced when set, so we can quickly see if they're equal */ + if (f1->denominator == f2->num && f1->numerator == f2->denom) + return 0; + + /* extend to 64 bits */ + n1 = ((int64_t) f1->denominator) * f2->denom; + n2 = ((int64_t) f1->numerator) * f2->num; + if (n1 < n2) + return -1; + return 1; +} + +static bool +filter_framerate(struct v4l2_frmivalenum *frmival, + const struct spa_fraction *min, + const struct spa_fraction *max, + const struct spa_fraction *step) +{ + if (frmival->type == V4L2_FRMIVAL_TYPE_DISCRETE) { + if (compare_fraction(&frmival->discrete, min) < 0 || + compare_fraction(&frmival->discrete, max) > 0) + return false; + } else if (frmival->type == V4L2_FRMIVAL_TYPE_CONTINUOUS || + frmival->type == V4L2_FRMIVAL_TYPE_STEPWISE) { + /* FIXME, use LCM */ + frmival->stepwise.step.denominator *= step->num; + frmival->stepwise.step.numerator *= step->denom; + + if (compare_fraction(&frmival->stepwise.max, min) < 0 || + compare_fraction(&frmival->stepwise.min, max) > 0) + return false; + + if (compare_fraction(&frmival->stepwise.min, min) < 0) { + frmival->stepwise.min.denominator = min->num; + frmival->stepwise.min.numerator = min->denom; + } + if (compare_fraction(&frmival->stepwise.max, max) > 0) { + frmival->stepwise.max.denominator = max->num; + frmival->stepwise.max.numerator = max->denom; + } + } else + return false; + + return true; +} + +#define FOURCC_ARGS(f) (f)&0x7f,((f)>>8)&0x7f,((f)>>16)&0x7f,((f)>>24)&0x7f + +static int +spa_v4l2_enum_format(struct impl *this, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct port *port = &this->out_ports[0]; + int res, n_fractions; + const struct format_info *info; + struct spa_pod_choice *choice; + uint32_t filter_media_type, filter_media_subtype, video_format; + struct spa_v4l2_device *dev = &port->dev; + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + uint32_t count = 0; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + if (result.next == 0) { + spa_zero(port->fmtdesc); + port->fmtdesc.index = 0; + port->fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + port->next_fmtdesc = true; + spa_zero(port->frmsize); + port->next_frmsize = true; + spa_zero(port->frmival); + } + + if (filter) { + if ((res = spa_format_parse(filter, &filter_media_type, &filter_media_subtype)) < 0) + return res; + } + + if (false) { + next_fmtdesc: + port->fmtdesc.index++; + port->next_fmtdesc = true; + } + + next: + result.index = result.next++; + + while (port->next_fmtdesc) { + if (filter) { + struct v4l2_format fmt; + + video_format = enum_filter_format(filter_media_type, + filter_media_subtype, + filter, port->fmtdesc.index); + + if (video_format == SPA_VIDEO_FORMAT_UNKNOWN) + goto enum_end; + + info = find_format_info_by_media_type(filter_media_type, + filter_media_subtype, + video_format, 0); + if (info == NULL) + goto next_fmtdesc; + + port->fmtdesc.pixelformat = info->fourcc; + + spa_zero(fmt); + fmt.type = port->fmtdesc.type; + fmt.fmt.pix.pixelformat = info->fourcc; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + fmt.fmt.pix.width = 0; + fmt.fmt.pix.height = 0; + + if ((res = xioctl(dev->fd, VIDIOC_TRY_FMT, &fmt)) < 0) { + spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT %08x: %m", + this->props.device, info->fourcc); + goto next_fmtdesc; + } + if (fmt.fmt.pix.pixelformat != info->fourcc) { + spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT wanted %.4s gave %.4s", + this->props.device, (char*)&info->fourcc, + (char*)&fmt.fmt.pix.pixelformat); + goto next_fmtdesc; + } + + } else { + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FMT, &port->fmtdesc)) < 0) { + if (errno == EINVAL) + goto enum_end; + + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FMT: %m", + this->props.device); + goto exit; + } + } + port->next_fmtdesc = false; + port->frmsize.index = 0; + port->frmsize.pixel_format = port->fmtdesc.pixelformat; + port->next_frmsize = true; + } + if (!(info = fourcc_to_format_info(port->fmtdesc.pixelformat))) + goto next_fmtdesc; + + next_frmsize: + while (port->next_frmsize) { + if (filter) { + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t n_vals, choice; + + /* check if we have a fixed frame size */ + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size))) + goto do_frmsize; + + val = spa_pod_get_values(&p->value, &n_vals, &choice); + if (val->type != SPA_TYPE_Rectangle) + goto enum_end; + + if (choice == SPA_CHOICE_None) { + const struct spa_rectangle *values = SPA_POD_BODY(val); + + if (port->frmsize.index > 0) + goto next_fmtdesc; + + port->frmsize.type = V4L2_FRMSIZE_TYPE_DISCRETE; + port->frmsize.discrete.width = values[0].width; + port->frmsize.discrete.height = values[0].height; + goto have_size; + } + } + do_frmsize: + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMESIZES, &port->frmsize)) < 0) { + if (errno == EINVAL) + goto next_fmtdesc; + + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMESIZES: %m", + this->props.device); + goto exit; + } + if (filter) { + static const struct spa_rectangle step = {1, 1}; + + const struct spa_rectangle *values; + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t choice, i, n_values; + + /* check if we have a fixed frame size */ + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_size))) + goto have_size; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + if (val->type != SPA_TYPE_Rectangle) + goto have_size; + + values = SPA_POD_BODY_CONST(val); + + if (choice == SPA_CHOICE_Range && n_values > 2) { + if (filter_framesize(&port->frmsize, &values[1], &values[2], &step)) + goto have_size; + } else if (choice == SPA_CHOICE_Step && n_values > 3) { + if (filter_framesize(&port->frmsize, &values[1], &values[2], &values[3])) + goto have_size; + } else if (choice == SPA_CHOICE_Enum) { + for (i = 1; i < n_values; i++) { + if (filter_framesize(&port->frmsize, &values[i], &values[i], &step)) + goto have_size; + } + } + /* nothing matches the filter, get next frame size */ + port->frmsize.index++; + continue; + } + + have_size: + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + /* we have a fixed size, use this to get the frame intervals */ + port->frmival.index = 0; + port->frmival.pixel_format = port->frmsize.pixel_format; + port->frmival.width = port->frmsize.discrete.width; + port->frmival.height = port->frmsize.discrete.height; + port->next_frmsize = false; + } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + /* we have a non fixed size, fix to something sensible to get the + * framerate */ + port->frmival.index = 0; + port->frmival.pixel_format = port->frmsize.pixel_format; + port->frmival.width = port->frmsize.stepwise.min_width; + port->frmival.height = port->frmsize.stepwise.min_height; + port->next_frmsize = false; + } else { + port->frmsize.index++; + } + } + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b, + SPA_FORMAT_mediaType, SPA_POD_Id(info->media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype), + 0); + + if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id(&b, info->format); + } + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0); + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + spa_pod_builder_rectangle(&b, + port->frmsize.discrete.width, + port->frmsize.discrete.height); + } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS || + port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]); + + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.min_width, + port->frmsize.stepwise.min_height); + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.max_width, + port->frmsize.stepwise.max_height); + + if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) { + choice->body.type = SPA_CHOICE_Range; + } else { + choice->body.type = SPA_CHOICE_Step; + spa_pod_builder_rectangle(&b, + port->frmsize.stepwise.max_width, + port->frmsize.stepwise.max_height); + } + spa_pod_builder_pop(&b, &f[1]); + } + + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_framerate, 0); + + n_fractions = 0; + + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(&b, &f[1]); + port->frmival.index = 0; + + while (true) { + if ((res = xioctl(dev->fd, VIDIOC_ENUM_FRAMEINTERVALS, &port->frmival)) < 0) { + res = -errno; + if (errno == EINVAL) { + port->frmsize.index++; + port->next_frmsize = true; + if (port->frmival.index == 0) + goto next_frmsize; + break; + } + spa_log_error(this->log, "'%s' VIDIOC_ENUM_FRAMEINTERVALS: %m", + this->props.device); + goto exit; + } + if (filter) { + static const struct spa_fraction step = {1, 1}; + + const struct spa_fraction *values; + const struct spa_pod_prop *p; + struct spa_pod *val; + uint32_t i, n_values, choice; + + if (!(p = spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_framerate))) + goto have_framerate; + + val = spa_pod_get_values(&p->value, &n_values, &choice); + + if (val->type != SPA_TYPE_Fraction) + goto enum_end; + + values = SPA_POD_BODY(val); + + switch (choice) { + case SPA_CHOICE_None: + if (filter_framerate(&port->frmival, &values[0], &values[0], &step)) + goto have_framerate; + break; + + case SPA_CHOICE_Range: + if (n_values > 2 && filter_framerate(&port->frmival, &values[1], &values[2], &step)) + goto have_framerate; + break; + + case SPA_CHOICE_Step: + if (n_values > 3 && filter_framerate(&port->frmival, &values[1], &values[2], &values[3])) + goto have_framerate; + break; + + case SPA_CHOICE_Enum: + for (i = 1; i < n_values; i++) { + if (filter_framerate(&port->frmival, &values[i], &values[i], &step)) + goto have_framerate; + } + break; + default: + break; + } + port->frmival.index++; + continue; + } + + have_framerate: + + if (port->frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE) { + choice->body.type = SPA_CHOICE_Enum; + if (n_fractions == 0) + spa_pod_builder_fraction(&b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + spa_pod_builder_fraction(&b, + port->frmival.discrete.denominator, + port->frmival.discrete.numerator); + port->frmival.index++; + } else if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS || + port->frmival.type == V4L2_FRMIVAL_TYPE_STEPWISE) { + if (n_fractions == 0) + spa_pod_builder_fraction(&b, 25, 1); + spa_pod_builder_fraction(&b, + port->frmival.stepwise.min.denominator, + port->frmival.stepwise.min.numerator); + spa_pod_builder_fraction(&b, + port->frmival.stepwise.max.denominator, + port->frmival.stepwise.max.numerator); + + if (port->frmival.type == V4L2_FRMIVAL_TYPE_CONTINUOUS) { + choice->body.type = SPA_CHOICE_Range; + } else { + choice->body.type = SPA_CHOICE_Step; + spa_pod_builder_fraction(&b, + port->frmival.stepwise.step.denominator, + port->frmival.stepwise.step.numerator); + } + + port->frmsize.index++; + port->next_frmsize = true; + break; + } + n_fractions++; + } + if (n_fractions <= 1) + choice->body.type = SPA_CHOICE_None; + + spa_pod_builder_pop(&b, &f[1]); + result.param = spa_pod_builder_pop(&b, &f[0]); + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + exit: + spa_v4l2_close(dev); + return res; +} + +static int spa_v4l2_set_format(struct impl *this, struct spa_video_info *format, uint32_t flags) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + int res, cmd; + struct v4l2_format reqfmt, fmt; + struct v4l2_streamparm streamparm; + const struct format_info *info = NULL; + uint32_t video_format; + struct spa_rectangle *size = NULL; + struct spa_fraction *framerate = NULL; + bool match; + + spa_zero(fmt); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + spa_zero(streamparm); + streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + switch (format->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + video_format = format->info.raw.format; + size = &format->info.raw.size; + framerate = &format->info.raw.framerate; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.mjpg.size; + framerate = &format->info.mjpg.framerate; + break; + case SPA_MEDIA_SUBTYPE_h264: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.h264.size; + framerate = &format->info.h264.framerate; + break; + default: + video_format = SPA_VIDEO_FORMAT_ENCODED; + break; + } + + info = find_format_info_by_media_type(format->media_type, + format->media_subtype, video_format, 0); + if (info == NULL || size == NULL || framerate == NULL) { + spa_log_error(this->log, "unknown media type %d %d %d", format->media_type, + format->media_subtype, video_format); + return -EINVAL; + } + + fmt.fmt.pix.pixelformat = info->fourcc; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + fmt.fmt.pix.width = size->width; + fmt.fmt.pix.height = size->height; + streamparm.parm.capture.timeperframe.numerator = framerate->denom; + streamparm.parm.capture.timeperframe.denominator = framerate->num; + + spa_log_debug(this->log, "set %.4s %dx%d %d/%d", (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height, + streamparm.parm.capture.timeperframe.denominator, + streamparm.parm.capture.timeperframe.numerator); + + reqfmt = fmt; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + cmd = (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) ? VIDIOC_TRY_FMT : VIDIOC_S_FMT; + if (xioctl(dev->fd, cmd, &fmt) < 0) { + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_S_FMT: %m", + this->props.device); + return res; + } + + /* some cheap USB cam's won't accept any change */ + if (xioctl(dev->fd, VIDIOC_S_PARM, &streamparm) < 0) + spa_log_warn(this->log, "VIDIOC_S_PARM: %m"); + + match = (reqfmt.fmt.pix.pixelformat == fmt.fmt.pix.pixelformat && + reqfmt.fmt.pix.width == fmt.fmt.pix.width && + reqfmt.fmt.pix.height == fmt.fmt.pix.height); + + if (!match && !SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) { + spa_log_error(this->log, "wanted %.4s %dx%d, got %.4s %dx%d", + (char *)&reqfmt.fmt.pix.pixelformat, + reqfmt.fmt.pix.width, reqfmt.fmt.pix.height, + (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height); + return -EINVAL; + } + + if (flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) + return match ? 0 : 1; + + spa_log_info(this->log, "'%s' got %.4s %dx%d %d/%d", + dev->path, (char *)&fmt.fmt.pix.pixelformat, + fmt.fmt.pix.width, fmt.fmt.pix.height, + streamparm.parm.capture.timeperframe.denominator, + streamparm.parm.capture.timeperframe.numerator); + + dev->have_format = true; + size->width = fmt.fmt.pix.width; + size->height = fmt.fmt.pix.height; + port->rate.denom = framerate->num = streamparm.parm.capture.timeperframe.denominator; + port->rate.num = framerate->denom = streamparm.parm.capture.timeperframe.numerator; + + port->fmt = fmt; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE; + port->info.flags = (port->alloc_buffers ? SPA_PORT_FLAG_CAN_ALLOC_BUFFERS : 0) | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom); + + return match ? 0 : 1; +} + +static int query_ext_ctrl_ioctl(struct port *port, struct v4l2_query_ext_ctrl *qctrl) +{ + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_queryctrl qc; + int res; + + if (port->have_query_ext_ctrl) { + res = xioctl(dev->fd, VIDIOC_QUERY_EXT_CTRL, qctrl); + if (errno != ENOTTY) + return res; + port->have_query_ext_ctrl = false; + } + spa_zero(qc); + qc.id = qctrl->id; + res = xioctl(dev->fd, VIDIOC_QUERYCTRL, &qc); + if (res == 0) { + qctrl->type = qc.type; + memcpy(qctrl->name, qc.name, sizeof(qctrl->name)); + qctrl->minimum = qc.minimum; + if (qc.type == V4L2_CTRL_TYPE_BITMASK) { + qctrl->maximum = (__u32)qc.maximum; + qctrl->default_value = (__u32)qc.default_value; + } else { + qctrl->maximum = qc.maximum; + qctrl->default_value = qc.default_value; + } + qctrl->step = qc.step; + qctrl->flags = qc.flags; + qctrl->elems = 1; + qctrl->nr_of_dims = 0; + memset(qctrl->dims, 0, sizeof(qctrl->dims)); + switch (qctrl->type) { + case V4L2_CTRL_TYPE_INTEGER64: + qctrl->elem_size = sizeof(__s64); + break; + case V4L2_CTRL_TYPE_STRING: + qctrl->elem_size = qc.maximum + 1; + break; + default: + qctrl->elem_size = sizeof(__s32); + break; + } + memset(qctrl->reserved, 0, sizeof(qctrl->reserved)); + } + qctrl->id = qc.id; + return res; +} + +static struct { + uint32_t v4l2_id; + uint32_t spa_id; +} control_map[] = { + { V4L2_CID_BRIGHTNESS, SPA_PROP_brightness }, + { V4L2_CID_CONTRAST, SPA_PROP_contrast }, + { V4L2_CID_SATURATION, SPA_PROP_saturation }, + { V4L2_CID_HUE, SPA_PROP_hue }, + { V4L2_CID_GAMMA, SPA_PROP_gamma }, + { V4L2_CID_EXPOSURE, SPA_PROP_exposure }, + { V4L2_CID_GAIN, SPA_PROP_gain }, + { V4L2_CID_SHARPNESS, SPA_PROP_sharpness }, +}; + +static uint32_t control_to_prop_id(struct impl *impl, uint32_t control_id) +{ + SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { + if (c->v4l2_id == control_id) + return c->spa_id; + } + return SPA_PROP_START_CUSTOM + control_id; +} + +static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id) +{ + SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { + if (c->spa_id == prop_id) + return c->v4l2_id; + } + if (prop_id >= SPA_PROP_START_CUSTOM) + return prop_id - SPA_PROP_START_CUSTOM; + return SPA_ID_INVALID; +} + +static int +spa_v4l2_enum_controls(struct impl *this, int seq, + uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_query_ext_ctrl queryctrl; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint32_t prop_id, ctrl_id; + uint8_t buffer[1024]; + int res; + const unsigned next_fl = V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + uint32_t count = 0; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + result.id = SPA_PARAM_PropInfo; + result.next = start; + next: + result.index = result.next; + + spa_zero(queryctrl); + + if (result.next == 0) { + result.next |= next_fl; + port->n_controls = 0; + } + + queryctrl.id = result.next; + spa_log_debug(this->log, "test control %08x", queryctrl.id); + + if (query_ext_ctrl_ioctl(port, &queryctrl) != 0) { + if (errno == ENOTTY) + goto enum_end; + if (errno == EINVAL) { + if (queryctrl.id != next_fl) + goto enum_end; + + if (result.next & next_fl) + result.next = V4L2_CID_USER_BASE; + else if (result.next >= V4L2_CID_USER_BASE && result.next < V4L2_CID_LASTP1) + result.next++; + else if (result.next >= V4L2_CID_LASTP1) + result.next = V4L2_CID_PRIVATE_BASE; + else + goto enum_end; + goto next; + } + res = -errno; + spa_log_error(this->log, "'%s' VIDIOC_QUERYCTRL: %m", this->props.device); + spa_v4l2_close(dev); + return res; + } + if (result.next & next_fl) + result.next = queryctrl.id | next_fl; + else + result.next++; + + if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) + goto next; + + if (port->n_controls >= MAX_CONTROLS) + goto enum_end; + + ctrl_id = queryctrl.id & ~next_fl; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + prop_id = control_to_prop_id(this, ctrl_id); + + port->controls[port->n_controls].id = prop_id; + port->controls[port->n_controls].ctrl_id = ctrl_id; + port->controls[port->n_controls].value = queryctrl.default_value; + + spa_log_debug(this->log, "Control '%s' %d %d", queryctrl.name, prop_id, ctrl_id); + + port->n_controls++; + + switch (queryctrl.type) { + case V4L2_CTRL_TYPE_INTEGER: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_STEP_Int( + (int32_t)queryctrl.default_value, + (int32_t)queryctrl.minimum, + (int32_t)queryctrl.maximum, + (int32_t)queryctrl.step), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name)); + break; + case V4L2_CTRL_TYPE_BOOLEAN: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool((bool)queryctrl.default_value), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name)); + break; + case V4L2_CTRL_TYPE_MENU: + { + struct v4l2_querymenu querymenu; + struct spa_pod_builder_state state; + + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(prop_id), + SPA_PROP_INFO_type, SPA_POD_CHOICE_ENUM_Int(1, (int32_t)queryctrl.default_value), + SPA_PROP_INFO_description, SPA_POD_String(queryctrl.name), + 0); + + spa_zero(querymenu); + querymenu.id = queryctrl.id; + + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + + spa_pod_builder_get_state(&b, &state); + spa_pod_builder_push_struct(&b, &f[1]); + for (querymenu.index = queryctrl.minimum; + querymenu.index <= queryctrl.maximum; + querymenu.index++) { + if (xioctl(dev->fd, VIDIOC_QUERYMENU, &querymenu) == 0) { + spa_pod_builder_int(&b, querymenu.index); + spa_pod_builder_string(&b, (const char *)querymenu.name); + } + } + if (spa_pod_builder_pop(&b, &f[1]) == NULL) { + spa_log_warn(this->log, "can't create Control '%s' overflow %d", + queryctrl.name, b.state.offset); + spa_pod_builder_reset(&b, &state); + spa_pod_builder_none(&b); + } + param = spa_pod_builder_pop(&b, &f[0]); + break; + } + case V4L2_CTRL_TYPE_INTEGER_MENU: + case V4L2_CTRL_TYPE_BITMASK: + case V4L2_CTRL_TYPE_BUTTON: + case V4L2_CTRL_TYPE_INTEGER64: + case V4L2_CTRL_TYPE_STRING: + default: + goto next; + + } + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + spa_v4l2_close(dev); + return res; +} + +static int +spa_v4l2_set_control(struct impl *this, uint32_t id, + const struct spa_pod_prop *prop) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_control control; + int res; + + spa_zero(control); + control.id = prop_id_to_control(this, prop->key); + if (control.id == SPA_ID_INVALID) + return -ENOENT; + + if ((res = spa_v4l2_open(dev, this->props.device)) < 0) + return res; + + switch (SPA_POD_TYPE(&prop->value)) { + case SPA_TYPE_Bool: + { + bool val; + if ((res = spa_pod_get_bool(&prop->value, &val)) < 0) + goto done; + control.value = val; + break; + } + case SPA_TYPE_Int: + { + int32_t val; + if ((res = spa_pod_get_int(&prop->value, &val)) < 0) + goto done; + control.value = val; + break; + } + default: + res = -EINVAL; + goto done; + } + if (xioctl(dev->fd, VIDIOC_S_CTRL, &control) < 0) { + res = -errno; + goto done; + } + + res = 0; + +done: + spa_v4l2_close(dev); + return res; +} + +static int mmap_read(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_buffer buf; + struct buffer *b; + struct spa_data *d; + int64_t pts; + + spa_zero(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = port->memtype; + + if (xioctl(dev->fd, VIDIOC_DQBUF, &buf) < 0) + return -errno; + + pts = SPA_TIMEVAL_TO_NSEC(&buf.timestamp); + spa_log_trace(this->log, "v4l2 %p: have output %d", this, buf.index); + + if (this->clock) { + this->clock->nsec = pts; + this->clock->rate = port->rate; + this->clock->position = buf.sequence; + this->clock->duration = 1; + this->clock->delay = 0; + this->clock->rate_diff = 1.0; + this->clock->next_nsec = pts + 1000000000LL / port->rate.denom; + } + + b = &port->buffers[buf.index]; + if (b->h) { + b->h->flags = 0; + if (buf.flags & V4L2_BUF_FLAG_ERROR) + b->h->flags |= SPA_META_HEADER_FLAG_CORRUPTED; + b->h->offset = 0; + b->h->seq = buf.sequence; + b->h->pts = pts; + b->h->dts_offset = 0; + } + + d = b->outbuf->datas; + d[0].chunk->offset = 0; + d[0].chunk->size = buf.bytesused; + d[0].chunk->stride = port->fmt.fmt.pix.bytesperline; + d[0].chunk->flags = 0; + if (buf.flags & V4L2_BUF_FLAG_ERROR) + d[0].chunk->flags |= SPA_CHUNK_FLAG_CORRUPTED; + + spa_list_append(&port->queue, &b->link); + return 0; +} + +static void v4l2_on_fd_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct spa_io_buffers *io; + struct port *port = &this->out_ports[0]; + struct buffer *b; + + if (source->rmask & SPA_IO_ERR) { + struct port *port = &this->out_ports[0]; + spa_log_error(this->log, "'%p' error %08x", this->props.device, source->rmask); + if (port->source.loop) + spa_loop_remove_source(this->data_loop, &port->source); + return; + } + + if (!(source->rmask & SPA_IO_IN)) { + spa_log_warn(this->log, "v4l2 %p: spurious wakeup %d", this, source->rmask); + return; + } + + if (mmap_read(this) < 0) + return; + + if (spa_list_is_empty(&port->queue)) + return; + + io = port->io; + if (io == NULL) { + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_v4l2_buffer_recycle(this, b->id); + } + else if (io->status != SPA_STATUS_HAVE_DATA) { + if (io->buffer_id < port->n_buffers) + spa_v4l2_buffer_recycle(this, io->buffer_id); + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace(this->log, "v4l2 %p: now queued %d", this, b->id); + } + spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); +} + +static int spa_v4l2_use_buffers(struct impl *this, struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_requestbuffers reqbuf; + unsigned int i; + struct spa_data *d; + + if (n_buffers > 0) { + d = buffers[0]->datas; + + if (d[0].type == SPA_DATA_MemFd || + (d[0].type == SPA_DATA_MemPtr && d[0].data != NULL)) { + port->memtype = V4L2_MEMORY_USERPTR; + } else if (d[0].type == SPA_DATA_DmaBuf) { + port->memtype = V4L2_MEMORY_DMABUF; + } else { + spa_log_error(this->log, "can't use buffers of type %d", d[0].type); + return -EINVAL; + } + } + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = n_buffers; + + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS %m", this->props.device); + return -errno; + } + spa_log_debug(this->log, "got %d buffers", reqbuf.count); + if (reqbuf.count < n_buffers) { + spa_log_error(this->log, "'%s' can't allocate enough buffers %d < %d", + this->props.device, reqbuf.count, n_buffers); + return -ENOMEM; + } + + for (i = 0; i < reqbuf.count; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_OUTSTANDING; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_log_debug(this->log, "import buffer %p", buffers[i]); + + if (buffers[i]->n_datas < 1) { + spa_log_error(this->log, "invalid memory on buffer %p", buffers[i]); + return -EINVAL; + } + d = buffers[i]->datas; + + spa_zero(b->v4l2_buffer); + b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + b->v4l2_buffer.memory = port->memtype; + b->v4l2_buffer.index = i; + + if (port->memtype == V4L2_MEMORY_USERPTR) { + if (d[0].data == NULL) { + void *data; + + data = mmap(NULL, + d[0].maxsize, + PROT_READ | PROT_WRITE, MAP_SHARED, + d[0].fd, + d[0].mapoffset); + if (data == MAP_FAILED) + return -errno; + + b->ptr = data; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + } + else + b->ptr = d[0].data; + + b->v4l2_buffer.m.userptr = (unsigned long) b->ptr; + b->v4l2_buffer.length = d[0].maxsize; + } + else if (port->memtype == V4L2_MEMORY_DMABUF) { + b->v4l2_buffer.m.fd = d[0].fd; + } + else { + spa_log_error(this->log, "invalid port memory %d", + port->memtype); + return -EIO; + } + + spa_v4l2_buffer_recycle(this, i); + } + port->n_buffers = reqbuf.count; + + return 0; +} + +static int +mmap_init(struct impl *this, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + struct v4l2_requestbuffers reqbuf; + unsigned int i; + bool use_expbuf = false; + + port->memtype = V4L2_MEMORY_MMAP; + + spa_zero(reqbuf); + reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + reqbuf.memory = port->memtype; + reqbuf.count = n_buffers; + + if (xioctl(dev->fd, VIDIOC_REQBUFS, &reqbuf) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_REQBUFS: %m", this->props.device); + return -errno; + } + + spa_log_debug(this->log, "got %d buffers", reqbuf.count); + + if (reqbuf.count < n_buffers) { + spa_log_error(this->log, "'%s' can't allocate enough buffers (%d < %d)", + this->props.device, reqbuf.count, n_buffers); + return -ENOMEM; + } + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d; + + if (buffers[i]->n_datas < 1) { + spa_log_error(this->log, "invalid buffer data"); + return -EINVAL; + } + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_OUTSTANDING; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_zero(b->v4l2_buffer); + b->v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + b->v4l2_buffer.memory = port->memtype; + b->v4l2_buffer.index = i; + + if (xioctl(dev->fd, VIDIOC_QUERYBUF, &b->v4l2_buffer) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_QUERYBUF: %m", this->props.device); + return -errno; + } + + if (b->v4l2_buffer.flags & V4L2_BUF_FLAG_QUEUED) { + /* some drivers can give us an already queued buffer. */ + spa_log_warn(this->log, "buffer %d was already queued", i); + n_buffers = i; + break; + } + + d = buffers[i]->datas; + d[0].mapoffset = 0; + d[0].maxsize = b->v4l2_buffer.length; + d[0].chunk->offset = 0; + d[0].chunk->size = 0; + d[0].chunk->stride = port->fmt.fmt.pix.bytesperline; + d[0].chunk->flags = 0; + + spa_log_debug(this->log, "data types %08x", d[0].type); + + if (port->have_expbuf && + d[0].type != SPA_ID_INVALID && + (d[0].type & ((1u << SPA_DATA_DmaBuf)|(1u<fd, VIDIOC_EXPBUF, &expbuf) < 0) { + if (errno == ENOTTY || errno == EINVAL) { + spa_log_debug(this->log, "'%s' VIDIOC_EXPBUF not supported: %m", + this->props.device); + port->have_expbuf = false; + goto fallback; + } + spa_log_error(this->log, "'%s' VIDIOC_EXPBUF: %m", this->props.device); + return -errno; + } + if (d[0].type & (1u<flags, BUFFER_FLAG_ALLOCATED); + spa_log_debug(this->log, "EXPBUF fd:%d", expbuf.fd); + use_expbuf = true; + } else if (d[0].type & (1u << SPA_DATA_MemPtr)) { +fallback: + d[0].type = SPA_DATA_MemPtr; + d[0].flags = SPA_DATA_FLAG_READABLE; + d[0].fd = -1; + d[0].mapoffset = b->v4l2_buffer.m.offset; + d[0].data = mmap(NULL, + b->v4l2_buffer.length, + PROT_READ, MAP_SHARED, + dev->fd, + b->v4l2_buffer.m.offset); + if (d[0].data == MAP_FAILED) { + spa_log_error(this->log, "'%s' mmap: %m", this->props.device); + return -errno; + } + b->ptr = d[0].data; + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(this->log, "mmap offset:%u data:%p", d[0].mapoffset, b->ptr); + use_expbuf = false; + } else { + spa_log_error(this->log, "unsupported data type:%08x", d[0].type); + return -ENOTSUP; + } + spa_v4l2_buffer_recycle(this, i); + } + spa_log_info(this->log, "%s: have %u buffers using %s", dev->path, n_buffers, + use_expbuf ? "EXPBUF" : "MMAP"); + + port->n_buffers = n_buffers; + + return 0; +} + +static int userptr_init(struct impl *this) +{ + return -ENOTSUP; +} + +static int read_init(struct impl *this) +{ + return -ENOTSUP; +} + +static int +spa_v4l2_alloc_buffers(struct impl *this, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + int res; + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + + if (port->n_buffers > 0) + return -EIO; + + if (dev->cap.capabilities & V4L2_CAP_STREAMING) { + if ((res = mmap_init(this, buffers, n_buffers)) < 0) + if ((res = userptr_init(this)) < 0) + return res; + } else if (dev->cap.capabilities & V4L2_CAP_READWRITE) { + if ((res = read_init(this)) < 0) + return res; + } else { + spa_log_error(this->log, "invalid capabilities %08x", + dev->cap.capabilities); + return -EIO; + } + + return 0; +} + +static int spa_v4l2_stream_on(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + enum v4l2_buf_type type; + + if (dev->fd == -1) + return -EIO; + + if (!dev->have_format) + return -EIO; + + if (dev->active) + return 0; + + spa_log_debug(this->log, "starting"); + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(dev->fd, VIDIOC_STREAMON, &type) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_STREAMON: %m", this->props.device); + return -errno; + } + + port->source.func = v4l2_on_fd_events; + port->source.data = this; + port->source.fd = dev->fd; + port->source.mask = SPA_IO_IN | SPA_IO_ERR; + port->source.rmask = 0; + spa_loop_add_source(this->data_loop, &port->source); + + dev->active = true; + + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct port *port = user_data; + if (port->source.loop) + spa_loop_remove_source(loop, &port->source); + return 0; +} + +static int spa_v4l2_stream_off(struct impl *this) +{ + struct port *port = &this->out_ports[0]; + struct spa_v4l2_device *dev = &port->dev; + enum v4l2_buf_type type; + uint32_t i; + + if (!dev->active) + return 0; + + if (dev->fd == -1) + return -EIO; + + spa_log_debug(this->log, "stopping"); + + spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, port); + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(dev->fd, VIDIOC_STREAMOFF, &type) < 0) { + spa_log_error(this->log, "'%s' VIDIOC_STREAMOFF: %m", this->props.device); + return -errno; + } + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) { + if (xioctl(dev->fd, VIDIOC_QBUF, &b->v4l2_buffer) < 0) + spa_log_warn(this->log, "VIDIOC_QBUF: %s", strerror(errno)); + } + } + spa_list_init(&port->queue); + dev->active = false; + + return 0; +} diff --git a/spa/plugins/v4l2/v4l2.c b/spa/plugins/v4l2/v4l2.c new file mode 100644 index 0000000..77fb4ce --- /dev/null +++ b/spa/plugins/v4l2/v4l2.c @@ -0,0 +1,59 @@ +/* Spa V4l2 support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include + +extern const struct spa_handle_factory spa_v4l2_source_factory; +extern const struct spa_handle_factory spa_v4l2_udev_factory; +extern const struct spa_handle_factory spa_v4l2_device_factory; + +struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.v4l2"); +struct spa_log_topic *v4l2_log_topic = &log_topic; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_v4l2_source_factory; + break; + case 1: + *factory = &spa_v4l2_udev_factory; + break; + case 2: + *factory = &spa_v4l2_device_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/v4l2/v4l2.h b/spa/plugins/v4l2/v4l2.h new file mode 100644 index 0000000..e2293c7 --- /dev/null +++ b/spa/plugins/v4l2/v4l2.h @@ -0,0 +1,49 @@ +/* Spa V4l2 support + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#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..24673a5 --- /dev/null +++ b/spa/plugins/videoconvert/meson.build @@ -0,0 +1,15 @@ +videoconvert_sources = [ + 'videoadapter.c', + 'plugin.c' +] + +simd_cargs = [] +simd_dependencies = [] + +videoconvertlib = shared_library('spa-videoconvert', + videoconvert_sources, + c_args : simd_cargs, + dependencies : [ spa_dep, mathlib ], + link_with : simd_dependencies, + install : true, + install_dir : spa_plugindir / 'videoconvert') diff --git a/spa/plugins/videoconvert/plugin.c b/spa/plugins/videoconvert/plugin.c new file mode 100644 index 0000000..0f24495 --- /dev/null +++ b/spa/plugins/videoconvert/plugin.c @@ -0,0 +1,46 @@ +/* Spa videoconvert plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_videoadapter_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_videoadapter_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c new file mode 100644 index 0000000..2f6f068 --- /dev/null +++ b/spa/plugins/videoconvert/videoadapter.c @@ -0,0 +1,1671 @@ +/* SPA + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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 +static struct spa_log_topic *log_topic = &SPA_LOG_TOPIC(0, "spa.videoadapter"); + +#define DEFAULT_ALIGN 16 + +#define MAX_PORTS 1 + +/** \cond */ + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_cpu *cpu; + + uint32_t max_align; + enum spa_direction direction; + + struct spa_node *target; + + struct spa_node *follower; + struct spa_hook follower_listener; + uint32_t follower_flags; + struct spa_video_info follower_current_format; + struct spa_video_info default_format; + + struct spa_handle *hnd_convert; + struct spa_node *convert; + struct spa_hook convert_listener; + uint32_t convert_flags; + + uint32_t n_buffers; + struct spa_buffer **buffers; + + struct spa_io_buffers io_buffers; + struct spa_io_rate_match io_rate_match; + struct spa_io_position *io_position; + + uint64_t info_all; + struct spa_node_info info; +#define IDX_EnumFormat 0 +#define IDX_PropInfo 1 +#define IDX_Props 2 +#define IDX_Format 3 +#define IDX_EnumPortConfig 4 +#define IDX_PortConfig 5 +#define IDX_Latency 6 +#define IDX_ProcessLatency 7 +#define N_NODE_PARAMS 8 + struct spa_param_info params[N_NODE_PARAMS]; + uint32_t convert_params_flags[N_NODE_PARAMS]; + uint32_t follower_params_flags[N_NODE_PARAMS]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + unsigned int add_listener:1; + unsigned int have_format:1; + unsigned int started:1; + unsigned int driver:1; + unsigned int async:1; + unsigned int passthrough:1; + unsigned int follower_removing:1; +}; + +/** \endcond */ + +static int follower_enum_params(struct impl *this, + uint32_t id, + uint32_t idx, + struct spa_result_node_params *result, + const struct spa_pod *filter, + struct spa_pod_builder *builder) +{ + int res; + if (result->next < 0x100000) { + if (this->convert != NULL && + (res = spa_node_enum_params_sync(this->convert, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + result->next = 0x100000; + } + if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + result->next &= 0xfffff; + if ((res = spa_node_enum_params_sync(this->follower, + id, &result->next, filter, &result->param, builder)) == 1) { + result->next |= 0x100000; + return res; + } + result->next = 0x200000; + } + return 0; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + uint8_t buffer[4096]; + struct spa_pod_dynamic_builder b; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next; + + spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + + switch (id) { + case SPA_PARAM_EnumPortConfig: + case SPA_PARAM_PortConfig: + if (this->convert == NULL) + return 0; + res = spa_node_enum_params(this->convert, seq, id, start, num, filter); + return res; + case SPA_PARAM_PropInfo: + res = follower_enum_params(this, + id, IDX_PropInfo, &result, filter, &b.b); + break; + case SPA_PARAM_Props: + res = follower_enum_params(this, + id, IDX_Props, &result, filter, &b.b); + break; + case SPA_PARAM_ProcessLatency: + res = follower_enum_params(this, + id, IDX_ProcessLatency, &result, filter, &b.b); + break; + case SPA_PARAM_EnumFormat: + case SPA_PARAM_Format: + case SPA_PARAM_Latency: + res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + id, &result.next, filter, &result.param, &b.b); + break; + default: + return -ENOENT; + } + + if (res == 1) { + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + count++; + } + spa_pod_dynamic_builder_clean(&b); + + if (res != 1) + return res; + + if (count != num) + goto next; + + return 0; +} + +static int link_io(struct impl *this) +{ + int res; + + if (this->convert == NULL) + return 0; + + spa_log_debug(this->log, "%p: controls", this); + + spa_zero(this->io_rate_match); + this->io_rate_match.rate = 1.0; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_RateMatch, + &this->io_rate_match, sizeof(this->io_rate_match))) < 0) { + spa_log_debug(this->log, "%p: set RateMatch on follower disabled %d %s", this, + res, spa_strerror(res)); + } + else if ((res = spa_node_port_set_io(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_RateMatch, + &this->io_rate_match, sizeof(this->io_rate_match))) < 0) { + spa_log_warn(this->log, "%p: set RateMatch on convert failed %d %s", this, + res, spa_strerror(res)); + } + + this->io_buffers = SPA_IO_BUFFERS_INIT; + + if ((res = spa_node_port_set_io(this->follower, + this->direction, 0, + SPA_IO_Buffers, + &this->io_buffers, sizeof(this->io_buffers))) < 0) { + spa_log_warn(this->log, "%p: set Buffers on follower failed %d %s", this, + res, spa_strerror(res)); + return res; + } + else if ((res = spa_node_port_set_io(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_IO_Buffers, + &this->io_buffers, sizeof(this->io_buffers))) < 0) { + spa_log_warn(this->log, "%p: set Buffers on convert failed %d %s", this, + res, spa_strerror(res)); + return res; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint32_t i; + uint64_t old = full ? this->info.change_mask : 0; + + spa_log_debug(this->log, "%p: info full:%d change:%08"PRIx64, + this, full, this->info.change_mask); + + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < this->info.n_params; i++) { + if (this->params[i].user > 0) { + this->params[i].flags ^= SPA_PARAM_INFO_SERIAL; + this->params[i].user = 0; + spa_log_debug(this->log, "param %d flags:%08x", + i, this->params[i].flags); + } + } + } + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static int debug_params(struct impl *this, struct spa_node *node, + enum spa_direction direction, uint32_t port_id, uint32_t id, struct spa_pod *filter, + const char *debug, int err) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + uint32_t state; + struct spa_pod *param; + int res, count = 0; + + spa_log_error(this->log, "params %s: %d:%d (%s) %s", + spa_debug_type_find_name(spa_type_param, id), + direction, port_id, debug, err ? spa_strerror(err) : "no matching params"); + if (err == -EBUSY) + return 0; + + if (filter) { + spa_log_error(this->log, "with this filter:"); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 2, NULL, filter); + } else { + spa_log_error(this->log, "there was no filter"); + } + + state = 0; + while (true) { + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + res = spa_node_port_enum_params_sync(node, + direction, port_id, + id, &state, + NULL, ¶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_DEBUG, 2, NULL, param); + count++; + } + if (count == 0) + spa_log_error(this->log, "could not get any %s", debug); + + return 0; +} + +static int negotiate_buffers(struct impl *this) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + uint32_t state; + struct spa_pod *param; + int res; + bool follower_alloc, conv_alloc; + uint32_t i, size, buffers, blocks, align, flags, stride = 0; + uint32_t *aligns; + struct spa_data *datas; + uint32_t follower_flags, conv_flags; + + spa_log_debug(this->log, "%p: %d", this, this->n_buffers); + + if (this->target == this->follower) + return 0; + + if (this->n_buffers > 0) + return 0; + + state = 0; + param = NULL; + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_Buffers, &state, + param, ¶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->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, &state, + param, ¶m, &b)) != 1) { + debug_params(this, this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "convert buffers", res); + return -ENOTSUP; + } + if (param == NULL) + return -ENOTSUP; + + spa_pod_fixate(param); + + follower_flags = this->follower_flags; + conv_flags = this->convert_flags; + + follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); + + flags = 0; + if (conv_alloc || follower_alloc) { + flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA; + if (conv_alloc) + follower_alloc = false; + } + + align = DEFAULT_ALIGN; + + if ((res = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamBuffers, NULL, + SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride), + SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align))) < 0) + return res; + + spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); + + align = SPA_MAX(align, this->max_align); + + datas = alloca(sizeof(struct spa_data) * blocks); + memset(datas, 0, sizeof(struct spa_data) * blocks); + aligns = alloca(sizeof(uint32_t) * blocks); + for (i = 0; i < blocks; i++) { + datas[i].type = SPA_DATA_MemPtr; + datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC; + datas[i].maxsize = size; + aligns[i] = align; + } + + free(this->buffers); + this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns); + if (this->buffers == NULL) + return -errno; + this->n_buffers = buffers; + + if ((res = spa_node_port_use_buffers(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + this->buffers, this->n_buffers)) < 0) + return res; + + if ((res = spa_node_port_use_buffers(this->follower, + this->direction, 0, + follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + this->buffers, this->n_buffers)) < 0) + return res; + + return 0; +} + +static int configure_format(struct impl *this, uint32_t flags, const struct spa_pod *format) +{ + int res; + + spa_log_debug(this->log, "%p: configure format:", this); + if (format) + spa_debug_log_format(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + + if ((res = spa_node_port_set_param(this->follower, + this->direction, 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + if (res > 0) { + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + uint32_t state = 0; + struct spa_pod *fmt; + + /* format was changed to nearest compatible format */ + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_Format, &state, + NULL, &fmt, &b)) != 1) + return -EIO; + + format = fmt; + } + + if (this->target != this->follower && this->convert) { + if ((res = spa_node_port_set_param(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Format, flags, + format)) < 0) + return res; + } + + this->have_format = format != NULL; + if (format == NULL) { + this->n_buffers = 0; + } else { + res = negotiate_buffers(this); + } + + return res; +} + +static int configure_convert(struct impl *this, uint32_t mode) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + + if (this->convert == NULL) + return 0; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_log_debug(this->log, "%p: configure convert %p", this, this->target); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(this->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(mode)); + + return spa_node_set_param(this->convert, SPA_PARAM_PortConfig, 0, param); +} + +extern const struct spa_handle_factory spa_videoconvert_factory; + +static const struct spa_node_events follower_node_events; + +static int reconfigure_mode(struct impl *this, bool passthrough, + enum spa_direction direction, struct spa_pod *format) +{ + int res = 0; + struct spa_hook l; + + spa_log_info(this->log, "%p: passthrough mode %d", this, passthrough); + + if (this->passthrough != passthrough) { + if (passthrough) { + /* remove converter split/merge ports */ + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); + } else { + /* remove follower ports */ + this->follower_removing = true; + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + this->follower_removing = false; + } + } + + /* set new target */ + this->target = passthrough ? this->follower : this->convert; + + if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0) + return res; + + if (this->passthrough != passthrough) { + this->passthrough = passthrough; + if (passthrough) { + /* add follower ports */ + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + } else { + /* add converter ports */ + configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp); + link_io(this); + } + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_Props].user++; + + emit_node_info(this, false); + + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + int res = 0, res2 = 0; + struct impl *this = object; + struct spa_video_info info = { 0 }; + + spa_log_debug(this->log, "%p: set param %d", this, id); + + switch (id) { + case SPA_PARAM_Format: + if (this->started) + return -EIO; + if (param == NULL) + return -EINVAL; + + if ((res = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0) + return res; + if (info.media_type != SPA_MEDIA_TYPE_video || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_video_raw_parse(param, &info.info.raw) < 0) + return -EINVAL; + + this->follower_current_format = info; + break; + + case SPA_PARAM_PortConfig: + { + enum spa_direction dir; + enum spa_param_port_config_mode mode; + struct spa_pod *format = NULL; + + if (this->started) { + spa_log_error(this->log, "was started"); + return -EIO; + } + + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&dir), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; + + if (format) { + struct spa_video_info info; + + spa_zero(info); + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + if (info.media_type != SPA_MEDIA_TYPE_video || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -ENOTSUP; + + if (spa_format_video_raw_parse(format, &info.info.raw) >= 0) + this->default_format = info; + } + + switch (mode) { + case SPA_PARAM_PORT_CONFIG_MODE_none: + return -ENOTSUP; + case SPA_PARAM_PORT_CONFIG_MODE_passthrough: + if ((res = reconfigure_mode(this, true, dir, format)) < 0) + return res; + break; + case SPA_PARAM_PORT_CONFIG_MODE_convert: + case SPA_PARAM_PORT_CONFIG_MODE_dsp: + if (this->convert == NULL) + return -ENOTSUP; + if ((res = reconfigure_mode(this, false, dir, NULL)) < 0) + return res; + break; + default: + return -EINVAL; + } + + if (this->target != this->follower) { + if ((res = spa_node_set_param(this->target, id, flags, param)) < 0) + return res; + } + break; + } + + case SPA_PARAM_Props: + if (this->target != this->follower) + res = spa_node_set_param(this->target, id, flags, param); + res2 = spa_node_set_param(this->follower, id, flags, param); + if (res < 0 && res2 < 0) + return res; + res = 0; + break; + case SPA_PARAM_ProcessLatency: + res = spa_node_set_param(this->follower, id, flags, param); + break; + default: + res = -ENOTSUP; + break; + } + return res; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + int res = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Position: + this->io_position = data; + break; + default: + break; + } + + if (this->target) + res = spa_node_set_io(this->target, id, data, size); + + if (this->target != this->follower) + res = spa_node_set_io(this->follower, id, data, size); + + return res; +} + +static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *b, uint32_t id, + struct spa_pod_object *o1, struct spa_pod_object *o2) +{ + const struct spa_pod_prop *p1, *p2; + struct spa_pod_frame f; + struct spa_pod_builder_state state; + int res = 0; + + if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2)) + return (struct spa_pod*)o1; + + spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id); + p2 = NULL; + SPA_POD_OBJECT_FOREACH(o1, p1) { + p2 = spa_pod_object_find_prop(o2, p2, p1->key); + if (p2 != NULL) { + spa_pod_builder_get_state(b, &state); + res = spa_pod_filter_prop(b, p1, p2); + if (res < 0) + spa_pod_builder_reset(b, &state); + } + if (p2 == NULL || res < 0) + spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1)); + } + p1 = NULL; + SPA_POD_OBJECT_FOREACH(o2, p2) { + p1 = spa_pod_object_find_prop(o1, p1, p2->key); + if (p1 != NULL) + continue; + spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2)); + } + return spa_pod_builder_pop(b, &f); +} + +static int negotiate_format(struct impl *this) +{ + uint32_t state; + struct spa_pod *format, *def; + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + int res; + + if (this->have_format) + return 0; + + if (this->target == this->follower) + return 0; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + spa_log_debug(this->log, "%p: negiotiate", this); + + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); + + state = 0; + format = NULL; + if ((res = spa_node_port_enum_params_sync(this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &state, + format, &format, &b)) < 0) { + if (res == -ENOENT) + format = NULL; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_EnumFormat, format, "follower format", res); + goto done; + } + } + if (this->convert) { + state = 0; + if ((res = spa_node_port_enum_params_sync(this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &state, + format, &format, &b)) != 1) { + debug_params(this, this->convert, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, format, "convert format", res); + res = -ENOTSUP; + goto done; + } + } + if (format == NULL) { + res = -ENOTSUP; + goto done; + } + + def = spa_format_video_raw_build(&b, + SPA_PARAM_Format, &this->default_format.info.raw); + + format = merge_objects(this, &b, SPA_PARAM_Format, + (struct spa_pod_object*)format, + (struct spa_pod_object*)def); + + spa_pod_fixate(format); + + res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format); + +done: + spa_node_send_command(this->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamEnd)); + + return res; +} + + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "%p: command %d", this, SPA_NODE_COMMAND_ID(command)); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if ((res = negotiate_format(this)) < 0) + return res; + if ((res = negotiate_buffers(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + configure_format(this, 0, NULL); + SPA_FALLTHROUGH + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + case SPA_NODE_COMMAND_Flush: + this->io_buffers.status = SPA_STATUS_OK; + break; + default: + break; + } + + if ((res = spa_node_send_command(this->target, command)) < 0) { + spa_log_error(this->log, "%p: can't send command %d: %s", + this, SPA_NODE_COMMAND_ID(command), + spa_strerror(res)); + return res; + } + + if (this->target != this->follower) { + if ((res = spa_node_send_command(this->follower, command)) < 0) { + spa_log_error(this->log, "%p: can't send command %d: %s", + this, SPA_NODE_COMMAND_ID(command), + spa_strerror(res)); + return res; + } + } + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + } + return res; +} + +static void convert_node_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + uint32_t i; + + spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, + info->change_mask); + + if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_EnumPortConfig: + idx = IDX_EnumPortConfig; + break; + case SPA_PARAM_PortConfig: + idx = IDX_PortConfig; + break; + case SPA_PARAM_PropInfo: + idx = IDX_PropInfo; + break; + case SPA_PARAM_Props: + idx = IDX_Props; + break; + default: + continue; + } + if (!this->add_listener && + this->convert_params_flags[idx] == info->params[i].flags) + continue; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->convert_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (!this->add_listener) { + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + } + emit_node_info(this, false); +} + +static void convert_port_info(void *data, + enum spa_direction direction, uint32_t port_id, + const struct spa_port_info *info) +{ + struct impl *this = data; + + if (direction != this->direction) { + if (port_id == 0) + return; + else + port_id--; + } + + spa_log_debug(this->log, "%p: port info %d:%d", this, + direction, port_id); + + if (this->target != this->follower) + spa_node_emit_port_info(&this->hooks, direction, port_id, info); +} + +static void convert_result(void *data, int seq, int res, uint32_t type, const void *result) +{ + struct impl *this = data; + + if (this->target == this->follower) + return; + + spa_log_trace(this->log, "%p: result %d %d", this, seq, res); + spa_node_emit_result(&this->hooks, seq, res, type, result); +} + +static const struct spa_node_events convert_node_events = { + SPA_VERSION_NODE_EVENTS, + .info = convert_node_info, + .port_info = convert_port_info, + .result = convert_result, +}; + +static void follower_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + uint32_t i; + + spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, + info->change_mask); + + if (this->follower_removing) + return; + + this->async = (info->flags & SPA_NODE_FLAG_ASYNC) != 0; + + if (info->max_input_ports > 0) + this->direction = SPA_DIRECTION_INPUT; + else + this->direction = SPA_DIRECTION_OUTPUT; + + if (this->direction == SPA_DIRECTION_INPUT) { + this->info.flags |= SPA_NODE_FLAG_IN_PORT_CONFIG; + this->info.max_input_ports = MAX_PORTS; + } else { + this->info.flags |= SPA_NODE_FLAG_OUT_PORT_CONFIG; + this->info.max_output_ports = MAX_PORTS; + } + + spa_log_debug(this->log, "%p: follower info %s", this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output"); + + if (info->change_mask & SPA_NODE_CHANGE_MASK_PROPS) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + this->info.props = info->props; + } + if (info->change_mask & SPA_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_PropInfo: + idx = IDX_PropInfo; + break; + case SPA_PARAM_Props: + idx = IDX_Props; + break; + case SPA_PARAM_ProcessLatency: + idx = IDX_ProcessLatency; + break; + default: + continue; + } + if (!this->add_listener && + this->follower_params_flags[idx] == info->params[i].flags) + continue; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->follower_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (!this->add_listener) { + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + } + emit_node_info(this, false); + + spa_zero(this->info.props); + this->info.change_mask &= ~SPA_NODE_CHANGE_MASK_PROPS; + +} + +static int recalc_latency(struct impl *this, enum spa_direction direction, uint32_t port_id) +{ + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + uint32_t index = 0; + struct spa_latency_info latency; + int res; + + spa_log_debug(this->log, "%p: ", this); + + if (this->target == this->follower) + return 0; + + while (true) { + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if ((res = spa_node_port_enum_params_sync(this->follower, + direction, port_id, SPA_PARAM_Latency, + &index, NULL, ¶m, &b)) != 1) + return res; + if ((res = spa_latency_parse(param, &latency)) < 0) + return res; + if (latency.direction == direction) + break; + } + if ((res = spa_node_port_set_param(this->target, + SPA_DIRECTION_REVERSE(direction), 0, + SPA_PARAM_Latency, 0, param)) < 0) + return res; + + return 0; +} + +static void follower_port_info(void *data, + enum spa_direction direction, uint32_t port_id, + const struct spa_port_info *info) +{ + struct impl *this = data; + uint32_t i; + int res; + + if (this->follower_removing) { + spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); + return; + } + + spa_log_debug(this->log, "%p: follower port info %s %p %08"PRIx64, this, + this->direction == SPA_DIRECTION_INPUT ? + "Input" : "Output", info, info->change_mask); + + if (info->change_mask & SPA_PORT_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t idx; + + switch (info->params[i].id) { + case SPA_PARAM_EnumFormat: + idx = IDX_EnumFormat; + break; + case SPA_PARAM_Format: + idx = IDX_Format; + break; + case SPA_PARAM_Latency: + idx = IDX_Latency; + break; + default: + continue; + } + if (!this->add_listener && + this->follower_params_flags[idx] == info->params[i].flags) + continue; + + this->follower_params_flags[idx] = info->params[i].flags; + this->params[idx].flags = + (this->params[idx].flags & SPA_PARAM_INFO_SERIAL) | + (info->params[i].flags & SPA_PARAM_INFO_READWRITE); + + if (idx == IDX_Latency) { + res = recalc_latency(this, direction, port_id); + spa_log_debug(this->log, "latency: %d (%s)", res, + spa_strerror(res)); + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + if (!this->add_listener) { + this->params[idx].user++; + spa_log_debug(this->log, "param %d changed", info->params[i].id); + } + } + } + emit_node_info(this, false); + + if (this->target == this->follower) + spa_node_emit_port_info(&this->hooks, direction, port_id, info); +} + +static void follower_result(void *data, int seq, int res, uint32_t type, const void *result) +{ + struct impl *this = data; + + if (this->target != this->follower) + return; + + spa_log_trace(this->log, "%p: result %d %d", this, seq, res); + spa_node_emit_result(&this->hooks, seq, res, type, result); +} + +static void follower_event(void *data, const struct spa_event *event) +{ + struct impl *this = data; + + spa_log_trace(this->log, "%p: event %d", this, SPA_EVENT_TYPE(event)); + + switch (SPA_NODE_EVENT_ID(event)) { + case SPA_NODE_EVENT_Error: + /* Forward errors */ + spa_node_emit_event(&this->hooks, event); + break; + default: + /* Ignore other events */ + break; + } +} + +static const struct spa_node_events follower_node_events = { + SPA_VERSION_NODE_EVENTS, + .info = follower_info, + .port_info = follower_port_info, + .result = follower_result, + .event = follower_event, +}; + +static int follower_ready(void *data, int status) +{ + struct impl *this = data; + + spa_log_trace_fp(this->log, "%p: ready %d", this, status); + + if (this->target != this->follower) { + this->driver = true; + + if (this->direction == SPA_DIRECTION_OUTPUT) { + int retry = 8; + while (retry--) { + status = spa_node_process(this->convert); + if (status & SPA_STATUS_HAVE_DATA) + break; + + if (status & SPA_STATUS_NEED_DATA) { + status = spa_node_process(this->follower); + if (!(status & SPA_STATUS_HAVE_DATA)) + break; + } + } + + } + } + + return spa_node_call_ready(&this->callbacks, status); +} + +static int follower_reuse_buffer(void *data, uint32_t port_id, uint32_t buffer_id) +{ + int res; + struct impl *this = data; + + if (this->target != this->follower && this->convert) + res = spa_node_port_reuse_buffer(this->convert, port_id, buffer_id); + else + res = spa_node_call_reuse_buffer(&this->callbacks, port_id, buffer_id); + + return res; +} + +static int follower_xrun(void *data, uint64_t trigger, uint64_t delay, struct spa_pod *info) +{ + struct impl *this = data; + return spa_node_call_xrun(&this->callbacks, trigger, delay, info); +} + +static const struct spa_node_callbacks follower_node_callbacks = { + SPA_VERSION_NODE_CALLBACKS, + .ready = follower_ready, + .reuse_buffer = follower_reuse_buffer, + .xrun = follower_xrun, +}; + +static int impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook l; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_trace(this->log, "%p: add listener %p", this, listener); + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + if (events->info || events->port_info) { + this->add_listener = true; + + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + + if (this->convert) { + spa_zero(l); + spa_node_add_listener(this->convert, &l, &convert_node_events, this); + spa_hook_remove(&l); + } + this->add_listener = false; + + emit_node_info(this, true); + } + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +impl_node_sync(void *object, int seq) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return spa_node_sync(this->follower, seq); +} + +static int +impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + return -EINVAL; + + return spa_node_add_port(this->target, direction, port_id, props); +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + return -EINVAL; + + return spa_node_remove_port(this->target, direction, port_id); +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + if (direction != this->direction) + port_id++; + + spa_log_debug(this->log, "%p: %d %u", this, seq, id); + + return spa_node_port_enum_params(this->target, seq, direction, port_id, id, + start, num, filter); +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, " %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + if ((res = spa_node_port_set_param(this->target, direction, port_id, id, + flags, param)) < 0) + return res; + + if ((id == SPA_PARAM_Latency) && + direction == this->direction) { + if ((res = spa_node_port_set_param(this->follower, direction, 0, id, + flags, param)) < 0) + return res; + } + + return res; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_log_debug(this->log, "set io %d %d %d %d", port_id, id, direction, this->direction); + + if (direction != this->direction) + port_id++; + + return spa_node_port_set_io(this->target, direction, port_id, id, data, size); +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if (direction != this->direction) + port_id++; + + spa_log_debug(this->log, "%p: %d %d:%d", this, + n_buffers, direction, port_id); + + if ((res = spa_node_port_use_buffers(this->target, + direction, port_id, flags, buffers, n_buffers)) < 0) + return res; + + return res; +} + +static int +impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + return spa_node_port_reuse_buffer(this->target, port_id, buffer_id); +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + int status = 0, fstatus, retry = 8; + + spa_log_trace_fp(this->log, "%p: process convert:%p driver:%d", + this, this->convert, this->driver); + + if (this->target == this->follower) { + if (this->io_position) + this->io_rate_match.size = this->io_position->clock.duration; + return spa_node_process(this->follower); + } + + if (this->direction == SPA_DIRECTION_INPUT) { + /* an input node (sink). + * First we run the converter to process the input for the follower + * then if it produced data, we run the follower. */ + while (retry--) { + status = this->convert ? spa_node_process(this->convert) : 0; + /* schedule the follower when the converter needed + * a recycled buffer */ + if (status == -EPIPE || status == 0) + status = SPA_STATUS_HAVE_DATA; + else if (status < 0) + break; + + if (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) { + /* as long as the converter produced something or + * is drained, process the follower. */ + fstatus = spa_node_process(this->follower); + if (fstatus < 0) { + status = fstatus; + break; + } + /* if the follower doesn't need more data or is + * drained we can stop */ + if ((fstatus & SPA_STATUS_NEED_DATA) == 0 || + (fstatus & SPA_STATUS_DRAINED)) + break; + } + /* the converter needs more data */ + if ((status & SPA_STATUS_NEED_DATA)) + break; + } + } else if (!this->driver) { + bool done = false; + while (retry--) { + /* output node (source). First run the converter to make + * sure we push out any queued data. Then when it needs + * more data, schedule the follower. */ + status = this->convert ? spa_node_process(this->convert) : 0; + if (status == 0) + status = SPA_STATUS_NEED_DATA; + else if (status < 0) + break; + + done = (status & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)); + + /* when not async, we can return the data when we are done. + * In async mode we might first need to wake up the follower + * to asynchronously provide more data for the next round. */ + if (!this->async && done) + break; + + if (status & SPA_STATUS_NEED_DATA) { + /* the converter needs more data, schedule the + * follower */ + fstatus = spa_node_process(this->follower); + if (fstatus < 0) { + status = fstatus; + break; + } + /* if the follower didn't produce more data or is + * not drained we can stop now */ + if ((fstatus & (SPA_STATUS_HAVE_DATA | SPA_STATUS_DRAINED)) == 0) + break; + } + /* converter produced something or is drained and we + * scheduled the follower above, we can stop now*/ + if (done) + break; + } + if (!done) + spa_node_call_xrun(&this->callbacks, 0, 0, NULL); + + } else { + status = spa_node_process(this->follower); + } + spa_log_trace_fp(this->log, "%p: process status:%d", this, status); + + this->driver = false; + + return status; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + spa_hook_remove(&this->follower_listener); + spa_node_set_callbacks(this->follower, NULL, NULL); + + if (this->hnd_convert) + spa_handle_clear(this->hnd_convert); + + if (this->buffers) + free(this->buffers); + this->buffers = NULL; + + return 0; +} + + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + size_t size = 0; + +#if 0 + size += spa_handle_factory_get_size(&spa_videoconvert_factory, params); +#endif + size += sizeof(struct impl); + + return size; +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; +#if 0 + void *iface; +#endif + const char *str; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + spa_log_topic_init(this->log, log_topic); + + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + + if (info == NULL || + (str = spa_dict_lookup(info, "video.adapt.follower")) == NULL) + return -EINVAL; + + sscanf(str, "pointer:%p", &this->follower); + if (this->follower == NULL) + return -EINVAL; + + if (this->cpu) + this->max_align = spa_cpu_get_max_align(this->cpu); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + +#if 0 + this->hnd_convert = SPA_PTROFF(this, sizeof(struct impl), struct spa_handle); + spa_handle_factory_init(&spa_videoconvert_factory, + this->hnd_convert, + info, support, n_support); + + spa_handle_get_interface(this->hnd_convert, SPA_TYPE_INTERFACE_Node, &iface); + this->convert = iface; +#endif + this->target = this->convert; + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->params[IDX_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->params[IDX_PortConfig] = SPA_PARAM_INFO(SPA_PARAM_PortConfig, SPA_PARAM_INFO_READWRITE); + this->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->params[IDX_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + spa_node_add_listener(this->follower, + &this->follower_listener, &follower_node_events, this); + spa_node_set_callbacks(this->follower, &follower_node_callbacks, this); + + if (this->convert) + spa_node_add_listener(this->convert, + &this->convert_listener, &convert_node_events, this); + + reconfigure_mode(this, true, this->direction, NULL); + + link_io(this); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_Node, }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_videoadapter_factory = { + .version = SPA_VERSION_HANDLE_FACTORY, + .name = SPA_NAME_VIDEO_ADAPT, + .get_size = impl_get_size, + .init = impl_init, + .enum_interface_info = impl_enum_interface_info, +}; diff --git a/spa/plugins/videotestsrc/draw.c b/spa/plugins/videotestsrc/draw.c new file mode 100644 index 0000000..20fed49 --- /dev/null +++ b/spa/plugins/videotestsrc/draw.c @@ -0,0 +1,288 @@ +/* Spa Video Test Source + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +typedef enum { + GRAY = 0, + YELLOW, + CYAN, + GREEN, + MAGENTA, + RED, + BLUE, + BLACK, + NEG_I, + WHITE, + POS_Q, + DARK_BLACK, + LIGHT_BLACK, + N_COLORS +} Color; + +typedef struct _Pixel Pixel; + +struct _Pixel { + unsigned char R; + unsigned char G; + unsigned char B; + unsigned char Y; + unsigned char U; + unsigned char V; +}; + +static Pixel colors[N_COLORS] = { + {191, 191, 191, 0, 0, 0}, /* GRAY */ + {191, 191, 0, 0, 0, 0}, /* YELLOW */ + {0, 191, 191, 0, 0, 0}, /* CYAN */ + {0, 191, 0, 0, 0, 0}, /* GREEN */ + {191, 0, 191, 0, 0, 0}, /* MAGENTA */ + {191, 0, 0, 0, 0, 0}, /* RED */ + {0, 0, 191, 0, 0, 0}, /* BLUE */ + {19, 19, 19, 0, 0, 0}, /* BLACK */ + {0, 33, 76, 0, 0, 0}, /* NEGATIVE I */ + {255, 255, 255, 0, 0, 0}, /* WHITE */ + {49, 0, 107, 0, 0, 0}, /* POSITIVE Q */ + {9, 9, 9, 0, 0, 0}, /* DARK BLACK */ + {29, 29, 29, 0, 0, 0}, /* LIGHT BLACK */ +}; + +/* YUV values are computed in init_colors() */ + +typedef struct _DrawingData DrawingData; + +typedef void (*DrawPixelFunc) (DrawingData * dd, int x, Pixel * pixel); + +struct _DrawingData { + char *line; + int width; + int height; + int stride; + DrawPixelFunc draw_pixel; +}; + +static inline void update_yuv(Pixel * pixel) +{ + uint16_t y, u, v; + + /* see https://en.wikipedia.org/wiki/YUV#Studio_swing_for_BT.601 */ + + y = 76 * pixel->R + 150 * pixel->G + 29 * pixel->B; + u = -43 * pixel->R - 84 * pixel->G + 127 * pixel->B; + v = 127 * pixel->R - 106 * pixel->G - 21 * pixel->B; + + y = (y + 128) >> 8; + u = (u + 128) >> 8; + v = (v + 128) >> 8; + + pixel->Y = y; + pixel->U = u + 128; + pixel->V = v + 128; +} + +static void init_colors(void) +{ + int i; + + if (colors[WHITE].Y != 0) { + /* already computed */ + return; + } + + for (i = 0; i < N_COLORS; i++) { + update_yuv(&colors[i]); + } +} + +static void draw_pixel_rgb(DrawingData * dd, int x, Pixel * color) +{ + dd->line[3 * x + 0] = color->R; + dd->line[3 * x + 1] = color->G; + dd->line[3 * x + 2] = color->B; +} + +static void draw_pixel_uyvy(DrawingData * dd, int x, Pixel * color) +{ + if (x & 1) { + /* odd pixel */ + dd->line[2 * (x - 1) + 3] = color->Y; + } else { + /* even pixel */ + dd->line[2 * x + 0] = color->U; + dd->line[2 * x + 1] = color->Y; + dd->line[2 * x + 2] = color->V; + } +} + +static int drawing_data_init(DrawingData * dd, struct impl *this, char *data) +{ + struct port *port = &this->port; + struct spa_video_info *format = &port->current_format; + struct spa_rectangle *size = &format->info.raw.size; + + if ((format->media_type != SPA_MEDIA_TYPE_video) || + (format->media_subtype != SPA_MEDIA_SUBTYPE_raw)) + return -ENOTSUP; + + if (format->info.raw.format == SPA_VIDEO_FORMAT_RGB) { + dd->draw_pixel = draw_pixel_rgb; + } else if (format->info.raw.format == SPA_VIDEO_FORMAT_UYVY) { + dd->draw_pixel = draw_pixel_uyvy; + } else + return -ENOTSUP; + + dd->line = data; + dd->width = size->width; + dd->height = size->height; + dd->stride = port->stride; + + return 0; +} + +static inline void draw_pixels(DrawingData * dd, int offset, Color color, int length) +{ + int x; + + for (x = offset; x < offset + length; x++) { + dd->draw_pixel(dd, x, &colors[color]); + } +} + +static inline void next_line(DrawingData * dd) +{ + dd->line += dd->stride; +} + +static void draw_smpte_snow(DrawingData * dd) +{ + int h, w; + int y1, y2; + int i, j; + + w = dd->width; + h = dd->height; + y1 = 2 * h / 3; + y2 = 3 * h / 4; + + for (i = 0; i < y1; i++) { + for (j = 0; j < 7; j++) { + int x1 = j * w / 7; + int x2 = (j + 1) * w / 7; + draw_pixels(dd, x1, j, x2 - x1); + } + next_line(dd); + } + + for (i = y1; i < y2; i++) { + for (j = 0; j < 7; j++) { + int x1 = j * w / 7; + int x2 = (j + 1) * w / 7; + Color c = (j & 1) ? BLACK : BLUE - j; + + draw_pixels(dd, x1, c, x2 - x1); + } + next_line(dd); + } + + for (i = y2; i < h; i++) { + int x = 0; + + /* negative I */ + draw_pixels(dd, x, NEG_I, w / 6); + x += w / 6; + + /* white */ + draw_pixels(dd, x, WHITE, w / 6); + x += w / 6; + + /* positive Q */ + draw_pixels(dd, x, POS_Q, w / 6); + x += w / 6; + + /* pluge */ + draw_pixels(dd, x, DARK_BLACK, w / 12); + x += w / 12; + draw_pixels(dd, x, BLACK, w / 12); + x += w / 12; + draw_pixels(dd, x, LIGHT_BLACK, w / 12); + x += w / 12; + + /* war of the ants (a.k.a. snow) */ + for (j = x; j < w; j++) { + Pixel p; + unsigned char r = rand(); + + p.R = r; + p.G = r; + p.B = r; + update_yuv(&p); + dd->draw_pixel(dd, j, &p); + } + + next_line(dd); + } +} + +static void draw_snow(DrawingData * dd) +{ + int x, y; + + for (y = 0; y < dd->height; y++) { + for (x = 0; x < dd->width; x++) { + Pixel p; + unsigned char r = rand(); + + p.R = r; + p.G = r; + p.B = r; + update_yuv(&p); + dd->draw_pixel(dd, x, &p); + } + + next_line(dd); + } +} + +static int draw(struct impl *this, char *data) +{ + DrawingData dd; + int res; + + init_colors(); + + if ((res = drawing_data_init(&dd, this, data)) < 0) + return res; + + switch (this->props.pattern) { + case PATTERN_SMPTE_SNOW: + draw_smpte_snow(&dd); + break; + case PATTERN_SNOW: + draw_snow(&dd); + break; + default: + return -ENOTSUP; + } + return 0; +} diff --git a/spa/plugins/videotestsrc/meson.build b/spa/plugins/videotestsrc/meson.build new file mode 100644 index 0000000..01a33ee --- /dev/null +++ b/spa/plugins/videotestsrc/meson.build @@ -0,0 +1,7 @@ +videotestsrc_sources = ['videotestsrc.c', 'plugin.c'] + +videotestsrclib = shared_library('spa-videotestsrc', + videotestsrc_sources, + dependencies : [ spa_dep, pthread_lib ], + install : true, + install_dir : spa_plugindir / 'videotestsrc') diff --git a/spa/plugins/videotestsrc/plugin.c b/spa/plugins/videotestsrc/plugin.c new file mode 100644 index 0000000..dd87ebf --- /dev/null +++ b/spa/plugins/videotestsrc/plugin.c @@ -0,0 +1,46 @@ +/* Spa Video Test Source plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_videotestsrc_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_videotestsrc_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c new file mode 100644 index 0000000..8d96b1b --- /dev/null +++ b/spa/plugins/videotestsrc/videotestsrc.c @@ -0,0 +1,999 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "videotestsrc" + +#define FRAMES_TO_TIME(port,f) ((port->current_format.info.raw.framerate.denom * (f) * SPA_NSEC_PER_SEC) / \ + (port->current_format.info.raw.framerate.num)) + +enum pattern { + PATTERN_SMPTE_SNOW, + PATTERN_SNOW, +}; + +#define DEFAULT_LIVE true +#define DEFAULT_PATTERN PATTERN_SMPTE_SNOW + +struct props { + bool live; + uint32_t pattern; +}; + +static void reset_props(struct props *props) +{ + props->live = DEFAULT_LIVE; + props->pattern = DEFAULT_PATTERN; +} + +#define MAX_BUFFERS 16 +#define MAX_PORTS 1 + +struct buffer { + uint32_t id; + struct spa_buffer *outbuf; + bool outstanding; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + struct spa_video_info current_format; + size_t bpp; + int stride; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + bool async; + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + uint64_t start_time; + uint64_t elapsed_time; + + uint64_t frame_count; + + struct port port; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live), + SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), + SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); + break; + case 1: + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_patternType), + SPA_PROP_INFO_description, SPA_POD_String("The pattern"), + SPA_PROP_INFO_type, SPA_POD_Int(p->pattern), + 0); + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0), + spa_pod_builder_push_struct(&b, &f[1]); + spa_pod_builder_int(&b, PATTERN_SMPTE_SNOW); + spa_pod_builder_string(&b, "SMPTE snow"); + spa_pod_builder_int(&b, PATTERN_SNOW); + spa_pod_builder_string(&b, "Snow"); + spa_pod_builder_pop(&b, &f[1]); + param = spa_pod_builder_pop(&b, &f[0]); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_live, SPA_POD_Bool(p->live), + SPA_PROP_patternType, SPA_POD_Int(p->pattern)); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct port *port = &this->port; + + if (param == NULL) { + reset_props(p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_live, SPA_POD_OPT_Bool(&p->live), + SPA_PROP_patternType, SPA_POD_OPT_Int(&p->pattern)); + + if (p->live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + else + port->info.flags &= ~SPA_PORT_FLAG_LIVE; + break; + } + default: + return -ENOENT; + } + return 0; +} + +#include "draw.c" + +static int fill_buffer(struct impl *this, struct buffer *b) +{ + return draw(this, b->outbuf->datas[0].data); +} + +static void set_timer(struct impl *this, bool enabled) +{ + if (this->async || this->props.live) { + if (enabled) { + if (this->props.live) { + uint64_t next_time = this->start_time + this->elapsed_time; + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 1; + } + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + } + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + } +} + +static int read_timer(struct impl *this) +{ + uint64_t expirations; + int res = 0; + + if (this->async || this->props.live) { + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return res; +} + +static int make_buffer(struct impl *this) +{ + struct buffer *b; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + uint32_t n_bytes; + + if (read_timer(this) < 0) + return 0; + + if (spa_list_is_empty(&port->empty)) { + set_timer(this, false); + spa_log_error(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + b->outstanding = true; + + n_bytes = b->outbuf->datas[0].maxsize; + + spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id); + + fill_buffer(this, b); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = n_bytes; + b->outbuf->datas[0].chunk->stride = port->stride; + + if (b->h) { + b->h->seq = this->frame_count; + b->h->pts = this->start_time + this->elapsed_time; + b->h->dts_offset = 0; + } + + this->frame_count++; + this->elapsed_time = FRAMES_TO_TIME(port, this->frame_count); + set_timer(this, true); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return io->status; +} + +static void on_output(struct spa_source *source) +{ + struct impl *this = source->data; + int res; + + res = make_buffer(this); + + if (res == SPA_STATUS_HAVE_DATA) + spa_node_call_ready(&this->callbacks, res); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct timespec now; + + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (this->started) + return 0; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (this->props.live) + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); + else + this->start_time = 0; + this->frame_count = 0; + this->elapsed_time = 0; + + this->started = true; + set_timer(this, true); + break; + } + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + this->started = false; + set_timer(this, false); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_MEDIA_CLASS, "Video/Source" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_UYVY), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(INT32_MAX, INT32_MAX)), + SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(25,1), + &SPA_FRACTION(0, 1), + &SPA_FRACTION(INT32_MAX, 1))); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->empty); + this->started = false; + set_timer(this, false); + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_video_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video && + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_video_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format == SPA_VIDEO_FORMAT_RGB) + port->bpp = 3; + else if (info.info.raw.format == SPA_VIDEO_FORMAT_UYVY) + port->bpp = 2; + else + return -EINVAL; + + if (info.info.raw.size.width == 0 || + info.info.raw.size.height == 0 || + info.info.raw.framerate.num == 0 || + info.info.raw.framerate.denom == 0) + return -EINVAL; + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + struct spa_video_info_raw *raw_info = &port->current_format.info.raw; + port->stride = SPA_ROUND_UP_N(port->bpp * raw_info->size.width, 4); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + if (id == SPA_PARAM_Format) { + return port_set_format(this, port, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->outstanding = false; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + spa_list_append(&port->empty, &b->link); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + spa_return_if_fail(b->outstanding); + + spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id); + + b->outstanding = false; + spa_list_append(&port->empty, &b->link); + + if (!this->props.live) + set_timer(this, true); +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (!this->props.live) + return make_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (this->data_loop) + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + reset_props(&this->props); + + this->timer_source.func = on_output; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + if (this->data_loop) + spa_loop_add_source(this->data_loop, &this->timer_source); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Generate a video test pattern" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_videotestsrc_factory = { + SPA_VERSION_HANDLE_FACTORY, + NAME, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/volume/meson.build b/spa/plugins/volume/meson.build new file mode 100644 index 0000000..2445e2b --- /dev/null +++ b/spa/plugins/volume/meson.build @@ -0,0 +1,7 @@ +volume_sources = ['volume.c', 'plugin.c'] + +volumelib = shared_library('spa-volume', + volume_sources, + dependencies : [ spa_dep ], + install : true, + install_dir : spa_plugindir / 'volume') diff --git a/spa/plugins/volume/plugin.c b/spa/plugins/volume/plugin.c new file mode 100644 index 0000000..2ceb250 --- /dev/null +++ b/spa/plugins/volume/plugin.c @@ -0,0 +1,46 @@ +/* Spa Volume plugin + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_volume_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_volume_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/volume/volume.c b/spa/plugins/volume/volume.c new file mode 100644 index 0000000..6323766 --- /dev/null +++ b/spa/plugins/volume/volume.c @@ -0,0 +1,894 @@ +/* Spa + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NAME "volume" + +#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 2 + +#define DEFAULT_VOLUME 1.0 +#define DEFAULT_MUTE false + +struct props { + double volume; + bool mute; +}; + +static void reset_props(struct props *props) +{ + props->volume = DEFAULT_VOLUME; + props->mute = DEFAULT_MUTE; +} + +#define MAX_BUFFERS 16 + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_meta_header *h; + void *ptr; + size_t size; + struct spa_list link; +}; + +struct port { + enum spa_direction direction; + uint32_t id; + + bool have_format; + + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_io_buffers *io; + + struct spa_list empty; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + uint32_t quantum_limit; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[5]; + struct props props; + + struct spa_hook_list hooks; + + struct spa_audio_info current_format; + int bpf; + + struct port in_ports[1]; + struct port out_ports[1]; + + bool started; +}; + +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0) +#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) +#define CHECK_PORT(this,d,p) ((p) == 0) +#define GET_IN_PORT(this,p) (&this->in_ports[p]) +#define GET_OUT_PORT(this,p) (&this->out_ports[p]) +#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct props *p; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + p = &this->props; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), + SPA_PROP_INFO_description, SPA_POD_String("The volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, 0.0, 10.0)); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute), + SPA_PROP_INFO_description, SPA_POD_String("Mute"), + SPA_PROP_INFO_type, SPA_POD_Bool(p->mute)); + break; + default: + return 0; + } + break; + case SPA_PARAM_Props: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_volume, SPA_POD_Float(p->volume), + SPA_PROP_mute, SPA_POD_Bool(p->mute)); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + return -ENOTSUP; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + if (param == NULL) { + reset_props(p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume), + SPA_PROP_mute, SPA_POD_OPT_Bool(&p->mute)); + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + this->started = true; + break; + case SPA_NODE_COMMAND_Pause: + this->started = false; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + spa_node_emit_port_info(&this->hooks, + port->direction, port->id, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, GET_IN_PORT(this, 0), true); + emit_port_info(this, GET_OUT_PORT(this, 0), true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(2, + SPA_AUDIO_FORMAT_S16, + SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_RATE, 1, INT32_MAX), + SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( + DEFAULT_CHANNELS, 1, INT32_MAX)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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, NAME " %p: clear buffers", this); + port->n_buffers = 0; + spa_list_init(&port->empty); + } + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct impl *this = object; + struct port *port; + int res; + + port = GET_PORT(this, direction, port_id); + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.format != SPA_AUDIO_FORMAT_S16 || + info.info.raw.channels == 0 || + info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + return -EINVAL; + + this->bpf = 2 * info.info.raw.channels; + this->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + spa_return_val_if_fail(object != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(object, direction, port_id), -EINVAL); + + if (id == SPA_PARAM_Format) { + return port_set_format(object, direction, port_id, flags, param); + } + else + return -ENOENT; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = direction == SPA_DIRECTION_INPUT ? BUFFER_FLAG_OUT : 0; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + b->ptr = d[0].data; + b->size = d[0].maxsize; + } else { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) + spa_list_append(&port->empty, &b->link); + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = GET_PORT(this, direction, port_id); + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static void recycle_buffer(struct impl *this, uint32_t id) +{ + struct port *port = GET_OUT_PORT(this, 0); + struct buffer *b = &port->buffers[id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_warn(this->log, NAME " %p: buffer %d not outstanding", this, id); + return; + } + + spa_list_append(&port->empty, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + spa_log_trace(this->log, NAME " %p: recycle buffer %d", this, id); +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), + -EINVAL); + + port = GET_OUT_PORT(this, port_id); + + if (buffer_id >= port->n_buffers) + return -EINVAL; + + recycle_buffer(this, buffer_id); + + return 0; +} + +static struct buffer *find_free_buffer(struct impl *this, struct port *port) +{ + struct buffer *b; + + if (spa_list_is_empty(&port->empty)) + return NULL; + + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + return b; +} + +static void do_volume(struct impl *this, struct spa_buffer *dbuf, struct spa_buffer *sbuf) +{ + uint32_t i, n_samples, n_bytes; + struct spa_data *sd, *dd; + int16_t *src, *dst; + double volume; + uint32_t written, towrite, savail, davail; + uint32_t sindex, dindex; + + volume = this->props.volume; + + sd = sbuf->datas; + dd = dbuf->datas; + + savail = SPA_MIN(sd[0].chunk->size, sd[0].maxsize); + sindex = sd[0].chunk->offset; + davail = 0; + dindex = 0; + davail = dd[0].maxsize - davail; + + towrite = SPA_MIN(savail, davail); + written = 0; + + while (written < towrite) { + uint32_t soffset = sindex % sd[0].maxsize; + uint32_t doffset = dindex % dd[0].maxsize; + + src = SPA_PTROFF(sd[0].data, soffset, int16_t); + dst = SPA_PTROFF(dd[0].data, doffset, int16_t); + + n_bytes = SPA_MIN(towrite, sd[0].maxsize - soffset); + n_bytes = SPA_MIN(n_bytes, dd[0].maxsize - doffset); + + n_samples = n_bytes / sizeof(int16_t); + for (i = 0; i < n_samples; i++) + dst[i] = src[i] * volume; + + sindex += n_bytes; + dindex += n_bytes; + written += n_bytes; + } + dd[0].chunk->offset = 0; + dd[0].chunk->size = written; + dd[0].chunk->stride = 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *in_port, *out_port; + struct spa_io_buffers *input, *output; + struct buffer *dbuf, *sbuf; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + out_port = GET_OUT_PORT(this, 0); + if ((output = out_port->io) == NULL) + return -EIO; + + if (output->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + /* recycle */ + if (output->buffer_id < out_port->n_buffers) { + recycle_buffer(this, output->buffer_id); + output->buffer_id = SPA_ID_INVALID; + } + + in_port = GET_IN_PORT(this, 0); + if ((input = in_port->io) == NULL) + return -EIO; + + if (input->status != SPA_STATUS_HAVE_DATA) + return SPA_STATUS_NEED_DATA; + + if (input->buffer_id >= in_port->n_buffers) { + input->status = -EINVAL; + return -EINVAL; + } + + if ((dbuf = find_free_buffer(this, out_port)) == NULL) { + spa_log_error(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + + sbuf = &in_port->buffers[input->buffer_id]; + + spa_log_trace(this->log, NAME " %p: do volume %d -> %d", this, sbuf->id, dbuf->id); + do_volume(this, dbuf->outbuf, sbuf->outbuf); + + output->buffer_id = dbuf->id; + output->status = SPA_STATUS_HAVE_DATA; + + input->status = SPA_STATUS_NEED_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + } + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + reset_props(&this->props); + + port = GET_IN_PORT(this, 0); + port->direction = SPA_DIRECTION_INPUT; + port->id = 0; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_IN_PLACE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + + port = GET_OUT_PORT(this, 0); + port->direction = SPA_DIRECTION_OUTPUT; + port->id = 0; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +const struct spa_handle_factory spa_volume_factory = { + SPA_VERSION_HANDLE_FACTORY, + NAME, + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/meson.build b/spa/plugins/vulkan/meson.build new file mode 100644 index 0000000..0657d21 --- /dev/null +++ b/spa/plugins/vulkan/meson.build @@ -0,0 +1,12 @@ +spa_vulkan_sources = [ + 'plugin.c', + 'vulkan-compute-filter.c', + 'vulkan-compute-source.c', + 'vulkan-utils.c' +] + +spa_vulkan = shared_library('spa-vulkan', + spa_vulkan_sources, + dependencies : [ spa_dep, vulkan_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'vulkan') diff --git a/spa/plugins/vulkan/plugin.c b/spa/plugins/vulkan/plugin.c new file mode 100644 index 0000000..e9f40ba --- /dev/null +++ b/spa/plugins/vulkan/plugin.c @@ -0,0 +1,50 @@ +/* Spa vulkan plugin + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include + +extern const struct spa_handle_factory spa_vulkan_compute_filter_factory; +extern const struct spa_handle_factory spa_vulkan_compute_source_factory; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_vulkan_compute_source_factory; + break; + case 1: + *factory = &spa_vulkan_compute_filter_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/vulkan/shaders/disk-intersection.comp b/spa/plugins/vulkan/shaders/disk-intersection.comp new file mode 100644 index 0000000..7b92fdf --- /dev/null +++ b/spa/plugins/vulkan/shaders/disk-intersection.comp @@ -0,0 +1,143 @@ +// The MIT License +// Copyright © 2013 Inigo Quilez +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +// Other intersectors: +// +// Box: https://www.shadertoy.com/view/ld23DV +// Triangle: https://www.shadertoy.com/view/MlGcDz +// Capsule: https://www.shadertoy.com/view/Xt3SzX +// Ellipsoid: https://www.shadertoy.com/view/MlsSzn +// Sphere: https://www.shadertoy.com/view/4d2XWV +// Capped Cylinder: https://www.shadertoy.com/view/4lcSRn +// Disk: https://www.shadertoy.com/view/lsfGDB +// Capped Cone: https://www.shadertoy.com/view/llcfRf +// Rounded Box: https://www.shadertoy.com/view/WlSXRW +// Rounded Cone: https://www.shadertoy.com/view/MlKfzm +// Torus: https://www.shadertoy.com/view/4sBGDy +// Sphere4: https://www.shadertoy.com/view/3tj3DW +// Goursat: https://www.shadertoy.com/view/3lj3DW + + +#define SC 3.0 + +#if 1 +// +// Elegant way to intersect a planar coordinate system (3x3 linear system) +// +vec3 intersectCoordSys(in vec3 o, in vec3 d, vec3 c, vec3 u, vec3 v) +{ + vec3 q = o - c; + return vec3(dot(cross(u, v), q), + dot(cross(q, u), d), + dot(cross(v, q), d)) / dot(cross(v, u), d); +} + +#else +// +// Ugly (but faster) way to intersect a planar coordinate system: plane + projection +// +vec3 intersectCoordSys(in vec3 o, in vec3 d, vec3 c, vec3 u, vec3 v) +{ + vec3 q = o - c; + vec3 n = cross(u, v); + float t = -dot(n, q) / dot(d, n); + float r = dot(u, q + d * t); + float s = dot(v, q + d * t); + return vec3(t, s, r); +} + +#endif + +vec3 hash3(float n) +{ + return fract(sin(vec3(n, n + 1.0, n + 2.0)) * + vec3(43758.5453123, 12578.1459123, 19642.3490423)); +} + +vec3 shade(in vec4 res) +{ + float ra = length(res.yz); + float an = atan(res.y, res.z) + 8.0 * iTime; + float pa = sin(3.0 * an); + + vec3 cola = + 0.5 + 0.5 * sin((res.w / 64.0) * 3.5 + vec3(0.0, 1.0, 2.0)); + + vec3 col = vec3(0.0); + col += cola * 0.4 * (1.0 - smoothstep(0.90, 1.00, ra)); + col += + cola * 1.0 * (1.0 - + smoothstep(0.00, 0.03, + abs(ra - 0.8))) * (0.5 + 0.5 * pa); + col += + cola * 1.0 * (1.0 - + smoothstep(0.00, 0.20, + abs(ra - 0.8))) * (0.5 + 0.5 * pa); + col += + cola * 0.5 * (1.0 - + smoothstep(0.05, 0.10, + abs(ra - 0.5))) * (0.5 + 0.5 * pa); + col += + cola * 0.7 * (1.0 - + smoothstep(0.00, 0.30, + abs(ra - 0.5))) * (0.5 + 0.5 * pa); + + return col * 0.3; +} + +vec3 render(in vec3 ro, in vec3 rd) +{ + // raytrace + vec3 col = vec3(0.0); + for (int i = 0; i < 64; i++) { + // position disk + vec3 r = 2.5 * (-1.0 + 2.0 * hash3(float (i))); + r *= SC; + // orientate disk + vec3 u = normalize(r.zxy); + vec3 v = normalize(cross(u, vec3(0.0, 1.0, 0.0))); + + // intersect coord sys + vec3 tmp = intersectCoordSys(ro, rd, r, u, v); + tmp /= SC; + if (dot(tmp.yz, tmp.yz) < 1.0 && tmp.x > 0.0) { + // shade + col += shade(vec4(tmp, float (i))); + } + } + + return col; +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) +{ + vec2 q = fragCoord.xy / iResolution.xy; + vec2 p = -1.0 + 2.0 * q; + p.x *= iResolution.x / iResolution.y; + + // camera + vec3 ro = + 2.0 * vec3(cos(0.5 * iTime * 1.1), 0.0, + sin(0.5 * iTime * 1.1)); + vec3 ta = vec3(0.0, 0.0, 0.0); + // camera matrix + vec3 ww = normalize(ta - ro); + vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0))); + vec3 vv = normalize(cross(uu, ww)); + // create view ray + vec3 rd = normalize(p.x * uu + p.y * vv + 1.0 * ww); + + vec3 col = render(ro * SC, rd); + + fragColor = vec4(col, 1.0); +} + +void mainVR(out vec4 fragColor, in vec2 fragCoord, in vec3 fragRayOri, + in vec3 fragRayDir) +{ + vec3 col = render(fragRayOri + vec3(0.0, 0.0, 0.0), fragRayDir); + + fragColor = vec4(col, 1.0); +} diff --git a/spa/plugins/vulkan/shaders/filter-color.comp b/spa/plugins/vulkan/shaders/filter-color.comp new file mode 100644 index 0000000..e08b715 --- /dev/null +++ b/spa/plugins/vulkan/shaders/filter-color.comp @@ -0,0 +1,39 @@ +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec2 p = fragCoord.xy/iResolution.xy; + + vec4 col = texture(iChannel0, p); + + + //Desaturate + if(p.x<.25) + { + col = vec4( (col.r+col.g+col.b)/3. ); + } + //Invert + else if (p.x<.5) + { + col = vec4(1.) - texture(iChannel0, p); + } + //Chromatic aberration + else if (p.x<.75) + { + vec2 offset = vec2(.01,.0); + col.r = texture(iChannel0, p+offset.xy).r; + col.g = texture(iChannel0, p ).g; + col.b = texture(iChannel0, p+offset.yx).b; + } + //Color switching + else + { + col.rgb = texture(iChannel0, p).brg; + } + + + //Line + if( mod(abs(p.x+.5/iResolution.y),.25)<0.5/iResolution.y ) + col = vec4(1.); + + + fragColor = col; +} diff --git a/spa/plugins/vulkan/shaders/filter.comp b/spa/plugins/vulkan/shaders/filter.comp new file mode 100644 index 0000000..f1ca486 --- /dev/null +++ b/spa/plugins/vulkan/shaders/filter.comp @@ -0,0 +1,44 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +#define WORKGROUP_SIZE 32 +layout (local_size_x = WORKGROUP_SIZE, local_size_y = WORKGROUP_SIZE, local_size_z = 1 ) in; + +layout(rgba32f, set = 0, binding = 0) uniform image2D resultImage; +layout(set = 0, binding = 1) uniform sampler2D iChannel0; + +layout( push_constant ) uniform Constants { + float time; + int frame; + int width; + int height; +} PushConstant; + +float iTime; +int iFrame; +vec3 iResolution; +vec4 iMouse; + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); + +void main() +{ + iTime = PushConstant.time; + iFrame = PushConstant.frame; + iResolution = vec3(float(PushConstant.width), float(PushConstant.height), 0.0); + iMouse = vec4(0.0, 0.0, 0.0, 0.0); + vec2 coord = vec2(float(gl_GlobalInvocationID.x), + iResolution.y - float(gl_GlobalInvocationID.y)); + vec4 outColor; + + if(coord.x >= iResolution.x || coord.y >= iResolution.y) + return; + + mainImage(outColor, coord); + + imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), outColor); +} + +//#include "smearcam.comp" +#include "filter-color.comp" +//#include "filter-ripple.comp" diff --git a/spa/plugins/vulkan/shaders/filter.spv b/spa/plugins/vulkan/shaders/filter.spv new file mode 100644 index 0000000..5bfd245 Binary files /dev/null and b/spa/plugins/vulkan/shaders/filter.spv differ diff --git a/spa/plugins/vulkan/shaders/main.comp b/spa/plugins/vulkan/shaders/main.comp new file mode 100644 index 0000000..368b7cf --- /dev/null +++ b/spa/plugins/vulkan/shaders/main.comp @@ -0,0 +1,50 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +#define WORKGROUP_SIZE 32 +layout (local_size_x = WORKGROUP_SIZE, local_size_y = WORKGROUP_SIZE, local_size_z = 1 ) in; + +layout(rgba32f, binding = 0) uniform image2D resultImage; + +layout( push_constant ) uniform Constants { + float time; + int frame; + int width; + int height; +} PushConstant; + +float iTime; +int iFrame; +vec3 iResolution; +vec4 iMouse; + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); + +void main() +{ + iTime = PushConstant.time; + iFrame = PushConstant.frame; + iResolution = vec3(float(PushConstant.width), float(PushConstant.height), 0.0); + iMouse = vec4(0.0, 0.0, 0.0, 0.0); + vec2 coord = vec2(float(gl_GlobalInvocationID.x), + iResolution.y - float(gl_GlobalInvocationID.y)); + vec4 outColor; + + if(coord.x >= iResolution.x || coord.y >= iResolution.y) + return; + + mainImage(outColor, coord); + + imageStore(resultImage, ivec2(gl_GlobalInvocationID.xy), outColor); +} + +//#include "plasma-globe.comp" +//#include "mandelbrot-distance.comp" +#include "disk-intersection.comp" +//#include "ring-twister.comp" +//#include "gears.comp" +//#include "protean-clouds.comp" +//#include "flame.comp" +//#include "shader.comp" +//#include "raymarching-primitives.comp" +//#include "3d-primitives.comp" diff --git a/spa/plugins/vulkan/shaders/main.spv b/spa/plugins/vulkan/shaders/main.spv new file mode 100644 index 0000000..5da1755 Binary files /dev/null and b/spa/plugins/vulkan/shaders/main.spv differ diff --git a/spa/plugins/vulkan/vulkan-compute-filter.c b/spa/plugins/vulkan/vulkan-compute-filter.c new file mode 100644 index 0000000..94efec3 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-filter.c @@ -0,0 +1,808 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vulkan-utils.h" + +#define NAME "vulkan-compute-filter" + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + uint64_t info_all; + struct spa_port_info info; + + enum spa_direction direction; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + struct spa_video_info current_format; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; + struct spa_list ready; + uint32_t stream_id; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + + struct spa_io_position *position; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + bool started; + + struct vulkan_state state; + struct port port[2]; +}; + +#define CHECK_PORT(this,d,p) ((p) < 1) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Position: + if (size > 0 && size < sizeof(struct spa_io_position)) + return -EINVAL; + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + default: + return -ENOENT; + } + return 0; +} + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_debug(this->log, NAME " %p: reuse buffer %d", this, id); + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + spa_list_append(&port->empty, &b->link); + } +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + if (this->started) + return 0; + + this->started = true; + spa_vulkan_start(&this->state); + break; + + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + this->started = false; + spa_vulkan_stop(&this->state); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_MEDIA_CLASS, "Video/Filter" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[1]; + + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float RGBA video"); + port->info.props = &SPA_DICT_INIT(items, 1); + spa_node_emit_port_info(&this->hooks, + port->direction, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port[0], true); + emit_port_info(this, &this->port[1], true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port[direction]; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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, NAME" %p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(this->position->video.stride * + this->position->video.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, NAME " %p: clear buffers", this); + spa_vulkan_stop(&this->state); + spa_vulkan_use_buffers(&this->state, &this->state.streams[port->stream_id], 0, 0, NULL); + port->n_buffers = 0; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + this->started = false; + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + spa_vulkan_unprepare(&this->state); + } else { + struct spa_video_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video && + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) + return -EINVAL; + + if (spa_format_video_dsp_parse(format, &info.info.dsp) < 0) + return -EINVAL; + + if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32) + return -EINVAL; + + this->state.constants.width = this->position->video.size.width; + this->state.constants.height = this->position->video.size.height; + + port->current_format = info; + port->have_format = true; + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port[direction]; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + default: + return -ENOENT; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port[direction]; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = 0; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_log_info(this->log, "%p: %d:%d add buffer %p", port, direction, port_id, b); + spa_list_append(&port->empty, &b->link); + } + spa_vulkan_use_buffers(&this->state, &this->state.streams[port->stream_id], flags, n_buffers, buffers); + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port[direction]; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + + port = &this->port[SPA_DIRECTION_OUTPUT]; + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *inport, *outport; + struct spa_io_buffers *inio, *outio; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + inport = &this->port[SPA_DIRECTION_INPUT]; + if ((inio = inport->io) == NULL) + return -EIO; + + if (inio->status != SPA_STATUS_HAVE_DATA) + return inio->status; + + if (inio->buffer_id >= inport->n_buffers) { + inio->status = -EINVAL; + return -EINVAL; + } + + outport = &this->port[SPA_DIRECTION_OUTPUT]; + if ((outio = outport->io) == NULL) + return -EIO; + + if (outio->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (outio->buffer_id < outport->n_buffers) { + reuse_buffer(this, outport, outio->buffer_id); + outio->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&outport->empty)) { + spa_log_debug(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + b = &inport->buffers[inio->buffer_id]; + this->state.streams[inport->stream_id].pending_buffer_id = b->id; + inio->status = SPA_STATUS_NEED_DATA; + + b = spa_list_first(&outport->empty, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + this->state.streams[outport->stream_id].pending_buffer_id = b->id; + + this->state.constants.time += 0.025; + this->state.constants.frame++; + + spa_log_debug(this->log, "filter into %d", b->id); + + spa_vulkan_process(&this->state); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = b->outbuf->datas[0].maxsize; + b->outbuf->datas[0].chunk->stride = this->position->video.stride; + + outio->buffer_id = b->id; + outio->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_NEED_DATA | SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->state.log = this->log; + this->state.shaderName = "spa/plugins/vulkan/shaders/filter.spv"; + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + + port = &this->port[0]; + port->stream_id = 1; + port->direction = SPA_DIRECTION_INPUT; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS | + SPA_PORT_CHANGE_MASK_PROPS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_vulkan_init_stream(&this->state, &this->state.streams[port->stream_id], + SPA_DIRECTION_INPUT, NULL); + spa_list_init(&port->empty); + spa_list_init(&port->ready); + + port = &this->port[1]; + port->stream_id = 0; + port->direction = SPA_DIRECTION_OUTPUT; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS | + SPA_PORT_CHANGE_MASK_PROPS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + spa_vulkan_init_stream(&this->state, &this->state.streams[port->stream_id], + SPA_DIRECTION_OUTPUT, NULL); + + this->state.n_streams = 2; + spa_vulkan_prepare(&this->state); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Filter video frames using a vulkan compute shader" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_vulkan_compute_filter_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_VULKAN_COMPUTE_FILTER, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c new file mode 100644 index 0000000..4602e5d --- /dev/null +++ b/spa/plugins/vulkan/vulkan-compute-source.c @@ -0,0 +1,1016 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vulkan-utils.h" + +#define NAME "vulkan-compute-source" + +#define FRAMES_TO_TIME(this,f) ((this->position->video.framerate.denom * (f) * SPA_NSEC_PER_SEC) / \ + (this->position->video.framerate.num)) + +#define DEFAULT_LIVE true + +struct props { + bool live; +}; + +static void reset_props(struct props *props) +{ + props->live = DEFAULT_LIVE; +} + +struct buffer { + uint32_t id; +#define BUFFER_FLAG_OUT (1<<0) + uint32_t flags; + struct spa_buffer *outbuf; + struct spa_meta_header *h; + struct spa_list link; +}; + +struct port { + uint64_t info_all; + struct spa_port_info info; + struct spa_param_info params[5]; + + struct spa_io_buffers *io; + + bool have_format; + struct spa_video_info current_format; + + struct buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; + + struct spa_list empty; + struct spa_list ready; +}; + +struct impl { + struct spa_handle handle; + struct spa_node node; + + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; + + struct spa_io_clock *clock; + struct spa_io_position *position; + + uint64_t info_all; + struct spa_node_info info; + struct spa_param_info params[2]; + struct props props; + + struct spa_hook_list hooks; + struct spa_callbacks callbacks; + + bool async; + struct spa_source timer_source; + struct itimerspec timerspec; + + bool started; + uint64_t start_time; + uint64_t elapsed_time; + + uint64_t frame_count; + + struct vulkan_state state; + struct port port; +}; + +#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < 1) + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_live), + SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), + SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); + break; + default: + return 0; + } + break; + } + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_live, SPA_POD_Bool(p->live)); + break; + default: + return 0; + } + break; + } + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_IO_Clock: + if (size > 0 && size < sizeof(struct spa_io_clock)) + return -EINVAL; + this->clock = data; + break; + case SPA_IO_Position: + this->position = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + struct port *port = &this->port; + + if (param == NULL) { + reset_props(p); + return 0; + } + spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_live, SPA_POD_OPT_Bool(&p->live)); + + if (p->live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + else + port->info.flags &= ~SPA_PORT_FLAG_LIVE; + break; + } + default: + return -ENOENT; + } + return 0; +} + + +static void set_timer(struct impl *this, bool enabled) +{ + if (this->async || this->props.live) { + if (enabled) { + if (this->props.live) { + uint64_t next_time = this->start_time + this->elapsed_time; + this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 1; + } + } else { + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + } + spa_system_timerfd_settime(this->data_system, + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + } +} + +static int read_timer(struct impl *this) +{ + uint64_t expirations; + int res = 0; + + if (this->async || this->props.live) { + if ((res = spa_system_timerfd_read(this->data_system, + this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, NAME " %p: timerfd error: %s", + this, spa_strerror(res)); + } + } + return res; +} + +static int make_buffer(struct impl *this) +{ + struct buffer *b; + struct port *port = &this->port; + uint32_t n_bytes; + int res; + + if (read_timer(this) < 0) + return 0; + + if ((res = spa_vulkan_ready(&this->state)) < 0) { + res = SPA_STATUS_OK; + goto next; + } + + if (spa_list_is_empty(&port->empty)) { + set_timer(this, false); + spa_log_error(this->log, NAME " %p: out of buffers", this); + return -EPIPE; + } + b = spa_list_first(&port->empty, struct buffer, link); + spa_list_remove(&b->link); + + n_bytes = b->outbuf->datas[0].maxsize; + + spa_log_trace(this->log, NAME " %p: dequeue buffer %d", this, b->id); + + this->state.constants.time = this->elapsed_time / (float) SPA_NSEC_PER_SEC; + this->state.constants.frame = this->frame_count; + + this->state.streams[0].pending_buffer_id = b->id; + spa_vulkan_process(&this->state); + + if (this->state.streams[0].ready_buffer_id != SPA_ID_INVALID) { + struct buffer *b = &port->buffers[this->state.streams[0].ready_buffer_id]; + + this->state.streams[0].ready_buffer_id = SPA_ID_INVALID; + + spa_log_trace(this->log, NAME " %p: ready buffer %d", this, b->id); + + b->outbuf->datas[0].chunk->offset = 0; + b->outbuf->datas[0].chunk->size = n_bytes; + b->outbuf->datas[0].chunk->stride = this->position->video.stride; + + if (b->h) { + b->h->seq = this->frame_count; + b->h->pts = this->start_time + this->elapsed_time; + b->h->dts_offset = 0; + } + + spa_list_append(&port->ready, &b->link); + + res = SPA_STATUS_HAVE_DATA; + } +next: + this->frame_count++; + this->elapsed_time = FRAMES_TO_TIME(this, this->frame_count); + set_timer(this, true); + + return res; +} + +static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t id) +{ + struct buffer *b = &port->buffers[id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace(this->log, NAME " %p: reuse buffer %d", this, id); + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + spa_list_append(&port->empty, &b->link); + + if (!this->props.live) + set_timer(this, true); + } +} + +static void on_output(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + struct spa_io_buffers *io = port->io; + int res; + + if (io == NULL) + return; + + if (io->status == SPA_STATUS_HAVE_DATA) + return; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + res = make_buffer(this); + + if (!spa_list_is_empty(&port->ready)) { + struct buffer *b = spa_list_first(&port->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + } + spa_node_call_ready(&this->callbacks, res); +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + port = &this->port; + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Start: + { + struct timespec now; + + if (!port->have_format) + return -EIO; + if (port->n_buffers == 0) + return -EIO; + + if (this->started) + return 0; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (this->props.live) + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); + else + this->start_time = 0; + this->frame_count = 0; + this->elapsed_time = 0; + + this->started = true; + set_timer(this, true); + spa_vulkan_start(&this->state); + break; + } + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if (!this->started) + return 0; + + this->started = false; + set_timer(this, false); + spa_vulkan_stop(&this->state); + break; + default: + return -ENOTSUP; + } + return 0; +} + +static const struct spa_dict_item node_info_items[] = { + { SPA_KEY_MEDIA_CLASS, "Video/Source" }, + { SPA_KEY_NODE_DRIVER, "true" }, +}; + +static void emit_node_info(struct impl *this, bool full) +{ + uint64_t old = full ? this->info.change_mask : 0; + if (full) + this->info.change_mask = this->info_all; + if (this->info.change_mask) { + this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + spa_node_emit_info(&this->hooks, &this->info); + this->info.change_mask = old; + } +} + +static void emit_port_info(struct impl *this, struct port *port, bool full) +{ + uint64_t old = full ? port->info.change_mask : 0; + if (full) + port->info.change_mask = port->info_all; + if (port->info.change_mask) { + struct spa_dict_item items[1]; + + items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit float RGBA video"); + port->info.props = &SPA_DICT_INIT(items, 1); + spa_node_emit_port_info(&this->hooks, + SPA_DIRECTION_OUTPUT, 0, &port->info); + port->info.change_mask = old; + } +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct impl *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, &this->port, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int +impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int port_enum_formats(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(builder, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32)); + break; + default: + return 0; + } + return 1; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct port *port; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_pod *param; + struct spa_result_node_params result; + uint32_t count = 0; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + if ((res = port_enum_formats(this, direction, port_id, + result.index, filter, ¶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, NAME" %p: %dx%d stride %d", this, + this->position->video.size.width, + this->position->video.size.height, + this->position->video.stride); + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(this->position->video.stride * + this->position->video.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->position->video.stride)); + break; + } + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_debug(this->log, NAME " %p: clear buffers", this); + spa_vulkan_use_buffers(&this->state, &this->state.streams[0], 0, 0, NULL); + port->n_buffers = 0; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + this->started = false; + set_timer(this, false); + } + return 0; +} + +static int port_set_format(struct impl *this, struct port *port, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + spa_vulkan_unprepare(&this->state); + } else { + struct spa_video_info info = { 0 }; + + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return res; + + if (info.media_type != SPA_MEDIA_TYPE_video && + info.media_subtype != SPA_MEDIA_SUBTYPE_dsp) + return -EINVAL; + + if (spa_format_video_dsp_parse(format, &info.info.dsp) < 0) + return -EINVAL; + + if (info.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32) + return -EINVAL; + + this->state.constants.width = this->position->video.size.width; + this->state.constants.height = this->position->video.size.height; + + port->current_format = info; + port->have_format = true; + spa_vulkan_prepare(&this->state); + } + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (port->have_format) { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + struct port *port; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, port, flags, param); + break; + default: + return -ENOENT; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + clear_buffers(this, port); + + if (n_buffers > 0 && !port->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = 0; + b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + spa_list_append(&port->empty, &b->link); + } + spa_vulkan_use_buffers(&this->state, &this->state.streams[0], flags, n_buffers, buffers); + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(port_id == 0, -EINVAL); + port = &this->port; + spa_return_val_if_fail(buffer_id < port->n_buffers, -EINVAL); + + reuse_buffer(this, port, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + if ((io = port->io) == NULL) + return -EIO; + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < port->n_buffers) { + reuse_buffer(this, port, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (!this->props.live) + return make_buffer(this); + else + return SPA_STATUS_OK; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int do_remove_timer(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + struct impl *this = user_data; + spa_loop_remove_source(this->data_loop, &this->timer_source); + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (this->data_loop) + spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_system_close(this->data_system, this->timer_source.fd); + + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[1] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = 2; + reset_props(&this->props); + + this->timer_source.func = on_output; + this->timer_source.data = this; + this->timer_source.fd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, + SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + this->timer_source.mask = SPA_IO_IN; + this->timer_source.rmask = 0; + this->timerspec.it_value.tv_sec = 0; + this->timerspec.it_value.tv_nsec = 0; + this->timerspec.it_interval.tv_sec = 0; + this->timerspec.it_interval.tv_nsec = 0; + + if (this->data_loop) + spa_loop_add_source(this->data_loop, &this->timer_source); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS | + SPA_PORT_CHANGE_MASK_PROPS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + if (this->props.live) + port->info.flags |= SPA_PORT_FLAG_LIVE; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[4] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 5; + spa_list_init(&port->empty); + spa_list_init(&port->ready); + + this->state.log = this->log; + spa_vulkan_init_stream(&this->state, &this->state.streams[0], + SPA_DIRECTION_OUTPUT, NULL); + this->state.shaderName = "spa/plugins/vulkan/shaders/main.spv"; + this->state.n_streams = 1; + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans " }, + { SPA_KEY_FACTORY_DESCRIPTION, "Generate video frames using a vulkan compute shader" }, +}; + +static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); + +const struct spa_handle_factory spa_vulkan_compute_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_VULKAN_COMPUTE_SOURCE, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c new file mode 100644 index 0000000..ae3337b --- /dev/null +++ b/spa/plugins/vulkan/vulkan-utils.c @@ -0,0 +1,758 @@ +#include + +#include +#include +#include +#include +#include +#include +#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#include +#endif +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "vulkan-utils.h" + +//#define ENABLE_VALIDATION + +#define VULKAN_INSTANCE_FUNCTION(name) \ + PFN_##name name = (PFN_##name)vkGetInstanceProcAddr(s->instance, #name) + +static int vkresult_to_errno(VkResult result) +{ + switch (result) { + case VK_SUCCESS: + case VK_EVENT_SET: + case VK_EVENT_RESET: + return 0; + case VK_NOT_READY: + case VK_INCOMPLETE: + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: + return EBUSY; + case VK_TIMEOUT: + return ETIMEDOUT; + case VK_ERROR_OUT_OF_HOST_MEMORY: + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + case VK_ERROR_MEMORY_MAP_FAILED: + case VK_ERROR_OUT_OF_POOL_MEMORY: + case VK_ERROR_FRAGMENTED_POOL: +#ifdef VK_ERROR_FRAGMENTATION_EXT + case VK_ERROR_FRAGMENTATION_EXT: +#endif + return ENOMEM; + case VK_ERROR_INITIALIZATION_FAILED: + return EIO; + case VK_ERROR_DEVICE_LOST: + case VK_ERROR_SURFACE_LOST_KHR: +#ifdef VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT + case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: +#endif + return ENODEV; + case VK_ERROR_LAYER_NOT_PRESENT: + case VK_ERROR_EXTENSION_NOT_PRESENT: + case VK_ERROR_FEATURE_NOT_PRESENT: + return ENOENT; + case VK_ERROR_INCOMPATIBLE_DRIVER: + case VK_ERROR_FORMAT_NOT_SUPPORTED: + case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: + return ENOTSUP; + case VK_ERROR_TOO_MANY_OBJECTS: + return ENFILE; + case VK_SUBOPTIMAL_KHR: + case VK_ERROR_OUT_OF_DATE_KHR: + return EIO; + case VK_ERROR_INVALID_EXTERNAL_HANDLE: + case VK_ERROR_INVALID_SHADER_NV: +#ifdef VK_ERROR_VALIDATION_FAILED_EXT + case VK_ERROR_VALIDATION_FAILED_EXT: +#endif +#ifdef VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT + case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT: +#endif +#ifdef VK_ERROR_INVALID_DEVICE_ADDRESS_EXT + case VK_ERROR_INVALID_DEVICE_ADDRESS_EXT: +#endif + return EINVAL; +#ifdef VK_ERROR_NOT_PERMITTED_EXT + case VK_ERROR_NOT_PERMITTED_EXT: + return EPERM; +#endif + default: + return EIO; + } +} + +#define VK_CHECK_RESULT(f) \ +{ \ + VkResult _result = (f); \ + int _r = -vkresult_to_errno(_result); \ + if (_result != VK_SUCCESS) { \ + spa_log_error(s->log, "error: %d (%d %s)", _result, _r, spa_strerror(_r)); \ + return _r; \ + } \ +} +#define CHECK(f) \ +{ \ + int _res = (f); \ + if (_res < 0) \ + return _res; \ +} + +static int createInstance(struct vulkan_state *s) +{ + static const VkApplicationInfo applicationInfo = { + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName = "PipeWire", + .applicationVersion = 0, + .pEngineName = "PipeWire Vulkan Engine", + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_1 + }; + static const char * const extensions[] = { + VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME + }; + static const char * const checkLayers[] = { +#ifdef ENABLE_VALIDATION + "VK_LAYER_KHRONOS_validation", +#endif + NULL + }; + uint32_t i, j, layerCount, n_layers = 0; + const char *layers[1]; + vkEnumerateInstanceLayerProperties(&layerCount, NULL); + + VkLayerProperties availableLayers[layerCount]; + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers); + + for (i = 0; i < layerCount; i++) { + for (j = 0; j < SPA_N_ELEMENTS(checkLayers); j++) { + if (spa_streq(availableLayers[i].layerName, checkLayers[j])) + layers[n_layers++] = checkLayers[j]; + } + } + + const VkInstanceCreateInfo createInfo = { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &applicationInfo, + .enabledExtensionCount = 1, + .ppEnabledExtensionNames = extensions, + .enabledLayerCount = n_layers, + .ppEnabledLayerNames = layers, + }; + + VK_CHECK_RESULT(vkCreateInstance(&createInfo, NULL, &s->instance)); + + return 0; +} + +static uint32_t getComputeQueueFamilyIndex(struct vulkan_state *s) +{ + uint32_t i, queueFamilyCount; + VkQueueFamilyProperties *queueFamilies; + + vkGetPhysicalDeviceQueueFamilyProperties(s->physicalDevice, &queueFamilyCount, NULL); + + queueFamilies = alloca(queueFamilyCount * sizeof(VkQueueFamilyProperties)); + vkGetPhysicalDeviceQueueFamilyProperties(s->physicalDevice, &queueFamilyCount, queueFamilies); + + for (i = 0; i < queueFamilyCount; i++) { + VkQueueFamilyProperties props = queueFamilies[i]; + + if (props.queueCount > 0 && (props.queueFlags & VK_QUEUE_COMPUTE_BIT)) + break; + } + if (i == queueFamilyCount) + return -ENODEV; + + return i; +} + +static int findPhysicalDevice(struct vulkan_state *s) +{ + uint32_t deviceCount; + VkPhysicalDevice *devices; + + vkEnumeratePhysicalDevices(s->instance, &deviceCount, NULL); + if (deviceCount == 0) + return -ENODEV; + + devices = alloca(deviceCount * sizeof(VkPhysicalDevice)); + vkEnumeratePhysicalDevices(s->instance, &deviceCount, devices); + + s->physicalDevice = devices[0]; + + s->queueFamilyIndex = getComputeQueueFamilyIndex(s); + + return 0; +} + +static int createDevice(struct vulkan_state *s) +{ + + const VkDeviceQueueCreateInfo queueCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .queueFamilyIndex = s->queueFamilyIndex, + .queueCount = 1, + .pQueuePriorities = (const float[]) { 1.0f } + }; + static const char * const extensions[] = { + VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME, + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME + }; + const VkDeviceCreateInfo deviceCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &queueCreateInfo, + .enabledExtensionCount = 2, + .ppEnabledExtensionNames = extensions, + }; + + VK_CHECK_RESULT(vkCreateDevice(s->physicalDevice, &deviceCreateInfo, NULL, &s->device)); + + vkGetDeviceQueue(s->device, s->queueFamilyIndex, 0, &s->queue); + + static const VkFenceCreateInfo fenceCreateInfo = { + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .flags = 0, + }; + VK_CHECK_RESULT(vkCreateFence(s->device, &fenceCreateInfo, NULL, &s->fence)); + + return 0; +} + +static uint32_t findMemoryType(struct vulkan_state *s, + uint32_t memoryTypeBits, VkMemoryPropertyFlags properties) +{ + uint32_t i; + VkPhysicalDeviceMemoryProperties memoryProperties; + + vkGetPhysicalDeviceMemoryProperties(s->physicalDevice, &memoryProperties); + + for (i = 0; i < memoryProperties.memoryTypeCount; i++) { + if ((memoryTypeBits & (1 << i)) && + ((memoryProperties.memoryTypes[i].propertyFlags & properties) == properties)) + return i; + } + return -1; +} + +static int createDescriptors(struct vulkan_state *s) +{ + uint32_t i; + + VkDescriptorPoolSize descriptorPoolSizes[2] = { + { + .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + }, + { + .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = s->n_streams - 1, + }, + }; + const VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .maxSets = s->n_streams, + .poolSizeCount = s->n_streams > 1 ? 2 : 1, + .pPoolSizes = descriptorPoolSizes, + }; + + VK_CHECK_RESULT(vkCreateDescriptorPool(s->device, + &descriptorPoolCreateInfo, NULL, + &s->descriptorPool)); + + VkDescriptorSetLayoutBinding descriptorSetLayoutBinding[s->n_streams]; + descriptorSetLayoutBinding[0] = (VkDescriptorSetLayoutBinding) { + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT + }; + for (i = 1; i < s->n_streams; i++) { + descriptorSetLayoutBinding[i] = (VkDescriptorSetLayoutBinding) { + .binding = i, + .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT + }; + }; + const VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = s->n_streams, + .pBindings = descriptorSetLayoutBinding + }; + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(s->device, + &descriptorSetLayoutCreateInfo, NULL, + &s->descriptorSetLayout)); + + const VkDescriptorSetAllocateInfo descriptorSetAllocateInfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = s->descriptorPool, + .descriptorSetCount = 1, + .pSetLayouts = &s->descriptorSetLayout + }; + + VK_CHECK_RESULT(vkAllocateDescriptorSets(s->device, + &descriptorSetAllocateInfo, + &s->descriptorSet)); + + const VkSamplerCreateInfo samplerInfo = { + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, + .unnormalizedCoordinates = VK_FALSE, + .compareEnable = VK_FALSE, + .compareOp = VK_COMPARE_OP_ALWAYS, + .mipLodBias = 0.0f, + .minLod = 0, + .maxLod = 5, + }; + VK_CHECK_RESULT(vkCreateSampler(s->device, &samplerInfo, NULL, &s->sampler)); + + return 0; +} + +static int updateDescriptors(struct vulkan_state *s) +{ + uint32_t i; + VkDescriptorImageInfo descriptorImageInfo[s->n_streams]; + VkWriteDescriptorSet writeDescriptorSet[s->n_streams]; + + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + if (p->current_buffer_id == p->pending_buffer_id || + p->pending_buffer_id == SPA_ID_INVALID) + continue; + + p->current_buffer_id = p->pending_buffer_id; + p->busy_buffer_id = p->current_buffer_id; + p->pending_buffer_id = SPA_ID_INVALID; + + descriptorImageInfo[i] = (VkDescriptorImageInfo) { + .sampler = s->sampler, + .imageView = p->buffers[p->current_buffer_id].view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + writeDescriptorSet[i] = (VkWriteDescriptorSet) { + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = s->descriptorSet, + .dstBinding = i, + .descriptorCount = 1, + .descriptorType = i == 0 ? + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .pImageInfo = &descriptorImageInfo[i], + }; + } + vkUpdateDescriptorSets(s->device, s->n_streams, + writeDescriptorSet, 0, NULL); + + return 0; +} + +static VkShaderModule createShaderModule(struct vulkan_state *s, const char* shaderFile) +{ + VkShaderModule shaderModule = VK_NULL_HANDLE; + VkResult result; + void *data; + int fd; + struct stat stat; + + if ((fd = open(shaderFile, 0, O_RDONLY)) == -1) { + spa_log_error(s->log, "can't open %s: %m", shaderFile); + return VK_NULL_HANDLE; + } + if (fstat(fd, &stat) < 0) { + spa_log_error(s->log, "can't stat %s: %m", shaderFile); + close(fd); + return VK_NULL_HANDLE; + } + + data = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + + const VkShaderModuleCreateInfo shaderModuleCreateInfo = { + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .codeSize = stat.st_size, + .pCode = data, + }; + result = vkCreateShaderModule(s->device, + &shaderModuleCreateInfo, 0, &shaderModule); + + munmap(data, stat.st_size); + close(fd); + + if (result != VK_SUCCESS) { + spa_log_error(s->log, "can't create shader %s: %m", shaderFile); + return VK_NULL_HANDLE; + } + return shaderModule; +} + +static int createComputePipeline(struct vulkan_state *s, const char *shader_file) +{ + static const VkPushConstantRange range = { + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(struct push_constants) + }; + + const VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = &s->descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + VK_CHECK_RESULT(vkCreatePipelineLayout(s->device, + &pipelineLayoutCreateInfo, NULL, + &s->pipelineLayout)); + + s->computeShaderModule = createShaderModule(s, shader_file); + if (s->computeShaderModule == VK_NULL_HANDLE) + return -ENOENT; + + const VkPipelineShaderStageCreateInfo shaderStageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = s->computeShaderModule, + .pName = "main", + }; + const VkComputePipelineCreateInfo pipelineCreateInfo = { + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = shaderStageCreateInfo, + .layout = s->pipelineLayout, + }; + VK_CHECK_RESULT(vkCreateComputePipelines(s->device, VK_NULL_HANDLE, + 1, &pipelineCreateInfo, NULL, + &s->pipeline)); + return 0; +} + +static int createCommandBuffer(struct vulkan_state *s) +{ + const VkCommandPoolCreateInfo commandPoolCreateInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = s->queueFamilyIndex, + }; + VK_CHECK_RESULT(vkCreateCommandPool(s->device, + &commandPoolCreateInfo, NULL, + &s->commandPool)); + + const VkCommandBufferAllocateInfo commandBufferAllocateInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = s->commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VK_CHECK_RESULT(vkAllocateCommandBuffers(s->device, + &commandBufferAllocateInfo, + &s->commandBuffer)); + + return 0; +} + +static int runCommandBuffer(struct vulkan_state *s) +{ + static const VkCommandBufferBeginInfo beginInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + }; + VK_CHECK_RESULT(vkBeginCommandBuffer(s->commandBuffer, &beginInfo)); + + VkImageMemoryBarrier barrier[s->n_streams]; + uint32_t i; + + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + + barrier[i]= (VkImageMemoryBarrier) { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = 0, + .dstAccessMask = 0, + .image = p->buffers[p->current_buffer_id].image, + }; + } + + vkCmdPipelineBarrier(s->commandBuffer, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, 0, NULL, 0, NULL, + s->n_streams, barrier); + + vkCmdBindPipeline(s->commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, s->pipeline); + vkCmdPushConstants (s->commandBuffer, + s->pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, + 0, sizeof(struct push_constants), (const void *) &s->constants); + vkCmdBindDescriptorSets(s->commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + s->pipelineLayout, 0, 1, &s->descriptorSet, 0, NULL); + + vkCmdDispatch(s->commandBuffer, + (uint32_t)ceil(s->constants.width / (float)WORKGROUP_SIZE), + (uint32_t)ceil(s->constants.height / (float)WORKGROUP_SIZE), 1); + + VK_CHECK_RESULT(vkEndCommandBuffer(s->commandBuffer)); + + VK_CHECK_RESULT(vkResetFences(s->device, 1, &s->fence)); + + const VkSubmitInfo submitInfo = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .commandBufferCount = 1, + .pCommandBuffers = &s->commandBuffer, + }; + VK_CHECK_RESULT(vkQueueSubmit(s->queue, 1, &submitInfo, s->fence)); + s->started = true; + + return 0; +} + +static void clear_buffers(struct vulkan_state *s, struct vulkan_stream *p) +{ + uint32_t i; + + for (i = 0; i < p->n_buffers; i++) { + if (p->buffers[i].fd != -1) + close(p->buffers[i].fd); + vkFreeMemory(s->device, p->buffers[i].memory, NULL); + vkDestroyImage(s->device, p->buffers[i].image, NULL); + vkDestroyImageView(s->device, p->buffers[i].view, NULL); + } + p->n_buffers = 0; +} + +static void clear_streams(struct vulkan_state *s) +{ + uint32_t i; + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + clear_buffers(s, p); + } +} + +int spa_vulkan_use_buffers(struct vulkan_state *s, struct vulkan_stream *p, uint32_t flags, + uint32_t n_buffers, struct spa_buffer **buffers) +{ + uint32_t i; + VULKAN_INSTANCE_FUNCTION(vkGetMemoryFdKHR); + + clear_buffers(s, p); + + for (i = 0; i < n_buffers; i++) { + VkExternalMemoryImageCreateInfo extInfo; + VkImageCreateInfo imageCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_R32G32B32A32_SFLOAT, + .extent.width = s->constants.width, + .extent.height = s->constants.height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_LINEAR, + .usage = p->direction == SPA_DIRECTION_OUTPUT ? + VK_IMAGE_USAGE_STORAGE_BIT: + VK_IMAGE_USAGE_SAMPLED_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + + if (!(flags & SPA_NODE_BUFFERS_FLAG_ALLOC)) { + extInfo = (VkExternalMemoryImageCreateInfo) { + .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + imageCreateInfo.pNext = &extInfo; + } + + VK_CHECK_RESULT(vkCreateImage(s->device, + &imageCreateInfo, NULL, &p->buffers[i].image)); + + VkMemoryRequirements memoryRequirements; + vkGetImageMemoryRequirements(s->device, + p->buffers[i].image, &memoryRequirements); + + VkMemoryAllocateInfo allocateInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memoryRequirements.size, + .memoryTypeIndex = findMemoryType(s, + memoryRequirements.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), + }; + + if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { + VK_CHECK_RESULT(vkAllocateMemory(s->device, + &allocateInfo, NULL, &p->buffers[i].memory)); + + const VkMemoryGetFdInfoKHR getFdInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, + .memory = p->buffers[i].memory, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT + }; + int fd; + + VK_CHECK_RESULT(vkGetMemoryFdKHR(s->device, &getFdInfo, &fd)); + + spa_log_info(s->log, "export DMABUF %zd", memoryRequirements.size); + +// buffers[i]->datas[0].type = SPA_DATA_DmaBuf; + buffers[i]->datas[0].type = SPA_DATA_MemFd; + buffers[i]->datas[0].fd = fd; + buffers[i]->datas[0].flags = SPA_DATA_FLAG_READABLE; + buffers[i]->datas[0].mapoffset = 0; + buffers[i]->datas[0].maxsize = memoryRequirements.size; + p->buffers[i].fd = fd; + } else { + VkImportMemoryFdInfoKHR importInfo = { + .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + .fd = fcntl(buffers[i]->datas[0].fd, F_DUPFD_CLOEXEC, 0), + }; + allocateInfo.pNext = &importInfo; + p->buffers[i].fd = -1; + spa_log_info(s->log, "import DMABUF"); + + VK_CHECK_RESULT(vkAllocateMemory(s->device, + &allocateInfo, NULL, &p->buffers[i].memory)); + } + VK_CHECK_RESULT(vkBindImageMemory(s->device, + p->buffers[i].image, p->buffers[i].memory, 0)); + + VkImageViewCreateInfo viewInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = p->buffers[i].image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = VK_FORMAT_R32G32B32A32_SFLOAT, + .components.r = VK_COMPONENT_SWIZZLE_R, + .components.g = VK_COMPONENT_SWIZZLE_G, + .components.b = VK_COMPONENT_SWIZZLE_B, + .components.a = VK_COMPONENT_SWIZZLE_A, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + }; + + VK_CHECK_RESULT(vkCreateImageView(s->device, + &viewInfo, NULL, &p->buffers[i].view)); + } + p->n_buffers = n_buffers; + + return 0; +} + +int spa_vulkan_init_stream(struct vulkan_state *s, struct vulkan_stream *stream, + enum spa_direction direction, struct spa_dict *props) +{ + spa_zero(*stream); + stream->direction = direction; + stream->current_buffer_id = SPA_ID_INVALID; + stream->busy_buffer_id = SPA_ID_INVALID; + stream->ready_buffer_id = SPA_ID_INVALID; + return 0; +} + +int spa_vulkan_prepare(struct vulkan_state *s) +{ + if (!s->prepared) { + CHECK(createInstance(s)); + CHECK(findPhysicalDevice(s)); + CHECK(createDevice(s)); + CHECK(createDescriptors(s)); + CHECK(createComputePipeline(s, s->shaderName)); + CHECK(createCommandBuffer(s)); + s->prepared = true; + } + return 0; +} + +int spa_vulkan_unprepare(struct vulkan_state *s) +{ + if (s->prepared) { + vkDestroyShaderModule(s->device, s->computeShaderModule, NULL); + vkDestroySampler(s->device, s->sampler, NULL); + vkDestroyDescriptorPool(s->device, s->descriptorPool, NULL); + vkDestroyDescriptorSetLayout(s->device, s->descriptorSetLayout, NULL); + vkDestroyPipelineLayout(s->device, s->pipelineLayout, NULL); + vkDestroyPipeline(s->device, s->pipeline, NULL); + vkDestroyCommandPool(s->device, s->commandPool, NULL); + vkDestroyFence(s->device, s->fence, NULL); + vkDestroyDevice(s->device, NULL); + vkDestroyInstance(s->instance, NULL); + s->prepared = false; + } + return 0; +} + +int spa_vulkan_start(struct vulkan_state *s) +{ + uint32_t i; + + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + p->current_buffer_id = SPA_ID_INVALID; + p->busy_buffer_id = SPA_ID_INVALID; + p->ready_buffer_id = SPA_ID_INVALID; + } + return 0; +} + +int spa_vulkan_stop(struct vulkan_state *s) +{ + VK_CHECK_RESULT(vkDeviceWaitIdle(s->device)); + clear_streams(s); + s->started = false; + return 0; +} + +int spa_vulkan_ready(struct vulkan_state *s) +{ + uint32_t i; + VkResult result; + + if (!s->started) + return 0; + + result = vkGetFenceStatus(s->device, s->fence); + if (result == VK_NOT_READY) + return -EBUSY; + VK_CHECK_RESULT(result); + + s->started = false; + + for (i = 0; i < s->n_streams; i++) { + struct vulkan_stream *p = &s->streams[i]; + p->ready_buffer_id = p->busy_buffer_id; + p->busy_buffer_id = SPA_ID_INVALID; + } + return 0; +} + +int spa_vulkan_process(struct vulkan_state *s) +{ + CHECK(updateDescriptors(s)); + CHECK(runCommandBuffer(s)); + VK_CHECK_RESULT(vkDeviceWaitIdle(s->device)); + + return 0; +} diff --git a/spa/plugins/vulkan/vulkan-utils.h b/spa/plugins/vulkan/vulkan-utils.h new file mode 100644 index 0000000..c818322 --- /dev/null +++ b/spa/plugins/vulkan/vulkan-utils.h @@ -0,0 +1,86 @@ +#include + +#include +#include + +#define MAX_STREAMS 2 +#define MAX_BUFFERS 16 +#define WORKGROUP_SIZE 32 + +struct pixel { + float r, g, b, a; +}; + +struct push_constants { + float time; + int frame; + int width; + int height; +}; + +struct vulkan_buffer { + int fd; + VkImage image; + VkImageView view; + VkDeviceMemory memory; +}; + +struct vulkan_stream { + enum spa_direction direction; + + uint32_t pending_buffer_id; + uint32_t current_buffer_id; + uint32_t busy_buffer_id; + uint32_t ready_buffer_id; + + struct vulkan_buffer buffers[MAX_BUFFERS]; + uint32_t n_buffers; +}; + +struct vulkan_state { + struct spa_log *log; + + struct push_constants constants; + + VkInstance instance; + + VkPhysicalDevice physicalDevice; + VkDevice device; + + VkPipeline pipeline; + VkPipelineLayout pipelineLayout; + const char *shaderName; + VkShaderModule computeShaderModule; + + VkCommandPool commandPool; + VkCommandBuffer commandBuffer; + + VkQueue queue; + uint32_t queueFamilyIndex; + VkFence fence; + unsigned int prepared:1; + unsigned int started:1; + + VkDescriptorPool descriptorPool; + VkDescriptorSetLayout descriptorSetLayout; + + VkSampler sampler; + + uint32_t n_streams; + VkDescriptorSet descriptorSet; + struct vulkan_stream streams[MAX_STREAMS]; +}; + +int spa_vulkan_init_stream(struct vulkan_state *s, struct vulkan_stream *stream, enum spa_direction, + struct spa_dict *props); + +int spa_vulkan_prepare(struct vulkan_state *s); +int spa_vulkan_use_buffers(struct vulkan_state *s, struct vulkan_stream *stream, uint32_t flags, + uint32_t n_buffers, struct spa_buffer **buffers); +int spa_vulkan_unprepare(struct vulkan_state *s); + +int spa_vulkan_start(struct vulkan_state *s); +int spa_vulkan_stop(struct vulkan_state *s); +int spa_vulkan_ready(struct vulkan_state *s); +int spa_vulkan_process(struct vulkan_state *s); +int spa_vulkan_cleanup(struct vulkan_state *s); diff --git a/spa/tests/benchmark-dict.c b/spa/tests/benchmark-dict.c new file mode 100644 index 0000000..c47845e --- /dev/null +++ b/spa/tests/benchmark-dict.c @@ -0,0 +1,145 @@ +/* Spa + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..8af1a5f --- /dev/null +++ b/spa/tests/benchmark-pod.c @@ -0,0 +1,294 @@ +/* Spa + * + * Copyright © 2019 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..6a7e98f --- /dev/null +++ b/spa/tests/stress-ringbuffer.c @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define DEFAULT_SIZE 0x2000 +#define ARRAY_SIZE 63 +#define MAX_VALUE 0x10000 + +#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#include +#if (__FreeBSD_version >= 1400000 && __FreeBSD_version < 1400043) \ + || (__FreeBSD_version < 1300523) || defined(__MidnightBSD__) +static int sched_getcpu(void) { return -1; }; +#endif +#endif + +static struct spa_ringbuffer rb; +static uint32_t size; +static void *data; +static sem_t sem; + +static int fill_int_array(int *array, int start, int count) +{ + int i, j = start; + for (i = 0; i < count; i++) { + array[i] = j; + j = (j + 1) % MAX_VALUE; + } + return j; +} + +static int cmp_array(int *array1, int *array2, int count) +{ + int i; + for (i = 0; i < count; i++) + if (array1[i] != array2[i]) { + printf("%d != %d at offset %d\n", array1[i], array2[i], i); + return 0; + } + + return 1; +} + +static void *reader_start(void *arg) +{ + int i = 0, a[ARRAY_SIZE], b[ARRAY_SIZE]; + + printf("reader started on cpu: %d\n", sched_getcpu()); + + i = fill_int_array(a, i, ARRAY_SIZE); + + while (1) { + uint32_t index; + int32_t avail; + + avail = spa_ringbuffer_get_read_index(&rb, &index); + + if (avail >= (int32_t)(sizeof(b))) { + spa_ringbuffer_read_data(&rb, data, size, index % size, b, sizeof(b)); + spa_ringbuffer_read_update(&rb, index + sizeof(b)); + + if (index >= INT32_MAX - sizeof(a)) + break; + + spa_assert(cmp_array(a, b, ARRAY_SIZE)); + i = fill_int_array(a, i, ARRAY_SIZE); + } + } + sem_post(&sem); + + return NULL; +} + +static void *writer_start(void *arg) +{ + int i = 0, a[ARRAY_SIZE]; + printf("writer started on cpu: %d\n", sched_getcpu()); + + i = fill_int_array(a, i, ARRAY_SIZE); + + while (1) { + uint32_t index; + int32_t avail; + + avail = size - spa_ringbuffer_get_write_index(&rb, &index); + + if (avail >= (int32_t)(sizeof(a))) { + spa_ringbuffer_write_data(&rb, data, size, index % size, a, sizeof(a)); + spa_ringbuffer_write_update(&rb, index + sizeof(a)); + + if (index >= INT32_MAX - sizeof(a)) + break; + + i = fill_int_array(a, i, ARRAY_SIZE); + } + } + sem_post(&sem); + + return NULL; +} + +#define exit_error(msg) \ +do { perror(msg); exit(EXIT_FAILURE); } while (0) + +int main(int argc, char *argv[]) +{ + pthread_t reader_thread, writer_thread; + struct timespec ts; + + printf("starting ringbuffer stress test\n"); + + if (argc > 1) + sscanf(argv[1], "%d", &size); + else + size = DEFAULT_SIZE; + + printf("buffer size (bytes): %d\n", size); + printf("array size (bytes): %zd\n", sizeof(int) * ARRAY_SIZE); + + spa_ringbuffer_init(&rb); + data = malloc(size); + + if (sem_init(&sem, 0, 0) != 0) + exit_error("init_sem"); + + pthread_create(&reader_thread, NULL, reader_start, NULL); + pthread_create(&writer_thread, NULL, writer_start, NULL); + + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) + exit_error("clock_gettime"); + + ts.tv_sec += 2; + + while (sem_timedwait(&sem, &ts) == -1 && errno == EINTR) + continue; + while (sem_timedwait(&sem, &ts) == -1 && errno == EINTR) + continue; + + printf("read %u, written %u\n", rb.readindex, rb.writeindex); + + return 0; +} diff --git a/spa/tools/meson.build b/spa/tools/meson.build new file mode 100644 index 0000000..6f12e9c --- /dev/null +++ b/spa/tools/meson.build @@ -0,0 +1,11 @@ +executable('spa-inspect', 'spa-inspect.c', + dependencies : [ spa_dep, dl_lib ], + install : true) + +executable('spa-monitor', 'spa-monitor.c', + dependencies : [ spa_dep, dl_lib ], + install : true) + +executable('spa-json-dump', 'spa-json-dump.c', + dependencies : [ spa_dep, dl_lib, ], + install : true) diff --git a/spa/tools/spa-inspect.c b/spa/tools/spa-inspect.c new file mode 100644 index 0000000..4e79104 --- /dev/null +++ b/spa/tools/spa-inspect.c @@ -0,0 +1,319 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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..ae0c67d --- /dev/null +++ b/spa/tools/spa-json-dump.c @@ -0,0 +1,165 @@ +/* Simple Plugin API + * + * Copyright © 2021 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; + int count = 0; + char key[1024]; + + if (spa_json_is_array(value, len)) { + fprintf(file, "["); + spa_json_enter(it, &sub); + while ((len = spa_json_next(&sub, &value)) > 0) { + fprintf(file, "%s\n%*s", count++ > 0 ? "," : "", + indent+2, ""); + dump(file, indent+2, &sub, value, len); + } + fprintf(file, "%s%*s]", count > 0 ? "\n" : "", + count > 0 ? indent : 0, ""); + } else if (spa_json_is_object(value, len)) { + fprintf(file, "{"); + spa_json_enter(it, &sub); + while (spa_json_get_string(&sub, key, sizeof(key)) > 0) { + fprintf(file, "%s\n%*s", + count++ > 0 ? "," : "", + indent+2, ""); + encode_string(file, key, strlen(key)); + fprintf(file, ": "); + if ((len = spa_json_next(&sub, &value)) <= 0) + break; + dump(file, indent+2, &sub, value, len); + } + fprintf(file, "%s%*s}", count > 0 ? "\n" : "", + count > 0 ? indent : 0, ""); + } else if (spa_json_is_string(value, len) || + spa_json_is_null(value, len) || + spa_json_is_bool(value, len) || + spa_json_is_int(value, len) || + spa_json_is_float(value, len)) { + fprintf(file, "%.*s", len, value); + } else { + encode_string(file, value, len); + } + return 0; +} + +int main(int argc, char *argv[]) +{ + int fd, len, res, exit_code = EXIT_FAILURE; + void *data; + struct stat sbuf; + struct spa_json it; + const char *value; + + if (argc < 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + goto error; + } + if ((fd = open(argv[1], O_CLOEXEC | O_RDONLY)) < 0) { + fprintf(stderr, "error opening file '%s': %m\n", argv[1]); + goto error; + } + if (fstat(fd, &sbuf) < 0) { + fprintf(stderr, "error statting file '%s': %m\n", argv[1]); + goto error_close; + } + if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) { + fprintf(stderr, "error mmapping file '%s': %m\n", argv[1]); + goto error_close; + } + + spa_json_init(&it, data, sbuf.st_size); + if ((len = spa_json_next(&it, &value)) <= 0) { + fprintf(stderr, "not a valid file '%s': %s\n", argv[1], spa_strerror(len)); + goto error_unmap; + } + if (!spa_json_is_container(value, len)) { + spa_json_init(&it, data, sbuf.st_size); + value = "{"; + len = 1; + } + if ((res = dump(stdout, 0, &it, value, len)) < 0) { + fprintf(stderr, "error parsing file '%s': %s\n", argv[1], spa_strerror(res)); + goto error_unmap; + } + fprintf(stdout, "\n"); + exit_code = EXIT_SUCCESS; + +error_unmap: + munmap(data, sbuf.st_size); +error_close: + close(fd); +error: + return exit_code; +} diff --git a/spa/tools/spa-monitor.c b/spa/tools/spa-monitor.c new file mode 100644 index 0000000..28159f3 --- /dev/null +++ b/spa/tools/spa-monitor.c @@ -0,0 +1,229 @@ +/* Simple Plugin API + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#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; +} -- cgit v1.2.3