summaryrefslogtreecommitdiffstats
path: root/spa/plugins/alsa
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:28:17 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:28:17 +0000
commit7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch)
treed483300dab478b994fe199a5d19d18d74153718a /spa/plugins/alsa
parentInitial commit. (diff)
downloadpipewire-upstream.tar.xz
pipewire-upstream.zip
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'spa/plugins/alsa')
-rw-r--r--spa/plugins/alsa/90-pipewire-alsa.rules214
-rw-r--r--spa/plugins/alsa/acp-tool.c786
-rw-r--r--spa/plugins/alsa/acp/acp.c1983
-rw-r--r--spa/plugins/alsa/acp/acp.h308
-rw-r--r--spa/plugins/alsa/acp/alsa-mixer.c5398
-rw-r--r--spa/plugins/alsa/acp/alsa-mixer.h459
-rw-r--r--spa/plugins/alsa/acp/alsa-ucm.c2420
-rw-r--r--spa/plugins/alsa/acp/alsa-ucm.h301
-rw-r--r--spa/plugins/alsa/acp/alsa-util.c1904
-rw-r--r--spa/plugins/alsa/acp/alsa-util.h176
-rw-r--r--spa/plugins/alsa/acp/array.h150
-rw-r--r--spa/plugins/alsa/acp/card.h75
-rw-r--r--spa/plugins/alsa/acp/channelmap.h476
-rw-r--r--spa/plugins/alsa/acp/compat.c210
-rw-r--r--spa/plugins/alsa/acp/compat.h656
-rw-r--r--spa/plugins/alsa/acp/conf-parser.c387
-rw-r--r--spa/plugins/alsa/acp/conf-parser.h88
-rw-r--r--spa/plugins/alsa/acp/device-port.h119
-rw-r--r--spa/plugins/alsa/acp/dynarray.h159
-rw-r--r--spa/plugins/alsa/acp/hashmap.h219
-rw-r--r--spa/plugins/alsa/acp/idxset.h200
-rw-r--r--spa/plugins/alsa/acp/llist.h109
-rw-r--r--spa/plugins/alsa/acp/meson.build22
-rw-r--r--spa/plugins/alsa/acp/proplist.h211
-rw-r--r--spa/plugins/alsa/acp/volume.h207
-rw-r--r--spa/plugins/alsa/alsa-acp-device.c1133
-rw-r--r--spa/plugins/alsa/alsa-compress-offload-sink.c1143
-rw-r--r--spa/plugins/alsa/alsa-pcm-device.c581
-rw-r--r--spa/plugins/alsa/alsa-pcm-sink.c1014
-rw-r--r--spa/plugins/alsa/alsa-pcm-source.c964
-rw-r--r--spa/plugins/alsa/alsa-pcm.c2696
-rw-r--r--spa/plugins/alsa/alsa-pcm.h374
-rw-r--r--spa/plugins/alsa/alsa-seq-bridge.c1006
-rw-r--r--spa/plugins/alsa/alsa-seq.c983
-rw-r--r--spa/plugins/alsa/alsa-seq.h199
-rw-r--r--spa/plugins/alsa/alsa-udev.c1014
-rw-r--r--spa/plugins/alsa/alsa.c80
-rw-r--r--spa/plugins/alsa/alsa.h39
-rw-r--r--spa/plugins/alsa/meson.build60
-rw-r--r--spa/plugins/alsa/mixer/meson.build7
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-aux.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf104
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-fm.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf104
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf102
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf114
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf133
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf154
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-linein.conf144
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf66
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic.conf141
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common60
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf107
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf65
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input-video.conf64
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input.conf102
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-input.conf.common289
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-chat.conf5
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf118
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-headphones.conf180
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-lineout.conf214
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-mono.conf99
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf187
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output-speaker.conf246
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output.conf88
-rw-r--r--spa/plugins/alsa/mixer/paths/analog-output.conf.common199
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-0.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-1.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-10.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-2.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-3.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-4.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-5.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-6.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-7.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-8.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/hdmi-output-9.conf12
-rw-r--r--spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf20
-rw-r--r--spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf18
-rw-r--r--spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf27
-rw-r--r--spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf27
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf34
-rw-r--r--spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf5
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/analog-only.conf102
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf93
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/audigy.conf94
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf66
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/default.conf580
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf55
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf153
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/force-speaker.conf152
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf35
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf36
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf38
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf86
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf90
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf161
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf110
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf84
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf130
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf53
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf91
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf80
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf112
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf58
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf42
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf23
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf75
-rw-r--r--spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf64
-rw-r--r--spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0150
-rw-r--r--spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x24
-rw-r--r--spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3135
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI4
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD198162
-rw-r--r--spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A113
-rw-r--r--spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A128
-rw-r--r--spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer27
-rw-r--r--spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer37
-rw-r--r--spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer5
-rw-r--r--spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888211
-rw-r--r--spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+160
-rw-r--r--spa/plugins/alsa/test-hw-params.c173
-rw-r--r--spa/plugins/alsa/test-timer.c310
125 files changed, 36278 insertions, 0 deletions
diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules
new file mode 100644
index 0000000..54e0244
--- /dev/null
+++ b/spa/plugins/alsa/90-pipewire-alsa.rules
@@ -0,0 +1,214 @@
+# do not edit this file, it will be overwritten on update
+
+# This file is part of PipeWire adapted from PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+SUBSYSTEM!="sound", GOTO="pipewire_end"
+ACTION!="change", GOTO="pipewire_end"
+KERNEL!="card*", GOTO="pipewire_end"
+SUBSYSTEMS=="usb", GOTO="pipewire_check_usb"
+SUBSYSTEMS=="pci", GOTO="pipewire_check_pci"
+SUBSYSTEMS=="firewire", GOTO="pipewire_firewire_quirk"
+
+SUBSYSTEMS=="platform", DRIVERS=="thinkpad_acpi", ENV{ACP_IGNORE}="1"
+
+# Force enable speaker and internal mic for some laptops
+# This should only be necessary for kernels 3.3, 3.4 and 3.5 (as they are lacking the phantom jack kctls).
+# Acer AOA150
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x015b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer Aspire 4810TZ
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x022a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Packard bell dot m/a
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x028c", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer Aspire 1810TZ
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x029b", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Acer AOD260 and AO532h
+ATTRS{subsystem_vendor}=="0x1025", ATTRS{subsystem_device}=="0x0349", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell MXC051
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01b5", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron 6400 and E1505
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01bd", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Latitude D620
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01c2", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D820
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01cc", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Latitude D420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x01d6", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 1525
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x022f", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 1011
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x02f4", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 14 (L401X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0468", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 15 (L501X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x046e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell XPS 15 (L502X)
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x050e", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Dell Inspiron 3420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0553", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron 3520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0555", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Vostro 2420
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0556", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Vostro 2520
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0558", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+# Dell Inspiron One 2020
+ATTRS{subsystem_vendor}=="0x1028", ATTRS{subsystem_device}=="0x0579", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Asus 904HA (1000H)
+ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x831a", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Asus T101MT
+ATTRS{subsystem_vendor}=="0x1043", ATTRS{subsystem_device}=="0x83ce", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Sony Vaio VGN-SR21M
+ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9033", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Sony Vaio VPC-W115XG
+ATTRS{subsystem_vendor}=="0x104d", ATTRS{subsystem_device}=="0x9064", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Fujitsu Lifebook S7110
+ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1397", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Fujitsu Lifebook A530
+ATTRS{subsystem_vendor}=="0x10cf", ATTRS{subsystem_device}=="0x1531", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Toshiba A200
+ATTRS{subsystem_vendor}=="0x1179", ATTRS{subsystem_device}=="0xff00", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# MSI X360
+ATTRS{subsystem_vendor}=="0x1462", ATTRS{subsystem_device}=="0x1053", ENV{ACP_PROFILE_SET}="force-speaker-and-int-mic.conf"
+# Lenovo 3000 Y410
+ATTRS{subsystem_vendor}=="0x17aa", ATTRS{subsystem_device}=="0x384e", ENV{ACP_PROFILE_SET}="force-speaker.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_check_usb"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1978", ENV{ACP_PROFILE_SET}="native-instruments-audio8dj.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="0839", ENV{ACP_PROFILE_SET}="native-instruments-audio4dj.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="baff", ENV{ACP_PROFILE_SET}="native-instruments-traktorkontrol-s4.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="4711", ENV{ACP_PROFILE_SET}="native-instruments-korecontroller.conf"
+
+# This ID 17cc:041c is verified for the older Audio 2 DJ model (pre-2014 ish).
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041c", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="041d", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio2.conf"
+
+# There appear to be two IDs in use for Traktor Audio 6 (or maybe 17cc:1011
+# is just incorrect - 17cc:1010 has been verified to be correct at least
+# for some hardware).
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1010", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1011", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio6.conf"
+
+
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1001", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf"
+# This entry is for the Komplete Audio 6 MK2, which has a different ID, but is functionally identical to the Komplete Audio 6.
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1870", ENV{ACP_PROFILE_SET}="native-instruments-komplete-audio6.conf"
+ATTRS{idVendor}=="17cc", ATTRS{idProduct}=="1021", ENV{ACP_PROFILE_SET}="native-instruments-traktor-audio10.conf"
+ATTRS{idVendor}=="0763", ATTRS{idProduct}=="2012", ENV{ACP_PROFILE_SET}="maudio-fasttrack-pro.conf"
+ATTRS{idVendor}=="045e", ATTRS{idProduct}=="02bb", ENV{ACP_PROFILE_SET}="kinect-audio.conf"
+ATTRS{idVendor}=="041e", ATTRS{idProduct}=="322c", ENV{ACP_PROFILE_SET}="sb-omni-surround-5.1.conf"
+ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="4014", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf"
+ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="402e", ENV{ACP_PROFILE_SET}="dell-dock-tb16-usb-audio.conf"
+ATTRS{idVendor}=="08bb", ATTRS{idProduct}=="2902", ENV{ACP_PROFILE_SET}="texas-instruments-pcm2902.conf"
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0269", ENV{ACP_PROFILE_SET}="hp-tbt-dock-120w-g2.conf"
+ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0567", ENV{ACP_PROFILE_SET}="hp-tbt-dock-audio-module.conf"
+
+# ID 1038:12ad is for the 2018 refresh of the Arctis 7.
+# ID 1038:1294 is for Arctis Pro Wireless (which works with the Arctis 7 configuration).
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1260", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12ad", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1294", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1730", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1038:1282 is for SteelSeries GameDAC
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1282", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1038:12c4 is for Arctis 9
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12c4", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# Lucidsound LS31
+ATTRS{idVendor}=="2f12", ATTRS{idProduct}=="0109", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 9886:002c is for the Astro A50 Gen4
+ATTRS{idVendor}=="9886", ATTRS{idProduct}=="002c", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 9886:0045 is for the Astro A20 Gen2
+ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0045", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# ID 1532:0520 is for the Razer Kraken Tournament Edition
+ATTRS{idVendor}=="1532", ATTRS{idProduct}=="0520", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+
+
+# ID 1038:1250 is for the Arctis 5
+# ID 1037:12aa is for the Arctis 5 2019
+# ID 1038:1252 is for the Arctis Pro 2019 edition
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1250", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="12aa", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+ATTRS{idVendor}=="1038", ATTRS{idProduct}=="1252", ENV{ACP_PROFILE_SET}="steelseries-arctis-common-usb-audio.conf"
+
+ATTRS{idVendor}=="147a", ATTRS{idProduct}=="e055", ENV{ACP_PROFILE_SET}="cmedia-high-speed-true-hdaudio.conf"
+
+# HyperX Cloud Orbit S has three modes. Each mode has a separate product ID.
+# ID_SERIAL for this device is the device name + mode repeated three times.
+# ID_SERIAL is used for the ID_ID property, and the ID_ID property is used in
+# the card name in PulseAudio. The resulting card name is too long for the name
+# length limit, so we set a more sensible ID_ID here (the same as the default
+# ID_ID, but without repetition in the serial part).
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="16ff", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_2Ch-$env{ID_USB_INTERFACE_NUM}"
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1702", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_Hi-Res_2Ch-$env{ID_USB_INTERFACE_NUM}"
+ATTRS{idVendor}=="0951", ATTRS{idProduct}=="1703", ENV{ID_ID}="usb-HyperX_Cloud_Orbit_S_3D_8Ch-$env{ID_USB_INTERFACE_NUM}"
+
+# OnePlus Type-C Bullets (ED117)
+ATTRS{idVendor}=="2a70", ATTRS{idProduct}=="1881", ENV{ACP_PROFILE_SET}="simple-headphones-mic.conf"
+
+# ID 1395:005e is for Sennheiser GSX 1000
+# ID 1395:00a0 is for Sennheiser GSX 1000
+# ID 1395:00b1 is for Sennheiser GSX 1000 v2
+# ID 1395:005f is for Sennheiser GSX 1200
+# ID 1395:00a1 is for Sennheiser GSX 1200
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005e", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a0", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00b1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="005f", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="00a1", ENV{ACP_PROFILE_SET}="sennheiser-gsx.conf"
+
+# Sennheiser GSA 70 wireless USB dongle for GSP 670
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0089", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# EPOS GSA 70 wireless USB dongle for GSP 670 (Sennheiser GSA 70 with updated firmware)
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="0300", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+# Sennheiser GSP 670 USB headset
+ATTRS{idVendor}=="1395", ATTRS{idProduct}=="008a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf"
+
+# Audioengine HD3 powered speakers support IEC958 but don't actually
+# have any digital outputs.
+ATTRS{idVendor}=="0a12", ATTRS{idProduct}=="4007", ENV{ACP_PROFILE_SET}="analog-only.conf"
+
+# Asus Xonar SE
+ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="189d", ENV{ACP_PROFILE_SET}="asus-xonar-se.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_check_pci"
+
+# Creative SoundBlaster Audigy-based cards
+# EMU10k2/CA0100/CA0102/CA10200
+ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0004", ENV{ACP_PROFILE_SET}="audigy.conf"
+# CA0108/CA10300
+ATTRS{vendor}=="0x1102", ATTRS{device}=="0x0008", ENV{ACP_PROFILE_SET}="audigy.conf"
+
+GOTO="pipewire_end"
+
+LABEL="pipewire_firewire_quirk"
+
+# Focusrite Saffire Pro 10 i/o and Pro 26 i/o have a quirk to disappear from
+# IEEE 1394 bus when breaking connections. This brings an issue for PulseAudio
+# to continue the routine for ever that:
+# - detecting sound card
+# - starting PCM substream
+# - stopping the PCM substream
+# - the card disappears
+# - the card appears
+# In detail, see: https://bugzilla.kernel.org/show_bug.cgi?id=199365
+ATTRS{vendor}=="0x00130e", ATTRS{model}=="0x00000[36]", ATTRS{units}=="0x00a02d:0x010001", ENV{ACP_IGNORE}="1"
+
+LABEL="pipewire_end"
diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c
new file mode 100644
index 0000000..0fc92a4
--- /dev/null
+++ b/spa/plugins/alsa/acp-tool.c
@@ -0,0 +1,786 @@
+/* ALSA card profile test
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <stdbool.h>
+#include <getopt.h>
+
+#include <spa/utils/string.h>
+
+#include <acp/acp.h>
+
+#define WHITESPACE "\n\r\t "
+
+struct data {
+ int verbose;
+ int card_index;
+ char *properties;
+ struct acp_card *card;
+ bool quit;
+};
+
+static void acp_debug_dict(struct acp_dict *dict, int indent)
+{
+ const struct acp_dict_item *it;
+ fprintf(stderr, "%*sproperties: (%d)\n", indent, "", dict->n_items);
+ acp_dict_for_each(it, dict) {
+ fprintf(stderr, "%*s%s = \"%s\"\n", indent+4, "", it->key, it->value);
+ }
+}
+
+static char *split_walk(char *str, const char *delimiter, size_t *len, char **state)
+{
+ char *s = *state ? *state : str;
+
+ if (*s == '\0')
+ return NULL;
+
+ *len = strcspn(s, delimiter);
+ *state = s + *len;
+ *state += strspn(*state, delimiter);
+ return s;
+}
+
+static int split_ip(char *str, const char *delimiter, int max_tokens, char *tokens[])
+{
+ char *state = NULL, *s;
+ size_t len;
+ int n = 0;
+
+ while (true) {
+ if ((s = split_walk(str, delimiter, &len, &state)) == NULL)
+ break;
+ tokens[n++] = s;
+ if (n >= max_tokens)
+ break;
+ s[len] = '\0';
+ }
+ return n;
+}
+
+
+static void card_props_changed(void *data)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ fprintf(stderr, "*** properties changed:\n");
+ acp_debug_dict(&card->props, 4);
+ fprintf(stderr, "***\n");
+}
+
+static void card_profile_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_card_profile *op = card->profiles[old_index];
+ struct acp_card_profile *np = card->profiles[new_index];
+ fprintf(stderr, "*** profile changed from %s to %s\n", op->name, np->name);
+}
+
+static void card_profile_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_card_profile *p = card->profiles[index];
+ fprintf(stderr, "*** profile %s available %s\n", p->name, acp_available_str(available));
+}
+
+static void card_port_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct data *d = data;
+ struct acp_card *card = d->card;
+ struct acp_port *p = card->ports[index];
+ fprintf(stderr, "*** port %s available %s\n", p->name, acp_available_str(available));
+}
+
+static void on_volume_changed(void *data, struct acp_device *dev)
+{
+ float vol;
+ acp_device_get_volume(dev, &vol, 1);
+ fprintf(stderr, "*** volume %s changed to %f\n", dev->name, vol);
+}
+
+static void on_mute_changed(void *data, struct acp_device *dev)
+{
+ bool mute;
+ acp_device_get_mute(dev, &mute);
+ fprintf(stderr, "*** mute %s changed to %d\n", dev->name, mute);
+}
+
+static const struct acp_card_events card_events = {
+ ACP_VERSION_CARD_EVENTS,
+ .props_changed = card_props_changed,
+ .profile_changed = card_profile_changed,
+ .profile_available = card_profile_available,
+ .port_available = card_port_available,
+ .volume_changed = on_volume_changed,
+ .mute_changed = on_mute_changed,
+};
+
+static ACP_PRINTF_FUNC(6,0) void log_func(void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg)
+{
+ vfprintf(stderr, fmt, arg);
+ fprintf(stderr, "\n");
+}
+
+static void show_prompt(struct data *data)
+{
+ fprintf(stderr, ">>>");
+}
+
+struct command {
+ const char *name;
+ const char *args;
+ const char *alias;
+ const char *description;
+ int (*func) (struct data *data, const struct command *cmd, int argc, char *argv[]);
+ void *extra;
+};
+
+static int cmd_help(struct data *data, const struct command *cmd, int argc, char *argv[]);
+
+static int cmd_quit(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ data->quit = true;
+ return 0;
+}
+
+static void print_profile(struct data *data, struct acp_card_profile *p, int indent, int level);
+static void print_device(struct data *data, struct acp_device *d, int indent, int level);
+
+static void print_port(struct data *data, struct acp_port *p, int indent, int level)
+{
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c port %u: name:\"%s\" direction:%s prio:%d (available: %s)\n",
+ indent, "", p->flags & ACP_PORT_ACTIVE ? '*' : ' ', p->index,
+ p->name, acp_direction_str(p->direction), p->priority,
+ acp_available_str(p->available));
+ if (level > 0) {
+ acp_debug_dict(&p->props, indent + 8);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sprofiles: (%d)\n", indent+8, "", p->n_profiles);
+ for (i = 0; i < p->n_profiles; i++) {
+ struct acp_card_profile *pr = p->profiles[i];
+ print_profile(data, pr, indent + 8, 0);
+ }
+ fprintf(stderr, "%*sdevices: (%d)\n", indent+8, "", p->n_devices);
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ print_device(data, d, indent + 8, 0);
+ }
+ }
+}
+
+static void print_device(struct data *data, struct acp_device *d, int indent, int level)
+{
+ const char **s;
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c device %u: direction:%s name:\"%s\" prio:%d flags:%08x devices: ",
+ indent, "", d->flags & ACP_DEVICE_ACTIVE ? '*' : ' ', d->index,
+ acp_direction_str(d->direction), d->name, d->priority, d->flags);
+ for (s = d->device_strings; *s; s++)
+ fprintf(stderr, "\"%s\" ", *s);
+ fprintf(stderr, "\n");
+ if (level > 0) {
+ fprintf(stderr, "%*srate:%d channels:%d\n", indent+8, "",
+ d->format.rate_mask, d->format.channels);
+ acp_debug_dict(&d->props, indent + 8);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sports: (%d)\n", indent+8, "", d->n_ports);
+ for (i = 0; i < d->n_ports; i++) {
+ struct acp_port *p = d->ports[i];
+ print_port(data, p, indent + 8, 0);
+ }
+ }
+}
+
+static void print_profile(struct data *data, struct acp_card_profile *p, int indent, int level)
+{
+ uint32_t i;
+
+ fprintf(stderr, "%*s %c profile %u: name:\"%s\" prio:%d (available: %s)\n",
+ indent, "", p->flags & ACP_PROFILE_ACTIVE ? '*' : ' ', p->index,
+ p->name, p->priority, acp_available_str(p->available));
+ if (level > 0) {
+ fprintf(stderr, "%*sdescription:\"%s\"\n",
+ indent+8, "", p->description);
+ }
+ if (level > 1) {
+ fprintf(stderr, "%*sdevices: (%d)\n", indent+8, "", p->n_devices);
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ print_device(data, d, indent + 8, 0);
+ }
+ }
+}
+
+static void print_card(struct data *data, struct acp_card *card, int indent, int level)
+{
+ fprintf(stderr, "%*scard %d: profiles:%d devices:%d ports:%d\n", indent, "",
+ card->index, card->n_profiles, card->n_devices, card->n_ports);
+ if (level > 0) {
+ acp_debug_dict(&card->props, 4);
+ }
+}
+
+static int cmd_info(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ print_card(data, card, 0, 2);
+ return 0;
+}
+
+static int cmd_card(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <card_index> missing\n");
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int cmd_list(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t i;
+ int level = 0;
+
+ if (spa_streq(cmd->name, "list-verbose"))
+ level = 2;
+
+ print_card(data, card, 0, level);
+ for (i = 0; i < card->n_profiles; i++)
+ print_profile(data, card->profiles[i], 0, level);
+
+ for (i = 0; i < card->n_ports; i++)
+ print_port(data, card->ports[i], 0, level);
+
+ for (i = 0; i < card->n_devices; i++)
+ print_device(data, card->devices[i], 0, level);
+
+ return 0;
+}
+
+static int cmd_list_profiles(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_profiles)
+ return -EINVAL;
+ print_profile(data, card->profiles[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_profiles; i++)
+ print_profile(data, card->profiles[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_set_profile(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t index;
+
+ if (argc > 1)
+ index = atoi(argv[1]);
+ else
+ index = card->active_profile_index;
+
+ return acp_card_set_profile(card, index, 0);
+}
+
+static int cmd_list_ports(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_ports)
+ return -EINVAL;
+ print_port(data, card->ports[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_ports; i++)
+ print_port(data, card->ports[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_set_port(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id, port_id;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <port_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ port_id = atoi(argv[2]);
+
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ return acp_device_set_port(card->devices[dev_id], port_id, 0);
+}
+
+static int cmd_list_devices(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ uint32_t i;
+ struct acp_card *card = data->card;
+
+ if (argc > 1) {
+ i = atoi(argv[1]);
+ if (i >= card->n_devices)
+ return -EINVAL;
+ print_device(data, card->devices[i], 0, 2);
+ } else {
+ for (i = 0; i < card->n_devices; i++)
+ print_device(data, card->devices[i], 0, 0);
+ }
+ return 0;
+}
+
+static int cmd_get_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_get_volume(card->devices[dev_id], &vol, 1);
+
+ fprintf(stderr, "volume: %f\n", vol);
+ return 0;
+}
+
+static int cmd_set_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <volume> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ vol = atof(argv[2]);
+
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ return acp_device_set_volume(card->devices[dev_id], &vol, 1);
+}
+
+static int adjust_volume(struct data *data, const struct command *cmd, int argc, char *argv[], float adjust)
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ float vol;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+ acp_device_get_volume(card->devices[dev_id], &vol, 1);
+ vol += adjust;
+ acp_device_set_volume(card->devices[dev_id], &vol, 1);
+ fprintf(stderr, "volume: %f\n", vol);
+ return 0;
+}
+
+static int cmd_inc_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ return adjust_volume(data, cmd, argc, argv, 0.2);
+}
+
+static int cmd_dec_volume(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ return adjust_volume(data, cmd, argc, argv, -0.2);
+}
+
+static int cmd_get_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_get_mute(card->devices[dev_id], &mute);
+
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static int cmd_set_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 3) {
+ fprintf(stderr, "arguments: <device_id> <mute> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ mute = atoi(argv[2]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+
+ acp_device_set_mute(card->devices[dev_id], mute);
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static int cmd_toggle_mute(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ struct acp_card *card = data->card;
+ uint32_t dev_id;
+ bool mute;
+
+ if (argc < 2) {
+ fprintf(stderr, "arguments: <device_id> missing\n");
+ return -EINVAL;
+ }
+ dev_id = atoi(argv[1]);
+ if (dev_id >= card->n_devices)
+ return -EINVAL;
+ acp_device_get_mute(card->devices[dev_id], &mute);
+ mute = !mute;
+ acp_device_set_mute(card->devices[dev_id], mute);
+ fprintf(stderr, "muted: %s\n", mute ? "yes" : "no");
+ return 0;
+}
+
+static const struct command command_list[] = {
+ { "help", "", "h", "Show available commands", cmd_help },
+ { "quit", "", "q", "Quit", cmd_quit },
+ { "card", "<id>", "c", "Probe card", cmd_card },
+ { "info", "", "i", "List card info", cmd_info },
+ { "list", "", "l", "List all objects", cmd_list },
+ { "list-verbose", "", "lv", "List all data", cmd_list },
+ { "list-profiles", "[id]", "lpr", "List profiles", cmd_list_profiles },
+ { "set-profile", "<id>", "spr", "Activate a profile", cmd_set_profile },
+ { "list-ports", "[id]", "lp", "List ports", cmd_list_ports },
+ { "set-port", "<id>", "sp", "Activate a port", cmd_set_port },
+ { "list-devices", "[id]", "ld", "List available devices", cmd_list_devices },
+ { "get-volume", "<id>", "gv", "Get volume from device", cmd_get_volume },
+ { "set-volume", "<id> <vol>", "v", "Set volume on device", cmd_set_volume },
+ { "inc-volume", "<id>", "v+", "Increase volume on device", cmd_inc_volume },
+ { "dec-volume", "<id>", "v-", "Decrease volume on device", cmd_dec_volume },
+ { "get-mute", "<id>", "gm", "Get mute state from device", cmd_get_mute },
+ { "set-mute", "<id> <val>", "sm", "Set mute on device", cmd_set_mute },
+ { "toggle-mute", "<id>", "m", "Toggle mute on device", cmd_toggle_mute },
+};
+#define N_COMMANDS sizeof(command_list)/sizeof(command_list[0])
+
+static const struct command *find_command(struct data *data, const char *cmd)
+{
+ size_t i;
+ for (i = 0; i < N_COMMANDS; i++) {
+ if (spa_streq(command_list[i].name, cmd) ||
+ spa_streq(command_list[i].alias, cmd))
+ return &command_list[i];
+ }
+ return NULL;
+}
+
+static int cmd_help(struct data *data, const struct command *cmd, int argc, char *argv[])
+{
+ size_t i;
+ fprintf(stderr, "Available commands:\n");
+ for (i = 0; i < N_COMMANDS; i++) {
+ fprintf(stdout, "\t%-15.15s %-10.10s\t%s (%s)\n",
+ command_list[i].name,
+ command_list[i].args,
+ command_list[i].description,
+ command_list[i].alias);
+ }
+ return 0;
+}
+
+static int run_command(struct data *data, int argc, char *argv[64])
+{
+ const struct command *command;
+ int res;
+
+ command = find_command(data, argv[0]);
+ if (command == NULL) {
+ fprintf(stderr, "unknown command %s\n", argv[0]);
+ cmd_help(data, NULL, argc, argv);
+ res = -EINVAL;
+ } else if (command->func) {
+ res = command->func(data, command, argc, argv);
+ if (res < 0) {
+ fprintf(stderr, "error: %s\n", strerror(-res));
+ }
+ } else {
+ res = -ENOTSUP;
+ }
+ return res;
+}
+
+static int handle_input(struct data *data)
+{
+ char buf[4096] = { 0, }, *p, *argv[64];
+ ssize_t r;
+ int res, argc;
+
+ if ((r = read(STDIN_FILENO, buf, sizeof(buf)-1)) < 0)
+ return -errno;
+ buf[r] = 0;
+
+ if (r == 0)
+ return -EPIPE;
+
+ if ((p = strchr(buf, '#')))
+ *p = '\0';
+
+ argc = split_ip(buf, WHITESPACE, 64, argv);
+ if (argc < 1)
+ return -EINVAL;
+
+ res = run_command(data, argc, argv);
+
+ if (!data->quit)
+ show_prompt(data);
+
+ return res;
+}
+
+static int do_probe(struct data *data)
+{
+ uint32_t n_items = 0;
+ struct acp_dict_item items[64];
+ struct acp_dict props;
+
+ acp_set_log_func(log_func, data);
+ acp_set_log_level(data->verbose);
+
+ items[n_items++] = ACP_DICT_ITEM_INIT("use-ucm", "true");
+ items[n_items++] = ACP_DICT_ITEM_INIT("verbose", data->verbose ? "true" : "false");
+ if (data->properties != NULL) {
+ char *p = data->properties, *e, f;
+
+ while (*p) {
+ const char *k, *v;
+
+ if ((e = strchr(p, '=')) == NULL)
+ break;
+ *e = '\0';
+ k = p;
+ p = e+1;
+
+ if (*p == '\"') {
+ p++;
+ f = '\"';
+ } else {
+ f = ' ';
+ }
+ if ((e = strchr(p, f)) == NULL &&
+ (e = strchr(p, '\0')) == NULL)
+ break;
+ *e = '\0';
+ v = p;
+ p = e+1;
+ items[n_items++] = ACP_DICT_ITEM_INIT(k, v);
+ if (n_items == 64)
+ break;
+ }
+ }
+ props = ACP_DICT_INIT(items, n_items);
+
+ data->card = acp_card_new(data->card_index, &props);
+ if (data->card == NULL)
+ return -errno;
+ return 0;
+}
+
+static int do_prompt(struct data *data)
+{
+ struct pollfd *pfds;
+ int err, count;
+
+ acp_card_add_listener(data->card, &card_events, data);
+
+ count = acp_card_poll_descriptors_count(data->card);
+ if (count == 0)
+ fprintf(stderr, "card has no events\n");
+
+ count++;
+ pfds = alloca(sizeof(struct pollfd) * count);
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+
+ print_card(data, data->card, 0, 0);
+
+ fprintf(stderr, "type 'help' for usage.\n");
+ show_prompt(data);
+
+ while (!data->quit) {
+ unsigned short revents;
+
+ err = acp_card_poll_descriptors(data->card, &pfds[1], count-1);
+ if (err < 0)
+ return err;
+
+ err = poll(pfds, (unsigned int) count, -1);
+ if (err < 0)
+ return -errno;
+
+ if (pfds[0].revents & POLLIN) {
+ if ((err = handle_input(data)) < 0) {
+ if (err == -EPIPE)
+ break;
+ }
+ }
+
+ if (count < 2)
+ continue;
+
+ err = acp_card_poll_descriptors_revents(data->card, &pfds[1], count-1, &revents);
+ if (err < 0)
+ return err;
+
+ if (revents)
+ acp_card_handle_events(data->card);
+ }
+ return 0;
+}
+
+#define OPTIONS "hvc:p:"
+static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h'},
+ { "verbose", no_argument, NULL, 'v'},
+
+ { "card", required_argument, NULL, 'c' },
+ { "properties", required_argument, NULL, 'p' },
+
+ { NULL, 0, NULL, 0 }
+};
+
+static void show_usage(struct data *data, const char *name, bool is_error)
+{
+ FILE *fp;
+
+ fp = is_error ? stderr : stdout;
+
+ fprintf(fp, "%s [options] [COMMAND]\n", name);
+ fprintf(fp,
+ " -h, --help Show this help\n"
+ " -v --verbose Be verbose\n"
+ " -c --card Card number\n"
+ " -p --properties Extra properties:\n"
+ " 'key=value ... '\n"
+ "\n");
+ cmd_help(data, NULL, 0, NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ int c, res;
+ int longopt_index = 0, ret;
+ struct data data = { 0, };
+
+ data.verbose = 1;
+
+ while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) {
+ switch (c) {
+ case 'h':
+ show_usage(&data, argv[0], false);
+ return EXIT_SUCCESS;
+ case 'v':
+ data.verbose++;
+ break;
+ case 'c':
+ ret = atoi(optarg);
+ if (ret < 0) {
+ fprintf(stderr, "error: bad card %s\n", optarg);
+ goto error_usage;
+ }
+ data.card_index = ret;
+ break;
+ case 'p':
+ data.properties = strdup(optarg);
+ break;
+ default:
+ fprintf(stderr, "error: unknown option '%c'\n", c);
+ goto error_usage;
+ }
+ }
+
+ if ((res = do_probe(&data)) < 0) {
+ fprintf(stderr, "failed to probe card: %s\n", strerror(-res));
+ return res;
+ }
+
+ if (optind < argc)
+ run_command(&data, argc - optind, &argv[optind]);
+ else
+ do_prompt(&data);
+
+ if (data.card)
+ acp_card_destroy(data.card);
+
+ free(data.properties);
+
+ return 0;
+
+error_usage:
+ show_usage(&data, argv[0], true);
+ return EXIT_FAILURE;
+}
diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c
new file mode 100644
index 0000000..f9985b4
--- /dev/null
+++ b/spa/plugins/alsa/acp/acp.c
@@ -0,0 +1,1983 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "acp.h"
+#include "alsa-mixer.h"
+#include "alsa-ucm.h"
+
+#include <spa/utils/string.h>
+
+int _acp_log_level = 1;
+acp_log_func _acp_log_func;
+void *_acp_log_data;
+
+struct spa_i18n *acp_i18n;
+
+#define DEFAULT_RATE 48000
+
+#define VOLUME_ACCURACY (PA_VOLUME_NORM/100) /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */
+
+static const uint32_t channel_table[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = ACP_CHANNEL_MONO,
+
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = ACP_CHANNEL_FL,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = ACP_CHANNEL_FR,
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = ACP_CHANNEL_FC,
+
+ [PA_CHANNEL_POSITION_REAR_CENTER] = ACP_CHANNEL_RC,
+ [PA_CHANNEL_POSITION_REAR_LEFT] = ACP_CHANNEL_RL,
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = ACP_CHANNEL_RR,
+
+ [PA_CHANNEL_POSITION_LFE] = ACP_CHANNEL_LFE,
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = ACP_CHANNEL_FLC,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = ACP_CHANNEL_FRC,
+
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = ACP_CHANNEL_SL,
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = ACP_CHANNEL_SR,
+
+ [PA_CHANNEL_POSITION_AUX0] = ACP_CHANNEL_START_Aux + 0,
+ [PA_CHANNEL_POSITION_AUX1] = ACP_CHANNEL_START_Aux + 1,
+ [PA_CHANNEL_POSITION_AUX2] = ACP_CHANNEL_START_Aux + 2,
+ [PA_CHANNEL_POSITION_AUX3] = ACP_CHANNEL_START_Aux + 3,
+ [PA_CHANNEL_POSITION_AUX4] = ACP_CHANNEL_START_Aux + 4,
+ [PA_CHANNEL_POSITION_AUX5] = ACP_CHANNEL_START_Aux + 5,
+ [PA_CHANNEL_POSITION_AUX6] = ACP_CHANNEL_START_Aux + 6,
+ [PA_CHANNEL_POSITION_AUX7] = ACP_CHANNEL_START_Aux + 7,
+ [PA_CHANNEL_POSITION_AUX8] = ACP_CHANNEL_START_Aux + 8,
+ [PA_CHANNEL_POSITION_AUX9] = ACP_CHANNEL_START_Aux + 9,
+ [PA_CHANNEL_POSITION_AUX10] = ACP_CHANNEL_START_Aux + 10,
+ [PA_CHANNEL_POSITION_AUX11] = ACP_CHANNEL_START_Aux + 11,
+ [PA_CHANNEL_POSITION_AUX12] = ACP_CHANNEL_START_Aux + 12,
+ [PA_CHANNEL_POSITION_AUX13] = ACP_CHANNEL_START_Aux + 13,
+ [PA_CHANNEL_POSITION_AUX14] = ACP_CHANNEL_START_Aux + 14,
+ [PA_CHANNEL_POSITION_AUX15] = ACP_CHANNEL_START_Aux + 15,
+ [PA_CHANNEL_POSITION_AUX16] = ACP_CHANNEL_START_Aux + 16,
+ [PA_CHANNEL_POSITION_AUX17] = ACP_CHANNEL_START_Aux + 17,
+ [PA_CHANNEL_POSITION_AUX18] = ACP_CHANNEL_START_Aux + 18,
+ [PA_CHANNEL_POSITION_AUX19] = ACP_CHANNEL_START_Aux + 19,
+ [PA_CHANNEL_POSITION_AUX20] = ACP_CHANNEL_START_Aux + 20,
+ [PA_CHANNEL_POSITION_AUX21] = ACP_CHANNEL_START_Aux + 21,
+ [PA_CHANNEL_POSITION_AUX22] = ACP_CHANNEL_START_Aux + 22,
+ [PA_CHANNEL_POSITION_AUX23] = ACP_CHANNEL_START_Aux + 23,
+ [PA_CHANNEL_POSITION_AUX24] = ACP_CHANNEL_START_Aux + 24,
+ [PA_CHANNEL_POSITION_AUX25] = ACP_CHANNEL_START_Aux + 25,
+ [PA_CHANNEL_POSITION_AUX26] = ACP_CHANNEL_START_Aux + 26,
+ [PA_CHANNEL_POSITION_AUX27] = ACP_CHANNEL_START_Aux + 27,
+ [PA_CHANNEL_POSITION_AUX28] = ACP_CHANNEL_START_Aux + 28,
+ [PA_CHANNEL_POSITION_AUX29] = ACP_CHANNEL_START_Aux + 29,
+ [PA_CHANNEL_POSITION_AUX30] = ACP_CHANNEL_START_Aux + 30,
+ [PA_CHANNEL_POSITION_AUX31] = ACP_CHANNEL_START_Aux + 31,
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = ACP_CHANNEL_TC,
+
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = ACP_CHANNEL_TFL,
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = ACP_CHANNEL_TFR,
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = ACP_CHANNEL_TFC,
+
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = ACP_CHANNEL_TRL,
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = ACP_CHANNEL_TRR,
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = ACP_CHANNEL_TRC,
+};
+
+static const char *channel_names[] = {
+ [ACP_CHANNEL_UNKNOWN] = "UNK",
+ [ACP_CHANNEL_NA] = "NA",
+ [ACP_CHANNEL_MONO] = "MONO",
+ [ACP_CHANNEL_FL] = "FL",
+ [ACP_CHANNEL_FR] = "FR",
+ [ACP_CHANNEL_FC] = "FC",
+ [ACP_CHANNEL_LFE] = "LFE",
+ [ACP_CHANNEL_SL] = "SL",
+ [ACP_CHANNEL_SR] = "SR",
+ [ACP_CHANNEL_FLC] = "FLC",
+ [ACP_CHANNEL_FRC] = "FRC",
+ [ACP_CHANNEL_RC] = "RC",
+ [ACP_CHANNEL_RL] = "RL",
+ [ACP_CHANNEL_RR] = "RR",
+ [ACP_CHANNEL_TC] = "TC",
+ [ACP_CHANNEL_TFL] = "TFL",
+ [ACP_CHANNEL_TFC] = "TFC",
+ [ACP_CHANNEL_TFR] = "TFR",
+ [ACP_CHANNEL_TRL] = "TRL",
+ [ACP_CHANNEL_TRC] = "TRC",
+ [ACP_CHANNEL_TRR] = "TRR",
+ [ACP_CHANNEL_RLC] = "RLC",
+ [ACP_CHANNEL_RRC] = "RRC",
+ [ACP_CHANNEL_FLW] = "FLW",
+ [ACP_CHANNEL_FRW] = "FRW",
+ [ACP_CHANNEL_LFE2] = "LFE2",
+ [ACP_CHANNEL_FLH] = "FLH",
+ [ACP_CHANNEL_FCH] = "FCH",
+ [ACP_CHANNEL_FRH] = "FRH",
+ [ACP_CHANNEL_TFLC] = "TFLC",
+ [ACP_CHANNEL_TFRC] = "TFRC",
+ [ACP_CHANNEL_TSL] = "TSL",
+ [ACP_CHANNEL_TSR] = "TSR",
+ [ACP_CHANNEL_LLFE] = "LLFE",
+ [ACP_CHANNEL_RLFE] = "RLFE",
+ [ACP_CHANNEL_BC] = "BC",
+ [ACP_CHANNEL_BLC] = "BLC",
+ [ACP_CHANNEL_BRC] = "BRC",
+};
+
+#define ACP_N_ELEMENTS(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+static inline uint32_t channel_pa2acp(pa_channel_position_t channel)
+{
+ if (channel < 0 || (size_t)channel >= ACP_N_ELEMENTS(channel_table))
+ return ACP_CHANNEL_UNKNOWN;
+ return channel_table[channel];
+}
+
+char *acp_channel_str(char *buf, size_t len, enum acp_channel ch)
+{
+ if (ch >= ACP_CHANNEL_START_Aux && ch <= ACP_CHANNEL_LAST_Aux) {
+ snprintf(buf, len, "AUX%d", ch - ACP_CHANNEL_START_Aux);
+ } else if (ch >= ACP_CHANNEL_UNKNOWN && ch <= ACP_CHANNEL_BRC) {
+ snprintf(buf, len, "%s", channel_names[ch]);
+ } else {
+ snprintf(buf, len, "UNK");
+ }
+ return buf;
+}
+
+
+const char *acp_available_str(enum acp_available status)
+{
+ switch (status) {
+ case ACP_AVAILABLE_UNKNOWN:
+ return "unknown";
+ case ACP_AVAILABLE_NO:
+ return "no";
+ case ACP_AVAILABLE_YES:
+ return "yes";
+ }
+ return "error";
+}
+
+const char *acp_direction_str(enum acp_direction direction)
+{
+ switch (direction) {
+ case ACP_DIRECTION_CAPTURE:
+ return "capture";
+ case ACP_DIRECTION_PLAYBACK:
+ return "playback";
+ }
+ return "error";
+}
+
+static void port_free(void *data)
+{
+ pa_device_port *dp = data;
+ pa_dynarray_clear(&dp->devices);
+ pa_dynarray_clear(&dp->prof);
+ pa_device_port_free(dp);
+}
+
+static void device_free(void *data)
+{
+ pa_alsa_device *dev = data;
+ pa_dynarray_clear(&dev->port_array);
+ pa_proplist_free(dev->proplist);
+ pa_hashmap_free(dev->ports);
+}
+
+static inline void channelmap_to_acp(pa_channel_map *m, uint32_t *map)
+{
+ uint32_t i, j;
+ for (i = 0; i < m->channels; i++) {
+ map[i] = channel_pa2acp(m->map[i]);
+ for (j = 0; j < i; j++) {
+ if (map[i] == map[j])
+ map[i] += 32;
+ }
+
+ }
+}
+
+static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t direction,
+ pa_alsa_mapping *m, uint32_t index)
+{
+ char **d;
+
+ dev->card = impl;
+ dev->mapping = m;
+ dev->device.index = index;
+ dev->device.name = m->name;
+ dev->device.description = m->description;
+ dev->device.priority = m->priority;
+ dev->device.device_strings = (const char **)m->device_strings;
+ dev->device.format.format_mask = m->sample_spec.format;
+ dev->device.format.rate_mask = m->sample_spec.rate;
+ dev->device.format.channels = m->channel_map.channels;
+ pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
+ pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
+ channelmap_to_acp(&m->channel_map, dev->device.format.map);
+ dev->direction = direction;
+ dev->proplist = pa_proplist_new();
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->proplist);
+ if (direction == PA_ALSA_DIRECTION_OUTPUT) {
+ dev->mixer_path_set = m->output_path_set;
+ dev->pcm_handle = m->output_pcm;
+ dev->device.direction = ACP_DIRECTION_PLAYBACK;
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->output_proplist);
+ } else {
+ dev->mixer_path_set = m->input_path_set;
+ dev->pcm_handle = m->input_pcm;
+ dev->device.direction = ACP_DIRECTION_CAPTURE;
+ pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist);
+ }
+ pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_NAME, m->name);
+ pa_proplist_sets(dev->proplist, PA_PROP_DEVICE_PROFILE_DESCRIPTION, m->description);
+ pa_proplist_setf(dev->proplist, "card.profile.device", "%u", index);
+ pa_proplist_as_dict(dev->proplist, &dev->device.props);
+
+ dev->ports = pa_hashmap_new(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func);
+ if (m->ucm_context.ucm) {
+ dev->ucm_context = &m->ucm_context;
+ if (impl->ucm.alib_prefix != NULL) {
+ for (d = m->device_strings; *d; d++) {
+ if (pa_startswith(*d, impl->ucm.alib_prefix)) {
+ size_t plen = strlen(impl->ucm.alib_prefix);
+ size_t len = strlen(*d);
+ memmove(*d, (*d) + plen, len - plen + 1);
+ dev->device.flags |= ACP_DEVICE_UCM_DEVICE;
+ }
+ }
+ }
+ }
+ for (d = m->device_strings; *d; d++) {
+ if (pa_startswith(*d, "iec958") ||
+ pa_startswith(*d, "hdmi"))
+ dev->device.flags |= ACP_DEVICE_IEC958;
+ }
+ pa_dynarray_init(&dev->port_array, NULL);
+}
+
+static int compare_profile(const void *a, const void *b)
+{
+ const pa_hashmap_item *i1 = a;
+ const pa_hashmap_item *i2 = b;
+ const pa_alsa_profile *p1, *p2;
+ if (i1->key == NULL || i2->key == NULL)
+ return 0;
+ p1 = i1->value;
+ p2 = i2->value;
+ if (p1->profile.priority == 0 || p2->profile.priority == 0)
+ return 0;
+ return p2->profile.priority - p1->profile.priority;
+}
+
+static void profile_free(void *data)
+{
+ pa_alsa_profile *ap = data;
+ pa_dynarray_clear(&ap->out.devices);
+ if (ap->profile.flags & ACP_PROFILE_OFF) {
+ free(ap->name);
+ free(ap->description);
+ free(ap);
+ }
+}
+
+static int add_pro_profile(pa_card *impl, uint32_t index)
+{
+ snd_ctl_t *ctl_hndl;
+ int err, dev, count = 0;
+ pa_alsa_profile *ap;
+ pa_alsa_profile_set *ps = impl->profile_set;
+ pa_alsa_mapping *m;
+ char *device;
+ snd_pcm_info_t *pcminfo;
+ pa_sample_spec ss;
+ snd_pcm_uframes_t try_period_size, try_buffer_size;
+
+ ss.format = PA_SAMPLE_S32LE;
+ ss.rate = impl->rate;
+ ss.channels = 64;
+
+ ap = pa_xnew0(pa_alsa_profile, 1);
+ ap->profile_set = ps;
+ ap->profile.name = ap->name = pa_xstrdup("pro-audio");
+ ap->profile.description = ap->description = pa_xstrdup(_("Pro Audio"));
+ ap->profile.available = ACP_AVAILABLE_YES;
+ ap->profile.flags = ACP_PROFILE_PRO;
+ ap->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ap->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_hashmap_put(ps->profiles, ap->name, ap);
+
+ ap->output_name = pa_xstrdup("pro-output");
+ ap->input_name = pa_xstrdup("pro-input");
+ ap->priority = 1;
+
+ pa_assert_se(asprintf(&device, "hw:%d", index) >= 0);
+
+ if ((err = snd_ctl_open(&ctl_hndl, device, 0)) < 0) {
+ pa_log_error("can't open control for card %s: %s",
+ device, snd_strerror(err));
+ free(device);
+ return err;
+ }
+ free(device);
+
+ snd_pcm_info_alloca(&pcminfo);
+
+ dev = -1;
+ while (1) {
+ char desc[128], devstr[128], *name;
+
+ if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) {
+ pa_log_error("error iterating devices: %s", snd_strerror(err));
+ break;
+ }
+ if (dev < 0)
+ break;
+
+ snd_pcm_info_set_device(pcminfo, dev);
+ snd_pcm_info_set_subdevice(pcminfo, 0);
+
+ snprintf(devstr, sizeof(devstr), "hw:%d,%d", index, dev);
+ if (count++ == 0)
+ snprintf(desc, sizeof(desc), "Pro");
+ else
+ snprintf(desc, sizeof(desc), "Pro %d", dev);
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ pa_log_error("error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ pa_assert_se(asprintf(&name, "Mapping pro-output-%d", dev) >= 0);
+ m = pa_alsa_mapping_get(ps, name);
+ m->description = pa_xstrdup(desc);
+ m->device_strings = pa_split_spaces_strv(devstr);
+
+ try_period_size = 1024;
+ try_buffer_size = 1024 * 64;
+ m->sample_spec = ss;
+
+ if ((m->output_pcm = pa_alsa_open_by_template(m->device_strings,
+ devstr, NULL, &m->sample_spec,
+ &m->channel_map, SND_PCM_STREAM_PLAYBACK,
+ &try_period_size, &try_buffer_size,
+ 0, NULL, NULL, false))) {
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_proplist_setf(m->output_proplist, "clock.name", "api.alsa.%u", index);
+ pa_alsa_close(&m->output_pcm);
+ m->supported = true;
+ pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
+ }
+ pa_idxset_put(ap->output_mappings, m, NULL);
+ free(name);
+ }
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ pa_log_error("error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ pa_assert_se(asprintf(&name, "Mapping pro-input-%d", dev) >= 0);
+ m = pa_alsa_mapping_get(ps, name);
+ m->description = pa_xstrdup(desc);
+ m->device_strings = pa_split_spaces_strv(devstr);
+
+ try_period_size = 1024;
+ try_buffer_size = 1024 * 64;
+ m->sample_spec = ss;
+
+ if ((m->input_pcm = pa_alsa_open_by_template(m->device_strings,
+ devstr, NULL, &m->sample_spec,
+ &m->channel_map, SND_PCM_STREAM_CAPTURE,
+ &try_period_size, &try_buffer_size,
+ 0, NULL, NULL, false))) {
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_proplist_setf(m->input_proplist, "clock.name", "api.alsa.%u", index);
+ pa_alsa_close(&m->input_pcm);
+ m->supported = true;
+ pa_channel_map_init_auto(&m->channel_map, m->sample_spec.channels, PA_CHANNEL_MAP_AUX);
+ }
+ pa_idxset_put(ap->input_mappings, m, NULL);
+ free(name);
+ }
+ }
+ snd_ctl_close(ctl_hndl);
+
+ return 0;
+}
+
+
+static void add_profiles(pa_card *impl)
+{
+ pa_alsa_profile *ap;
+ void *state;
+ struct acp_card_profile *cp;
+ pa_device_port *dp;
+ pa_alsa_device *dev;
+ int n_profiles, n_ports, n_devices;
+ uint32_t idx;
+
+ n_devices = 0;
+ pa_dynarray_init(&impl->out.devices, device_free);
+
+ ap = pa_xnew0(pa_alsa_profile, 1);
+ ap->profile.name = ap->name = pa_xstrdup("off");
+ ap->profile.description = ap->description = pa_xstrdup(_("Off"));
+ ap->profile.available = ACP_AVAILABLE_YES;
+ ap->profile.flags = ACP_PROFILE_OFF;
+ pa_hashmap_put(impl->profiles, ap->name, ap);
+
+ add_pro_profile(impl, impl->card.index);
+
+ PA_HASHMAP_FOREACH(ap, impl->profile_set->profiles, state) {
+ pa_alsa_mapping *m;
+
+ cp = &ap->profile;
+ cp->name = ap->name;
+ cp->description = ap->description;
+ cp->priority = ap->priority ? ap->priority : 1;
+
+ pa_dynarray_init(&ap->out.devices, NULL);
+
+ if (ap->output_mappings) {
+ PA_IDXSET_FOREACH(m, ap->output_mappings, idx) {
+ dev = &m->output;
+ if (dev->mapping == NULL) {
+ init_device(impl, dev, PA_ALSA_DIRECTION_OUTPUT, m, n_devices++);
+ pa_dynarray_append(&impl->out.devices, dev);
+ }
+ if (impl->use_ucm) {
+ if (m->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context,
+ true, impl->ports, ap, NULL);
+ pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
+ true, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
+ }
+ }
+ else
+ pa_alsa_path_set_add_ports(m->output_path_set, ap, impl->ports,
+ dev->ports, NULL);
+
+ pa_dynarray_append(&ap->out.devices, dev);
+ }
+ }
+
+ if (ap->input_mappings) {
+ PA_IDXSET_FOREACH(m, ap->input_mappings, idx) {
+ dev = &m->input;
+ if (dev->mapping == NULL) {
+ init_device(impl, dev, PA_ALSA_DIRECTION_INPUT, m, n_devices++);
+ pa_dynarray_append(&impl->out.devices, dev);
+ }
+
+ if (impl->use_ucm) {
+ if (m->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(NULL, &m->ucm_context,
+ false, impl->ports, ap, NULL);
+ pa_alsa_ucm_add_ports(&dev->ports, m->proplist, &m->ucm_context,
+ false, impl, dev->pcm_handle, impl->profile_set->ignore_dB);
+ }
+ } else
+ pa_alsa_path_set_add_ports(m->input_path_set, ap, impl->ports,
+ dev->ports, NULL);
+
+ pa_dynarray_append(&ap->out.devices, dev);
+ }
+ }
+ cp->n_devices = pa_dynarray_size(&ap->out.devices);
+ cp->devices = ap->out.devices.array.data;
+ pa_hashmap_put(impl->profiles, ap->name, cp);
+ }
+
+ pa_dynarray_init(&impl->out.ports, NULL);
+ n_ports = 0;
+ PA_HASHMAP_FOREACH(dp, impl->ports, state) {
+ void *state2;
+ dp->card = impl;
+ dp->port.index = n_ports++;
+ dp->port.priority = dp->priority;
+ pa_dynarray_init(&dp->prof, NULL);
+ pa_dynarray_init(&dp->devices, NULL);
+ n_profiles = 0;
+ PA_HASHMAP_FOREACH(cp, dp->profiles, state2) {
+ pa_dynarray_append(&dp->prof, cp);
+ n_profiles++;
+ }
+ dp->port.n_profiles = n_profiles;
+ dp->port.profiles = dp->prof.array.data;
+
+ pa_proplist_setf(dp->proplist, "card.profile.port", "%u", dp->port.index);
+ pa_proplist_as_dict(dp->proplist, &dp->port.props);
+ pa_dynarray_append(&impl->out.ports, dp);
+ }
+ PA_DYNARRAY_FOREACH(dev, &impl->out.devices, idx) {
+ PA_HASHMAP_FOREACH(dp, dev->ports, state) {
+ pa_dynarray_append(&dev->port_array, dp);
+ pa_dynarray_append(&dp->devices, dev);
+ }
+ dev->device.ports = dev->port_array.array.data;
+ dev->device.n_ports = pa_dynarray_size(&dev->port_array);
+ }
+ PA_HASHMAP_FOREACH(dp, impl->ports, state) {
+ dp->port.devices = dp->devices.array.data;
+ dp->port.n_devices = pa_dynarray_size(&dp->devices);
+ }
+
+ pa_hashmap_sort(impl->profiles, compare_profile);
+
+ n_profiles = 0;
+ pa_dynarray_init(&impl->out.profiles, NULL);
+ PA_HASHMAP_FOREACH(cp, impl->profiles, state) {
+ cp->index = n_profiles++;
+ pa_dynarray_append(&impl->out.profiles, cp);
+ }
+}
+
+static pa_available_t calc_port_state(pa_device_port *p, pa_card *impl)
+{
+ void *state;
+ pa_alsa_jack *jack;
+ pa_available_t pa = PA_AVAILABLE_UNKNOWN;
+ pa_device_port *port;
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
+ pa_available_t cpa;
+
+ if (impl->use_ucm)
+ port = pa_hashmap_get(impl->ports, jack->name);
+ else {
+ if (jack->path)
+ port = jack->path->port;
+ else
+ continue;
+ }
+
+ if (p != port)
+ continue;
+
+ cpa = jack->plugged_in ? jack->state_plugged : jack->state_unplugged;
+
+ if (cpa == PA_AVAILABLE_NO) {
+ /* If a plugged-in jack causes the availability to go to NO, it
+ * should override all other availability information (like a
+ * blacklist) so set and bail */
+ if (jack->plugged_in) {
+ pa = cpa;
+ break;
+ }
+
+ /* If the current availability is unknown go the more precise no,
+ * but otherwise don't change state */
+ if (pa == PA_AVAILABLE_UNKNOWN)
+ pa = cpa;
+ } else if (cpa == PA_AVAILABLE_YES) {
+ /* Output is available through at least one jack, so go to that
+ * level of availability. We still need to continue iterating through
+ * the jacks in case a jack is plugged in that forces the state to no
+ */
+ pa = cpa;
+ }
+ }
+ return pa;
+}
+
+static void profile_set_available(pa_card *impl, uint32_t index,
+ enum acp_available status, bool emit)
+{
+ struct acp_card_profile *p = impl->card.profiles[index];
+ enum acp_available old = p->available;
+
+ if (old != status)
+ pa_log_info("Profile %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(status));
+
+ p->available = status;
+
+ if (emit && impl->events && impl->events->profile_available)
+ impl->events->profile_available(impl->user_data, index,
+ old, status);
+}
+
+struct temp_port_avail {
+ pa_device_port *port;
+ pa_available_t avail;
+};
+
+static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask)
+{
+ pa_card *impl = snd_mixer_elem_get_callback_private(melem);
+ snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem);
+ snd_ctl_elem_value_t *elem_value;
+ bool plugged_in, any_input_port_available;
+ void *state;
+ pa_alsa_jack *jack;
+ struct temp_port_avail *tp, *tports;
+ pa_alsa_profile *profile;
+ enum acp_available active_available = ACP_AVAILABLE_UNKNOWN;
+ size_t size;
+
+#if 0
+ /* Changing the jack state may cause a port change, and a port change will
+ * make the sink or source change the mixer settings. If there are multiple
+ * users having pulseaudio running, the mixer changes done by inactive
+ * users may mess up the volume settings for the active users, because when
+ * the inactive users change the mixer settings, those changes are picked
+ * up by the active user's pulseaudio instance and the changes are
+ * interpreted as if the active user changed the settings manually e.g.
+ * with alsamixer. Even single-user systems suffer from this, because gdm
+ * runs its own pulseaudio instance.
+ *
+ * We rerun this function when being unsuspended to catch up on jack state
+ * changes */
+ if (u->card->suspend_cause & PA_SUSPEND_SESSION)
+ return 0;
+#endif
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ snd_ctl_elem_value_alloca(&elem_value);
+ if (snd_hctl_elem_read(elem, elem_value) < 0) {
+ pa_log_warn("Failed to read jack detection from '%s'", pa_strnull(snd_hctl_elem_get_name(elem)));
+ return 0;
+ }
+
+ plugged_in = !!snd_ctl_elem_value_get_boolean(elem_value, 0);
+
+ pa_log_debug("Jack '%s' is now %s", pa_strnull(snd_hctl_elem_get_name(elem)),
+ plugged_in ? "plugged in" : "unplugged");
+
+ size = sizeof(struct temp_port_avail) * (pa_hashmap_size(impl->jacks)+1);
+ tports = tp = alloca(size);
+ memset(tports, 0, size);
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state)
+ if (jack->melem == melem) {
+ pa_alsa_jack_set_plugged_in(jack, plugged_in);
+
+ if (impl->use_ucm) {
+ /* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack
+ * state to port availability. */
+ continue;
+ }
+
+ /* When not using UCM, we have to do the jack state -> port
+ * availability mapping ourselves. */
+ pa_assert_se(tp->port = jack->path->port);
+ tp->avail = calc_port_state(tp->port, impl);
+ tp++;
+ }
+
+ /* Report available ports before unavailable ones: in case port 1
+ * becomes available when port 2 becomes unavailable,
+ * this prevents an unnecessary switch port 1 -> port 3 -> port 2 */
+
+ for (tp = tports; tp->port; tp++)
+ if (tp->avail != PA_AVAILABLE_NO)
+ pa_device_port_set_available(tp->port, tp->avail);
+ for (tp = tports; tp->port; tp++)
+ if (tp->avail == PA_AVAILABLE_NO)
+ pa_device_port_set_available(tp->port, tp->avail);
+
+ for (tp = tports; tp->port; tp++) {
+ pa_alsa_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(tp->port);
+
+ if (!data->suspend_when_unavailable)
+ continue;
+
+#if 0
+ pa_sink *sink;
+ uint32_t idx;
+ PA_IDXSET_FOREACH(sink, u->core->sinks, idx) {
+ if (sink->active_port == tp->port)
+ pa_sink_suspend(sink, tp->avail == PA_AVAILABLE_NO, PA_SUSPEND_UNAVAILABLE);
+ }
+#endif
+ }
+
+ /* Update profile availabilities. Ideally we would mark all profiles
+ * unavailable that contain unavailable devices. We can't currently do that
+ * in all cases, because if there are multiple sinks in a profile, and the
+ * profile contains a mix of available and unavailable ports, we don't know
+ * how the ports are distributed between the different sinks. It's possible
+ * that some sinks contain only unavailable ports, in which case we should
+ * mark the profile as unavailable, but it's also possible that all sinks
+ * contain at least one available port, in which case we should mark the
+ * profile as available. Until the data structures are improved so that we
+ * can distinguish between these two cases, we mark the problematic cases
+ * as available (well, "unknown" to be precise, but there's little
+ * practical difference).
+ *
+ * When all output ports are unavailable, we know that all sinks are
+ * unavailable, and therefore the profile is marked unavailable as well.
+ * The same applies to input ports as well, of course.
+ *
+ * If there are no output ports at all, but the profile contains at least
+ * one sink, then the output is considered to be available. */
+ if (impl->card.active_profile_index != ACP_INVALID_INDEX)
+ active_available = impl->card.profiles[impl->card.active_profile_index]->available;
+
+ /* First round - detect, if we have any input port available.
+ If the hardware can report the state for all I/O jacks, only speakers
+ may be plugged in. */
+ any_input_port_available = false;
+ PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
+ pa_device_port *port;
+ void *state2;
+
+ if (profile->profile.flags & ACP_PROFILE_OFF)
+ continue;
+
+ PA_HASHMAP_FOREACH(port, impl->ports, state2) {
+ if (!pa_hashmap_get(port->profiles, profile->profile.name))
+ continue;
+
+ if (port->port.direction == ACP_DIRECTION_CAPTURE &&
+ port->port.available != ACP_AVAILABLE_NO) {
+ any_input_port_available = true;
+ goto input_port_found;
+ }
+ }
+ }
+input_port_found:
+
+ /* Second round */
+ PA_HASHMAP_FOREACH(profile, impl->profiles, state) {
+ pa_device_port *port;
+ void *state2;
+ bool has_input_port = false;
+ bool has_output_port = false;
+ bool found_available_input_port = false;
+ bool found_available_output_port = false;
+ enum acp_available available = ACP_AVAILABLE_UNKNOWN;
+
+ if (profile->profile.flags & ACP_PROFILE_OFF)
+ continue;
+
+ PA_HASHMAP_FOREACH(port, impl->ports, state2) {
+ if (!pa_hashmap_get(port->profiles, profile->profile.name))
+ continue;
+
+ if (port->port.direction == ACP_DIRECTION_CAPTURE) {
+ has_input_port = true;
+ if (port->port.available != ACP_AVAILABLE_NO)
+ found_available_input_port = true;
+ } else {
+ has_output_port = true;
+ if (port->port.available != ACP_AVAILABLE_NO)
+ found_available_output_port = true;
+ }
+ }
+
+ if ((has_input_port && !found_available_input_port) ||
+ (has_output_port && !found_available_output_port))
+ available = ACP_AVAILABLE_NO;
+
+ if (has_input_port && !has_output_port && found_available_input_port)
+ available = ACP_AVAILABLE_YES;
+ if (has_output_port && (!has_input_port || !any_input_port_available) && found_available_output_port)
+ available = ACP_AVAILABLE_YES;
+ if (has_output_port && has_input_port && found_available_output_port && found_available_input_port)
+ available = ACP_AVAILABLE_YES;
+
+ /* We want to update the active profile's status last, so logic that
+ * may change the active profile based on profile availability status
+ * has an updated view of all profiles' availabilities. */
+ if (profile->profile.index == impl->card.active_profile_index)
+ active_available = available;
+ else
+ profile_set_available(impl, profile->profile.index, available, false);
+ }
+
+ if (impl->card.active_profile_index != ACP_INVALID_INDEX)
+ profile_set_available(impl, impl->card.active_profile_index, active_available, true);
+
+ return 0;
+}
+
+static void init_jacks(pa_card *impl)
+{
+ void *state;
+ pa_alsa_path* path;
+ pa_alsa_jack* jack;
+ char buf[64];
+
+ impl->jacks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (impl->use_ucm) {
+ PA_LLIST_FOREACH(jack, impl->ucm.jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+ } else {
+ /* See if we have any jacks */
+ if (impl->profile_set->output_paths)
+ PA_HASHMAP_FOREACH(path, impl->profile_set->output_paths, state)
+ PA_LLIST_FOREACH(jack, path->jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+
+ if (impl->profile_set->input_paths)
+ PA_HASHMAP_FOREACH(path, impl->profile_set->input_paths, state)
+ PA_LLIST_FOREACH(jack, path->jacks)
+ if (jack->has_control)
+ pa_hashmap_put(impl->jacks, jack, jack);
+ }
+
+ pa_log_debug("Found %d jacks.", pa_hashmap_size(impl->jacks));
+
+ if (pa_hashmap_size(impl->jacks) == 0)
+ return;
+
+ PA_HASHMAP_FOREACH(jack, impl->jacks, state) {
+ if (!jack->mixer_device_name) {
+ jack->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, false);
+ if (!jack->mixer_handle) {
+ pa_log("Failed to open mixer for card %d for jack detection", impl->card.index);
+ continue;
+ }
+ } else {
+ jack->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, jack->mixer_device_name, false);
+ if (!jack->mixer_handle) {
+ pa_log("Failed to open mixer '%s' for jack detection", jack->mixer_device_name);
+ continue;
+ }
+ }
+
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, jack->mixer_handle);
+ jack->melem = pa_alsa_mixer_find_card(jack->mixer_handle, &jack->alsa_id, 0);
+ if (!jack->melem) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &jack->alsa_id);
+ pa_log_warn("Jack '%s' seems to have disappeared.", buf);
+ pa_alsa_jack_set_has_control(jack, false);
+ continue;
+ }
+ snd_mixer_elem_set_callback(jack->melem, report_jack_state);
+ snd_mixer_elem_set_callback_private(jack->melem, impl);
+ report_jack_state(jack->melem, 0);
+ }
+}
+static pa_device_port* find_port_with_eld_device(pa_card *impl, int device)
+{
+ void *state;
+ pa_device_port *p;
+
+ if (impl->use_ucm) {
+ PA_HASHMAP_FOREACH(p, impl->ports, state) {
+ pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(p);
+ pa_assert(data->eld_mixer_device_name);
+ if (device == data->eld_device)
+ return p;
+ }
+ } else {
+ PA_HASHMAP_FOREACH(p, impl->ports, state) {
+ pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(p);
+ pa_assert(data->path);
+ if (device == data->path->eld_device)
+ return p;
+ }
+ }
+ return NULL;
+}
+
+static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask)
+{
+ pa_card *impl = snd_mixer_elem_get_callback_private(melem);
+ snd_hctl_elem_t *elem = snd_mixer_elem_get_private(melem);
+ int device = snd_hctl_elem_get_device(elem);
+ const char *old_monitor_name;
+ pa_device_port *p;
+ pa_hdmi_eld eld;
+ bool changed = false;
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ p = find_port_with_eld_device(impl, device);
+ if (p == NULL) {
+ pa_log_error("Invalid device changed in ALSA: %d", device);
+ return 0;
+ }
+
+ if (pa_alsa_get_hdmi_eld(elem, &eld) < 0)
+ memset(&eld, 0, sizeof(eld));
+
+ old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
+ if (eld.monitor_name[0] == '\0') {
+ changed |= old_monitor_name != NULL;
+ pa_proplist_unset(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);
+ } else {
+ changed |= (old_monitor_name == NULL) || (!spa_streq(old_monitor_name, eld.monitor_name));
+ pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);
+ }
+ pa_proplist_as_dict(p->proplist, &p->port.props);
+
+ if (changed && mask != 0 && impl->events && impl->events->props_changed)
+ impl->events->props_changed(impl->user_data);
+ return 0;
+}
+
+static void init_eld_ctls(pa_card *impl)
+{
+ void *state;
+ pa_device_port *port;
+
+ /* The code in this function expects ports to have a pa_alsa_port_data
+ * struct as their data, but in UCM mode ports don't have any data. Hence,
+ * the ELD controls can't currently be used in UCM mode. */
+ PA_HASHMAP_FOREACH(port, impl->ports, state) {
+ snd_mixer_t *mixer_handle;
+ snd_mixer_elem_t* melem;
+ int device;
+
+ if (impl->use_ucm) {
+ pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port);
+ device = data->eld_device;
+ if (device < 0 || !data->eld_mixer_device_name)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, data->eld_mixer_device_name, true);
+ } else {
+ pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(port);
+
+ pa_assert(data->path);
+
+ device = data->path->eld_device;
+ if (device < 0)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
+ }
+
+ if (!mixer_handle)
+ continue;
+
+ melem = pa_alsa_mixer_find_pcm(mixer_handle, "ELD", device);
+ if (melem) {
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, mixer_handle);
+ snd_mixer_elem_set_callback(melem, hdmi_eld_changed);
+ snd_mixer_elem_set_callback_private(melem, impl);
+ hdmi_eld_changed(melem, 0);
+ pa_log_info("ELD device found for port %s (%d).", port->port.name, device);
+ }
+ else
+ pa_log_debug("No ELD device found for port %s (%d).", port->port.name, device);
+ }
+}
+
+uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
+{
+ uint32_t i;
+ uint32_t best, best2, off;
+ struct acp_card_profile **profiles = card->profiles;
+
+ best = best2 = ACP_INVALID_INDEX;
+ off = 0;
+
+ for (i = 0; i < card->n_profiles; i++) {
+ struct acp_card_profile *p = profiles[i];
+
+ if (name) {
+ if (spa_streq(name, p->name))
+ best = i;
+ } else if (p->flags & ACP_PROFILE_OFF) {
+ off = i;
+ } else if (p->available == ACP_AVAILABLE_YES) {
+ if (best == ACP_INVALID_INDEX || p->priority > profiles[best]->priority)
+ best = i;
+ } else if (p->available != ACP_AVAILABLE_NO) {
+ if (best2 == ACP_INVALID_INDEX || p->priority > profiles[best2]->priority)
+ best2 = i;
+ }
+ }
+ if (best == ACP_INVALID_INDEX)
+ best = best2;
+ if (best == ACP_INVALID_INDEX)
+ best = off;
+ return best;
+}
+
+static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, bool ignore_dB)
+{
+ const char *mdev;
+ pa_alsa_mapping *mapping = dev->mapping;
+
+ if (!mapping && !element)
+ return;
+
+ if (!element && mapping && pa_alsa_path_set_is_empty(dev->mixer_path_set))
+ return;
+
+ mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device");
+ if (mdev) {
+ dev->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, mdev, true);
+ } else {
+ dev->mixer_handle = pa_alsa_open_mixer(impl->ucm.mixers, impl->card.index, true);
+ }
+ if (!dev->mixer_handle) {
+ pa_log_info("Failed to find a working mixer device.");
+ return;
+ }
+
+ if (element) {
+ if (!(dev->mixer_path = pa_alsa_path_synthesize(element, dev->direction)))
+ goto fail;
+
+ if (pa_alsa_path_probe(dev->mixer_path, NULL, dev->mixer_handle, ignore_dB) < 0)
+ goto fail;
+
+ pa_log_debug("Probed mixer path %s:", dev->mixer_path->name);
+ pa_alsa_path_dump(dev->mixer_path);
+ }
+ return;
+
+fail:
+ if (dev->mixer_path) {
+ pa_alsa_path_free(dev->mixer_path);
+ dev->mixer_path = NULL;
+ }
+ dev->mixer_handle = NULL;
+}
+
+static int mixer_callback(snd_mixer_elem_t *elem, unsigned int mask)
+{
+ pa_alsa_device *dev = snd_mixer_elem_get_callback_private(elem);
+
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ return 0;
+
+ pa_log_info("%p mixer changed %d", dev, mask);
+
+ if (mask & SND_CTL_EVENT_MASK_VALUE) {
+ if (dev->read_volume)
+ dev->read_volume(dev);
+ if (dev->read_mute)
+ dev->read_mute(dev);
+ }
+ return 0;
+}
+
+static int read_volume(pa_alsa_device *dev)
+{
+ pa_card *impl = dev->card;
+ pa_cvolume r;
+ uint32_t i;
+ int res;
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
+ return res;
+
+ /* Shift down by the base volume, so that 0dB becomes maximum volume */
+ pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
+
+ if (pa_cvolume_equal(&dev->hardware_volume, &r))
+ return 0;
+
+ dev->real_volume = dev->hardware_volume = r;
+
+ pa_log_info("New hardware volume: min:%d max:%d",
+ pa_cvolume_min(&r), pa_cvolume_max(&r));
+
+ for (i = 0; i < r.channels; i++)
+ pa_log_debug(" %d: %d", i, r.values[i]);
+
+ pa_cvolume_reset(&dev->soft_volume, r.channels);
+
+ if (impl->events && impl->events->volume_changed)
+ impl->events->volume_changed(impl->user_data, &dev->device);
+
+ return 0;
+}
+
+static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
+{
+ pa_cvolume r;
+
+ dev->real_volume = *v;
+
+ if (!dev->mixer_handle)
+ return;
+
+ /* Shift up by the base volume */
+ pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
+
+ if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
+ &r, false, true) < 0)
+ return;
+
+ /* Shift down by the base volume, so that 0dB becomes maximum volume */
+ pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
+
+ dev->hardware_volume = r;
+
+ if (dev->mixer_path->has_dB) {
+ pa_cvolume new_soft_volume;
+ bool accurate_enough;
+
+ /* Match exactly what the user requested by software */
+ pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
+
+ /* If the adjustment to do in software is only minimal we
+ * can skip it. That saves us CPU at the expense of a bit of
+ * accuracy */
+ accurate_enough =
+ (pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) &&
+ (pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY));
+
+ pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume));
+ pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume));
+ pa_log_debug("Calculated software volume: %d (accurate-enough=%s)",
+ pa_cvolume_max(&new_soft_volume),
+ pa_yes_no(accurate_enough));
+
+ if (accurate_enough)
+ pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
+
+ dev->soft_volume = new_soft_volume;
+ } else {
+ pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r));
+ /* We can't match exactly what the user requested, hence let's
+ * at least tell the user about it */
+ dev->real_volume = r;
+ }
+}
+
+static int read_mute(pa_alsa_device *dev)
+{
+ pa_card *impl = dev->card;
+ bool mute;
+ int res;
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
+ return res;
+
+ if (mute == dev->muted)
+ return 0;
+
+ dev->muted = mute;
+ pa_log_info("New hardware muted: %d", mute);
+
+ if (impl->events && impl->events->mute_changed)
+ impl->events->mute_changed(impl->user_data, &dev->device);
+
+ return 0;
+}
+
+static void set_mute(pa_alsa_device *dev, bool mute)
+{
+ dev->muted = mute;
+
+ if (!dev->mixer_handle)
+ return;
+
+ pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
+}
+
+static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
+{
+ pa_assert(dev);
+
+ if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
+ dev->read_volume = NULL;
+ dev->set_volume = NULL;
+ pa_log_info("Driver does not support hardware volume control, "
+ "falling back to software volume control.");
+ dev->base_volume = PA_VOLUME_NORM;
+ dev->n_volume_steps = PA_VOLUME_NORM+1;
+ dev->device.flags &= ~ACP_DEVICE_HW_VOLUME;
+ } else {
+ dev->read_volume = read_volume;
+ dev->set_volume = set_volume;
+ dev->device.flags |= ACP_DEVICE_HW_VOLUME;
+
+#if 0
+ if (u->mixer_path->has_dB && u->deferred_volume) {
+ pa_sink_set_write_volume_callback(u->sink, sink_write_volume_cb);
+ pa_log_info("Successfully enabled deferred volume.");
+ } else
+ pa_sink_set_write_volume_callback(u->sink, NULL);
+#endif
+
+ if (dev->mixer_path->has_dB) {
+ dev->decibel_volume = true;
+ pa_log_info("Hardware volume ranges from %0.2f dB to %0.2f dB.",
+ dev->mixer_path->min_dB, dev->mixer_path->max_dB);
+
+ dev->base_volume = pa_sw_volume_from_dB(-dev->mixer_path->max_dB);
+ dev->n_volume_steps = PA_VOLUME_NORM+1;
+
+ pa_log_info("Fixing base volume to %0.2f dB", pa_sw_volume_to_dB(dev->base_volume));
+ } else {
+ dev->decibel_volume = false;
+ pa_log_info("Hardware volume ranges from %li to %li.",
+ dev->mixer_path->min_volume, dev->mixer_path->max_volume);
+ dev->base_volume = PA_VOLUME_NORM;
+ dev->n_volume_steps = dev->mixer_path->max_volume - dev->mixer_path->min_volume + 1;
+ }
+ pa_log_info("Using hardware volume control. Hardware dB scale %s.",
+ dev->mixer_path->has_dB ? "supported" : "not supported");
+ }
+ dev->device.base_volume = pa_sw_volume_to_linear(dev->base_volume);
+ dev->device.volume_step = 1.0f / dev->n_volume_steps;
+
+ if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_mute) {
+ dev->read_mute = NULL;
+ dev->set_mute = NULL;
+ pa_log_info("Driver does not support hardware mute control, falling back to software mute control.");
+ dev->device.flags &= ~ACP_DEVICE_HW_MUTE;
+ } else {
+ dev->read_mute = read_mute;
+ dev->set_mute = set_mute;
+ pa_log_info("Using hardware mute control.");
+ dev->device.flags |= ACP_DEVICE_HW_MUTE;
+ }
+}
+
+
+static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
+{
+ int res;
+ bool need_mixer_callback = false;
+
+ /* This code is before the u->mixer_handle check, because if the UCM
+ * configuration doesn't specify volume or mute controls, u->mixer_handle
+ * will be NULL, but the UCM device enable sequence will still need to be
+ * executed. */
+ if (dev->active_port && dev->ucm_context) {
+ if ((res = pa_alsa_ucm_set_port(dev->ucm_context, dev->active_port,
+ dev->direction == PA_ALSA_DIRECTION_OUTPUT)) < 0)
+ return res;
+ }
+
+ if (!dev->mixer_handle)
+ return 0;
+
+ if (dev->active_port) {
+ if (!impl->use_ucm) {
+ pa_alsa_port_data *data;
+
+ /* We have a list of supported paths, so let's activate the
+ * one that has been chosen as active */
+ data = PA_DEVICE_PORT_DATA(dev->active_port);
+ dev->mixer_path = data->path;
+
+ pa_alsa_path_select(data->path, data->setting, dev->mixer_handle, dev->muted);
+ } else {
+ pa_alsa_ucm_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(dev->active_port);
+
+ /* Now activate volume controls, if any */
+ if (data->path) {
+ dev->mixer_path = data->path;
+ pa_alsa_path_select(dev->mixer_path, NULL, dev->mixer_handle, dev->muted);
+ }
+ }
+ } else {
+ if (!dev->mixer_path && dev->mixer_path_set)
+ dev->mixer_path = pa_hashmap_first(dev->mixer_path_set->paths);
+
+ if (dev->mixer_path) {
+ /* Hmm, we have only a single path, then let's activate it */
+ pa_alsa_path_select(dev->mixer_path, dev->mixer_path->settings,
+ dev->mixer_handle, dev->muted);
+ } else
+ return 0;
+ }
+
+ mixer_volume_init(impl, dev);
+
+ /* Will we need to register callbacks? */
+ if (dev->mixer_path_set && dev->mixer_path_set->paths) {
+ pa_alsa_path *p;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, dev->mixer_path_set->paths, state) {
+ if (p->has_volume || p->has_mute)
+ need_mixer_callback = true;
+ }
+ }
+ else if (dev->mixer_path)
+ need_mixer_callback = dev->mixer_path->has_volume || dev->mixer_path->has_mute;
+
+ if (!impl->soft_mixer && need_mixer_callback) {
+ pa_alsa_mixer_use_for_poll(impl->ucm.mixers, dev->mixer_handle);
+ if (dev->mixer_path_set)
+ pa_alsa_path_set_set_callback(dev->mixer_path_set, dev->mixer_handle, mixer_callback, dev);
+ else
+ pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev);
+ }
+ return 0;
+}
+
+static int device_disable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
+{
+ dev->device.flags &= ~ACP_DEVICE_ACTIVE;
+ if (dev->active_port) {
+ dev->active_port->port.flags &= ~ACP_PORT_ACTIVE;
+ dev->active_port = NULL;
+ }
+ return 0;
+}
+
+static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device *dev)
+{
+ const char *mod_name;
+ uint32_t i, port_index;
+ int res;
+
+ if (impl->use_ucm &&
+ (mod_name = pa_proplist_gets(mapping->proplist, PA_ALSA_PROP_UCM_MODIFIER))) {
+ if (snd_use_case_set(impl->ucm.ucm_mgr, "_enamod", mod_name) < 0)
+ pa_log("Failed to enable ucm modifier %s", mod_name);
+ else
+ pa_log_debug("Enabled ucm modifier %s", mod_name);
+ }
+
+ pa_log_info("Device: %s mapping '%s' (%s).", dev->device.description,
+ mapping->description, mapping->name);
+
+ dev->device.flags |= ACP_DEVICE_ACTIVE;
+
+ find_mixer(impl, dev, NULL, impl->ignore_dB);
+
+ /* Synchronize priority values, as it may have changed when setting the profile */
+ for (i = 0; i < impl->card.n_ports; i++) {
+ pa_device_port *p = (pa_device_port *)impl->card.ports[i];
+ p->port.priority = p->priority;
+ }
+
+ if (impl->auto_port)
+ port_index = acp_device_find_best_port_index(&dev->device, NULL);
+ else
+ port_index = ACP_INVALID_INDEX;
+
+ if (port_index == ACP_INVALID_INDEX)
+ dev->active_port = NULL;
+ else
+ dev->active_port = (pa_device_port*)impl->card.ports[port_index];
+
+ if (dev->active_port)
+ dev->active_port->port.flags |= ACP_PORT_ACTIVE;
+
+ if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0)
+ return res;
+
+ if (dev->read_volume)
+ dev->read_volume(dev);
+ else {
+ pa_cvolume_reset(&dev->real_volume, dev->device.format.channels);
+ pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels);
+ }
+ if (dev->read_mute)
+ dev->read_mute(dev);
+ else
+ dev->muted = false;
+
+ return 0;
+}
+
+int acp_card_set_profile(struct acp_card *card, uint32_t new_index, uint32_t flags)
+{
+ pa_card *impl = (pa_card *)card;
+ pa_alsa_mapping *am;
+ uint32_t old_index = impl->card.active_profile_index;
+ struct acp_card_profile **profiles = card->profiles;
+ pa_alsa_profile *op, *np;
+ uint32_t idx;
+ int res;
+
+ if (new_index >= card->n_profiles)
+ return -EINVAL;
+
+ op = old_index != ACP_INVALID_INDEX ? (pa_alsa_profile*)profiles[old_index] : NULL;
+ np = (pa_alsa_profile*)profiles[new_index];
+
+ if (op == np)
+ return 0;
+
+ pa_log_info("activate profile: %s (%d)", np->profile.name, new_index);
+
+ if (op && op->output_mappings) {
+ PA_IDXSET_FOREACH(am, op->output_mappings, idx) {
+ if (np->output_mappings &&
+ pa_idxset_get_by_data(np->output_mappings, am, NULL))
+ continue;
+
+ device_disable(impl, am, &am->output);
+ }
+ }
+ if (op && op->input_mappings) {
+ PA_IDXSET_FOREACH(am, op->input_mappings, idx) {
+ if (np->input_mappings &&
+ pa_idxset_get_by_data(np->input_mappings, am, NULL))
+ continue;
+
+ device_disable(impl, am, &am->input);
+ }
+ }
+
+ /* if UCM is available for this card then update the verb */
+ if (impl->use_ucm && !(np->profile.flags & ACP_PROFILE_PRO)) {
+ if ((res = pa_alsa_ucm_set_profile(&impl->ucm, impl,
+ np->profile.flags & ACP_PROFILE_OFF ? NULL : np->profile.name,
+ op ? op->profile.name : NULL)) < 0) {
+ return res;
+ }
+ }
+
+ if (np->output_mappings) {
+ PA_IDXSET_FOREACH(am, np->output_mappings, idx) {
+ if (impl->use_ucm) {
+ /* Update ports priorities */
+ if (am->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(am->output.ports, &am->ucm_context,
+ true, impl->ports, np, NULL);
+ }
+ }
+ device_enable(impl, am, &am->output);
+ }
+ }
+
+ if (np->input_mappings) {
+ PA_IDXSET_FOREACH(am, np->input_mappings, idx) {
+ if (impl->use_ucm) {
+ /* Update ports priorities */
+ if (am->ucm_context.ucm_devices) {
+ pa_alsa_ucm_add_ports_combination(am->input.ports, &am->ucm_context,
+ false, impl->ports, np, NULL);
+ }
+ }
+ device_enable(impl, am, &am->input);
+ }
+ }
+ if (op)
+ op->profile.flags &= ~(ACP_PROFILE_ACTIVE | ACP_PROFILE_SAVE);
+ np->profile.flags |= ACP_PROFILE_ACTIVE | flags;
+ impl->card.active_profile_index = new_index;
+
+ if (impl->events && impl->events->profile_changed)
+ impl->events->profile_changed(impl->user_data, old_index,
+ new_index);
+ return 0;
+}
+
+static void prune_singleton_availability_groups(pa_hashmap *ports) {
+ pa_device_port *p;
+ pa_hashmap *group_counts;
+ void *state, *count;
+ const char *group;
+
+ /* Collect groups and erase those that don't have more than 1 path */
+ group_counts = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+ PA_HASHMAP_FOREACH(p, ports, state) {
+ if (p->availability_group) {
+ count = pa_hashmap_get(group_counts, p->availability_group);
+ pa_hashmap_remove(group_counts, p->availability_group);
+ pa_hashmap_put(group_counts, p->availability_group, PA_UINT_TO_PTR(PA_PTR_TO_UINT(count) + 1));
+ }
+ }
+
+ /* Now we have an availability_group -> count map, let's drop all groups
+ * that have only one member */
+ PA_HASHMAP_FOREACH_KV(group, count, group_counts, state) {
+ if (count == PA_UINT_TO_PTR(1))
+ pa_hashmap_remove(group_counts, group);
+ }
+
+ PA_HASHMAP_FOREACH(p, ports, state) {
+ if (p->availability_group && !pa_hashmap_get(group_counts, p->availability_group)) {
+ pa_log_debug("Pruned singleton availability group %s from port %s", p->availability_group, p->name);
+ pa_xfree(p->availability_group);
+ p->availability_group = NULL;
+ }
+ }
+
+ pa_hashmap_free(group_counts);
+}
+
+static const char *acp_dict_lookup(const struct acp_dict *dict, const char *key)
+{
+ const struct acp_dict_item *it;
+ acp_dict_for_each(it, dict) {
+ if (spa_streq(key, it->key))
+ return it->value;
+ }
+ return NULL;
+}
+
+struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
+{
+ pa_card *impl;
+ struct acp_card *card;
+ const char *s, *profile_set = NULL, *profile = NULL;
+ char device_id[16];
+ uint32_t profile_index;
+ int res;
+
+ impl = calloc(1, sizeof(*impl));
+ if (impl == NULL)
+ return NULL;
+
+ pa_alsa_refcnt_inc();
+
+ snprintf(device_id, sizeof(device_id), "%d", index);
+
+ impl->proplist = pa_proplist_new_dict(props);
+
+ card = &impl->card;
+ card->index = index;
+ card->active_profile_index = ACP_INVALID_INDEX;
+
+ impl->use_ucm = true;
+ impl->auto_profile = true;
+ impl->auto_port = true;
+ impl->ignore_dB = false;
+ impl->rate = DEFAULT_RATE;
+
+ if (props) {
+ if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL)
+ impl->use_ucm = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.alsa.soft-mixer")) != NULL)
+ impl->soft_mixer = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.alsa.ignore-dB")) != NULL)
+ impl->ignore_dB = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "device.profile-set")) != NULL)
+ profile_set = s;
+ if ((s = acp_dict_lookup(props, "device.profile")) != NULL)
+ profile = s;
+ if ((s = acp_dict_lookup(props, "api.acp.auto-profile")) != NULL)
+ impl->auto_profile = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.acp.auto-port")) != NULL)
+ impl->auto_port = spa_atob(s);
+ if ((s = acp_dict_lookup(props, "api.acp.probe-rate")) != NULL)
+ impl->rate = atoi(s);
+ }
+
+ impl->ucm.default_sample_spec.format = PA_SAMPLE_S16NE;
+ impl->ucm.default_sample_spec.rate = impl->rate;
+ impl->ucm.default_sample_spec.channels = 2;
+ pa_channel_map_init_extend(&impl->ucm.default_channel_map,
+ impl->ucm.default_sample_spec.channels, PA_CHANNEL_MAP_ALSA);
+ impl->ucm.default_n_fragments = 4;
+ impl->ucm.default_fragment_size_msec = 25;
+
+ impl->ucm.mixers = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func,
+ pa_xfree, (pa_free_cb_t) pa_alsa_mixer_free);
+ impl->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) profile_free);
+ impl->ports = pa_hashmap_new_full(pa_idxset_string_hash_func,
+ pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) port_free);
+
+ snd_config_update_free_global();
+
+ res = impl->use_ucm ? pa_alsa_ucm_query_profiles(&impl->ucm, card->index) : -1;
+ if (res == -PA_ALSA_ERR_UCM_LINKED) {
+ res = -ENOENT;
+ goto error;
+ }
+ if (res == 0) {
+ pa_log_info("Found UCM profiles");
+ impl->profile_set = pa_alsa_ucm_add_profile_set(&impl->ucm, &impl->ucm.default_channel_map);
+ } else {
+ impl->use_ucm = false;
+ impl->profile_set = pa_alsa_profile_set_new(profile_set, &impl->ucm.default_channel_map);
+ }
+ if (impl->profile_set == NULL) {
+ res = -ENOTSUP;
+ goto error;
+ }
+
+ impl->profile_set->ignore_dB = impl->ignore_dB;
+
+ pa_alsa_profile_set_probe(impl->profile_set, impl->ucm.mixers,
+ device_id,
+ &impl->ucm.default_sample_spec,
+ impl->ucm.default_n_fragments,
+ impl->ucm.default_fragment_size_msec);
+
+ pa_alsa_init_proplist_card(NULL, impl->proplist, impl->card.index);
+ pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_STRING, device_id);
+ pa_alsa_init_description(impl->proplist, NULL);
+
+ add_profiles(impl);
+ prune_singleton_availability_groups(impl->ports);
+
+ card->n_profiles = pa_dynarray_size(&impl->out.profiles);
+ card->profiles = impl->out.profiles.array.data;
+
+ card->n_ports = pa_dynarray_size(&impl->out.ports);
+ card->ports = impl->out.ports.array.data;
+
+ card->n_devices = pa_dynarray_size(&impl->out.devices);
+ card->devices = impl->out.devices.array.data;
+
+ pa_proplist_as_dict(impl->proplist, &card->props);
+
+ init_jacks(impl);
+
+ if (!impl->auto_profile && profile == NULL)
+ profile = "off";
+
+ profile_index = acp_card_find_best_profile_index(&impl->card, profile);
+ acp_card_set_profile(&impl->card, profile_index, 0);
+
+ init_eld_ctls(impl);
+
+ return &impl->card;
+error:
+ pa_alsa_refcnt_dec();
+ free(impl);
+ errno = -res;
+ return NULL;
+}
+
+void acp_card_add_listener(struct acp_card *card,
+ const struct acp_card_events *events, void *user_data)
+{
+ pa_card *impl = (pa_card *)card;
+ impl->events = events;
+ impl->user_data = user_data;
+}
+
+void acp_card_destroy(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ if (impl->profiles)
+ pa_hashmap_free(impl->profiles);
+ if (impl->ports)
+ pa_hashmap_free(impl->ports);
+ pa_dynarray_clear(&impl->out.devices);
+ pa_dynarray_clear(&impl->out.profiles);
+ pa_dynarray_clear(&impl->out.ports);
+ if (impl->ucm.mixers)
+ pa_hashmap_free(impl->ucm.mixers);
+ if (impl->jacks)
+ pa_hashmap_free(impl->jacks);
+ if (impl->profile_set)
+ pa_alsa_profile_set_free(impl->profile_set);
+ pa_alsa_ucm_free(&impl->ucm);
+ pa_proplist_free(impl->proplist);
+ pa_alsa_refcnt_dec();
+ free(impl);
+}
+
+int acp_card_poll_descriptors_count(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+ n = snd_mixer_poll_descriptors_count(pm->mixer_handle);
+ if (n < 0)
+ return n;
+ count += n;
+ }
+ return count;
+}
+
+int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+
+ n = snd_mixer_poll_descriptors(pm->mixer_handle, pfds, space);
+ if (n < 0)
+ return n;
+ if (space >= (unsigned int) n) {
+ count += n;
+ space -= n;
+ pfds += n;
+ } else
+ space = 0;
+ }
+ return count;
+}
+
+int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds,
+ unsigned int nfds, unsigned short *revents)
+{
+ unsigned int idx;
+ unsigned short res;
+ if (nfds == 0)
+ return -EINVAL;
+ res = 0;
+ for (idx = 0; idx < nfds; idx++, pfds++)
+ res |= pfds->revents & (POLLIN|POLLERR|POLLNVAL);
+ *revents = res;
+ return 0;
+}
+
+int acp_card_handle_events(struct acp_card *card)
+{
+ pa_card *impl = (pa_card *)card;
+ void *state;
+ pa_alsa_mixer *pm;
+ int n, count = 0;
+
+ PA_HASHMAP_FOREACH(pm, impl->ucm.mixers, state) {
+ if (!pm->used_for_poll)
+ continue;
+
+ n = snd_mixer_handle_events(pm->mixer_handle);
+ if (n < 0)
+ return n;
+ count += n;
+ }
+ return count;
+}
+
+static void sync_mixer(pa_alsa_device *d, pa_device_port *port)
+{
+ pa_alsa_setting *setting = NULL;
+
+ if (!d->mixer_path)
+ return;
+
+ /* port may be NULL, because if we use a synthesized mixer path, then the
+ * sink has no ports. */
+ if (port && !d->ucm_context) {
+ pa_alsa_port_data *data;
+ data = PA_DEVICE_PORT_DATA(port);
+ setting = data->setting;
+ }
+
+ if (d->mixer_handle)
+ pa_alsa_path_select(d->mixer_path, setting, d->mixer_handle, d->muted);
+
+ if (d->set_mute)
+ d->set_mute(d, d->muted);
+ if (d->set_volume)
+ d->set_volume(d, &d->real_volume);
+}
+
+
+uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name)
+{
+ uint32_t i;
+ uint32_t best, best2, best3;
+ struct acp_port **ports = dev->ports;
+
+ best = best2 = best3 = ACP_INVALID_INDEX;
+
+ for (i = 0; i < dev->n_ports; i++) {
+ struct acp_port *p = ports[i];
+
+ if (name) {
+ if (spa_streq(name, p->name))
+ best = i;
+ } else if (p->available == ACP_AVAILABLE_YES) {
+ if (best == ACP_INVALID_INDEX || p->priority > ports[best]->priority)
+ best = i;
+ } else if (p->available != ACP_AVAILABLE_NO) {
+ if (best2 == ACP_INVALID_INDEX || p->priority > ports[best2]->priority)
+ best2 = i;
+ } else {
+ if (best3 == ACP_INVALID_INDEX || p->priority > ports[best3]->priority)
+ best3 = i;
+ }
+ }
+ if (best == ACP_INVALID_INDEX)
+ best = best2;
+ if (best == ACP_INVALID_INDEX)
+ best = best3;
+ if (best == ACP_INVALID_INDEX)
+ best = 0;
+ if (best < dev->n_ports)
+ return ports[best]->index;
+ else
+ return ACP_INVALID_INDEX;
+}
+
+int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ pa_device_port *p, *old = d->active_port;
+ int res;
+
+ if (port_index >= impl->card.n_ports)
+ return -EINVAL;
+
+ p = (pa_device_port*)impl->card.ports[port_index];
+ if (!pa_hashmap_get(d->ports, p->name))
+ return -EINVAL;
+
+ p->port.flags = ACP_PORT_ACTIVE | flags;
+ if (p == old)
+ return 0;
+ if (old)
+ old->port.flags &= ~(ACP_PORT_ACTIVE | ACP_PORT_SAVE);
+ d->active_port = p;
+
+ if (impl->use_ucm) {
+ pa_alsa_ucm_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(p);
+ d->mixer_path = data->path;
+ mixer_volume_init(impl, d);
+
+ sync_mixer(d, p);
+ res = pa_alsa_ucm_set_port(d->ucm_context, p,
+ dev->direction == ACP_DIRECTION_PLAYBACK);
+ } else {
+ pa_alsa_port_data *data;
+
+ data = PA_DEVICE_PORT_DATA(p);
+ d->mixer_path = data->path;
+ mixer_volume_init(impl, d);
+
+ sync_mixer(d, p);
+ res = 0;
+#if 0
+ if (data->suspend_when_unavailable && p->available == PA_AVAILABLE_NO)
+ pa_sink_suspend(s, true, PA_SUSPEND_UNAVAILABLE);
+ else
+ pa_sink_suspend(s, false, PA_SUSPEND_UNAVAILABLE);
+#endif
+ }
+ if (impl->events && impl->events->port_changed)
+ impl->events->port_changed(impl->user_data,
+ old ? old->port.index : 0, p->port.index);
+ return res;
+}
+
+int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ uint32_t i;
+ pa_cvolume v, old_volume;
+
+ if (n_volume == 0)
+ return -EINVAL;
+
+ old_volume = d->real_volume;
+
+ v.channels = d->mapping->channel_map.channels;
+ for (i = 0; i < v.channels; i++)
+ v.values[i] = pa_sw_volume_from_linear(volume[i % n_volume]);
+
+ pa_log_info("Set %s volume: min:%d max:%d",
+ d->set_volume ? "hardware" : "software",
+ pa_cvolume_min(&v), pa_cvolume_max(&v));
+
+ for (i = 0; i < v.channels; i++)
+ pa_log_debug(" %d: %d", i, v.values[i]);
+
+ if (d->set_volume) {
+ d->set_volume(d, &v);
+ } else {
+ d->real_volume = v;
+ d->soft_volume = v;
+ }
+ if (!pa_cvolume_equal(&d->real_volume, &old_volume))
+ if (impl->events && impl->events->volume_changed)
+ impl->events->volume_changed(impl->user_data, dev);
+ return 0;
+}
+
+static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume)
+{
+ uint32_t i;
+ if (v->channels == 0)
+ return -EIO;
+ for (i = 0; i < n_volume; i++)
+ volume[i] = pa_sw_volume_to_linear(v->values[i % v->channels]);
+ return 0;
+}
+
+int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ return get_volume(&d->soft_volume, volume, n_volume);
+}
+
+int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ return get_volume(&d->real_volume, volume, n_volume);
+}
+
+int acp_device_set_mute(struct acp_device *dev, bool mute)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ pa_card *impl = d->card;
+ bool old_muted = d->muted;
+
+ if (old_muted == mute)
+ return 0;
+
+ pa_log_info("Set %s mute: %d", d->set_mute ? "hardware" : "software", mute);
+
+ if (d->set_mute) {
+ d->set_mute(d, mute);
+ } else {
+ d->muted = mute;
+ }
+ if (old_muted != mute)
+ if (impl->events && impl->events->mute_changed)
+ impl->events->mute_changed(impl->user_data, dev);
+
+ return 0;
+}
+
+int acp_device_get_mute(struct acp_device *dev, bool *mute)
+{
+ pa_alsa_device *d = (pa_alsa_device*)dev;
+ *mute = d->muted;
+ return 0;
+}
+
+void acp_set_log_func(acp_log_func func, void *data)
+{
+ _acp_log_func = func;
+ _acp_log_data = data;
+}
+void acp_set_log_level(int level)
+{
+ _acp_log_level = level;
+}
diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h
new file mode 100644
index 0000000..3fed6f6
--- /dev/null
+++ b/spa/plugins/alsa/acp/acp.h
@@ -0,0 +1,308 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef ACP_H
+#define ACP_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <poll.h>
+
+#ifdef __GNUC__
+#define ACP_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#else
+#define ACP_PRINTF_FUNC(fmt, arg1)
+#endif
+
+#define ACP_INVALID_INDEX ((uint32_t)-1)
+#define ACP_MAX_CHANNELS 64
+
+struct acp_dict_item {
+ const char *key;
+ const char *value;
+};
+#define ACP_DICT_ITEM_INIT(key,value) ((struct acp_dict_item) { (key), (value) })
+
+struct acp_dict {
+ uint32_t flags;
+ uint32_t n_items;
+ const struct acp_dict_item *items;
+};
+
+enum acp_channel {
+ ACP_CHANNEL_UNKNOWN, /**< unspecified */
+ ACP_CHANNEL_NA, /**< N/A, silent */
+
+ ACP_CHANNEL_MONO, /**< mono stream */
+
+ ACP_CHANNEL_FL, /**< front left */
+ ACP_CHANNEL_FR, /**< front right */
+ ACP_CHANNEL_FC, /**< front center */
+ ACP_CHANNEL_LFE, /**< LFE */
+ ACP_CHANNEL_SL, /**< side left */
+ ACP_CHANNEL_SR, /**< side right */
+ ACP_CHANNEL_FLC, /**< front left center */
+ ACP_CHANNEL_FRC, /**< front right center */
+ ACP_CHANNEL_RC, /**< rear center */
+ ACP_CHANNEL_RL, /**< rear left */
+ ACP_CHANNEL_RR, /**< rear right */
+ ACP_CHANNEL_TC, /**< top center */
+ ACP_CHANNEL_TFL, /**< top front left */
+ ACP_CHANNEL_TFC, /**< top front center */
+ ACP_CHANNEL_TFR, /**< top front right */
+ ACP_CHANNEL_TRL, /**< top rear left */
+ ACP_CHANNEL_TRC, /**< top rear center */
+ ACP_CHANNEL_TRR, /**< top rear right */
+ ACP_CHANNEL_RLC, /**< rear left center */
+ ACP_CHANNEL_RRC, /**< rear right center */
+ ACP_CHANNEL_FLW, /**< front left wide */
+ ACP_CHANNEL_FRW, /**< front right wide */
+ ACP_CHANNEL_LFE2, /**< LFE 2 */
+ ACP_CHANNEL_FLH, /**< front left high */
+ ACP_CHANNEL_FCH, /**< front center high */
+ ACP_CHANNEL_FRH, /**< front right high */
+ ACP_CHANNEL_TFLC, /**< top front left center */
+ ACP_CHANNEL_TFRC, /**< top front right center */
+ ACP_CHANNEL_TSL, /**< top side left */
+ ACP_CHANNEL_TSR, /**< top side right */
+ ACP_CHANNEL_LLFE, /**< left LFE */
+ ACP_CHANNEL_RLFE, /**< right LFE */
+ ACP_CHANNEL_BC, /**< bottom center */
+ ACP_CHANNEL_BLC, /**< bottom left center */
+ ACP_CHANNEL_BRC, /**< bottom right center */
+
+ ACP_CHANNEL_START_Aux = 0x1000,
+ ACP_CHANNEL_LAST_Aux = 0x1fff,
+
+ ACP_CHANNEL_START_Custom = 0x10000,
+};
+
+char *acp_channel_str(char *buf, size_t len, enum acp_channel ch);
+
+struct acp_format {
+ uint32_t flags;
+ uint32_t format_mask;
+ uint32_t rate_mask;
+ uint32_t channels;
+ uint32_t map[ACP_MAX_CHANNELS];
+};
+
+#define ACP_DICT_INIT(items,n_items) ((struct acp_dict) { 0, (n_items), (items) })
+#define ACP_DICT_INIT_ARRAY(items) ((struct acp_dict) { 0, sizeof(items)/sizeof((items)[0]), (items) })
+
+#define acp_dict_for_each(item, dict) \
+ for ((item) = (dict)->items; \
+ (item) < &(dict)->items[(dict)->n_items]; \
+ (item)++)
+
+enum acp_direction {
+ ACP_DIRECTION_PLAYBACK = 1,
+ ACP_DIRECTION_CAPTURE = 2
+};
+
+const char *acp_direction_str(enum acp_direction direction);
+
+enum acp_available {
+ ACP_AVAILABLE_UNKNOWN = 0,
+ ACP_AVAILABLE_NO = 1,
+ ACP_AVAILABLE_YES = 2
+};
+
+const char *acp_available_str(enum acp_available status);
+
+#define ACP_KEY_PORT_TYPE "port.type" /**< a Port type, like "aux", "speaker", ... */
+#define ACP_KEY_PORT_AVAILABILITY_GROUP "port.availability-group"
+ /**< An identifier for the group of ports that share their availability status with
+ * each other. This is meant especially for handling cases where one 3.5 mm connector
+ * is used for headphones, headsets and microphones, and the hardware can only tell
+ * that something was plugged in but not what exactly. In this situation the ports for
+ * all those devices share their availability status, and ACP can't tell which
+ * one is actually plugged in, and some application may ask the user what was plugged
+ * in. Such applications should get a list of all card ports and compare their
+ * `available_group` fields. Ports that have the same group are those that need
+ * input from the user to determine which device was plugged in. The application should
+ * then activate the user-chosen port.
+ *
+ * May be NULL, in which case the port is not part of any availability group (which is
+ * the same as having a group with only one member).
+ *
+ * The group identifier must be treated as an opaque identifier. The string may look
+ * like an ALSA control name, but applications must not assume any such relationship.
+ * The group naming scheme can change without a warning.
+ */
+
+struct acp_device;
+
+struct acp_card_events {
+#define ACP_VERSION_CARD_EVENTS 0
+ uint32_t version;
+
+ void (*destroy) (void *data);
+
+ void (*props_changed) (void *data);
+
+ void (*profile_changed) (void *data, uint32_t old_index, uint32_t new_index);
+
+ void (*profile_available) (void *data, uint32_t index,
+ enum acp_available old, enum acp_available available);
+
+ void (*port_changed) (void *data, uint32_t old_index, uint32_t new_index);
+
+ void (*port_available) (void *data, uint32_t index,
+ enum acp_available old, enum acp_available available);
+
+ void (*volume_changed) (void *data, struct acp_device *dev);
+ void (*mute_changed) (void *data, struct acp_device *dev);
+};
+
+struct acp_port {
+ uint32_t index; /**< unique index for this port */
+#define ACP_PORT_ACTIVE (1<<0)
+#define ACP_PORT_SAVE (1<<1) /* if the port needs saving */
+ uint32_t flags; /**< extra port flags */
+
+ const char *name; /**< Name of this port */
+ const char *description; /**< Description of this port */
+ uint32_t priority; /**< The higher this value is, the more useful this port is as a default. */
+ enum acp_direction direction;
+ enum acp_available available; /**< A flags (see #acp_port_available), indicating availability status of this port. */
+ struct acp_dict props; /**< extra port properties */
+
+ uint32_t n_profiles; /**< number of elements in profiles array */
+ struct acp_card_profile **profiles; /**< array of profiles for this port */
+
+ uint32_t n_devices; /**< number of elements in devices array */
+ struct acp_device **devices; /**< array of devices */
+};
+
+struct acp_device {
+ uint32_t index;
+#define ACP_DEVICE_ACTIVE (1<<0)
+#define ACP_DEVICE_HW_VOLUME (1<<1)
+#define ACP_DEVICE_HW_MUTE (1<<2)
+#define ACP_DEVICE_UCM_DEVICE (1<<3)
+#define ACP_DEVICE_IEC958 (1<<4)
+ uint32_t flags;
+
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ enum acp_direction direction;
+ struct acp_dict props;
+
+ const char **device_strings;
+ struct acp_format format;
+
+ float base_volume;
+ float volume_step;
+
+ uint32_t n_ports;
+ struct acp_port **ports;
+
+ int64_t latency_ns;
+ uint32_t codecs[32];
+ uint32_t n_codecs;
+};
+
+struct acp_card_profile {
+ uint32_t index;
+#define ACP_PROFILE_ACTIVE (1<<0)
+#define ACP_PROFILE_OFF (1<<1) /* the Off profile */
+#define ACP_PROFILE_SAVE (1<<2) /* if the profile needs saving */
+#define ACP_PROFILE_PRO (1<<3) /* the Pro profile */
+ uint32_t flags;
+
+ const char *name;
+ const char *description;
+ uint32_t priority;
+ enum acp_available available;
+ struct acp_dict props;
+
+ uint32_t n_devices;
+ struct acp_device **devices;
+};
+
+struct acp_card {
+ uint32_t index;
+ uint32_t flags;
+
+ struct acp_dict props;
+
+ uint32_t n_profiles;
+ uint32_t active_profile_index;
+ struct acp_card_profile **profiles;
+
+ uint32_t n_devices;
+ struct acp_device **devices;
+
+ uint32_t n_ports;
+ struct acp_port **ports;
+ uint32_t preferred_input_port_index;
+ uint32_t preferred_output_port_index;
+};
+
+struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props);
+
+void acp_card_add_listener(struct acp_card *card,
+ const struct acp_card_events *events, void *user_data);
+
+void acp_card_destroy(struct acp_card *card);
+
+int acp_card_poll_descriptors_count(struct acp_card *card);
+int acp_card_poll_descriptors(struct acp_card *card, struct pollfd *pfds, unsigned int space);
+int acp_card_poll_descriptors_revents(struct acp_card *card, struct pollfd *pfds,
+ unsigned int nfds, unsigned short *revents);
+int acp_card_handle_events(struct acp_card *card);
+
+uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name);
+int acp_card_set_profile(struct acp_card *card, uint32_t profile_index, uint32_t flags);
+
+uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *name);
+int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags);
+
+int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume);
+int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
+int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
+int acp_device_set_mute(struct acp_device *dev, bool mute);
+int acp_device_get_mute(struct acp_device *dev, bool *mute);
+
+typedef void (*acp_log_func) (void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg) ACP_PRINTF_FUNC(6,0);
+
+void acp_set_log_func(acp_log_func, void *data);
+void acp_set_log_level(int level);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACP_H */
diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c
new file mode 100644
index 0000000..86425fd
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-mixer.c
@@ -0,0 +1,5398 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <alsa/asoundlib.h>
+#include <math.h>
+
+#include <valgrind/memcheck.h>
+
+#include "conf-parser.h"
+#include "alsa-mixer.h"
+#include "alsa-util.h"
+
+static int setting_select(pa_alsa_setting *s, snd_mixer_t *m);
+
+struct description_map {
+ const char *key;
+ const char *description;
+};
+
+struct description2_map {
+ const char *key;
+ const char *description;
+ pa_device_port_type_t type;
+};
+
+char *pa_alsa_mixer_id_to_string(char *dst, size_t dst_len, pa_alsa_mixer_id *id) {
+ if (id->index > 0) {
+ snprintf(dst, dst_len, "'%s',%d", id->name, id->index);
+ } else {
+ snprintf(dst, dst_len, "'%s'", id->name);
+ }
+ return dst;
+}
+
+static int alsa_id_decode(const char *src, char *name, int *index) {
+ char *idx, c;
+ int i;
+
+ *index = 0;
+ c = src[0];
+ /* Strip quotes in entries such as 'Speaker',1 or "Speaker",1 */
+ if (c == '\'' || c == '"') {
+ strcpy(name, src + 1);
+ for (i = 0; name[i] != '\0' && name[i] != c; i++);
+ idx = NULL;
+ if (name[i]) {
+ name[i] = '\0';
+ idx = strchr(name + i + 1, ',');
+ }
+ } else {
+ strcpy(name, src);
+ idx = strchr(name, ',');
+ }
+ if (idx == NULL)
+ return 0;
+ *idx = '\0';
+ idx++;
+ if (*idx < '0' || *idx > '9') {
+ pa_log("Element %s: index value is invalid", src);
+ return 1;
+ }
+ *index = atoi(idx);
+ return 0;
+}
+
+pa_alsa_jack *pa_alsa_jack_new(pa_alsa_path *path, const char *mixer_device_name, const char *name, int index) {
+ pa_alsa_jack *jack;
+
+ pa_assert(name);
+
+ jack = pa_xnew0(pa_alsa_jack, 1);
+ jack->path = path;
+ jack->mixer_device_name = pa_xstrdup(mixer_device_name);
+ jack->name = pa_xstrdup(name);
+ jack->alsa_id.name = pa_sprintf_malloc("%s Jack", name);
+ jack->alsa_id.index = index;
+ jack->state_unplugged = PA_AVAILABLE_NO;
+ jack->state_plugged = PA_AVAILABLE_YES;
+ jack->ucm_devices = pa_dynarray_new(NULL);
+ jack->ucm_hw_mute_devices = pa_dynarray_new(NULL);
+
+ return jack;
+}
+
+void pa_alsa_jack_free(pa_alsa_jack *jack) {
+ pa_assert(jack);
+
+ pa_dynarray_free(jack->ucm_hw_mute_devices);
+ pa_dynarray_free(jack->ucm_devices);
+
+ pa_xfree(jack->alsa_id.name);
+ pa_xfree(jack->name);
+ pa_xfree(jack->mixer_device_name);
+ pa_xfree(jack);
+}
+
+void pa_alsa_jack_set_has_control(pa_alsa_jack *jack, bool has_control) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+
+ pa_assert(jack);
+
+ if (has_control == jack->has_control)
+ return;
+
+ jack->has_control = has_control;
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_hw_mute_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+}
+
+void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+
+ pa_assert(jack);
+
+ if (plugged_in == jack->plugged_in)
+ return;
+
+ jack->plugged_in = plugged_in;
+
+ /* XXX: If this is a headphone jack that mutes speakers when plugged in,
+ * and the headphones get unplugged, then the headphone device must be set
+ * to unavailable and the speaker device must be set to unknown. So far so
+ * good. But there's an ugly detail: we must first set the availability of
+ * the speakers and then the headphones. We shouldn't need to care about
+ * the order, but we have to, because module-switch-on-port-available gets
+ * separate events for the two devices, and the intermediate state between
+ * the two events is such that the second event doesn't trigger the desired
+ * port switch, if the event order is "wrong".
+ *
+ * These are the transitions when the event order is "right":
+ *
+ * speakers: 1) unavailable -> 2) unknown -> 3) unknown
+ * headphones: 1) available -> 2) available -> 3) unavailable
+ *
+ * In the 2 -> 3 transition, headphones become unavailable, and
+ * module-switch-on-port-available sees that speakers can be used, so the
+ * port gets changed as it should.
+ *
+ * These are the transitions when the event order is "wrong":
+ *
+ * speakers: 1) unavailable -> 2) unavailable -> 3) unknown
+ * headphones: 1) available -> 2) unavailable -> 3) unavailable
+ *
+ * In the 1 -> 2 transition, headphones become unavailable, and there are
+ * no available ports to use, so no port change happens. In the 2 -> 3
+ * transition, speaker availability becomes unknown, but that's not
+ * a strong enough signal for module-switch-on-port-available, so it still
+ * doesn't do the port switch.
+ *
+ * We should somehow merge the two events so that
+ * module-switch-on-port-available would handle both transitions in one go.
+ * If module-switch-on-port-available used a defer event to delay
+ * the port availability processing, that would probably do the trick. */
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_hw_mute_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+
+ PA_DYNARRAY_FOREACH(device, jack->ucm_devices, idx)
+ pa_alsa_ucm_device_update_available(device);
+}
+
+void pa_alsa_jack_add_ucm_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device) {
+ pa_alsa_ucm_device *idevice;
+ unsigned idx, prio, iprio;
+
+ pa_assert(jack);
+ pa_assert(device);
+
+ /* store the ucm device with the sequence of priority from low to high. this
+ * could guarantee when the jack state is changed, the device with highest
+ * priority will send to the module-switch-on-port-available last */
+ prio = device->playback_priority ? device->playback_priority : device->capture_priority;
+
+ PA_DYNARRAY_FOREACH(idevice, jack->ucm_devices, idx) {
+ iprio = idevice->playback_priority ? idevice->playback_priority : idevice->capture_priority;
+ if (iprio > prio)
+ break;
+ }
+ pa_dynarray_insert_by_index(jack->ucm_devices, device, idx);
+}
+
+void pa_alsa_jack_add_ucm_hw_mute_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device) {
+ pa_assert(jack);
+ pa_assert(device);
+
+ pa_dynarray_append(jack->ucm_hw_mute_devices, device);
+}
+
+static const char *lookup_description(const char *key, const struct description_map dm[], unsigned n) {
+ unsigned i;
+
+ if (!key)
+ return NULL;
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dm[i].key, key))
+ return _(dm[i].description);
+
+ return NULL;
+}
+
+static const struct description2_map *lookup_description2(const char *key, const struct description2_map dm[], unsigned n) {
+ unsigned i;
+
+ if (!key)
+ return NULL;
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dm[i].key, key))
+ return &dm[i];
+
+ return NULL;
+}
+
+void pa_alsa_mixer_use_for_poll(pa_hashmap *mixers, snd_mixer_t *mixer_handle)
+{
+ pa_alsa_mixer *pm;
+ void *state;
+
+ PA_HASHMAP_FOREACH(pm, mixers, state) {
+ if (pm->mixer_handle == mixer_handle) {
+ pm->used_for_probe_only = false;
+ pm->used_for_poll = true;
+ }
+ }
+}
+
+#if 0
+struct pa_alsa_fdlist {
+ unsigned num_fds;
+ struct pollfd *fds;
+ /* This is a temporary buffer used to avoid lots of mallocs */
+ struct pollfd *work_fds;
+
+ snd_mixer_t *mixer;
+ snd_hctl_t *hctl;
+
+ pa_mainloop_api *m;
+ pa_defer_event *defer;
+ pa_io_event **ios;
+
+ bool polled;
+
+ void (*cb)(void *userdata);
+ void *userdata;
+};
+
+static void io_cb(pa_mainloop_api *a, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) {
+
+ struct pa_alsa_fdlist *fdl = userdata;
+ int err;
+ unsigned i;
+ unsigned short revents;
+
+ pa_assert(a);
+ pa_assert(fdl);
+ pa_assert(fdl->mixer || fdl->hctl);
+ pa_assert(fdl->fds);
+ pa_assert(fdl->work_fds);
+
+ if (fdl->polled)
+ return;
+
+ fdl->polled = true;
+
+ memcpy(fdl->work_fds, fdl->fds, sizeof(struct pollfd) * fdl->num_fds);
+
+ for (i = 0; i < fdl->num_fds; i++) {
+ if (e == fdl->ios[i]) {
+ if (events & PA_IO_EVENT_INPUT)
+ fdl->work_fds[i].revents |= POLLIN;
+ if (events & PA_IO_EVENT_OUTPUT)
+ fdl->work_fds[i].revents |= POLLOUT;
+ if (events & PA_IO_EVENT_ERROR)
+ fdl->work_fds[i].revents |= POLLERR;
+ if (events & PA_IO_EVENT_HANGUP)
+ fdl->work_fds[i].revents |= POLLHUP;
+ break;
+ }
+ }
+
+ pa_assert(i != fdl->num_fds);
+
+ if (fdl->hctl)
+ err = snd_hctl_poll_descriptors_revents(fdl->hctl, fdl->work_fds, fdl->num_fds, &revents);
+ else
+ err = snd_mixer_poll_descriptors_revents(fdl->mixer, fdl->work_fds, fdl->num_fds, &revents);
+
+ if (err < 0) {
+ pa_log_error("Unable to get poll revent: %s", pa_alsa_strerror(err));
+ return;
+ }
+
+ a->defer_enable(fdl->defer, 1);
+
+ if (revents) {
+ if (fdl->hctl)
+ snd_hctl_handle_events(fdl->hctl);
+ else
+ snd_mixer_handle_events(fdl->mixer);
+ }
+}
+
+static void defer_cb(pa_mainloop_api *a, pa_defer_event *e, void *userdata) {
+ struct pa_alsa_fdlist *fdl = userdata;
+ unsigned num_fds, i;
+ int err, n;
+ struct pollfd *temp;
+
+ pa_assert(a);
+ pa_assert(fdl);
+ pa_assert(fdl->mixer || fdl->hctl);
+
+ a->defer_enable(fdl->defer, 0);
+
+ if (fdl->hctl)
+ n = snd_hctl_poll_descriptors_count(fdl->hctl);
+ else
+ n = snd_mixer_poll_descriptors_count(fdl->mixer);
+
+ if (n < 0) {
+ pa_log("snd_mixer_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return;
+ }
+ else if (n == 0) {
+ pa_log_warn("Mixer has no poll descriptors. Please control mixer from PulseAudio only.");
+ return;
+ }
+ num_fds = (unsigned) n;
+
+ if (num_fds != fdl->num_fds) {
+ if (fdl->fds)
+ pa_xfree(fdl->fds);
+ if (fdl->work_fds)
+ pa_xfree(fdl->work_fds);
+ fdl->fds = pa_xnew0(struct pollfd, num_fds);
+ fdl->work_fds = pa_xnew(struct pollfd, num_fds);
+ }
+
+ memset(fdl->work_fds, 0, sizeof(struct pollfd) * num_fds);
+
+ if (fdl->hctl)
+ err = snd_hctl_poll_descriptors(fdl->hctl, fdl->work_fds, num_fds);
+ else
+ err = snd_mixer_poll_descriptors(fdl->mixer, fdl->work_fds, num_fds);
+
+ if (err < 0) {
+ pa_log_error("Unable to get poll descriptors: %s", pa_alsa_strerror(err));
+ return;
+ }
+
+ fdl->polled = false;
+
+ if (memcmp(fdl->fds, fdl->work_fds, sizeof(struct pollfd) * num_fds) == 0)
+ return;
+
+ if (fdl->ios) {
+ for (i = 0; i < fdl->num_fds; i++)
+ a->io_free(fdl->ios[i]);
+
+ if (num_fds != fdl->num_fds) {
+ pa_xfree(fdl->ios);
+ fdl->ios = NULL;
+ }
+ }
+
+ if (!fdl->ios)
+ fdl->ios = pa_xnew(pa_io_event*, num_fds);
+
+ /* Swap pointers */
+ temp = fdl->work_fds;
+ fdl->work_fds = fdl->fds;
+ fdl->fds = temp;
+
+ fdl->num_fds = num_fds;
+
+ for (i = 0;i < num_fds;i++)
+ fdl->ios[i] = a->io_new(a, fdl->fds[i].fd,
+ ((fdl->fds[i].events & POLLIN) ? PA_IO_EVENT_INPUT : 0) |
+ ((fdl->fds[i].events & POLLOUT) ? PA_IO_EVENT_OUTPUT : 0),
+ io_cb, fdl);
+}
+
+struct pa_alsa_fdlist *pa_alsa_fdlist_new(void) {
+ struct pa_alsa_fdlist *fdl;
+
+ fdl = pa_xnew0(struct pa_alsa_fdlist, 1);
+
+ return fdl;
+}
+
+void pa_alsa_fdlist_free(struct pa_alsa_fdlist *fdl) {
+ pa_assert(fdl);
+
+ if (fdl->defer) {
+ pa_assert(fdl->m);
+ fdl->m->defer_free(fdl->defer);
+ }
+
+ if (fdl->ios) {
+ unsigned i;
+ pa_assert(fdl->m);
+ for (i = 0; i < fdl->num_fds; i++)
+ fdl->m->io_free(fdl->ios[i]);
+ pa_xfree(fdl->ios);
+ }
+
+ if (fdl->fds)
+ pa_xfree(fdl->fds);
+ if (fdl->work_fds)
+ pa_xfree(fdl->work_fds);
+
+ pa_xfree(fdl);
+}
+
+/* We can listen to either a snd_hctl_t or a snd_mixer_t, but not both */
+int pa_alsa_fdlist_set_handle(struct pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, snd_hctl_t *hctl_handle, pa_mainloop_api *m) {
+ pa_assert(fdl);
+ pa_assert(hctl_handle || mixer_handle);
+ pa_assert(!(hctl_handle && mixer_handle));
+ pa_assert(m);
+ pa_assert(!fdl->m);
+
+ fdl->hctl = hctl_handle;
+ fdl->mixer = mixer_handle;
+ fdl->m = m;
+ fdl->defer = m->defer_new(m, defer_cb, fdl);
+
+ return 0;
+}
+
+struct pa_alsa_mixer_pdata {
+ pa_rtpoll *rtpoll;
+ pa_rtpoll_item *poll_item;
+ snd_mixer_t *mixer;
+};
+
+struct pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void) {
+ struct pa_alsa_mixer_pdata *pd;
+
+ pd = pa_xnew0(struct pa_alsa_mixer_pdata, 1);
+
+ return pd;
+}
+
+void pa_alsa_mixer_pdata_free(struct pa_alsa_mixer_pdata *pd) {
+ pa_assert(pd);
+
+ if (pd->poll_item) {
+ pa_rtpoll_item_free(pd->poll_item);
+ }
+
+ pa_xfree(pd);
+}
+
+static int rtpoll_work_cb(pa_rtpoll_item *i) {
+ struct pa_alsa_mixer_pdata *pd;
+ struct pollfd *p;
+ unsigned n_fds;
+ unsigned short revents = 0;
+ int err, ret = 0;
+
+ pd = pa_rtpoll_item_get_work_userdata(i);
+ pa_assert_fp(pd);
+ pa_assert_fp(i == pd->poll_item);
+
+ p = pa_rtpoll_item_get_pollfd(i, &n_fds);
+
+ if ((err = snd_mixer_poll_descriptors_revents(pd->mixer, p, n_fds, &revents)) < 0) {
+ pa_log_error("Unable to get poll revent: %s", pa_alsa_strerror(err));
+ ret = -1;
+ goto fail;
+ }
+
+ if (revents) {
+ if (revents & (POLLNVAL | POLLERR)) {
+ pa_log_debug("Device disconnected, stopping poll on mixer");
+ goto fail;
+ } else if (revents & POLLERR) {
+ /* This shouldn't happen. */
+ pa_log_error("Got a POLLERR (revents = %04x), stopping poll on mixer", revents);
+ goto fail;
+ }
+
+ err = snd_mixer_handle_events(pd->mixer);
+
+ if (PA_LIKELY(err >= 0)) {
+ pa_rtpoll_item_free(i);
+ pa_alsa_set_mixer_rtpoll(pd, pd->mixer, pd->rtpoll);
+ } else {
+ pa_log_error("Error handling mixer event: %s", pa_alsa_strerror(err));
+ ret = -1;
+ goto fail;
+ }
+ }
+
+ return ret;
+
+fail:
+ pa_rtpoll_item_free(i);
+
+ pd->poll_item = NULL;
+ pd->rtpoll = NULL;
+ pd->mixer = NULL;
+
+ return ret;
+}
+
+int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp) {
+ pa_rtpoll_item *i;
+ struct pollfd *p;
+ int err, n;
+
+ pa_assert(pd);
+ pa_assert(mixer);
+ pa_assert(rtp);
+
+ if ((n = snd_mixer_poll_descriptors_count(mixer)) < 0) {
+ pa_log("snd_mixer_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return -1;
+ }
+ else if (n == 0) {
+ pa_log_warn("Mixer has no poll descriptors. Please control mixer from PulseAudio only.");
+ return 0;
+ }
+
+ i = pa_rtpoll_item_new(rtp, PA_RTPOLL_LATE, (unsigned) n);
+
+ p = pa_rtpoll_item_get_pollfd(i, NULL);
+
+ memset(p, 0, sizeof(struct pollfd) * n);
+
+ if ((err = snd_mixer_poll_descriptors(mixer, p, (unsigned) n)) < 0) {
+ pa_log_error("Unable to get poll descriptors: %s", pa_alsa_strerror(err));
+ pa_rtpoll_item_free(i);
+ return -1;
+ }
+
+ pd->rtpoll = rtp;
+ pd->poll_item = i;
+ pd->mixer = mixer;
+
+ pa_rtpoll_item_set_work_callback(i, rtpoll_work_cb, pd);
+
+ return 0;
+}
+#endif
+
+static const snd_mixer_selem_channel_id_t alsa_channel_ids[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = SND_MIXER_SCHN_MONO, /* The ALSA name is just an alias! */
+
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = SND_MIXER_SCHN_FRONT_CENTER,
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = SND_MIXER_SCHN_FRONT_LEFT,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = SND_MIXER_SCHN_FRONT_RIGHT,
+
+ [PA_CHANNEL_POSITION_REAR_CENTER] = SND_MIXER_SCHN_REAR_CENTER,
+ [PA_CHANNEL_POSITION_REAR_LEFT] = SND_MIXER_SCHN_REAR_LEFT,
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = SND_MIXER_SCHN_REAR_RIGHT,
+
+ [PA_CHANNEL_POSITION_LFE] = SND_MIXER_SCHN_WOOFER,
+
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = SND_MIXER_SCHN_SIDE_LEFT,
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = SND_MIXER_SCHN_SIDE_RIGHT,
+
+ [PA_CHANNEL_POSITION_AUX0] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX1] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX2] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX3] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX4] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX5] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX6] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX7] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX8] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX9] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX10] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX11] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX12] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX13] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX14] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX15] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX16] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX17] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX18] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX19] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX20] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX21] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX22] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX23] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX24] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX25] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX26] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX27] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX28] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX29] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX30] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_AUX31] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = SND_MIXER_SCHN_UNKNOWN,
+
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = SND_MIXER_SCHN_UNKNOWN,
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = SND_MIXER_SCHN_UNKNOWN
+};
+
+static snd_mixer_selem_channel_id_t alsa_channel_positions[POSITION_MASK_CHANNELS] = {
+ SND_MIXER_SCHN_FRONT_LEFT,
+ SND_MIXER_SCHN_FRONT_RIGHT,
+ SND_MIXER_SCHN_REAR_LEFT,
+ SND_MIXER_SCHN_REAR_RIGHT,
+ SND_MIXER_SCHN_FRONT_CENTER,
+ SND_MIXER_SCHN_WOOFER,
+ SND_MIXER_SCHN_SIDE_LEFT,
+ SND_MIXER_SCHN_SIDE_RIGHT,
+#if POSITION_MASK_CHANNELS > 8
+#error "Extend alsa_channel_positions[] array (9+)"
+#endif
+};
+
+static void setting_free(pa_alsa_setting *s) {
+ pa_assert(s);
+
+ if (s->options)
+ pa_idxset_free(s->options, NULL);
+
+ pa_xfree(s->name);
+ pa_xfree(s->description);
+ pa_xfree(s);
+}
+
+static void option_free(pa_alsa_option *o) {
+ pa_assert(o);
+
+ pa_xfree(o->alsa_name);
+ pa_xfree(o->name);
+ pa_xfree(o->description);
+ pa_xfree(o);
+}
+
+static void decibel_fix_free(pa_alsa_decibel_fix *db_fix) {
+ pa_assert(db_fix);
+
+ pa_xfree(db_fix->name);
+ pa_xfree(db_fix->db_values);
+
+ pa_xfree(db_fix->key);
+ pa_xfree(db_fix);
+}
+
+static void element_free(pa_alsa_element *e) {
+ pa_alsa_option *o;
+ pa_assert(e);
+
+ while ((o = e->options)) {
+ PA_LLIST_REMOVE(pa_alsa_option, e->options, o);
+ option_free(o);
+ }
+
+ if (e->db_fix)
+ decibel_fix_free(e->db_fix);
+
+ pa_xfree(e->alsa_id.name);
+ pa_xfree(e);
+}
+
+void pa_alsa_path_free(pa_alsa_path *p) {
+ pa_alsa_jack *j;
+ pa_alsa_element *e;
+ pa_alsa_setting *s;
+
+ pa_assert(p);
+
+ while ((j = p->jacks)) {
+ PA_LLIST_REMOVE(pa_alsa_jack, p->jacks, j);
+ pa_alsa_jack_free(j);
+ }
+
+ while ((e = p->elements)) {
+ PA_LLIST_REMOVE(pa_alsa_element, p->elements, e);
+ element_free(e);
+ }
+
+ while ((s = p->settings)) {
+ PA_LLIST_REMOVE(pa_alsa_setting, p->settings, s);
+ setting_free(s);
+ }
+
+ pa_proplist_free(p->proplist);
+ pa_xfree(p->availability_group);
+ pa_xfree(p->name);
+ pa_xfree(p->description);
+ pa_xfree(p->description_key);
+ pa_xfree(p);
+}
+
+void pa_alsa_path_set_free(pa_alsa_path_set *ps) {
+ pa_assert(ps);
+
+ if (ps->paths)
+ pa_hashmap_free(ps->paths);
+
+ pa_xfree(ps);
+}
+
+int pa_alsa_path_set_is_empty(pa_alsa_path_set *ps) {
+ if (ps && !pa_hashmap_isempty(ps->paths))
+ return 0;
+ return 1;
+}
+
+static long to_alsa_dB(pa_volume_t v) {
+ return lround(pa_sw_volume_to_dB(v) * 100.0);
+}
+
+static pa_volume_t from_alsa_dB(long v) {
+ return pa_sw_volume_from_dB((double) v / 100.0);
+}
+
+static long to_alsa_volume(pa_volume_t v, long min, long max) {
+ long w;
+
+ w = (long) round(((double) v * (double) (max - min)) / PA_VOLUME_NORM) + min;
+ return PA_CLAMP_UNLIKELY(w, min, max);
+}
+
+static pa_volume_t from_alsa_volume(long v, long min, long max) {
+ return (pa_volume_t) round(((double) (v - min) * PA_VOLUME_NORM) / (double) (max - min));
+}
+
+#define SELEM_INIT(sid, aid) \
+ do { \
+ snd_mixer_selem_id_alloca(&(sid)); \
+ snd_mixer_selem_id_set_name((sid), (aid)->name); \
+ snd_mixer_selem_id_set_index((sid), (aid)->index); \
+ } while(false)
+
+static int element_get_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ pa_channel_position_mask_t mask = 0;
+ char buf[64];
+ unsigned k;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(cm);
+ pa_assert(v);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ pa_cvolume_mute(v, cm->channels);
+
+ /* We take the highest volume of all channels that match */
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ pa_volume_t f;
+
+ if (e->has_dB) {
+ long value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ if (e->db_fix) {
+ if ((r = snd_mixer_selem_get_playback_volume(me, c, &value)) >= 0) {
+ /* If the channel volume is outside the limits set
+ * by the dB fix, we clamp the hw volume to be
+ * within the limits. */
+ if (value < e->db_fix->min_step) {
+ value = e->db_fix->min_step;
+ snd_mixer_selem_set_playback_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Playback volume for element %s channel %i was below the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ } else if (value > e->db_fix->max_step) {
+ value = e->db_fix->max_step;
+ snd_mixer_selem_set_playback_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Playback volume for element %s channel %i was over the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ }
+
+ /* Volume step -> dB value conversion. */
+ value = e->db_fix->db_values[value - e->db_fix->min_step];
+ }
+ } else
+ r = snd_mixer_selem_get_playback_dB(me, c, &value);
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ if (e->db_fix) {
+ if ((r = snd_mixer_selem_get_capture_volume(me, c, &value)) >= 0) {
+ /* If the channel volume is outside the limits set
+ * by the dB fix, we clamp the hw volume to be
+ * within the limits. */
+ if (value < e->db_fix->min_step) {
+ value = e->db_fix->min_step;
+ snd_mixer_selem_set_capture_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Capture volume for element %s channel %i was below the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ } else if (value > e->db_fix->max_step) {
+ value = e->db_fix->max_step;
+ snd_mixer_selem_set_capture_volume(me, c, value);
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Capture volume for element %s channel %i was over the dB fix limit. "
+ "Volume reset to %0.2f dB.", buf, c,
+ e->db_fix->db_values[value - e->db_fix->min_step] / 100.0);
+ }
+
+ /* Volume step -> dB value conversion. */
+ value = e->db_fix->db_values[value - e->db_fix->min_step];
+ }
+ } else
+ r = snd_mixer_selem_get_capture_dB(me, c, &value);
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ VALGRIND_MAKE_MEM_DEFINED(&value, sizeof(value));
+
+ f = from_alsa_dB(value);
+
+ } else {
+ long value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c))
+ r = snd_mixer_selem_get_playback_volume(me, c, &value);
+ else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c))
+ r = snd_mixer_selem_get_capture_volume(me, c, &value);
+ else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_volume(value, e->min_volume, e->max_volume);
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k]))
+ if (v->values[k] < f)
+ v->values[k] = f;
+
+ mask |= e->masks[c][e->n_channels-1];
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (!(mask & PA_CHANNEL_POSITION_MASK(cm->map[k])))
+ v->values[k] = PA_VOLUME_NORM;
+
+ return 0;
+}
+
+int pa_alsa_path_get_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(cm);
+ pa_assert(v);
+
+ if (!p->has_volume)
+ return -1;
+
+ pa_cvolume_reset(v, cm->channels);
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_cvolume ev;
+
+ if (e->volume_use != PA_ALSA_VOLUME_MERGE)
+ continue;
+
+ pa_assert(!p->has_dB || e->has_dB);
+
+ if (element_get_volume(e, m, cm, &ev) < 0)
+ return -1;
+
+ /* If we have no dB information all we can do is take the first element and leave */
+ if (!p->has_dB) {
+ *v = ev;
+ return 0;
+ }
+
+ pa_sw_cvolume_multiply(v, v, &ev);
+ }
+
+ return 0;
+}
+
+static int element_get_switch(pa_alsa_element *e, snd_mixer_t *m, bool *b) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ char buf[64];
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(b);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ /* We return muted if at least one channel is muted */
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ int value = 0;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c))
+ r = snd_mixer_selem_get_playback_switch(me, c, &value);
+ else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c))
+ r = snd_mixer_selem_get_capture_switch(me, c, &value);
+ else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ if (!value) {
+ *b = false;
+ return 0;
+ }
+ }
+
+ *b = true;
+ return 0;
+}
+
+int pa_alsa_path_get_mute(pa_alsa_path *p, snd_mixer_t *m, bool *muted) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(muted);
+
+ if (!p->has_mute)
+ return -1;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ bool b;
+
+ if (e->switch_use != PA_ALSA_SWITCH_MUTE)
+ continue;
+
+ if (element_get_switch(e, m, &b) < 0)
+ return -1;
+
+ if (!b) {
+ *muted = true;
+ return 0;
+ }
+ }
+
+ *muted = false;
+ return 0;
+}
+
+/* Finds the closest item in db_fix->db_values and returns the corresponding
+ * step. *db_value is replaced with the value from the db_values table.
+ * Rounding is done based on the rounding parameter: -1 means rounding down and
+ * +1 means rounding up. */
+static long decibel_fix_get_step(pa_alsa_decibel_fix *db_fix, long *db_value, int rounding) {
+ unsigned i = 0;
+ unsigned max_i = 0;
+
+ pa_assert(db_fix);
+ pa_assert(db_value);
+ pa_assert(rounding != 0);
+
+ max_i = db_fix->max_step - db_fix->min_step;
+
+ if (rounding > 0) {
+ for (i = 0; i < max_i; i++) {
+ if (db_fix->db_values[i] >= *db_value)
+ break;
+ }
+ } else {
+ for (i = 0; i < max_i; i++) {
+ if (db_fix->db_values[i + 1] > *db_value)
+ break;
+ }
+ }
+
+ *db_value = db_fix->db_values[i];
+
+ return i + db_fix->min_step;
+}
+
+/* Alsa lib documentation says for snd_mixer_selem_set_playback_dB() direction argument,
+ * that "-1 = accurate or first below, 0 = accurate, 1 = accurate or first above".
+ * But even with accurate nearest dB volume step is not selected, so that is why we need
+ * this function. Returns 0 and nearest selectable volume in *value_dB on success or
+ * negative error code if fails. */
+static int element_get_nearest_alsa_dB(snd_mixer_elem_t *me, snd_mixer_selem_channel_id_t c, pa_alsa_direction_t d, long *value_dB) {
+
+ long alsa_val;
+ long value_high;
+ long value_low;
+ int r = -1;
+
+ pa_assert(me);
+ pa_assert(value_dB);
+
+ if (d == PA_ALSA_DIRECTION_OUTPUT) {
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, *value_dB, +1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value_high);
+
+ if (r < 0)
+ return r;
+
+ if (value_high == *value_dB)
+ return r;
+
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, *value_dB, -1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value_low);
+ } else {
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, *value_dB, +1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value_high);
+
+ if (r < 0)
+ return r;
+
+ if (value_high == *value_dB)
+ return r;
+
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, *value_dB, -1, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value_low);
+ }
+
+ if (r < 0)
+ return r;
+
+ if (labs(value_high - *value_dB) < labs(value_low - *value_dB))
+ *value_dB = value_high;
+ else
+ *value_dB = value_low;
+
+ return r;
+}
+
+static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw) {
+
+ snd_mixer_selem_id_t *sid;
+ pa_cvolume rv;
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_channel_id_t c;
+ pa_channel_position_mask_t mask = 0;
+ char buf[64];
+ unsigned k;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(cm);
+ pa_assert(v);
+ pa_assert(pa_cvolume_compatible_with_channel_map(v, cm));
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ pa_cvolume_mute(&rv, cm->channels);
+
+ for (c = 0; c <= SND_MIXER_SCHN_LAST; c++) {
+ int r;
+ pa_volume_t f = PA_VOLUME_MUTED;
+ bool found = false;
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k])) {
+ found = true;
+ if (v->values[k] > f)
+ f = v->values[k];
+ }
+
+ if (!found) {
+ /* Hmm, so this channel does not exist in the volume
+ * struct, so let's bind it to the overall max of the
+ * volume. */
+ f = pa_cvolume_max(v);
+ }
+
+ if (e->has_dB) {
+ long value = to_alsa_dB(f);
+ int rounding;
+
+ if (e->volume_limit >= 0 && value > (e->max_dB * 100))
+ value = e->max_dB * 100;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ /* If we call set_playback_volume() without checking first
+ * if the channel is available, ALSA behaves very
+ * strangely and doesn't fail the call */
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ rounding = +1;
+ if (e->db_fix) {
+ if (write_to_hw)
+ r = snd_mixer_selem_set_playback_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding));
+ else {
+ decibel_fix_get_step(e->db_fix, &value, rounding);
+ r = 0;
+ }
+
+ } else {
+ if (write_to_hw) {
+ if (deferred_volume) {
+ if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_OUTPUT, &value)) >= 0)
+ r = snd_mixer_selem_set_playback_dB(me, c, value, 0);
+ } else {
+ if ((r = snd_mixer_selem_set_playback_dB(me, c, value, rounding)) >= 0)
+ r = snd_mixer_selem_get_playback_dB(me, c, &value);
+ }
+ } else {
+ long alsa_val;
+ if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value, rounding, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value);
+ }
+ }
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ rounding = -1;
+ if (e->db_fix) {
+ if (write_to_hw)
+ r = snd_mixer_selem_set_capture_volume(me, c, decibel_fix_get_step(e->db_fix, &value, rounding));
+ else {
+ decibel_fix_get_step(e->db_fix, &value, rounding);
+ r = 0;
+ }
+
+ } else {
+ if (write_to_hw) {
+ if (deferred_volume) {
+ if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_INPUT, &value)) >= 0)
+ r = snd_mixer_selem_set_capture_dB(me, c, value, 0);
+ } else {
+ if ((r = snd_mixer_selem_set_capture_dB(me, c, value, rounding)) >= 0)
+ r = snd_mixer_selem_get_capture_dB(me, c, &value);
+ }
+ } else {
+ long alsa_val;
+ if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value, rounding, &alsa_val)) >= 0)
+ r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value);
+ }
+ }
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_dB(value);
+
+ } else {
+ long value;
+
+ value = to_alsa_volume(f, e->min_volume, e->max_volume);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_has_playback_channel(me, c)) {
+ if ((r = snd_mixer_selem_set_playback_volume(me, c, value)) >= 0)
+ r = snd_mixer_selem_get_playback_volume(me, c, &value);
+ } else
+ r = -1;
+ } else {
+ if (snd_mixer_selem_has_capture_channel(me, c)) {
+ if ((r = snd_mixer_selem_set_capture_volume(me, c, value)) >= 0)
+ r = snd_mixer_selem_get_capture_volume(me, c, &value);
+ } else
+ r = -1;
+ }
+
+ if (r < 0)
+ continue;
+
+ f = from_alsa_volume(value, e->min_volume, e->max_volume);
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (e->masks[c][e->n_channels-1] & PA_CHANNEL_POSITION_MASK(cm->map[k]))
+ if (rv.values[k] < f)
+ rv.values[k] = f;
+
+ mask |= e->masks[c][e->n_channels-1];
+ }
+
+ for (k = 0; k < cm->channels; k++)
+ if (!(mask & PA_CHANNEL_POSITION_MASK(cm->map[k])))
+ rv.values[k] = PA_VOLUME_NORM;
+
+ *v = rv;
+ return 0;
+}
+
+int pa_alsa_path_set_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw) {
+
+ pa_alsa_element *e;
+ pa_cvolume rv;
+
+ pa_assert(m);
+ pa_assert(p);
+ pa_assert(cm);
+ pa_assert(v);
+ pa_assert(pa_cvolume_compatible_with_channel_map(v, cm));
+
+ if (!p->has_volume)
+ return -1;
+
+ rv = *v; /* Remaining adjustment */
+ pa_cvolume_reset(v, cm->channels); /* Adjustment done */
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_cvolume ev;
+
+ if (e->volume_use != PA_ALSA_VOLUME_MERGE)
+ continue;
+
+ pa_assert(!p->has_dB || e->has_dB);
+
+ ev = rv;
+ if (element_set_volume(e, m, cm, &ev, deferred_volume, write_to_hw) < 0)
+ return -1;
+
+ if (!p->has_dB) {
+ *v = ev;
+ return 0;
+ }
+
+ pa_sw_cvolume_multiply(v, v, &ev);
+ pa_sw_cvolume_divide(&rv, &rv, &ev);
+ }
+
+ return 0;
+}
+
+static int element_set_switch(pa_alsa_element *e, snd_mixer_t *m, bool b) {
+ snd_mixer_elem_t *me;
+ snd_mixer_selem_id_t *sid;
+ char buf[64];
+ int r;
+
+ pa_assert(m);
+ pa_assert(e);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_switch_all(me, b);
+ else
+ r = snd_mixer_selem_set_capture_switch_all(me, b);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ return r;
+}
+
+int pa_alsa_path_set_mute(pa_alsa_path *p, snd_mixer_t *m, bool muted) {
+ pa_alsa_element *e;
+
+ pa_assert(m);
+ pa_assert(p);
+
+ if (!p->has_mute)
+ return -1;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+
+ if (e->switch_use != PA_ALSA_SWITCH_MUTE)
+ continue;
+
+ if (element_set_switch(e, m, !muted) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Depending on whether e->volume_use is _OFF, _ZERO or _CONSTANT, this
+ * function sets all channels of the volume element to e->min_volume, 0 dB or
+ * e->constant_volume. */
+static int element_set_constant_volume(pa_alsa_element *e, snd_mixer_t *m) {
+ snd_mixer_elem_t *me = NULL;
+ snd_mixer_selem_id_t *sid = NULL;
+ int r = 0;
+ long volume = -1;
+ bool volume_set = false;
+ char buf[64];
+
+ pa_assert(m);
+ pa_assert(e);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ switch (e->volume_use) {
+ case PA_ALSA_VOLUME_OFF:
+ volume = e->min_volume;
+ volume_set = true;
+ break;
+
+ case PA_ALSA_VOLUME_ZERO:
+ if (e->db_fix) {
+ long dB = 0;
+
+ volume = decibel_fix_get_step(e->db_fix, &dB, (e->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1));
+ volume_set = true;
+ }
+ break;
+
+ case PA_ALSA_VOLUME_CONSTANT:
+ volume = e->constant_volume;
+ volume_set = true;
+ break;
+
+ default:
+ pa_assert_not_reached();
+ }
+
+ if (volume_set) {
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_volume_all(me, volume);
+ else
+ r = snd_mixer_selem_set_capture_volume_all(me, volume);
+ } else {
+ pa_assert(e->volume_use == PA_ALSA_VOLUME_ZERO);
+ pa_assert(!e->db_fix);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_dB_all(me, 0, +1);
+ else
+ r = snd_mixer_selem_set_capture_dB_all(me, 0, -1);
+ }
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set volume of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ return r;
+}
+
+int pa_alsa_path_select(pa_alsa_path *p, pa_alsa_setting *s, snd_mixer_t *m, bool device_is_muted) {
+ pa_alsa_element *e;
+ int r = 0;
+
+ pa_assert(m);
+ pa_assert(p);
+
+ pa_log_info("Activating path %s", p->name);
+ pa_alsa_path_dump(p);
+
+ /* First turn on hw mute if available, to avoid noise
+ * when setting the mixer controls. */
+ if (p->mute_during_activation) {
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE)
+ /* If the muting fails here, that's not a critical problem for
+ * selecting a path, so we ignore the return value.
+ * element_set_switch() will print a warning anyway, so this
+ * won't be a silent failure either. */
+ (void) element_set_switch(e, m, false);
+ }
+ }
+
+ PA_LLIST_FOREACH(e, p->elements) {
+
+ switch (e->switch_use) {
+ case PA_ALSA_SWITCH_OFF:
+ r = element_set_switch(e, m, false);
+ break;
+
+ case PA_ALSA_SWITCH_ON:
+ r = element_set_switch(e, m, true);
+ break;
+
+ case PA_ALSA_SWITCH_MUTE:
+ case PA_ALSA_SWITCH_IGNORE:
+ case PA_ALSA_SWITCH_SELECT:
+ r = 0;
+ break;
+ }
+
+ if (r < 0)
+ return -1;
+
+ switch (e->volume_use) {
+ case PA_ALSA_VOLUME_OFF:
+ case PA_ALSA_VOLUME_ZERO:
+ case PA_ALSA_VOLUME_CONSTANT:
+ r = element_set_constant_volume(e, m);
+ break;
+
+ case PA_ALSA_VOLUME_MERGE:
+ case PA_ALSA_VOLUME_IGNORE:
+ r = 0;
+ break;
+ }
+
+ if (r < 0)
+ return -1;
+ }
+
+ if (s)
+ setting_select(s, m);
+
+ /* Finally restore hw mute to the device mute status. */
+ if (p->mute_during_activation) {
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE) {
+ if (element_set_switch(e, m, !device_is_muted) < 0)
+ return -1;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int check_required(pa_alsa_element *e, snd_mixer_elem_t *me) {
+ bool has_switch;
+ bool has_enumeration;
+ bool has_volume;
+
+ pa_assert(e);
+ pa_assert(me);
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ has_switch =
+ snd_mixer_selem_has_playback_switch(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_capture_switch(me));
+ } else {
+ has_switch =
+ snd_mixer_selem_has_capture_switch(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_playback_switch(me));
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ has_volume =
+ snd_mixer_selem_has_playback_volume(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_capture_volume(me));
+ } else {
+ has_volume =
+ snd_mixer_selem_has_capture_volume(me) ||
+ (e->direction_try_other && snd_mixer_selem_has_playback_volume(me));
+ }
+
+ has_enumeration = snd_mixer_selem_is_enumerated(me);
+
+ if ((e->required == PA_ALSA_REQUIRED_SWITCH && !has_switch) ||
+ (e->required == PA_ALSA_REQUIRED_VOLUME && !has_volume) ||
+ (e->required == PA_ALSA_REQUIRED_ENUMERATION && !has_enumeration))
+ return -1;
+
+ if (e->required == PA_ALSA_REQUIRED_ANY && !(has_switch || has_volume || has_enumeration))
+ return -1;
+
+ if ((e->required_absent == PA_ALSA_REQUIRED_SWITCH && has_switch) ||
+ (e->required_absent == PA_ALSA_REQUIRED_VOLUME && has_volume) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ENUMERATION && has_enumeration))
+ return -1;
+
+ if (e->required_absent == PA_ALSA_REQUIRED_ANY && (has_switch || has_volume || has_enumeration))
+ return -1;
+
+ if (e->required_any != PA_ALSA_REQUIRED_IGNORE) {
+ switch (e->required_any) {
+ case PA_ALSA_REQUIRED_VOLUME:
+ e->path->req_any_present |= (e->volume_use != PA_ALSA_VOLUME_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_SWITCH:
+ e->path->req_any_present |= (e->switch_use != PA_ALSA_SWITCH_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_ENUMERATION:
+ e->path->req_any_present |= (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE);
+ break;
+ case PA_ALSA_REQUIRED_ANY:
+ e->path->req_any_present |=
+ (e->volume_use != PA_ALSA_VOLUME_IGNORE) ||
+ (e->switch_use != PA_ALSA_SWITCH_IGNORE) ||
+ (e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE);
+ break;
+ default:
+ pa_assert_not_reached();
+ }
+ }
+
+ if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ pa_alsa_option *o;
+ PA_LLIST_FOREACH(o, e->options) {
+ e->path->req_any_present |= (o->required_any != PA_ALSA_REQUIRED_IGNORE) &&
+ (o->alsa_idx >= 0);
+ if (o->required != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx < 0)
+ return -1;
+ if (o->required_absent != PA_ALSA_REQUIRED_IGNORE && o->alsa_idx >= 0)
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static int element_ask_vol_dB(snd_mixer_elem_t *me, pa_alsa_direction_t dir, long value, long *dBvalue) {
+ if (dir == PA_ALSA_DIRECTION_OUTPUT)
+ return snd_mixer_selem_ask_playback_vol_dB(me, value, dBvalue);
+ else
+ return snd_mixer_selem_ask_capture_vol_dB(me, value, dBvalue);
+}
+
+static bool element_probe_volume(pa_alsa_element *e, snd_mixer_elem_t *me) {
+
+ long min_dB = 0, max_dB = 0;
+ int r;
+ bool is_mono;
+ pa_channel_position_t p;
+ char buf[64];
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (!snd_mixer_selem_has_playback_volume(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_capture_volume(me))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else
+ return false;
+ }
+ } else {
+ if (!snd_mixer_selem_has_capture_volume(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_playback_volume(me))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else
+ return false;
+ }
+ }
+
+ e->direction_try_other = false;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_get_playback_volume_range(me, &e->min_volume, &e->max_volume);
+ else
+ r = snd_mixer_selem_get_capture_volume_range(me, &e->min_volume, &e->max_volume);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to get volume range of %s: %s", buf, pa_alsa_strerror(r));
+ return false;
+ }
+
+ if (e->min_volume >= e->max_volume) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Your kernel driver is broken for element %s: it reports a volume range from %li to %li which makes no sense.",
+ buf, e->min_volume, e->max_volume);
+ return false;
+ }
+ if (e->volume_use == PA_ALSA_VOLUME_CONSTANT && (e->min_volume > e->constant_volume || e->max_volume < e->constant_volume)) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Constant volume %li configured for element %s, but the available range is from %li to %li.",
+ e->constant_volume, buf, e->min_volume, e->max_volume);
+ return false;
+ }
+
+
+ if (e->db_fix && ((e->min_volume > e->db_fix->min_step) || (e->max_volume < e->db_fix->max_step))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("The step range of the decibel fix for element %s (%li-%li) doesn't fit to the "
+ "real hardware range (%li-%li). Disabling the decibel fix.", buf,
+ e->db_fix->min_step, e->db_fix->max_step, e->min_volume, e->max_volume);
+
+ decibel_fix_free(e->db_fix);
+ e->db_fix = NULL;
+ }
+
+ if (e->db_fix) {
+ e->has_dB = true;
+ e->min_volume = e->db_fix->min_step;
+ e->max_volume = e->db_fix->max_step;
+ min_dB = e->db_fix->db_values[0];
+ max_dB = e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step];
+ } else if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ e->has_dB = snd_mixer_selem_get_playback_dB_range(me, &min_dB, &max_dB) >= 0;
+ else
+ e->has_dB = snd_mixer_selem_get_capture_dB_range(me, &min_dB, &max_dB) >= 0;
+
+ /* Assume decibel data to be incorrect if max_dB is negative. */
+ if (e->has_dB && max_dB < 0 && !e->db_fix) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("The decibel volume range for element %s (%li dB - %li dB) has negative maximum. "
+ "Disabling the decibel range.", buf, min_dB, max_dB);
+ e->has_dB = false;
+ }
+
+ /* Check that the kernel driver returns consistent limits with
+ * both _get_*_dB_range() and _ask_*_vol_dB(). */
+ if (e->has_dB && !e->db_fix) {
+ long min_dB_checked = 0;
+ long max_dB_checked = 0;
+
+ if (element_ask_vol_dB(me, e->direction, e->min_volume, &min_dB_checked) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->min_volume);
+ return false;
+ }
+
+ if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB_checked) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to query the dB value for %s at volume level %li", buf, e->max_volume);
+ return false;
+ }
+
+ if (min_dB != min_dB_checked || max_dB != max_dB_checked) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Your kernel driver is broken: the reported dB range for %s (from %0.2f dB to %0.2f dB) "
+ "doesn't match the dB values at minimum and maximum volume levels: %0.2f dB at level %li, "
+ "%0.2f dB at level %li.", buf, min_dB / 100.0, max_dB / 100.0,
+ min_dB_checked / 100.0, e->min_volume, max_dB_checked / 100.0, e->max_volume);
+ return false;
+ }
+ }
+
+ if (e->has_dB) {
+ e->min_dB = ((double) min_dB) / 100.0;
+ e->max_dB = ((double) max_dB) / 100.0;
+
+ if (min_dB >= max_dB) {
+ pa_assert(!e->db_fix);
+ pa_log_warn("Your kernel driver is broken: it reports a volume range from %0.2f dB to %0.2f dB which makes no sense.",
+ e->min_dB, e->max_dB);
+ e->has_dB = false;
+ }
+ }
+
+ if (e->volume_limit >= 0) {
+ if (e->volume_limit <= e->min_volume || e->volume_limit > e->max_volume) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume limit for element %s of path %s is invalid: %li isn't within the valid range "
+ "%li-%li. The volume limit is ignored.",
+ buf, e->path->name, e->volume_limit, e->min_volume + 1, e->max_volume);
+ } else {
+ e->max_volume = e->volume_limit;
+
+ if (e->has_dB) {
+ if (e->db_fix) {
+ e->db_fix->max_step = e->max_volume;
+ e->max_dB = ((double) e->db_fix->db_values[e->db_fix->max_step - e->db_fix->min_step]) / 100.0;
+ } else if (element_ask_vol_dB(me, e->direction, e->max_volume, &max_dB) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to get dB value of %s: %s", buf, pa_alsa_strerror(r));
+ e->has_dB = false;
+ } else
+ e->max_dB = ((double) max_dB) / 100.0;
+ }
+ }
+ }
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ is_mono = snd_mixer_selem_is_playback_mono(me) > 0;
+ else
+ is_mono = snd_mixer_selem_is_capture_mono(me) > 0;
+
+ if (is_mono) {
+ e->n_channels = 1;
+
+ if ((e->override_map & (1 << (e->n_channels-1))) && e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] == 0) {
+ pa_log_warn("Override map for mono element %s is invalid, ignoring override map", e->path->name);
+ e->override_map &= ~(1 << (e->n_channels-1));
+ }
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+ e->masks[alsa_channel_ids[p]][e->n_channels-1] = 0;
+ }
+ e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1] = PA_CHANNEL_POSITION_MASK_ALL;
+ }
+ e->merged_mask = e->masks[SND_MIXER_SCHN_MONO][e->n_channels-1];
+ return true;
+ }
+
+ e->n_channels = 0;
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ e->n_channels += snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0;
+ else
+ e->n_channels += snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0;
+ }
+
+ if (e->n_channels <= 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume element %s with no channels?", buf);
+ return false;
+ } else if (e->n_channels > POSITION_MASK_CHANNELS) {
+ /* FIXME: In some places code like this is used:
+ *
+ * e->masks[alsa_channel_ids[p]][e->n_channels-1]
+ *
+ * The definition of e->masks is
+ *
+ * pa_channel_position_mask_t masks[SND_MIXER_SCHN_LAST + 1][POSITION_MASK_CHANNELS];
+ *
+ * Since the array size is fixed at POSITION_MASK_CHANNELS, we obviously
+ * don't support elements with more than POSITION_MASK_CHANNELS
+ * channels... */
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Volume element %s has %u channels. That's too much! I can't handle that!", buf, e->n_channels);
+ return false;
+ }
+
+retry:
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ bool has_channel;
+
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ has_channel = snd_mixer_selem_has_playback_channel(me, alsa_channel_ids[p]) > 0;
+ else
+ has_channel = snd_mixer_selem_has_capture_channel(me, alsa_channel_ids[p]) > 0;
+
+ e->masks[alsa_channel_ids[p]][e->n_channels-1] = has_channel ? PA_CHANNEL_POSITION_MASK(p) : 0;
+ }
+ }
+
+ e->merged_mask = 0;
+ for (p = PA_CHANNEL_POSITION_FRONT_LEFT; p < PA_CHANNEL_POSITION_MAX; p++) {
+ if (alsa_channel_ids[p] == SND_MIXER_SCHN_UNKNOWN)
+ continue;
+
+ e->merged_mask |= e->masks[alsa_channel_ids[p]][e->n_channels-1];
+ }
+
+ if (e->merged_mask == 0) {
+ if (!(e->override_map & (1 << (e->n_channels-1)))) {
+ pa_log_warn("Channel map for element %s is invalid", e->path->name);
+ return false;
+ }
+ pa_log_warn("Override map for element %s has empty result, ignoring override map", e->path->name);
+ e->override_map &= ~(1 << (e->n_channels-1));
+ goto retry;
+ }
+
+ return true;
+}
+
+static int element_probe(pa_alsa_element *e, snd_mixer_t *m) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+
+ pa_assert(m);
+ pa_assert(e);
+ pa_assert(e->path);
+
+ SELEM_INIT(sid, &e->alsa_id);
+
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+
+ if (e->required != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE;
+
+ return 0;
+ }
+
+ if (e->switch_use != PA_ALSA_SWITCH_IGNORE) {
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT) {
+
+ if (!snd_mixer_selem_has_playback_switch(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_capture_switch(me))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ }
+
+ } else {
+
+ if (!snd_mixer_selem_has_capture_switch(me)) {
+ if (e->direction_try_other && snd_mixer_selem_has_playback_switch(me))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ }
+ }
+
+ if (e->switch_use != PA_ALSA_SWITCH_IGNORE)
+ e->direction_try_other = false;
+ }
+
+ if (!element_probe_volume(e, me))
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT) {
+ pa_alsa_option *o;
+
+ PA_LLIST_FOREACH(o, e->options)
+ o->alsa_idx = pa_streq(o->alsa_name, "on") ? 1 : 0;
+ } else if (e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ int n;
+ pa_alsa_option *o;
+
+ if ((n = snd_mixer_selem_get_enum_items(me)) < 0) {
+ pa_log("snd_mixer_selem_get_enum_items() failed: %s", pa_alsa_strerror(n));
+ return -1;
+ }
+
+ PA_LLIST_FOREACH(o, e->options) {
+ int i;
+
+ for (i = 0; i < n; i++) {
+ char buf[128];
+
+ if (snd_mixer_selem_get_enum_item_name(me, i, sizeof(buf), buf) < 0)
+ continue;
+
+ if (!pa_streq(buf, o->alsa_name))
+ continue;
+
+ o->alsa_idx = i;
+ }
+ }
+ }
+
+ if (check_required(e, me) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int jack_probe(pa_alsa_jack *j, pa_alsa_mapping *mapping, snd_mixer_t *m) {
+ bool has_control;
+
+ pa_assert(j);
+ pa_assert(j->path);
+
+ if (j->append_pcm_to_name) {
+ char *new_name;
+
+ if (!mapping) {
+ /* This could also be an assertion, because this should never
+ * happen. At the time of writing, mapping can only be NULL when
+ * module-alsa-sink/source synthesizes a path, and those
+ * synthesized paths never have any jacks, so jack_probe() should
+ * never be called with a NULL mapping. */
+ pa_log("Jack %s: append_pcm_to_name is set, but mapping is NULL. Can't use this jack.", j->name);
+ return -1;
+ }
+
+ new_name = pa_sprintf_malloc("%s,pcm=%i Jack", j->name, mapping->hw_device_index);
+ pa_xfree(j->alsa_id.name);
+ j->alsa_id.name = new_name;
+ j->append_pcm_to_name = false;
+ }
+
+ has_control = pa_alsa_mixer_find_card(m, &j->alsa_id, 0) != NULL;
+ pa_alsa_jack_set_has_control(j, has_control);
+
+ if (j->has_control) {
+ if (j->required_absent != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+ if (j->required_any != PA_ALSA_REQUIRED_IGNORE)
+ j->path->req_any_present = true;
+ } else {
+ if (j->required != PA_ALSA_REQUIRED_IGNORE)
+ return -1;
+ }
+
+ return 0;
+}
+
+pa_alsa_element * pa_alsa_element_get(pa_alsa_path *p, const char *section, bool prefixed) {
+ pa_alsa_element *e;
+ char *name;
+ int index;
+
+ pa_assert(p);
+ pa_assert(section);
+
+ if (prefixed) {
+ if (!pa_startswith(section, "Element "))
+ return NULL;
+
+ section += 8;
+ }
+
+ /* This is not an element section, but an enum section? */
+ if (strchr(section, ':'))
+ return NULL;
+
+ name = alloca(strlen(section) + 1);
+ if (alsa_id_decode(section, name, &index))
+ return NULL;
+
+ if (p->last_element && pa_streq(p->last_element->alsa_id.name, name) &&
+ p->last_element->alsa_id.index == index)
+ return p->last_element;
+
+ PA_LLIST_FOREACH(e, p->elements)
+ if (pa_streq(e->alsa_id.name, name) && e->alsa_id.index == index)
+ goto finish;
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = p->direction;
+ e->volume_limit = -1;
+
+ PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e);
+
+finish:
+ p->last_element = e;
+ return e;
+}
+
+static pa_alsa_jack* jack_get(pa_alsa_path *p, const char *section) {
+ pa_alsa_jack *j;
+ char *name;
+ int index;
+
+ if (!pa_startswith(section, "Jack "))
+ return NULL;
+ section += 5;
+
+ name = alloca(strlen(section) + 1);
+ if (alsa_id_decode(section, name, &index))
+ return NULL;
+
+ if (p->last_jack && pa_streq(p->last_jack->name, name) &&
+ p->last_jack->alsa_id.index == index)
+ return p->last_jack;
+
+ PA_LLIST_FOREACH(j, p->jacks)
+ if (pa_streq(j->name, name) && j->alsa_id.index == index)
+ goto finish;
+
+ j = pa_alsa_jack_new(p, NULL, name, index);
+ PA_LLIST_INSERT_AFTER(pa_alsa_jack, p->jacks, p->last_jack, j);
+
+finish:
+ p->last_jack = j;
+ return j;
+}
+
+static pa_alsa_option* option_get(pa_alsa_path *p, const char *section) {
+ char *en, *name;
+ const char *on;
+ pa_alsa_option *o;
+ pa_alsa_element *e;
+ size_t len;
+ int index;
+
+ if (!pa_startswith(section, "Option "))
+ return NULL;
+
+ section += 7;
+
+ /* This is not an enum section, but an element section? */
+ if (!(on = strchr(section, ':')))
+ return NULL;
+
+ len = on - section;
+ en = alloca(len + 1);
+ strncpy(en, section, len);
+ en[len] = '\0';
+
+ name = alloca(strlen(en) + 1);
+ if (alsa_id_decode(en, name, &index))
+ return NULL;
+
+ on++;
+
+ if (p->last_option &&
+ pa_streq(p->last_option->element->alsa_id.name, name) &&
+ p->last_option->element->alsa_id.index == index &&
+ pa_streq(p->last_option->alsa_name, on)) {
+ return p->last_option;
+ }
+
+ pa_assert_se(e = pa_alsa_element_get(p, en, false));
+
+ PA_LLIST_FOREACH(o, e->options)
+ if (pa_streq(o->alsa_name, on))
+ goto finish;
+
+ o = pa_xnew0(pa_alsa_option, 1);
+ o->element = e;
+ o->alsa_name = pa_xstrdup(on);
+ o->alsa_idx = -1;
+
+ if (p->last_option && p->last_option->element == e)
+ PA_LLIST_INSERT_AFTER(pa_alsa_option, e->options, p->last_option, o);
+ else
+ PA_LLIST_PREPEND(pa_alsa_option, e->options, o);
+
+finish:
+ p->last_option = o;
+ return o;
+}
+
+static int element_parse_switch(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Switch makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->switch_use = PA_ALSA_SWITCH_IGNORE;
+ else if (pa_streq(state->rvalue, "mute"))
+ e->switch_use = PA_ALSA_SWITCH_MUTE;
+ else if (pa_streq(state->rvalue, "off"))
+ e->switch_use = PA_ALSA_SWITCH_OFF;
+ else if (pa_streq(state->rvalue, "on"))
+ e->switch_use = PA_ALSA_SWITCH_ON;
+ else if (pa_streq(state->rvalue, "select"))
+ e->switch_use = PA_ALSA_SWITCH_SELECT;
+ else {
+ pa_log("[%s:%u] Switch invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int element_parse_volume(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Volume makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ else if (pa_streq(state->rvalue, "merge"))
+ e->volume_use = PA_ALSA_VOLUME_MERGE;
+ else if (pa_streq(state->rvalue, "off"))
+ e->volume_use = PA_ALSA_VOLUME_OFF;
+ else if (pa_streq(state->rvalue, "zero"))
+ e->volume_use = PA_ALSA_VOLUME_ZERO;
+ else {
+ uint32_t constant;
+
+ if (pa_atou(state->rvalue, &constant) >= 0) {
+ e->volume_use = PA_ALSA_VOLUME_CONSTANT;
+ e->constant_volume = constant;
+ } else {
+ pa_log("[%s:%u] Volume invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static int element_parse_enumeration(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Enumeration makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ e->enumeration_use = PA_ALSA_ENUMERATION_IGNORE;
+ else if (pa_streq(state->rvalue, "select"))
+ e->enumeration_use = PA_ALSA_ENUMERATION_SELECT;
+ else {
+ pa_log("[%s:%u] Enumeration invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int parse_type(pa_config_parser_state *state) {
+ struct device_port_types {
+ const char *name;
+ pa_device_port_type_t type;
+ } device_port_types[] = {
+ { "unknown", PA_DEVICE_PORT_TYPE_UNKNOWN },
+ { "aux", PA_DEVICE_PORT_TYPE_AUX },
+ { "speaker", PA_DEVICE_PORT_TYPE_SPEAKER },
+ { "headphones", PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "line", PA_DEVICE_PORT_TYPE_LINE },
+ { "mic", PA_DEVICE_PORT_TYPE_MIC },
+ { "headset", PA_DEVICE_PORT_TYPE_HEADSET },
+ { "handset", PA_DEVICE_PORT_TYPE_HANDSET },
+ { "earpiece", PA_DEVICE_PORT_TYPE_EARPIECE },
+ { "spdif", PA_DEVICE_PORT_TYPE_SPDIF },
+ { "hdmi", PA_DEVICE_PORT_TYPE_HDMI },
+ { "tv", PA_DEVICE_PORT_TYPE_TV },
+ { "radio", PA_DEVICE_PORT_TYPE_RADIO },
+ { "video", PA_DEVICE_PORT_TYPE_VIDEO },
+ { "usb", PA_DEVICE_PORT_TYPE_USB },
+ { "bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH },
+ { "portable", PA_DEVICE_PORT_TYPE_PORTABLE },
+ { "handsfree", PA_DEVICE_PORT_TYPE_HANDSFREE },
+ { "car", PA_DEVICE_PORT_TYPE_CAR },
+ { "hifi", PA_DEVICE_PORT_TYPE_HIFI },
+ { "phone", PA_DEVICE_PORT_TYPE_PHONE },
+ { "network", PA_DEVICE_PORT_TYPE_NETWORK },
+ { "analog", PA_DEVICE_PORT_TYPE_ANALOG },
+ };
+ pa_alsa_path *path;
+ unsigned int idx;
+
+ path = state->userdata;
+
+ for (idx = 0; idx < PA_ELEMENTSOF(device_port_types); idx++)
+ if (pa_streq(state->rvalue, device_port_types[idx].name)) {
+ path->device_port_type = device_port_types[idx].type;
+ return 0;
+ }
+
+ pa_log("[%s:%u] Invalid value for option 'type': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+}
+
+static int parse_eld_device(pa_config_parser_state *state) {
+ pa_alsa_path *path;
+ uint32_t eld_device;
+
+ path = state->userdata;
+
+ if (pa_atou(state->rvalue, &eld_device) >= 0) {
+ path->autodetect_eld_device = false;
+ path->eld_device = eld_device;
+ return 0;
+ }
+
+ if (pa_streq(state->rvalue, "auto")) {
+ path->autodetect_eld_device = true;
+ path->eld_device = -1;
+ return 0;
+ }
+
+ pa_log("[%s:%u] Invalid value for option 'eld-device': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+}
+
+static int option_parse_priority(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_option *o;
+ uint32_t prio;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(o = option_get(p, state->section))) {
+ pa_log("[%s:%u] Priority makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_atou(state->rvalue, &prio) < 0) {
+ pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ o->priority = prio;
+ return 0;
+}
+
+static int option_parse_name(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_option *o;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(o = option_get(p, state->section))) {
+ pa_log("[%s:%u] Name makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ pa_xfree(o->name);
+ o->name = pa_xstrdup(state->rvalue);
+
+ return 0;
+}
+
+static int element_parse_required(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ pa_alsa_option *o;
+ pa_alsa_jack *j;
+ pa_alsa_required_t req;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ e = pa_alsa_element_get(p, state->section, true);
+ o = option_get(p, state->section);
+ j = jack_get(p, state->section);
+ if (!e && !o && !j) {
+ pa_log("[%s:%u] Required makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "ignore"))
+ req = PA_ALSA_REQUIRED_IGNORE;
+ else if (pa_streq(state->rvalue, "switch") && e)
+ req = PA_ALSA_REQUIRED_SWITCH;
+ else if (pa_streq(state->rvalue, "volume") && e)
+ req = PA_ALSA_REQUIRED_VOLUME;
+ else if (pa_streq(state->rvalue, "enumeration"))
+ req = PA_ALSA_REQUIRED_ENUMERATION;
+ else if (pa_streq(state->rvalue, "any"))
+ req = PA_ALSA_REQUIRED_ANY;
+ else {
+ pa_log("[%s:%u] Required invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "required-absent")) {
+ if (e)
+ e->required_absent = req;
+ if (o)
+ o->required_absent = req;
+ if (j)
+ j->required_absent = req;
+ }
+ else if (pa_streq(state->lvalue, "required-any")) {
+ if (e) {
+ e->required_any = req;
+ e->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+ if (o) {
+ o->required_any = req;
+ o->element->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+ if (j) {
+ j->required_any = req;
+ j->path->has_req_any |= (req != PA_ALSA_REQUIRED_IGNORE);
+ }
+
+ }
+ else {
+ if (e)
+ e->required = req;
+ if (o)
+ o->required = req;
+ if (j)
+ j->required = req;
+ }
+
+ return 0;
+}
+
+static int element_parse_direction(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "playback"))
+ e->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else if (pa_streq(state->rvalue, "capture"))
+ e->direction = PA_ALSA_DIRECTION_INPUT;
+ else {
+ pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int element_parse_direction_try_other(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ int yes;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Direction makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((yes = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Direction invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ e->direction_try_other = !!yes;
+ return 0;
+}
+
+static int element_parse_volume_limit(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ long volume_limit;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] volume-limit makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_atol(state->rvalue, &volume_limit) < 0 || volume_limit < 0) {
+ pa_log("[%s:%u] Invalid value for volume-limit", state->filename, state->lineno);
+ return -1;
+ }
+
+ e->volume_limit = volume_limit;
+ return 0;
+}
+
+static unsigned int parse_channel_position(const char *m)
+{
+ pa_channel_position_t p;
+
+ if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID)
+ return SND_MIXER_SCHN_UNKNOWN;
+
+ return alsa_channel_ids[p];
+}
+
+static pa_channel_position_mask_t parse_mask(const char *m) {
+ pa_channel_position_mask_t v;
+
+ if (pa_streq(m, "all-left"))
+ v = PA_CHANNEL_POSITION_MASK_LEFT;
+ else if (pa_streq(m, "all-right"))
+ v = PA_CHANNEL_POSITION_MASK_RIGHT;
+ else if (pa_streq(m, "all-center"))
+ v = PA_CHANNEL_POSITION_MASK_CENTER;
+ else if (pa_streq(m, "all-front"))
+ v = PA_CHANNEL_POSITION_MASK_FRONT;
+ else if (pa_streq(m, "all-rear"))
+ v = PA_CHANNEL_POSITION_MASK_REAR;
+ else if (pa_streq(m, "all-side"))
+ v = PA_CHANNEL_POSITION_MASK_SIDE_OR_TOP_CENTER;
+ else if (pa_streq(m, "all-top"))
+ v = PA_CHANNEL_POSITION_MASK_TOP;
+ else if (pa_streq(m, "all-no-lfe"))
+ v = PA_CHANNEL_POSITION_MASK_ALL ^ PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_LFE);
+ else if (pa_streq(m, "all"))
+ v = PA_CHANNEL_POSITION_MASK_ALL;
+ else {
+ pa_channel_position_t p;
+
+ if ((p = pa_channel_position_from_string(m)) == PA_CHANNEL_POSITION_INVALID)
+ return 0;
+
+ v = PA_CHANNEL_POSITION_MASK(p);
+ }
+
+ return v;
+}
+
+static int element_parse_override_map(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ const char *split_state = NULL;
+ char *s;
+ unsigned i = 0;
+ int channel_count = 0;
+ char *n;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(e = pa_alsa_element_get(p, state->section, true))) {
+ pa_log("[%s:%u] Override map makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ s = strstr(state->lvalue, ".");
+ if (s) {
+ pa_atoi(s + 1, &channel_count);
+ if (channel_count < 1 || channel_count > POSITION_MASK_CHANNELS) {
+ pa_log("[%s:%u] Override map index '%s' invalid in '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return 0;
+ }
+ } else {
+ pa_log("[%s:%u] Invalid override map syntax '%s' in '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ while ((n = pa_split(state->rvalue, ",", &split_state))) {
+ pa_channel_position_mask_t m;
+ snd_mixer_selem_channel_id_t channel_position;
+
+ if (i >= (unsigned)channel_count) {
+ pa_log("[%s:%u] Invalid override map size (>%d) in '%s'", state->filename, state->lineno, channel_count, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ channel_position = alsa_channel_positions[i];
+
+ if (!*n)
+ m = 0;
+ else {
+ s = strstr(n, ":");
+ if (s) {
+ *s = '\0';
+ s++;
+ channel_position = parse_channel_position(n);
+ if (channel_position == SND_MIXER_SCHN_UNKNOWN) {
+ pa_log("[%s:%u] Override map position '%s' invalid in '%s'", state->filename, state->lineno, n, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ }
+ if ((m = parse_mask(s ? s : n)) == 0) {
+ pa_log("[%s:%u] Override map '%s' invalid in '%s'", state->filename, state->lineno, s ? s : n, state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ }
+
+ if (e->masks[channel_position][channel_count-1]) {
+ pa_log("[%s:%u] Override map '%s' duplicate position '%s' in '%s'", state->filename, state->lineno, s ? s : n, snd_mixer_selem_channel_name(channel_position), state->section);
+ pa_xfree(n);
+ return -1;
+ }
+ e->override_map |= (1 << (channel_count - 1));
+ e->masks[channel_position][channel_count-1] = m;
+ pa_xfree(n);
+ i++;
+ }
+
+ return 0;
+}
+
+static int jack_parse_state(pa_config_parser_state *state) {
+ pa_alsa_path *p;
+ pa_alsa_jack *j;
+ pa_available_t pa;
+
+ pa_assert(state);
+
+ p = state->userdata;
+
+ if (!(j = jack_get(p, state->section))) {
+ pa_log("[%s:%u] state makes no sense in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "yes"))
+ pa = PA_AVAILABLE_YES;
+ else if (pa_streq(state->rvalue, "no"))
+ pa = PA_AVAILABLE_NO;
+ else if (pa_streq(state->rvalue, "unknown"))
+ pa = PA_AVAILABLE_UNKNOWN;
+ else {
+ pa_log("[%s:%u] state must be 'yes', 'no' or 'unknown' in '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "state.unplugged"))
+ j->state_unplugged = pa;
+ else {
+ j->state_plugged = pa;
+ pa_assert(pa_streq(state->lvalue, "state.plugged"));
+ }
+
+ return 0;
+}
+
+static int jack_parse_append_pcm_to_name(pa_config_parser_state *state) {
+ pa_alsa_path *path;
+ pa_alsa_jack *jack;
+ int b;
+
+ pa_assert(state);
+
+ path = state->userdata;
+ if (!(jack = jack_get(path, state->section))) {
+ pa_log("[%s:%u] Option 'append_pcm_to_name' not expected in section '%s'",
+ state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ b = pa_parse_boolean(state->rvalue);
+ if (b < 0) {
+ pa_log("[%s:%u] Invalid value for 'append_pcm_to_name': %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ jack->append_pcm_to_name = b;
+ return 0;
+}
+
+static int element_set_option(pa_alsa_element *e, snd_mixer_t *m, int alsa_idx) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ char buf[64];
+ int r;
+
+ pa_assert(e);
+ pa_assert(m);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return -1;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT) {
+
+ if (e->direction == PA_ALSA_DIRECTION_OUTPUT)
+ r = snd_mixer_selem_set_playback_switch_all(me, alsa_idx);
+ else
+ r = snd_mixer_selem_set_capture_switch_all(me, alsa_idx);
+
+ if (r < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set switch of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+
+ } else {
+ pa_assert(e->enumeration_use == PA_ALSA_ENUMERATION_SELECT);
+
+ if ((r = snd_mixer_selem_set_enum_item(me, 0, alsa_idx)) < 0) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Failed to set enumeration of %s: %s", buf, pa_alsa_strerror(errno));
+ }
+ }
+
+ return r;
+}
+
+static int setting_select(pa_alsa_setting *s, snd_mixer_t *m) {
+ pa_alsa_option *o;
+ uint32_t idx;
+
+ pa_assert(s);
+ pa_assert(m);
+
+ PA_IDXSET_FOREACH(o, s->options, idx)
+ element_set_option(o->element, m, o->alsa_idx);
+
+ return 0;
+}
+
+static int option_verify(pa_alsa_option *o) {
+ static const struct description_map well_known_descriptions[] = {
+ { "input", N_("Input") },
+ { "input-docking", N_("Docking Station Input") },
+ { "input-docking-microphone", N_("Docking Station Microphone") },
+ { "input-docking-linein", N_("Docking Station Line In") },
+ { "input-linein", N_("Line In") },
+ { "input-microphone", N_("Microphone") },
+ { "input-microphone-front", N_("Front Microphone") },
+ { "input-microphone-rear", N_("Rear Microphone") },
+ { "input-microphone-external", N_("External Microphone") },
+ { "input-microphone-internal", N_("Internal Microphone") },
+ { "input-radio", N_("Radio") },
+ { "input-video", N_("Video") },
+ { "input-agc-on", N_("Automatic Gain Control") },
+ { "input-agc-off", N_("No Automatic Gain Control") },
+ { "input-boost-on", N_("Boost") },
+ { "input-boost-off", N_("No Boost") },
+ { "output-amplifier-on", N_("Amplifier") },
+ { "output-amplifier-off", N_("No Amplifier") },
+ { "output-bass-boost-on", N_("Bass Boost") },
+ { "output-bass-boost-off", N_("No Bass Boost") },
+ { "output-speaker", N_("Speaker") },
+ { "output-headphones", N_("Headphones") }
+ };
+ char buf[64];
+
+ pa_assert(o);
+
+ if (!o->name) {
+ pa_log("No name set for option %s", o->alsa_name);
+ return -1;
+ }
+
+ if (o->element->enumeration_use != PA_ALSA_ENUMERATION_SELECT &&
+ o->element->switch_use != PA_ALSA_SWITCH_SELECT) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id);
+ pa_log("Element %s of option %s not set for select.", buf, o->name);
+ return -1;
+ }
+
+ if (o->element->switch_use == PA_ALSA_SWITCH_SELECT &&
+ !pa_streq(o->alsa_name, "on") &&
+ !pa_streq(o->alsa_name, "off")) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &o->element->alsa_id);
+ pa_log("Switch %s options need be named off or on ", buf);
+ return -1;
+ }
+
+ if (!o->description)
+ o->description = pa_xstrdup(lookup_description(o->name,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+ if (!o->description)
+ o->description = pa_xstrdup(o->name);
+
+ return 0;
+}
+
+static int element_verify(pa_alsa_element *e) {
+ pa_alsa_option *o;
+ char buf[64];
+
+ pa_assert(e);
+
+// pa_log_debug("Element %s, path %s: r=%d, r-any=%d, r-abs=%d", e->alsa_name, e->path->name, e->required, e->required_any, e->required_absent);
+ if ((e->required != PA_ALSA_REQUIRED_IGNORE && e->required == e->required_absent) ||
+ (e->required_any != PA_ALSA_REQUIRED_IGNORE && e->required_any == e->required_absent) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required_any != PA_ALSA_REQUIRED_IGNORE) ||
+ (e->required_absent == PA_ALSA_REQUIRED_ANY && e->required != PA_ALSA_REQUIRED_IGNORE)) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log("Element %s cannot be required and absent at the same time.", buf);
+ return -1;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT && e->enumeration_use == PA_ALSA_ENUMERATION_SELECT) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log("Element %s cannot set select for both switch and enumeration.", buf);
+ return -1;
+ }
+
+ PA_LLIST_FOREACH(o, e->options)
+ if (option_verify(o) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int path_verify(pa_alsa_path *p) {
+ static const struct description2_map well_known_descriptions[] = {
+ { "analog-input", N_("Analog Input"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-input-microphone", N_("Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-front", N_("Front Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-rear", N_("Rear Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-dock", N_("Dock Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-internal", N_("Internal Microphone"), PA_DEVICE_PORT_TYPE_MIC },
+ { "analog-input-microphone-headset", N_("Headset Microphone"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-input-linein", N_("Line In"), PA_DEVICE_PORT_TYPE_LINE },
+ { "analog-input-radio", N_("Radio"), PA_DEVICE_PORT_TYPE_RADIO },
+ { "analog-input-video", N_("Video"), PA_DEVICE_PORT_TYPE_VIDEO },
+ { "analog-output", N_("Analog Output"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-output-headphones", N_("Headphones"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-headphones-2", N_("Headphones 2"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-headphones-mono", N_("Headphones Mono Output"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ { "analog-output-lineout", N_("Line Out"), PA_DEVICE_PORT_TYPE_LINE },
+ { "analog-output-mono", N_("Analog Mono Output"), PA_DEVICE_PORT_TYPE_ANALOG },
+ { "analog-output-speaker", N_("Speakers"), PA_DEVICE_PORT_TYPE_SPEAKER },
+ { "hdmi-output", N_("HDMI / DisplayPort"), PA_DEVICE_PORT_TYPE_HDMI },
+ { "iec958-stereo-output", N_("Digital Output (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF },
+ { "iec958-stereo-input", N_("Digital Input (S/PDIF)"), PA_DEVICE_PORT_TYPE_SPDIF },
+ { "multichannel-input", N_("Multichannel Input"), PA_DEVICE_PORT_TYPE_LINE },
+ { "multichannel-output", N_("Multichannel Output"), PA_DEVICE_PORT_TYPE_LINE },
+ { "steelseries-arctis-output-game-common", N_("Game Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "steelseries-arctis-output-chat-common", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-chat-output", N_("Chat Output"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "analog-chat-input", N_("Chat Input"), PA_DEVICE_PORT_TYPE_HEADSET },
+ { "virtual-surround-7.1", N_("Virtual Surround 7.1"), PA_DEVICE_PORT_TYPE_HEADPHONES },
+ };
+
+ pa_alsa_element *e;
+ const char *key = p->description_key ? p->description_key : p->name;
+ const struct description2_map *map = lookup_description2(key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions));
+
+ pa_assert(p);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ if (element_verify(e) < 0)
+ return -1;
+
+ if (map) {
+ if (p->device_port_type == PA_DEVICE_PORT_TYPE_UNKNOWN)
+ p->device_port_type = map->type;
+ if (!p->description)
+ p->description = pa_xstrdup(_(map->description));
+ }
+
+ if (!p->description) {
+ if (p->description_key)
+ pa_log_warn("Path %s: Unrecognized description key: %s", p->name, p->description_key);
+
+ p->description = pa_xstrdup(p->name);
+ }
+
+ return 0;
+}
+
+static const char *get_default_paths_dir(void) {
+ const char *str;
+#ifdef HAVE_RUNNING_FROM_BUILD_TREE
+ if (pa_run_from_build_tree())
+ return PA_SRCDIR "mixer/paths";
+ else
+#endif
+ if (getenv("ACP_BUILDDIR") != NULL)
+ return "mixer/paths";
+ if ((str = getenv("ACP_PATHS_DIR")) != NULL)
+ return str;
+ return PA_ALSA_PATHS_DIR;
+}
+
+pa_alsa_path* pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction) {
+ pa_alsa_path *p;
+ char *fn;
+ int r;
+ const char *n;
+ bool mute_during_activation = false;
+
+ pa_config_item items[] = {
+ /* [General] */
+ { "priority", pa_config_parse_unsigned, NULL, "General" },
+ { "description-key", pa_config_parse_string, NULL, "General" },
+ { "description", pa_config_parse_string, NULL, "General" },
+ { "mute-during-activation", pa_config_parse_bool, NULL, "General" },
+ { "type", parse_type, NULL, "General" },
+ { "eld-device", parse_eld_device, NULL, "General" },
+
+ /* [Option ...] */
+ { "priority", option_parse_priority, NULL, NULL },
+ { "name", option_parse_name, NULL, NULL },
+
+ /* [Jack ...] */
+ { "state.plugged", jack_parse_state, NULL, NULL },
+ { "state.unplugged", jack_parse_state, NULL, NULL },
+ { "append-pcm-to-name", jack_parse_append_pcm_to_name, NULL, NULL },
+
+ /* [Element ...] */
+ { "switch", element_parse_switch, NULL, NULL },
+ { "volume", element_parse_volume, NULL, NULL },
+ { "enumeration", element_parse_enumeration, NULL, NULL },
+ { "override-map.1", element_parse_override_map, NULL, NULL },
+ { "override-map.2", element_parse_override_map, NULL, NULL },
+ { "override-map.3", element_parse_override_map, NULL, NULL },
+ { "override-map.4", element_parse_override_map, NULL, NULL },
+ { "override-map.5", element_parse_override_map, NULL, NULL },
+ { "override-map.6", element_parse_override_map, NULL, NULL },
+ { "override-map.7", element_parse_override_map, NULL, NULL },
+ { "override-map.8", element_parse_override_map, NULL, NULL },
+#if POSITION_MASK_CHANNELS > 8
+#error "Add override-map.9+ definitions"
+#endif
+ /* ... later on we might add override-map.3 and so on here ... */
+ { "required", element_parse_required, NULL, NULL },
+ { "required-any", element_parse_required, NULL, NULL },
+ { "required-absent", element_parse_required, NULL, NULL },
+ { "direction", element_parse_direction, NULL, NULL },
+ { "direction-try-other", element_parse_direction_try_other, NULL, NULL },
+ { "volume-limit", element_parse_volume_limit, NULL, NULL },
+ { NULL, NULL, NULL, NULL }
+ };
+
+ pa_assert(fname);
+
+ p = pa_xnew0(pa_alsa_path, 1);
+ n = pa_path_get_filename(fname);
+ p->name = pa_xstrndup(n, strcspn(n, "."));
+ p->proplist = pa_proplist_new();
+ p->direction = direction;
+ p->eld_device = -1;
+
+ items[0].data = &p->priority;
+ items[1].data = &p->description_key;
+ items[2].data = &p->description;
+ items[3].data = &mute_during_activation;
+
+ if (!paths_dir)
+ paths_dir = get_default_paths_dir();
+
+ fn = pa_maybe_prefix_path(fname, paths_dir);
+
+ r = pa_config_parse(fn, NULL, items, p->proplist, false, p);
+ pa_xfree(fn);
+
+ if (r < 0)
+ goto fail;
+
+ p->mute_during_activation = mute_during_activation;
+
+ if (path_verify(p) < 0)
+ goto fail;
+
+ if (p->description) {
+ char *tmp = p->description;
+ p->description = pa_xstrdup(_(tmp));
+ free(tmp);
+ }
+
+ return p;
+
+fail:
+ pa_alsa_path_free(p);
+ return NULL;
+}
+
+pa_alsa_path *pa_alsa_path_synthesize(const char *element, pa_alsa_direction_t direction) {
+ pa_alsa_path *p;
+ pa_alsa_element *e;
+ char *name;
+ int index;
+
+ pa_assert(element);
+
+ name = alloca(strlen(element) + 1);
+ if (alsa_id_decode(element, name, &index))
+ return NULL;
+
+ p = pa_xnew0(pa_alsa_path, 1);
+ p->name = pa_xstrdup(element);
+ p->direction = direction;
+ p->proplist = pa_proplist_new();
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = direction;
+ e->volume_limit = -1;
+
+ e->switch_use = PA_ALSA_SWITCH_MUTE;
+ e->volume_use = PA_ALSA_VOLUME_MERGE;
+
+ PA_LLIST_PREPEND(pa_alsa_element, p->elements, e);
+ p->last_element = e;
+ return p;
+}
+
+static bool element_drop_unsupported(pa_alsa_element *e) {
+ pa_alsa_option *o, *n;
+
+ pa_assert(e);
+
+ for (o = e->options; o; o = n) {
+ n = o->next;
+
+ if (o->alsa_idx < 0) {
+ PA_LLIST_REMOVE(pa_alsa_option, e->options, o);
+ option_free(o);
+ }
+ }
+
+ return
+ e->switch_use != PA_ALSA_SWITCH_IGNORE ||
+ e->volume_use != PA_ALSA_VOLUME_IGNORE ||
+ e->enumeration_use != PA_ALSA_ENUMERATION_IGNORE;
+}
+
+static void path_drop_unsupported(pa_alsa_path *p) {
+ pa_alsa_element *e, *n;
+
+ pa_assert(p);
+
+ for (e = p->elements; e; e = n) {
+ n = e->next;
+
+ if (!element_drop_unsupported(e)) {
+ PA_LLIST_REMOVE(pa_alsa_element, p->elements, e);
+ element_free(e);
+ }
+ }
+}
+
+static void path_make_options_unique(pa_alsa_path *p) {
+ pa_alsa_element *e;
+ pa_alsa_option *o, *u;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ PA_LLIST_FOREACH(o, e->options) {
+ unsigned i;
+ char *m;
+
+ for (u = o->next; u; u = u->next)
+ if (pa_streq(u->name, o->name))
+ break;
+
+ if (!u)
+ continue;
+
+ m = pa_xstrdup(o->name);
+
+ /* OK, this name is not unique, hence let's rename */
+ for (i = 1, u = o; u; u = u->next) {
+ char *nn, *nd;
+
+ if (!pa_streq(u->name, m))
+ continue;
+
+ nn = pa_sprintf_malloc("%s-%u", m, i);
+ pa_xfree(u->name);
+ u->name = nn;
+
+ nd = pa_sprintf_malloc("%s %u", u->description, i);
+ pa_xfree(u->description);
+ u->description = nd;
+
+ i++;
+ }
+
+ pa_xfree(m);
+ }
+ }
+}
+
+static bool element_create_settings(pa_alsa_element *e, pa_alsa_setting *template) {
+ pa_alsa_option *o;
+
+ for (; e; e = e->next)
+ if (e->switch_use == PA_ALSA_SWITCH_SELECT ||
+ e->enumeration_use == PA_ALSA_ENUMERATION_SELECT)
+ break;
+
+ if (!e)
+ return false;
+
+ for (o = e->options; o; o = o->next) {
+ pa_alsa_setting *s;
+
+ if (template) {
+ s = pa_xnewdup(pa_alsa_setting, template, 1);
+ s->options = pa_idxset_copy(template->options, NULL);
+ s->name = pa_sprintf_malloc("%s+%s", template->name, o->name);
+ s->description =
+ (template->description[0] && o->description[0])
+ ? pa_sprintf_malloc("%s / %s", template->description, o->description)
+ : (template->description[0]
+ ? pa_xstrdup(template->description)
+ : pa_xstrdup(o->description));
+
+ s->priority = PA_MAX(template->priority, o->priority);
+ } else {
+ s = pa_xnew0(pa_alsa_setting, 1);
+ s->options = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ s->name = pa_xstrdup(o->name);
+ s->description = pa_xstrdup(o->description);
+ s->priority = o->priority;
+ }
+
+ pa_idxset_put(s->options, o, NULL);
+
+ if (element_create_settings(e->next, s))
+ /* This is not a leaf, so let's get rid of it */
+ setting_free(s);
+ else {
+ /* This is a leaf, so let's add it */
+ PA_LLIST_INSERT_AFTER(pa_alsa_setting, e->path->settings, e->path->last_setting, s);
+
+ e->path->last_setting = s;
+ }
+ }
+
+ return true;
+}
+
+static void path_create_settings(pa_alsa_path *p) {
+ pa_assert(p);
+
+ element_create_settings(p->elements, NULL);
+}
+
+int pa_alsa_path_probe(pa_alsa_path *p, pa_alsa_mapping *mapping, snd_mixer_t *m, bool ignore_dB) {
+ pa_alsa_element *e;
+ pa_alsa_jack *j;
+ double min_dB[PA_CHANNEL_POSITION_MAX], max_dB[PA_CHANNEL_POSITION_MAX];
+ pa_channel_position_t t;
+ pa_channel_position_mask_t path_volume_channels = 0;
+ bool min_dB_set, max_dB_set;
+ char buf[64];
+
+ pa_assert(p);
+ pa_assert(m);
+
+ if (p->probed)
+ return p->supported ? 0 : -1;
+ p->probed = true;
+
+ pa_zero(min_dB);
+ pa_zero(max_dB);
+
+ pa_log_debug("Probing path '%s'", p->name);
+
+ PA_LLIST_FOREACH(j, p->jacks) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &j->alsa_id);
+ if (jack_probe(j, mapping, m) < 0) {
+ p->supported = false;
+ pa_log_debug("Probe of jack %s failed.", buf);
+ return -1;
+ }
+ pa_log_debug("Probe of jack %s succeeded (%s)", buf, j->has_control ? "found!" : "not found");
+ }
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ if (element_probe(e, m) < 0) {
+ p->supported = false;
+ pa_log_debug("Probe of element %s failed.", buf);
+ return -1;
+ }
+ pa_log_debug("Probe of element %s succeeded (volume=%d, switch=%d, enumeration=%d, has_dB=%d).", buf, e->volume_use, e->switch_use, e->enumeration_use, e->has_dB);
+
+ if (ignore_dB)
+ e->has_dB = false;
+
+ if (e->volume_use == PA_ALSA_VOLUME_MERGE) {
+
+ if (!p->has_volume) {
+ p->min_volume = e->min_volume;
+ p->max_volume = e->max_volume;
+ }
+
+ if (e->has_dB) {
+ if (!p->has_volume) {
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++)
+ if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) {
+ min_dB[t] = e->min_dB;
+ max_dB[t] = e->max_dB;
+ path_volume_channels |= PA_CHANNEL_POSITION_MASK(t);
+ }
+
+ p->has_dB = true;
+ } else {
+
+ if (p->has_dB) {
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++)
+ if (PA_CHANNEL_POSITION_MASK(t) & e->merged_mask) {
+ min_dB[t] += e->min_dB;
+ max_dB[t] += e->max_dB;
+ path_volume_channels |= PA_CHANNEL_POSITION_MASK(t);
+ }
+ } else {
+ /* Hmm, there's another element before us
+ * which cannot do dB volumes, so we we need
+ * to 'neutralize' this slider */
+ e->volume_use = PA_ALSA_VOLUME_ZERO;
+ pa_log_info("Zeroing volume of %s on path '%s'", buf, p->name);
+ }
+ }
+ } else if (p->has_volume) {
+ /* We can't use this volume, so let's ignore it */
+ e->volume_use = PA_ALSA_VOLUME_IGNORE;
+ pa_log_info("Ignoring volume of %s on path '%s' (missing dB info)", buf, p->name);
+ }
+ p->has_volume = true;
+ }
+
+ if (e->switch_use == PA_ALSA_SWITCH_MUTE)
+ p->has_mute = true;
+ }
+
+ if (p->has_req_any && !p->req_any_present) {
+ p->supported = false;
+ pa_log_debug("Skipping path '%s', none of required-any elements preset.", p->name);
+ return -1;
+ }
+
+ path_drop_unsupported(p);
+ path_make_options_unique(p);
+ path_create_settings(p);
+
+ p->supported = true;
+
+ p->min_dB = INFINITY;
+ min_dB_set = false;
+ p->max_dB = -INFINITY;
+ max_dB_set = false;
+
+ for (t = 0; t < PA_CHANNEL_POSITION_MAX; t++) {
+ if (path_volume_channels & PA_CHANNEL_POSITION_MASK(t)) {
+ if (p->min_dB > min_dB[t]) {
+ p->min_dB = min_dB[t];
+ min_dB_set = true;
+ }
+
+ if (p->max_dB < max_dB[t]) {
+ p->max_dB = max_dB[t];
+ max_dB_set = true;
+ }
+ }
+ }
+
+ /* this is probably a wrong prediction, but it should be safe */
+ if (!min_dB_set)
+ p->min_dB = -INFINITY;
+ if (!max_dB_set)
+ p->max_dB = 0;
+
+ return 0;
+}
+
+void pa_alsa_setting_dump(pa_alsa_setting *s) {
+ pa_assert(s);
+
+ pa_log_debug("Setting %s (%s) priority=%u",
+ s->name,
+ pa_strnull(s->description),
+ s->priority);
+}
+
+void pa_alsa_jack_dump(pa_alsa_jack *j) {
+ pa_assert(j);
+
+ pa_log_debug("Jack %s, alsa_name='%s', index='%d', detection %s", j->name, j->alsa_id.name, j->alsa_id.index, j->has_control ? "possible" : "unavailable");
+}
+
+void pa_alsa_option_dump(pa_alsa_option *o) {
+ pa_assert(o);
+
+ pa_log_debug("Option %s (%s/%s) index=%i, priority=%u",
+ o->alsa_name,
+ pa_strnull(o->name),
+ pa_strnull(o->description),
+ o->alsa_idx,
+ o->priority);
+}
+
+void pa_alsa_element_dump(pa_alsa_element *e) {
+ char buf[64];
+
+ pa_alsa_option *o;
+ pa_assert(e);
+
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_debug("Element %s, direction=%i, switch=%i, volume=%i, volume_limit=%li, enumeration=%i, required=%i, required_any=%i, required_absent=%i, mask=0x%llx, n_channels=%u, override_map=%02x",
+ buf,
+ e->direction,
+ e->switch_use,
+ e->volume_use,
+ e->volume_limit,
+ e->enumeration_use,
+ e->required,
+ e->required_any,
+ e->required_absent,
+ (long long unsigned) e->merged_mask,
+ e->n_channels,
+ e->override_map);
+
+ PA_LLIST_FOREACH(o, e->options)
+ pa_alsa_option_dump(o);
+}
+
+void pa_alsa_path_dump(pa_alsa_path *p) {
+ pa_alsa_element *e;
+ pa_alsa_jack *j;
+ pa_alsa_setting *s;
+ pa_assert(p);
+
+ pa_log_debug("Path %s (%s), direction=%i, priority=%u, probed=%s, supported=%s, has_mute=%s, has_volume=%s, "
+ "has_dB=%s, min_volume=%li, max_volume=%li, min_dB=%g, max_dB=%g",
+ p->name,
+ pa_strnull(p->description),
+ p->direction,
+ p->priority,
+ pa_yes_no(p->probed),
+ pa_yes_no(p->supported),
+ pa_yes_no(p->has_mute),
+ pa_yes_no(p->has_volume),
+ pa_yes_no(p->has_dB),
+ p->min_volume, p->max_volume,
+ p->min_dB, p->max_dB);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ pa_alsa_element_dump(e);
+
+ PA_LLIST_FOREACH(j, p->jacks)
+ pa_alsa_jack_dump(j);
+
+ PA_LLIST_FOREACH(s, p->settings)
+ pa_alsa_setting_dump(s);
+}
+
+static void element_set_callback(pa_alsa_element *e, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+ char buf[64];
+
+ pa_assert(e);
+ pa_assert(m);
+ pa_assert(cb);
+
+ SELEM_INIT(sid, &e->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return;
+ }
+
+ snd_mixer_elem_set_callback(me, cb);
+ snd_mixer_elem_set_callback_private(me, userdata);
+}
+
+void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ pa_alsa_element *e;
+
+ pa_assert(p);
+ pa_assert(m);
+ pa_assert(cb);
+
+ PA_LLIST_FOREACH(e, p->elements)
+ element_set_callback(e, m, cb, userdata);
+}
+
+void pa_alsa_path_set_set_callback(pa_alsa_path_set *ps, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata) {
+ pa_alsa_path *p;
+ void *state;
+
+ pa_assert(ps);
+ pa_assert(m);
+ pa_assert(cb);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_alsa_path_set_callback(p, m, cb, userdata);
+}
+
+static pa_alsa_path *profile_set_get_path(pa_alsa_profile_set *ps, const char *path_name) {
+ pa_alsa_path *path;
+
+ pa_assert(ps);
+ pa_assert(path_name);
+
+ if ((path = pa_hashmap_get(ps->output_paths, path_name)))
+ return path;
+
+ return pa_hashmap_get(ps->input_paths, path_name);
+}
+
+static void profile_set_add_path(pa_alsa_profile_set *ps, pa_alsa_path *path) {
+ pa_assert(ps);
+ pa_assert(path);
+
+ switch (path->direction) {
+ case PA_ALSA_DIRECTION_OUTPUT:
+ pa_assert_se(pa_hashmap_put(ps->output_paths, path->name, path) >= 0);
+ break;
+
+ case PA_ALSA_DIRECTION_INPUT:
+ pa_assert_se(pa_hashmap_put(ps->input_paths, path->name, path) >= 0);
+ break;
+
+ default:
+ pa_assert_not_reached();
+ }
+}
+
+pa_alsa_path_set *pa_alsa_path_set_new(pa_alsa_mapping *m, pa_alsa_direction_t direction, const char *paths_dir) {
+ pa_alsa_path_set *ps;
+ char **pn = NULL, **en = NULL, **ie;
+ pa_alsa_decibel_fix *db_fix;
+ void *state, *state2;
+ char name[64];
+ int index;
+
+ pa_assert(m);
+ pa_assert(m->profile_set);
+ pa_assert(m->profile_set->decibel_fixes);
+ pa_assert(direction == PA_ALSA_DIRECTION_OUTPUT || direction == PA_ALSA_DIRECTION_INPUT);
+
+ if (m->direction != PA_ALSA_DIRECTION_ANY && m->direction != direction)
+ return NULL;
+
+ ps = pa_xnew0(pa_alsa_path_set, 1);
+ ps->direction = direction;
+ ps->paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT)
+ pn = m->output_path_names;
+ else
+ pn = m->input_path_names;
+
+ if (pn) {
+ char **in;
+
+ for (in = pn; *in; in++) {
+ pa_alsa_path *p = NULL;
+ bool duplicate = false;
+ char **kn;
+
+ for (kn = pn; kn < in; kn++)
+ if (pa_streq(*kn, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ p = profile_set_get_path(m->profile_set, *in);
+
+ if (p && p->direction != direction) {
+ pa_log("Configuration error: Path %s is used both as an input and as an output path.", p->name);
+ goto fail;
+ }
+
+ if (!p) {
+ char *fn = pa_sprintf_malloc("%s.conf", *in);
+ p = pa_alsa_path_new(paths_dir, fn, direction);
+ pa_xfree(fn);
+ if (p)
+ profile_set_add_path(m->profile_set, p);
+ }
+
+ if (p)
+ pa_hashmap_put(ps->paths, p, p);
+
+ }
+
+ goto finish;
+ }
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT)
+ en = m->output_element;
+ else
+ en = m->input_element;
+
+ if (!en)
+ goto fail;
+
+ for (ie = en; *ie; ie++) {
+ char **je;
+ pa_alsa_path *p;
+
+ p = pa_alsa_path_synthesize(*ie, direction);
+
+ /* Mark all other passed elements for require-absent */
+ for (je = en; *je; je++) {
+ pa_alsa_element *e;
+
+ if (je == ie)
+ continue;
+
+ if (strlen(*je) + 1 >= sizeof(name)) {
+ pa_log("Element identifier %s is too long!", *je);
+ continue;
+ }
+
+ if (alsa_id_decode(*je, name, &index))
+ continue;
+
+ e = pa_xnew0(pa_alsa_element, 1);
+ e->path = p;
+ e->alsa_id.name = pa_xstrdup(name);
+ e->alsa_id.index = index;
+ e->direction = direction;
+ e->required_absent = PA_ALSA_REQUIRED_ANY;
+ e->volume_limit = -1;
+
+ PA_LLIST_INSERT_AFTER(pa_alsa_element, p->elements, p->last_element, e);
+ p->last_element = e;
+ }
+
+ pa_hashmap_put(ps->paths, *ie, p);
+ }
+
+finish:
+ /* Assign decibel fixes to elements. */
+ PA_HASHMAP_FOREACH(db_fix, m->profile_set->decibel_fixes, state) {
+ pa_alsa_path *p;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state2) {
+ pa_alsa_element *e;
+
+ PA_LLIST_FOREACH(e, p->elements) {
+ if (e->volume_use != PA_ALSA_VOLUME_IGNORE && pa_streq(db_fix->name, e->alsa_id.name) &&
+ db_fix->index == e->alsa_id.index) {
+ /* The profile set that contains the dB fix may be freed
+ * before the element, so we have to copy the dB fix
+ * object. */
+ e->db_fix = pa_xnewdup(pa_alsa_decibel_fix, db_fix, 1);
+ e->db_fix->profile_set = NULL;
+ e->db_fix->key = pa_xstrdup(db_fix->key);
+ e->db_fix->name = pa_xstrdup(db_fix->name);
+ e->db_fix->db_values = pa_xmemdup(db_fix->db_values, (db_fix->max_step - db_fix->min_step + 1) * sizeof(long));
+ }
+ }
+ }
+ }
+
+ return ps;
+
+fail:
+ if (ps)
+ pa_alsa_path_set_free(ps);
+
+ return NULL;
+}
+
+void pa_alsa_path_set_dump(pa_alsa_path_set *ps) {
+ pa_alsa_path *p;
+ void *state;
+ pa_assert(ps);
+
+ pa_log_debug("Path Set %p, direction=%i",
+ (void*) ps,
+ ps->direction);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_alsa_path_dump(p);
+}
+
+static bool options_have_option(pa_alsa_option *options, const char *alsa_name) {
+ pa_alsa_option *o;
+
+ pa_assert(options);
+ pa_assert(alsa_name);
+
+ PA_LLIST_FOREACH(o, options) {
+ if (pa_streq(o->alsa_name, alsa_name))
+ return true;
+ }
+ return false;
+}
+
+static bool enumeration_is_subset(pa_alsa_option *a_options, pa_alsa_option *b_options) {
+ pa_alsa_option *oa, *ob;
+
+ if (!a_options) return true;
+ if (!b_options) return false;
+
+ /* If there is an option A offers that B does not, then A is not a subset of B. */
+ PA_LLIST_FOREACH(oa, a_options) {
+ bool found = false;
+ PA_LLIST_FOREACH(ob, b_options) {
+ if (pa_streq(oa->alsa_name, ob->alsa_name)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Compares two elements to see if a is a subset of b
+ */
+static bool element_is_subset(pa_alsa_element *a, pa_alsa_element *b, snd_mixer_t *m) {
+ char buf[64];
+
+ pa_assert(a);
+ pa_assert(b);
+ pa_assert(m);
+
+ /* General rules:
+ * Every state is a subset of itself (with caveats for volume_limits and options)
+ * IGNORE is a subset of every other state */
+
+ /* Check the volume_use */
+ if (a->volume_use != PA_ALSA_VOLUME_IGNORE) {
+
+ /* "Constant" is subset of "Constant" only when their constant values are equal */
+ if (a->volume_use == PA_ALSA_VOLUME_CONSTANT && b->volume_use == PA_ALSA_VOLUME_CONSTANT && a->constant_volume != b->constant_volume)
+ return false;
+
+ /* Different volume uses when b is not "Merge" means we are definitely not a subset */
+ if (a->volume_use != b->volume_use && b->volume_use != PA_ALSA_VOLUME_MERGE)
+ return false;
+
+ /* "Constant" is a subset of "Merge", if there is not a "volume-limit" in "Merge" below the actual constant.
+ * "Zero" and "Off" are just special cases of "Constant" when comparing to "Merge"
+ * "Merge" with a "volume-limit" is a subset of "Merge" without a "volume-limit" or with a higher "volume-limit" */
+ if (b->volume_use == PA_ALSA_VOLUME_MERGE && b->volume_limit >= 0) {
+ long a_limit;
+
+ if (a->volume_use == PA_ALSA_VOLUME_CONSTANT)
+ a_limit = a->constant_volume;
+ else if (a->volume_use == PA_ALSA_VOLUME_ZERO) {
+ long dB = 0;
+
+ if (a->db_fix) {
+ int rounding = (a->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1);
+ a_limit = decibel_fix_get_step(a->db_fix, &dB, rounding);
+ } else {
+ snd_mixer_selem_id_t *sid;
+ snd_mixer_elem_t *me;
+
+ SELEM_INIT(sid, &a->alsa_id);
+ if (!(me = snd_mixer_find_selem(m, sid))) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id);
+ pa_log_warn("Element %s seems to have disappeared.", buf);
+ return false;
+ }
+
+ if (a->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (snd_mixer_selem_ask_playback_dB_vol(me, dB, +1, &a_limit) < 0)
+ return false;
+ } else {
+ if (snd_mixer_selem_ask_capture_dB_vol(me, dB, -1, &a_limit) < 0)
+ return false;
+ }
+ }
+ } else if (a->volume_use == PA_ALSA_VOLUME_OFF)
+ a_limit = a->min_volume;
+ else if (a->volume_use == PA_ALSA_VOLUME_MERGE)
+ a_limit = a->volume_limit;
+ else
+ pa_assert_not_reached();
+
+ if (a_limit > b->volume_limit)
+ return false;
+ }
+
+ if (a->volume_use == PA_ALSA_VOLUME_MERGE) {
+ int s;
+ /* If override-maps are different, they're not subsets */
+ if (a->n_channels != b->n_channels)
+ return false;
+ for (s = 0; s <= SND_MIXER_SCHN_LAST; s++)
+ if (a->masks[s][a->n_channels-1] != b->masks[s][b->n_channels-1]) {
+ pa_alsa_mixer_id_to_string(buf, sizeof(buf), &a->alsa_id);
+ pa_log_debug("Element %s is not a subset - mask a: 0x%" PRIx64 ", mask b: 0x%" PRIx64 ", at channel %d",
+ buf, a->masks[s][a->n_channels-1], b->masks[s][b->n_channels-1], s);
+ return false;
+ }
+ }
+ }
+
+ if (a->switch_use != PA_ALSA_SWITCH_IGNORE) {
+ /* "On" is a subset of "Mute".
+ * "Off" is a subset of "Mute".
+ * "On" is a subset of "Select", if there is an "Option:On" in B.
+ * "Off" is a subset of "Select", if there is an "Option:Off" in B.
+ * "Select" is a subset of "Select", if they have the same options (is this always true?). */
+
+ if (a->switch_use != b->switch_use) {
+
+ if (a->switch_use == PA_ALSA_SWITCH_SELECT || a->switch_use == PA_ALSA_SWITCH_MUTE
+ || b->switch_use == PA_ALSA_SWITCH_OFF || b->switch_use == PA_ALSA_SWITCH_ON)
+ return false;
+
+ if (b->switch_use == PA_ALSA_SWITCH_SELECT) {
+ if (a->switch_use == PA_ALSA_SWITCH_ON) {
+ if (!options_have_option(b->options, "on"))
+ return false;
+ } else if (a->switch_use == PA_ALSA_SWITCH_OFF) {
+ if (!options_have_option(b->options, "off"))
+ return false;
+ }
+ }
+ } else if (a->switch_use == PA_ALSA_SWITCH_SELECT) {
+ if (!enumeration_is_subset(a->options, b->options))
+ return false;
+ }
+ }
+
+ if (a->enumeration_use != PA_ALSA_ENUMERATION_IGNORE) {
+ if (b->enumeration_use == PA_ALSA_ENUMERATION_IGNORE)
+ return false;
+ if (!enumeration_is_subset(a->options, b->options))
+ return false;
+ }
+
+ return true;
+}
+
+static void path_set_condense(pa_alsa_path_set *ps, snd_mixer_t *m) {
+ pa_alsa_path *p;
+ void *state;
+
+ pa_assert(ps);
+ pa_assert(m);
+
+ /* If we only have one path, then don't bother */
+ if (pa_hashmap_size(ps->paths) < 2)
+ return;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ pa_alsa_path *p2;
+ void *state2;
+
+ PA_HASHMAP_FOREACH(p2, ps->paths, state2) {
+ pa_alsa_element *ea, *eb;
+ pa_alsa_jack *ja, *jb;
+ bool is_subset = true;
+
+ if (p == p2)
+ continue;
+
+ /* If a has a jack that b does not have, a is not a subset */
+ PA_LLIST_FOREACH(ja, p->jacks) {
+ bool exists = false;
+
+ if (!ja->has_control)
+ continue;
+
+ PA_LLIST_FOREACH(jb, p2->jacks) {
+ if (jb->has_control && pa_streq(ja->alsa_id.name, jb->alsa_id.name) &&
+ (ja->alsa_id.index == jb->alsa_id.index) &&
+ (ja->state_plugged == jb->state_plugged) &&
+ (ja->state_unplugged == jb->state_unplugged)) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ is_subset = false;
+ break;
+ }
+ }
+
+ /* Compare the elements of each set... */
+ PA_LLIST_FOREACH(ea, p->elements) {
+ bool found_matching_element = false;
+
+ if (!is_subset)
+ break;
+
+ PA_LLIST_FOREACH(eb, p2->elements) {
+ if (pa_streq(ea->alsa_id.name, eb->alsa_id.name) &&
+ ea->alsa_id.index == eb->alsa_id.index) {
+ found_matching_element = true;
+ is_subset = element_is_subset(ea, eb, m);
+ break;
+ }
+ }
+
+ if (!found_matching_element)
+ is_subset = false;
+ }
+
+ if (is_subset) {
+ pa_log_debug("Removing path '%s' as it is a subset of '%s'.", p->name, p2->name);
+ pa_hashmap_remove(ps->paths, p);
+ break;
+ }
+ }
+ }
+}
+
+static pa_alsa_path* path_set_find_path_by_description(pa_alsa_path_set *ps, const char* description, pa_alsa_path *ignore) {
+ pa_alsa_path* p;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ if (p != ignore && pa_streq(p->description, description))
+ return p;
+
+ return NULL;
+}
+
+static void path_set_make_path_descriptions_unique(pa_alsa_path_set *ps) {
+ pa_alsa_path *p, *q;
+ void *state, *state2;
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ unsigned i;
+ char *old_description;
+
+ q = path_set_find_path_by_description(ps, p->description, p);
+
+ if (!q)
+ continue;
+
+ old_description = pa_xstrdup(p->description);
+
+ /* OK, this description is not unique, hence let's rename */
+ i = 1;
+ PA_HASHMAP_FOREACH(q, ps->paths, state2) {
+ char *new_description;
+
+ if (!pa_streq(q->description, old_description))
+ continue;
+
+ new_description = pa_sprintf_malloc("%s %u", q->description, i);
+ pa_xfree(q->description);
+ q->description = new_description;
+
+ i++;
+ }
+
+ pa_xfree(old_description);
+ }
+}
+
+void pa_alsa_mapping_free(pa_alsa_mapping *m) {
+ pa_assert(m);
+
+ pa_xfree(m->name);
+ pa_xfree(m->description);
+ pa_xfree(m->description_key);
+
+ pa_proplist_free(m->proplist);
+
+ pa_xstrfreev(m->device_strings);
+ pa_xstrfreev(m->input_path_names);
+ pa_xstrfreev(m->output_path_names);
+ pa_xstrfreev(m->input_element);
+ pa_xstrfreev(m->output_element);
+ if (m->input_path_set)
+ pa_alsa_path_set_free(m->input_path_set);
+ if (m->output_path_set)
+ pa_alsa_path_set_free(m->output_path_set);
+
+ pa_proplist_free(m->input_proplist);
+ pa_proplist_free(m->output_proplist);
+
+ pa_assert(!m->input_pcm);
+ pa_assert(!m->output_pcm);
+
+ pa_alsa_ucm_mapping_context_free(&m->ucm_context);
+
+ pa_xfree(m);
+}
+
+void pa_alsa_profile_free(pa_alsa_profile *p) {
+ pa_assert(p);
+
+ pa_xfree(p->name);
+ pa_xfree(p->description);
+ pa_xfree(p->description_key);
+ pa_xfree(p->input_name);
+ pa_xfree(p->output_name);
+
+ pa_xstrfreev(p->input_mapping_names);
+ pa_xstrfreev(p->output_mapping_names);
+
+ if (p->input_mappings)
+ pa_idxset_free(p->input_mappings, NULL);
+
+ if (p->output_mappings)
+ pa_idxset_free(p->output_mappings, NULL);
+
+ pa_xfree(p);
+}
+
+void pa_alsa_profile_set_free(pa_alsa_profile_set *ps) {
+ pa_assert(ps);
+
+ if (ps->input_paths)
+ pa_hashmap_free(ps->input_paths);
+
+ if (ps->output_paths)
+ pa_hashmap_free(ps->output_paths);
+
+ if (ps->profiles)
+ pa_hashmap_free(ps->profiles);
+
+ if (ps->mappings)
+ pa_hashmap_free(ps->mappings);
+
+ if (ps->decibel_fixes)
+ pa_hashmap_free(ps->decibel_fixes);
+
+ pa_xfree(ps);
+}
+
+pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name) {
+ pa_alsa_mapping *m;
+
+ if (!pa_startswith(name, "Mapping "))
+ return NULL;
+
+ name += 8;
+
+ if ((m = pa_hashmap_get(ps->mappings, name)))
+ return m;
+
+ m = pa_xnew0(pa_alsa_mapping, 1);
+ m->profile_set = ps;
+ m->exact_channels = true;
+ m->name = pa_xstrdup(name);
+ pa_sample_spec_init(&m->sample_spec);
+ pa_channel_map_init(&m->channel_map);
+ m->proplist = pa_proplist_new();
+ m->hw_device_index = -1;
+ m->input_proplist = pa_proplist_new();
+ m->output_proplist = pa_proplist_new();
+
+ pa_hashmap_put(ps->mappings, m->name, m);
+
+ return m;
+}
+
+static pa_alsa_profile *profile_get(pa_alsa_profile_set *ps, const char *name) {
+ pa_alsa_profile *p;
+
+ if (!pa_startswith(name, "Profile "))
+ return NULL;
+
+ name += 8;
+
+ if ((p = pa_hashmap_get(ps->profiles, name)))
+ return p;
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = pa_xstrdup(name);
+
+ pa_hashmap_put(ps->profiles, p->name, p);
+
+ return p;
+}
+
+static pa_alsa_decibel_fix *decibel_fix_get(pa_alsa_profile_set *ps, const char *alsa_id) {
+ pa_alsa_decibel_fix *db_fix;
+ char *name;
+ int index;
+
+ if (!pa_startswith(alsa_id, "DecibelFix "))
+ return NULL;
+
+ alsa_id += 11;
+
+ if ((db_fix = pa_hashmap_get(ps->decibel_fixes, alsa_id)))
+ return db_fix;
+
+ name = alloca(strlen(alsa_id) + 1);
+ if (alsa_id_decode(alsa_id, name, &index))
+ return NULL;
+
+ db_fix = pa_xnew0(pa_alsa_decibel_fix, 1);
+ db_fix->profile_set = ps;
+ db_fix->name = pa_xstrdup(name);
+ db_fix->index = index;
+ db_fix->key = pa_xstrdup(alsa_id);
+
+ pa_hashmap_put(ps->decibel_fixes, db_fix->key, db_fix);
+
+ return db_fix;
+}
+
+static int mapping_parse_device_strings(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ pa_xstrfreev(m->device_strings);
+ if (!(m->device_strings = pa_split_spaces_strv(state->rvalue))) {
+ pa_log("[%s:%u] Device string list empty of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_channel_map(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (!(pa_channel_map_parse(&m->channel_map, state->rvalue))) {
+ pa_log("[%s:%u] Channel map invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_paths(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "paths-input")) {
+ pa_xstrfreev(m->input_path_names);
+ m->input_path_names = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(m->output_path_names);
+ m->output_path_names = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int mapping_parse_exact_channels(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+ int b;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if ((b = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] %s has invalid value '%s'", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ m->exact_channels = b;
+
+ return 0;
+}
+
+static int mapping_parse_element(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "element-input")) {
+ pa_xstrfreev(m->input_element);
+ m->input_element = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(m->output_element);
+ m->output_element = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int mapping_parse_direction(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->rvalue, "input"))
+ m->direction = PA_ALSA_DIRECTION_INPUT;
+ else if (pa_streq(state->rvalue, "output"))
+ m->direction = PA_ALSA_DIRECTION_OUTPUT;
+ else if (pa_streq(state->rvalue, "any"))
+ m->direction = PA_ALSA_DIRECTION_ANY;
+ else {
+ pa_log("[%s:%u] Direction %s invalid.", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_description(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_xfree(m->description);
+ m->description = pa_xstrdup(_(state->rvalue));
+ } else if ((p = profile_get(ps, state->section))) {
+ pa_xfree(p->description);
+ p->description = pa_xstrdup(_(state->rvalue));
+ } else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_description_key(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_xfree(m->description_key);
+ m->description_key = pa_xstrdup(state->rvalue);
+ } else if ((p = profile_get(ps, state->section))) {
+ pa_xfree(p->description_key);
+ p->description_key = pa_xstrdup(state->rvalue);
+ } else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static int mapping_parse_priority(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ uint32_t prio;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (pa_atou(state->rvalue, &prio) < 0) {
+ pa_log("[%s:%u] Priority invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((m = pa_alsa_mapping_get(ps, state->section)))
+ m->priority = prio;
+ else if ((p = profile_get(ps, state->section)))
+ p->priority = prio;
+ else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_fallback(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ int k;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Fallback invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ if ((m = pa_alsa_mapping_get(ps, state->section)))
+ m->fallback = k;
+ else if ((p = profile_get(ps, state->section)))
+ p->fallback_input = p->fallback_output = k;
+ else {
+ pa_log("[%s:%u] Section name %s invalid.", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int mapping_parse_intended_roles(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_mapping *m;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(m = pa_alsa_mapping_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ pa_proplist_sets(m->proplist, PA_PROP_DEVICE_INTENDED_ROLES, state->rvalue);
+
+ return 0;
+}
+
+
+static int profile_parse_mappings(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(p = profile_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (pa_streq(state->lvalue, "input-mappings")) {
+ pa_xstrfreev(p->input_mapping_names);
+ p->input_mapping_names = pa_split_spaces_strv(state->rvalue);
+ } else {
+ pa_xstrfreev(p->output_mapping_names);
+ p->output_mapping_names = pa_split_spaces_strv(state->rvalue);
+ }
+
+ return 0;
+}
+
+static int profile_parse_skip_probe(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ int b;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(p = profile_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if ((b = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Skip probe invalid of '%s'", state->filename, state->lineno, state->section);
+ return -1;
+ }
+
+ p->supported = b;
+
+ return 0;
+}
+
+static int decibel_fix_parse_db_values(pa_config_parser_state *state) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_decibel_fix *db_fix;
+ char **items;
+ char *item;
+ long *db_values;
+ unsigned n = 8; /* Current size of the db_values table. */
+ unsigned min_step = 0;
+ unsigned max_step = 0;
+ unsigned i = 0; /* Index to the items table. */
+ unsigned prev_step = 0;
+ double prev_db = 0;
+
+ pa_assert(state);
+
+ ps = state->userdata;
+
+ if (!(db_fix = decibel_fix_get(ps, state->section))) {
+ pa_log("[%s:%u] %s invalid in section %s", state->filename, state->lineno, state->lvalue, state->section);
+ return -1;
+ }
+
+ if (!(items = pa_split_spaces_strv(state->rvalue))) {
+ pa_log("[%s:%u] Value missing", state->filename, state->lineno);
+ return -1;
+ }
+
+ db_values = pa_xnew(long, n);
+
+ while ((item = items[i++])) {
+ char *s = item; /* Step value string. */
+ char *d = item; /* dB value string. */
+ uint32_t step;
+ double db;
+
+ /* Move d forward until it points to a colon or to the end of the item. */
+ for (; *d && *d != ':'; ++d);
+
+ if (d == s) {
+ /* item started with colon. */
+ pa_log("[%s:%u] No step value found in %s", state->filename, state->lineno, item);
+ goto fail;
+ }
+
+ if (!*d || !*(d + 1)) {
+ /* No colon found, or it was the last character in item. */
+ pa_log("[%s:%u] No dB value found in %s", state->filename, state->lineno, item);
+ goto fail;
+ }
+
+ /* pa_atou() needs a null-terminating string. Let's replace the colon
+ * with a zero byte. */
+ *d++ = '\0';
+
+ if (pa_atou(s, &step) < 0) {
+ pa_log("[%s:%u] Invalid step value: %s", state->filename, state->lineno, s);
+ goto fail;
+ }
+
+ if (pa_atod(d, &db) < 0) {
+ pa_log("[%s:%u] Invalid dB value: %s", state->filename, state->lineno, d);
+ goto fail;
+ }
+
+ if (step <= prev_step && i != 1) {
+ pa_log("[%s:%u] Step value %u not greater than the previous value %u", state->filename, state->lineno, step, prev_step);
+ goto fail;
+ }
+
+ if (db < prev_db && i != 1) {
+ pa_log("[%s:%u] Decibel value %0.2f less than the previous value %0.2f", state->filename, state->lineno, db, prev_db);
+ goto fail;
+ }
+
+ if (i == 1) {
+ min_step = step;
+ db_values[0] = (long) (db * 100.0);
+ prev_step = step;
+ prev_db = db;
+ } else {
+ /* Interpolate linearly. */
+ double db_increment = (db - prev_db) / (step - prev_step);
+
+ for (; prev_step < step; ++prev_step, prev_db += db_increment) {
+
+ /* Reallocate the db_values table if it's about to overflow. */
+ if (prev_step + 1 - min_step == n) {
+ n *= 2;
+ db_values = pa_xrenew(long, db_values, n);
+ }
+
+ db_values[prev_step + 1 - min_step] = (long) ((prev_db + db_increment) * 100.0);
+ }
+ }
+
+ max_step = step;
+ }
+
+ db_fix->min_step = min_step;
+ db_fix->max_step = max_step;
+ pa_xfree(db_fix->db_values);
+ db_fix->db_values = db_values;
+
+ pa_xstrfreev(items);
+
+ return 0;
+
+fail:
+ pa_xstrfreev(items);
+ pa_xfree(db_values);
+
+ return -1;
+}
+
+/* the logic is simple: if we see the jack in multiple paths */
+/* assign all those paths to one availability_group */
+static void profile_set_set_availability_groups(pa_alsa_profile_set *ps) {
+ pa_dynarray *paths;
+ pa_alsa_path *p;
+ void *state;
+ unsigned idx1;
+ uint32_t num = 1;
+
+ /* Merge ps->input_paths and ps->output_paths into one dynarray. */
+ paths = pa_dynarray_new(NULL);
+ PA_HASHMAP_FOREACH(p, ps->input_paths, state)
+ pa_dynarray_append(paths, p);
+ PA_HASHMAP_FOREACH(p, ps->output_paths, state)
+ pa_dynarray_append(paths, p);
+
+ PA_DYNARRAY_FOREACH(p, paths, idx1) {
+ pa_alsa_jack *j;
+ const char *found = NULL;
+ bool has_control = false;
+
+ PA_LLIST_FOREACH(j, p->jacks) {
+ pa_alsa_path *p2;
+ unsigned idx2;
+
+ if (!j->has_control || j->state_plugged == PA_AVAILABLE_NO)
+ continue;
+ has_control = true;
+ PA_DYNARRAY_FOREACH(p2, paths, idx2) {
+ pa_alsa_jack *j2;
+
+ if (p2 == p)
+ break;
+ PA_LLIST_FOREACH(j2, p2->jacks) {
+ if (!j2->has_control || j2->state_plugged == PA_AVAILABLE_NO)
+ continue;
+ if (pa_streq(j->alsa_id.name, j2->alsa_id.name) &&
+ j->alsa_id.index == j2->alsa_id.index) {
+ j->state_plugged = PA_AVAILABLE_UNKNOWN;
+ j2->state_plugged = PA_AVAILABLE_UNKNOWN;
+ found = p2->availability_group;
+ break;
+ }
+ }
+ }
+ if (found)
+ break;
+ }
+ if (!has_control)
+ continue;
+ if (!found) {
+ p->availability_group = pa_sprintf_malloc("Legacy %d", num);
+ } else {
+ p->availability_group = pa_xstrdup(found);
+ }
+ if (!found)
+ num++;
+ }
+
+ pa_dynarray_free(paths);
+}
+
+static void mapping_paths_probe(pa_alsa_mapping *m, pa_alsa_profile *profile,
+ pa_alsa_direction_t direction, pa_hashmap *used_paths,
+ pa_hashmap *mixers) {
+
+ pa_alsa_path *p;
+ void *state;
+ snd_pcm_t *pcm_handle;
+ pa_alsa_path_set *ps;
+ snd_mixer_t *mixer_handle;
+
+ if (direction == PA_ALSA_DIRECTION_OUTPUT) {
+ if (m->output_path_set)
+ return; /* Already probed */
+ m->output_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */
+ pcm_handle = m->output_pcm;
+ } else {
+ if (m->input_path_set)
+ return; /* Already probed */
+ m->input_path_set = ps = pa_alsa_path_set_new(m, direction, NULL); /* FIXME: Handle paths_dir */
+ pcm_handle = m->input_pcm;
+ }
+
+ if (!ps)
+ return; /* No paths */
+
+ pa_assert(pcm_handle);
+
+ mixer_handle = pa_alsa_open_mixer_for_pcm(mixers, pcm_handle, true);
+ if (!mixer_handle) {
+ /* Cannot open mixer, remove all entries */
+ pa_hashmap_remove_all(ps->paths);
+ return;
+ }
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state) {
+ if (p->autodetect_eld_device)
+ p->eld_device = m->hw_device_index;
+
+ if (pa_alsa_path_probe(p, m, mixer_handle, m->profile_set->ignore_dB) < 0)
+ pa_hashmap_remove(ps->paths, p);
+ }
+
+ path_set_condense(ps, mixer_handle);
+ path_set_make_path_descriptions_unique(ps);
+
+ PA_HASHMAP_FOREACH(p, ps->paths, state)
+ pa_hashmap_put(used_paths, p, p);
+
+ pa_log_debug("Available mixer paths (after tidying):");
+ pa_alsa_path_set_dump(ps);
+}
+
+static int mapping_verify(pa_alsa_mapping *m, const pa_channel_map *bonus) {
+
+ static const struct description_map well_known_descriptions[] = {
+ { "analog-mono", N_("Analog Mono") },
+ { "analog-mono-left", N_("Analog Mono (Left)") },
+ { "analog-mono-right", N_("Analog Mono (Right)") },
+ { "analog-stereo", N_("Analog Stereo") },
+ { "mono-fallback", N_("Mono") },
+ { "stereo-fallback", N_("Stereo") },
+ /* Note: Not translated to "Analog Stereo Input", because the source
+ * name gets "Input" appended to it automatically, so adding "Input"
+ * here would lead to the source name to become "Analog Stereo Input
+ * Input". The same logic applies to analog-stereo-output,
+ * multichannel-input and multichannel-output. */
+ { "analog-stereo-input", N_("Analog Stereo") },
+ { "analog-stereo-output", N_("Analog Stereo") },
+ { "analog-stereo-headset", N_("Headset") },
+ { "analog-stereo-speakerphone", N_("Speakerphone") },
+ { "multichannel-input", N_("Multichannel") },
+ { "multichannel-output", N_("Multichannel") },
+ { "analog-surround-21", N_("Analog Surround 2.1") },
+ { "analog-surround-30", N_("Analog Surround 3.0") },
+ { "analog-surround-31", N_("Analog Surround 3.1") },
+ { "analog-surround-40", N_("Analog Surround 4.0") },
+ { "analog-surround-41", N_("Analog Surround 4.1") },
+ { "analog-surround-50", N_("Analog Surround 5.0") },
+ { "analog-surround-51", N_("Analog Surround 5.1") },
+ { "analog-surround-61", N_("Analog Surround 6.0") },
+ { "analog-surround-61", N_("Analog Surround 6.1") },
+ { "analog-surround-70", N_("Analog Surround 7.0") },
+ { "analog-surround-71", N_("Analog Surround 7.1") },
+ { "iec958-stereo", N_("Digital Stereo (IEC958)") },
+ { "iec958-ac3-surround-40", N_("Digital Surround 4.0 (IEC958/AC3)") },
+ { "iec958-ac3-surround-51", N_("Digital Surround 5.1 (IEC958/AC3)") },
+ { "iec958-dts-surround-51", N_("Digital Surround 5.1 (IEC958/DTS)") },
+ { "hdmi-stereo", N_("Digital Stereo (HDMI)") },
+ { "hdmi-surround-51", N_("Digital Surround 5.1 (HDMI)") },
+ { "gaming-headset-chat", N_("Chat") },
+ { "gaming-headset-game", N_("Game") },
+ };
+ const char *description_key = m->description_key ? m->description_key : m->name;
+
+ pa_assert(m);
+
+ if (!pa_channel_map_valid(&m->channel_map)) {
+ pa_log("Mapping %s is missing channel map.", m->name);
+ return -1;
+ }
+
+ if (!m->device_strings) {
+ pa_log("Mapping %s is missing device strings.", m->name);
+ return -1;
+ }
+
+ if ((m->input_path_names && m->input_element) ||
+ (m->output_path_names && m->output_element)) {
+ pa_log("Mapping %s must have either mixer path or mixer element, not both.", m->name);
+ return -1;
+ }
+
+ if (!m->description)
+ m->description = pa_xstrdup(lookup_description(description_key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+
+ if (!m->description)
+ m->description = pa_xstrdup(m->name);
+
+ if (bonus) {
+ if (pa_channel_map_equal(&m->channel_map, bonus))
+ m->priority += 50;
+ else if (m->channel_map.channels == bonus->channels)
+ m->priority += 30;
+ }
+
+ return 0;
+}
+
+void pa_alsa_mapping_dump(pa_alsa_mapping *m) {
+ char cm[PA_CHANNEL_MAP_SNPRINT_MAX];
+
+ pa_assert(m);
+
+ pa_log_debug("Mapping %s (%s), priority=%u, channel_map=%s, supported=%s, direction=%i",
+ m->name,
+ pa_strnull(m->description),
+ m->priority,
+ pa_channel_map_snprint(cm, sizeof(cm), &m->channel_map),
+ pa_yes_no(m->supported),
+ m->direction);
+}
+
+static void profile_set_add_auto_pair(
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping *m, /* output */
+ pa_alsa_mapping *n /* input */) {
+
+ char *name;
+ pa_alsa_profile *p;
+
+ pa_assert(ps);
+ pa_assert(m || n);
+
+ if (m && m->direction == PA_ALSA_DIRECTION_INPUT)
+ return;
+
+ if (n && n->direction == PA_ALSA_DIRECTION_OUTPUT)
+ return;
+
+ if (m && n)
+ name = pa_sprintf_malloc("output:%s+input:%s", m->name, n->name);
+ else if (m)
+ name = pa_sprintf_malloc("output:%s", m->name);
+ else
+ name = pa_sprintf_malloc("input:%s", n->name);
+
+ if (pa_hashmap_get(ps->profiles, name)) {
+ pa_xfree(name);
+ return;
+ }
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = name;
+
+ if (m) {
+ p->output_name = pa_xstrdup(m->name);
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_idxset_put(p->output_mappings, m, NULL);
+ p->priority += m->priority * 100;
+ p->fallback_output = m->fallback;
+ }
+
+ if (n) {
+ p->input_name = pa_xstrdup(n->name);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pa_idxset_put(p->input_mappings, n, NULL);
+ p->priority += n->priority;
+ p->fallback_input = n->fallback;
+ }
+
+ pa_hashmap_put(ps->profiles, p->name, p);
+}
+
+static void profile_set_add_auto(pa_alsa_profile_set *ps) {
+ pa_alsa_mapping *m, *n;
+ void *m_state, *n_state;
+
+ pa_assert(ps);
+
+ /* The order is important here:
+ 1) try single inputs and outputs before trying their
+ combination, because if the half-duplex test failed, we don't have
+ to try full duplex.
+ 2) try the output right before the input combinations with
+ that output, because then the output_pcm is not closed between tests.
+ */
+ PA_HASHMAP_FOREACH(n, ps->mappings, n_state)
+ profile_set_add_auto_pair(ps, NULL, n);
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, m_state) {
+ profile_set_add_auto_pair(ps, m, NULL);
+
+ PA_HASHMAP_FOREACH(n, ps->mappings, n_state)
+ profile_set_add_auto_pair(ps, m, n);
+ }
+
+}
+
+static int profile_verify(pa_alsa_profile *p) {
+
+ static const struct description_map well_known_descriptions[] = {
+ { "output:analog-mono+input:analog-mono", N_("Analog Mono Duplex") },
+ { "output:analog-stereo+input:analog-stereo", N_("Analog Stereo Duplex") },
+ { "output:analog-stereo-headset+input:analog-stereo-headset", N_("Headset") },
+ { "output:analog-stereo-speakerphone+input:analog-stereo-speakerphone", N_("Speakerphone") },
+ { "output:iec958-stereo+input:iec958-stereo", N_("Digital Stereo Duplex (IEC958)") },
+ { "output:multichannel-output+input:multichannel-input", N_("Multichannel Duplex") },
+ { "output:unknown-stereo+input:unknown-stereo", N_("Stereo Duplex") },
+ { "output:analog-output-surround71+output:analog-output-chat+input:analog-input", N_("Mono Chat + 7.1 Surround") },
+ { "off", N_("Off") }
+ };
+ const char *description_key = p->description_key ? p->description_key : p->name;
+
+ pa_assert(p);
+
+ /* Replace the output mapping names by the actual mappings */
+ if (p->output_mapping_names) {
+ char **name;
+
+ pa_assert(!p->output_mappings);
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ for (name = p->output_mapping_names; *name; name++) {
+ pa_alsa_mapping *m;
+ char **in;
+ bool duplicate = false;
+
+ for (in = name + 1; *in; in++)
+ if (pa_streq(*name, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_INPUT) {
+ pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name);
+ return -1;
+ }
+
+ pa_idxset_put(p->output_mappings, m, NULL);
+
+ if (p->supported)
+ m->supported++;
+ }
+
+ pa_xstrfreev(p->output_mapping_names);
+ p->output_mapping_names = NULL;
+ }
+
+ /* Replace the input mapping names by the actual mappings */
+ if (p->input_mapping_names) {
+ char **name;
+
+ pa_assert(!p->input_mappings);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ for (name = p->input_mapping_names; *name; name++) {
+ pa_alsa_mapping *m;
+ char **in;
+ bool duplicate = false;
+
+ for (in = name + 1; *in; in++)
+ if (pa_streq(*name, *in)) {
+ duplicate = true;
+ break;
+ }
+
+ if (duplicate)
+ continue;
+
+ if (!(m = pa_hashmap_get(p->profile_set->mappings, *name)) || m->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ pa_log("Profile '%s' refers to nonexistent mapping '%s'.", p->name, *name);
+ return -1;
+ }
+
+ pa_idxset_put(p->input_mappings, m, NULL);
+
+ if (p->supported)
+ m->supported++;
+ }
+
+ pa_xstrfreev(p->input_mapping_names);
+ p->input_mapping_names = NULL;
+ }
+
+ if (!p->input_mappings && !p->output_mappings) {
+ pa_log("Profile '%s' lacks mappings.", p->name);
+ return -1;
+ }
+
+ if (!p->description)
+ p->description = pa_xstrdup(lookup_description(description_key,
+ well_known_descriptions,
+ PA_ELEMENTSOF(well_known_descriptions)));
+
+ if (!p->description) {
+ uint32_t idx;
+ pa_alsa_mapping *m;
+ char *ptr;
+ size_t size;
+ FILE *f;
+ int count = 0;
+
+ f = open_memstream(&ptr, &size);
+ if (f == NULL) {
+ pa_log("failed to open memstream: %m");
+ return -1;
+ }
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (count++ > 0)
+ fprintf(f, " + ");
+ fprintf(f, _("%s Output"), m->description);
+ }
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (count++ > 0)
+ fprintf(f, " + ");
+ fprintf(f, _("%s Input"), m->description);
+ }
+
+ fclose(f);
+ p->description = ptr;
+ }
+
+ return 0;
+}
+
+void pa_alsa_profile_dump(pa_alsa_profile *p) {
+ uint32_t idx;
+ pa_alsa_mapping *m;
+ pa_assert(p);
+
+ pa_log_debug("Profile %s (%s), input=%s, output=%s priority=%u, supported=%s n_input_mappings=%u, n_output_mappings=%u",
+ p->name,
+ pa_strnull(p->description),
+ pa_strnull(p->input_name),
+ pa_strnull(p->output_name),
+ p->priority,
+ pa_yes_no(p->supported),
+ p->input_mappings ? pa_idxset_size(p->input_mappings) : 0,
+ p->output_mappings ? pa_idxset_size(p->output_mappings) : 0);
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ pa_log_debug("Input %s", m->name);
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ pa_log_debug("Output %s", m->name);
+}
+
+static int decibel_fix_verify(pa_alsa_decibel_fix *db_fix) {
+ pa_assert(db_fix);
+
+ /* Check that the dB mapping has been configured. Since "db-values" is
+ * currently the only option in the DecibelFix section, and decibel fix
+ * objects don't get created if a DecibelFix section is empty, this is
+ * actually a redundant check. Having this may prevent future bugs,
+ * however. */
+ if (!db_fix->db_values) {
+ pa_log("Decibel fix for element %s lacks the dB values.", db_fix->name);
+ return -1;
+ }
+
+ return 0;
+}
+
+void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix) {
+ char *db_values = NULL;
+
+ pa_assert(db_fix);
+
+ if (db_fix->db_values) {
+ unsigned long i, nsteps;
+ FILE *f;
+ char *ptr;
+ size_t size;
+
+ f = open_memstream(&ptr, &size);
+ if (f == NULL)
+ return;
+
+ pa_assert(db_fix->min_step <= db_fix->max_step);
+ nsteps = db_fix->max_step - db_fix->min_step + 1;
+
+ for (i = 0; i < nsteps; ++i)
+ fprintf(f, "[%li]:%0.2f ", i + db_fix->min_step, db_fix->db_values[i] / 100.0);
+
+ fclose(f);
+ db_values = ptr;
+ }
+
+ pa_log_debug("Decibel fix %s, min_step=%li, max_step=%li, db_values=%s",
+ db_fix->name, db_fix->min_step, db_fix->max_step, pa_strnull(db_values));
+
+ pa_xfree(db_values);
+}
+
+static const char *get_default_profile_dir(void) {
+ const char *str;
+#ifdef HAVE_RUNNING_FROM_BUILD_TREE
+ if (pa_run_from_build_tree())
+ return PA_SRCDIR "mixer/profile-sets";
+ else
+#endif
+ if (getenv("ACP_BUILDDIR") != NULL)
+ return "mixer/profile-sets";
+ if ((str = getenv("ACP_PROFILES_DIR")) != NULL)
+ return str;
+ return PA_ALSA_PROFILE_SETS_DIR;
+}
+
+pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) {
+ pa_alsa_profile_set *ps;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ pa_alsa_decibel_fix *db_fix;
+ char *fn;
+ int r;
+ void *state;
+
+ static pa_config_item items[] = {
+ /* [General] */
+ { "auto-profiles", pa_config_parse_bool, NULL, "General" },
+
+ /* [Mapping ...] */
+ { "device-strings", mapping_parse_device_strings, NULL, NULL },
+ { "channel-map", mapping_parse_channel_map, NULL, NULL },
+ { "paths-input", mapping_parse_paths, NULL, NULL },
+ { "paths-output", mapping_parse_paths, NULL, NULL },
+ { "element-input", mapping_parse_element, NULL, NULL },
+ { "element-output", mapping_parse_element, NULL, NULL },
+ { "direction", mapping_parse_direction, NULL, NULL },
+ { "exact-channels", mapping_parse_exact_channels, NULL, NULL },
+ { "intended-roles", mapping_parse_intended_roles, NULL, NULL },
+
+ /* Shared by [Mapping ...] and [Profile ...] */
+ { "description", mapping_parse_description, NULL, NULL },
+ { "description-key", mapping_parse_description_key,NULL, NULL },
+ { "priority", mapping_parse_priority, NULL, NULL },
+ { "fallback", mapping_parse_fallback, NULL, NULL },
+
+ /* [Profile ...] */
+ { "input-mappings", profile_parse_mappings, NULL, NULL },
+ { "output-mappings", profile_parse_mappings, NULL, NULL },
+ { "skip-probe", profile_parse_skip_probe, NULL, NULL },
+
+ /* [DecibelFix ...] */
+ { "db-values", decibel_fix_parse_db_values, NULL, NULL },
+ { NULL, NULL, NULL, NULL }
+ };
+
+ ps = pa_xnew0(pa_alsa_profile_set, 1);
+ ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_mapping_free);
+ ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_profile_free);
+ ps->decibel_fixes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) decibel_fix_free);
+ ps->input_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);
+ ps->output_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);
+
+ items[0].data = &ps->auto_profiles;
+
+ fn = pa_maybe_prefix_path(fname ? fname : "default.conf",
+ get_default_profile_dir());
+ if ((r = access(fn, R_OK)) != 0) {
+ if (fname != NULL) {
+ pa_log_warn("profile-set '%s' can't be accessed: %m", fn);
+ fn = pa_maybe_prefix_path("default.conf",
+ get_default_profile_dir());
+ r = access(fn, R_OK);
+ }
+ if (r != 0) {
+ pa_log_warn("profile-set '%s' can't be accessed: %m", fn);
+ }
+ }
+ r = pa_config_parse(fn, NULL, items, NULL, false, ps);
+ pa_xfree(fn);
+
+ if (r < 0)
+ goto fail;
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state)
+ if (mapping_verify(m, bonus) < 0)
+ goto fail;
+
+ if (ps->auto_profiles)
+ profile_set_add_auto(ps);
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state)
+ if (profile_verify(p) < 0)
+ goto fail;
+
+ PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state)
+ if (decibel_fix_verify(db_fix) < 0)
+ goto fail;
+
+ return ps;
+
+fail:
+ pa_alsa_profile_set_free(ps);
+ return NULL;
+}
+
+static void profile_finalize_probing(pa_alsa_profile *to_be_finalized, pa_alsa_profile *next) {
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ if (!to_be_finalized)
+ return;
+
+ if (to_be_finalized->output_mappings)
+ PA_IDXSET_FOREACH(m, to_be_finalized->output_mappings, idx) {
+
+ if (!m->output_pcm)
+ continue;
+
+ if (to_be_finalized->supported)
+ m->supported++;
+
+ /* If this mapping is also in the next profile, we won't close the
+ * pcm handle here, because it would get immediately reopened
+ * anyway. */
+ if (next && next->output_mappings && pa_idxset_get_by_data(next->output_mappings, m, NULL))
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_alsa_close(&m->output_pcm);
+ }
+
+ if (to_be_finalized->input_mappings)
+ PA_IDXSET_FOREACH(m, to_be_finalized->input_mappings, idx) {
+
+ if (!m->input_pcm)
+ continue;
+
+ if (to_be_finalized->supported)
+ m->supported++;
+
+ /* If this mapping is also in the next profile, we won't close the
+ * pcm handle here, because it would get immediately reopened
+ * anyway. */
+ if (next && next->input_mappings && pa_idxset_get_by_data(next->input_mappings, m, NULL))
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_alsa_close(&m->input_pcm);
+ }
+}
+
+static snd_pcm_t* mapping_open_pcm(pa_alsa_mapping *m,
+ const pa_sample_spec *ss,
+ const char *dev_id,
+ bool exact_channels,
+ int mode,
+ unsigned default_n_fragments,
+ unsigned default_fragment_size_msec) {
+
+ snd_pcm_t* handle;
+ pa_sample_spec try_ss = *ss;
+ pa_channel_map try_map = m->channel_map;
+ snd_pcm_uframes_t try_period_size, try_buffer_size;
+
+ try_ss.channels = try_map.channels;
+
+ try_period_size =
+ pa_usec_to_bytes(default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
+ pa_frame_size(&try_ss);
+ try_buffer_size = default_n_fragments * try_period_size;
+
+ handle = pa_alsa_open_by_template(
+ m->device_strings, dev_id, NULL, &try_ss,
+ &try_map, mode, &try_period_size,
+ &try_buffer_size, 0, NULL, NULL, exact_channels);
+ if (handle && !exact_channels && m->channel_map.channels != try_map.channels) {
+ char buf[PA_CHANNEL_MAP_SNPRINT_MAX];
+ pa_log_debug("Channel map for mapping '%s' permanently changed to '%s'", m->name,
+ pa_channel_map_snprint(buf, sizeof(buf), &try_map));
+ m->channel_map = try_map;
+ }
+ return handle;
+}
+
+static void paths_drop_unused(pa_hashmap* h, pa_hashmap *keep) {
+
+ void* state = NULL;
+ const void* key;
+ pa_alsa_path* p;
+
+ pa_assert(h);
+ pa_assert(keep);
+
+ p = pa_hashmap_iterate(h, &state, &key);
+ while (p) {
+ if (pa_hashmap_get(keep, p) == NULL)
+ pa_hashmap_remove_and_free(h, key);
+ p = pa_hashmap_iterate(h, &state, &key);
+ }
+}
+
+static int add_profiles_to_probe(
+ pa_alsa_profile **list,
+ pa_hashmap *profiles,
+ bool fallback_output,
+ bool fallback_input) {
+
+ int i = 0;
+ void *state;
+ pa_alsa_profile *p;
+ PA_HASHMAP_FOREACH(p, profiles, state)
+ if (p->fallback_input == fallback_input && p->fallback_output == fallback_output) {
+ *list = p;
+ list++;
+ i++;
+ }
+ return i;
+}
+
+static void mapping_query_hw_device(pa_alsa_mapping *mapping, snd_pcm_t *pcm) {
+ int r;
+ snd_pcm_info_t* pcm_info;
+ snd_pcm_info_alloca(&pcm_info);
+
+ r = snd_pcm_info(pcm, pcm_info);
+ if (r < 0) {
+ pa_log("Mapping %s: snd_pcm_info() failed %s: ", mapping->name, pa_alsa_strerror(r));
+ return;
+ }
+
+ /* XXX: It's not clear what snd_pcm_info_get_device() does if the device is
+ * not backed by a hw device or if it's backed by multiple hw devices. We
+ * only use hw_device_index for HDMI devices, however, and for those the
+ * return value is expected to be always valid, so this shouldn't be a
+ * significant problem. */
+ mapping->hw_device_index = snd_pcm_info_get_device(pcm_info);
+}
+
+void pa_alsa_profile_set_probe(
+ pa_alsa_profile_set *ps,
+ pa_hashmap *mixers,
+ const char *dev_id,
+ const pa_sample_spec *ss,
+ unsigned default_n_fragments,
+ unsigned default_fragment_size_msec) {
+
+ bool found_output = false, found_input = false;
+
+ pa_alsa_profile *p, *last = NULL;
+ pa_alsa_profile **pp, **probe_order;
+ pa_alsa_mapping *m;
+ pa_hashmap *broken_inputs, *broken_outputs, *used_paths;
+ pa_alsa_mapping *selected_fallback_input = NULL, *selected_fallback_output = NULL;
+
+ pa_assert(ps);
+ pa_assert(dev_id);
+ pa_assert(ss);
+
+ if (ps->probed)
+ return;
+
+ broken_inputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ broken_outputs = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ used_paths = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ pp = probe_order = pa_xnew0(pa_alsa_profile *, pa_hashmap_size(ps->profiles) + 1);
+
+ pp += add_profiles_to_probe(pp, ps->profiles, false, false);
+ pp += add_profiles_to_probe(pp, ps->profiles, false, true);
+ pp += add_profiles_to_probe(pp, ps->profiles, true, false);
+ pp += add_profiles_to_probe(pp, ps->profiles, true, true);
+
+ for (pp = probe_order; *pp; pp++) {
+ uint32_t idx;
+ p = *pp;
+
+ /* Skip if fallback and already found something, but still probe already selected fallbacks.
+ * If UCM is used then both fallback_input and fallback_output flags are false.
+ * If UCM is not used then there will be only a single entry in mappings.
+ */
+ if (found_input && p->fallback_input)
+ if (selected_fallback_input == NULL || pa_idxset_get_by_index(p->input_mappings, 0) != selected_fallback_input)
+ continue;
+ if (found_output && p->fallback_output)
+ if (selected_fallback_output == NULL || pa_idxset_get_by_index(p->output_mappings, 0) != selected_fallback_output)
+ continue;
+
+ /* Skip if this is already marked that it is supported (i.e. from the config file) */
+ if (!p->supported) {
+
+ profile_finalize_probing(last, p);
+ p->supported = true;
+
+ if (p->output_mappings) {
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (pa_hashmap_get(broken_outputs, m) == m) {
+ pa_log_debug("Skipping profile %s - will not be able to open output:%s", p->name, m->name);
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (p->input_mappings && p->supported) {
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (pa_hashmap_get(broken_inputs, m) == m) {
+ pa_log_debug("Skipping profile %s - will not be able to open input:%s", p->name, m->name);
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (p->supported)
+ pa_log_debug("Looking at profile %s", p->name);
+
+ /* Check if we can open all new ones */
+ if (p->output_mappings && p->supported)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+
+ if (m->output_pcm)
+ continue;
+
+ pa_log_debug("Checking for playback on %s (%s)", m->description, m->name);
+ if (!(m->output_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels,
+ SND_PCM_STREAM_PLAYBACK,
+ default_n_fragments,
+ default_fragment_size_msec))) {
+ p->supported = false;
+ if (pa_idxset_size(p->output_mappings) == 1 &&
+ ((!p->input_mappings) || pa_idxset_size(p->input_mappings) == 0)) {
+ pa_log_debug("Caching failure to open output:%s", m->name);
+ pa_hashmap_put(broken_outputs, m, m);
+ }
+ break;
+ }
+
+ if (m->hw_device_index < 0)
+ mapping_query_hw_device(m, m->output_pcm);
+ }
+
+ if (p->input_mappings && p->supported)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+
+ if (m->input_pcm)
+ continue;
+
+ pa_log_debug("Checking for recording on %s (%s)", m->description, m->name);
+ if (!(m->input_pcm = mapping_open_pcm(m, ss, dev_id, m->exact_channels,
+ SND_PCM_STREAM_CAPTURE,
+ default_n_fragments,
+ default_fragment_size_msec))) {
+ p->supported = false;
+ if (pa_idxset_size(p->input_mappings) == 1 &&
+ ((!p->output_mappings) || pa_idxset_size(p->output_mappings) == 0)) {
+ pa_log_debug("Caching failure to open input:%s", m->name);
+ pa_hashmap_put(broken_inputs, m, m);
+ }
+ break;
+ }
+
+ if (m->hw_device_index < 0)
+ mapping_query_hw_device(m, m->input_pcm);
+ }
+
+ last = p;
+
+ if (!p->supported)
+ continue;
+ }
+
+ pa_log_debug("Profile %s supported.", p->name);
+
+ if (p->output_mappings)
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ if (m->output_pcm) {
+ found_output = true;
+ if (p->fallback_output && selected_fallback_output == NULL) {
+ selected_fallback_output = m;
+ }
+ mapping_paths_probe(m, p, PA_ALSA_DIRECTION_OUTPUT, used_paths, mixers);
+ }
+
+ if (p->input_mappings)
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ if (m->input_pcm) {
+ found_input = true;
+ if (p->fallback_input && selected_fallback_input == NULL) {
+ selected_fallback_input = m;
+ }
+ mapping_paths_probe(m, p, PA_ALSA_DIRECTION_INPUT, used_paths, mixers);
+ }
+ }
+
+ /* Clean up */
+ profile_finalize_probing(last, NULL);
+
+ pa_alsa_profile_set_drop_unsupported(ps);
+
+ paths_drop_unused(ps->input_paths, used_paths);
+ paths_drop_unused(ps->output_paths, used_paths);
+ pa_hashmap_free(broken_inputs);
+ pa_hashmap_free(broken_outputs);
+ pa_hashmap_free(used_paths);
+ pa_xfree(probe_order);
+
+ profile_set_set_availability_groups(ps);
+
+ ps->probed = true;
+}
+
+void pa_alsa_profile_set_dump(pa_alsa_profile_set *ps) {
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ pa_alsa_decibel_fix *db_fix;
+ void *state;
+
+ pa_assert(ps);
+
+ pa_log_debug("Profile set %p, auto_profiles=%s, probed=%s, n_mappings=%u, n_profiles=%u, n_decibel_fixes=%u",
+ (void*)
+ ps,
+ pa_yes_no(ps->auto_profiles),
+ pa_yes_no(ps->probed),
+ pa_hashmap_size(ps->mappings),
+ pa_hashmap_size(ps->profiles),
+ pa_hashmap_size(ps->decibel_fixes));
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state)
+ pa_alsa_mapping_dump(m);
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state)
+ pa_alsa_profile_dump(p);
+
+ PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state)
+ pa_alsa_decibel_fix_dump(db_fix);
+}
+
+void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *ps) {
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ void *state;
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state) {
+ if (!p->supported)
+ pa_hashmap_remove_and_free(ps->profiles, p->name);
+ }
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state) {
+ if (m->supported <= 0)
+ pa_hashmap_remove_and_free(ps->mappings, m->name);
+ }
+}
+
+static pa_device_port* device_port_alsa_init(pa_hashmap *ports, /* card ports */
+ const char* name,
+ const char* description,
+ pa_alsa_path *path,
+ pa_alsa_setting *setting,
+ pa_card_profile *cp,
+ pa_hashmap *extra, /* sink/source ports */
+ pa_core *core) {
+
+ pa_device_port *p;
+
+ pa_assert(path);
+
+ p = pa_hashmap_get(ports, name);
+
+ if (!p) {
+ pa_alsa_port_data *data;
+ pa_device_port_new_data port_data;
+
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, name);
+ pa_device_port_new_data_set_description(&port_data, description);
+ pa_device_port_new_data_set_direction(&port_data, path->direction == PA_ALSA_DIRECTION_OUTPUT ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT);
+ pa_device_port_new_data_set_type(&port_data, path->device_port_type);
+ pa_device_port_new_data_set_availability_group(&port_data, path->availability_group);
+
+ p = pa_device_port_new(core, &port_data, sizeof(pa_alsa_port_data));
+ pa_device_port_new_data_done(&port_data);
+ pa_assert(p);
+ pa_hashmap_put(ports, p->name, p);
+ pa_proplist_update(p->proplist, PA_UPDATE_REPLACE, path->proplist);
+
+ data = PA_DEVICE_PORT_DATA(p);
+ /* Ownership of the path and setting is not transferred to the port data, so we don't deal with freeing them */
+ data->path = path;
+ data->setting = setting;
+ path->port = p;
+ }
+
+ if (cp)
+ pa_hashmap_put(p->profiles, cp->name, cp);
+
+ if (extra) {
+ pa_hashmap_put(extra, p->name, p);
+ }
+
+ return p;
+}
+
+void pa_alsa_path_set_add_ports(
+ pa_alsa_path_set *ps,
+ pa_card_profile *cp,
+ pa_hashmap *ports, /* card ports */
+ pa_hashmap *extra, /* sink/source ports */
+ pa_core *core) {
+
+ pa_alsa_path *path;
+ void *state;
+
+ pa_assert(ports);
+
+ if (!ps)
+ return;
+
+ PA_HASHMAP_FOREACH(path, ps->paths, state) {
+ if (!path->settings || !path->settings->next) {
+ /* If there is no or just one setting we only need a
+ * single entry */
+ pa_device_port *port = device_port_alsa_init(ports, path->name,
+ path->description, path, path->settings, cp, extra, core);
+ port->priority = path->priority * 100;
+
+ } else {
+ pa_alsa_setting *s;
+ PA_LLIST_FOREACH(s, path->settings) {
+ pa_device_port *port;
+ char *n, *d;
+
+ n = pa_sprintf_malloc("%s;%s", path->name, s->name);
+
+ if (s->description[0])
+ d = pa_sprintf_malloc("%s / %s", path->description, s->description);
+ else
+ d = pa_xstrdup(path->description);
+
+ port = device_port_alsa_init(ports, n, d, path, s, cp, extra, core);
+ port->priority = path->priority * 100 + s->priority;
+
+ pa_xfree(n);
+ pa_xfree(d);
+ }
+ }
+ }
+}
+
+void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card) {
+ pa_assert(ps);
+
+ if (ps->paths && pa_hashmap_size(ps->paths) > 0) {
+ pa_assert(card);
+ pa_alsa_path_set_add_ports(ps, NULL, card->ports, ports, card->core);
+ }
+
+ pa_log_debug("Added %u ports", pa_hashmap_size(ports));
+}
diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h
new file mode 100644
index 0000000..643f03d
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-mixer.h
@@ -0,0 +1,459 @@
+#ifndef fooalsamixerhfoo
+#define fooalsamixerhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <alsa/asoundlib.h>
+
+typedef struct pa_alsa_mixer pa_alsa_mixer;
+typedef struct pa_alsa_setting pa_alsa_setting;
+typedef struct pa_alsa_mixer_id pa_alsa_mixer_id;
+typedef struct pa_alsa_option pa_alsa_option;
+typedef struct pa_alsa_element pa_alsa_element;
+typedef struct pa_alsa_jack pa_alsa_jack;
+typedef struct pa_alsa_path pa_alsa_path;
+typedef struct pa_alsa_path_set pa_alsa_path_set;
+typedef struct pa_alsa_mapping pa_alsa_mapping;
+typedef struct pa_alsa_profile pa_alsa_profile;
+typedef struct pa_alsa_decibel_fix pa_alsa_decibel_fix;
+typedef struct pa_alsa_profile_set pa_alsa_profile_set;
+typedef struct pa_alsa_port_data pa_alsa_port_data;
+typedef struct pa_alsa_profile pa_alsa_profile;
+typedef struct pa_alsa_profile pa_card_profile;
+typedef struct pa_alsa_device pa_alsa_device;
+
+#define POSITION_MASK_CHANNELS 8
+
+typedef enum pa_alsa_switch_use {
+ PA_ALSA_SWITCH_IGNORE,
+ PA_ALSA_SWITCH_MUTE, /* make this switch follow mute status */
+ PA_ALSA_SWITCH_OFF, /* set this switch to 'off' unconditionally */
+ PA_ALSA_SWITCH_ON, /* set this switch to 'on' unconditionally */
+ PA_ALSA_SWITCH_SELECT /* allow the user to select switch status through a setting */
+} pa_alsa_switch_use_t;
+
+typedef enum pa_alsa_volume_use {
+ PA_ALSA_VOLUME_IGNORE,
+ PA_ALSA_VOLUME_MERGE, /* merge this volume slider into the global volume slider */
+ PA_ALSA_VOLUME_OFF, /* set this volume to minimal unconditionally */
+ PA_ALSA_VOLUME_ZERO, /* set this volume to 0dB unconditionally */
+ PA_ALSA_VOLUME_CONSTANT /* set this volume to a constant value unconditionally */
+} pa_alsa_volume_use_t;
+
+typedef enum pa_alsa_enumeration_use {
+ PA_ALSA_ENUMERATION_IGNORE,
+ PA_ALSA_ENUMERATION_SELECT
+} pa_alsa_enumeration_use_t;
+
+typedef enum pa_alsa_required {
+ PA_ALSA_REQUIRED_IGNORE,
+ PA_ALSA_REQUIRED_SWITCH,
+ PA_ALSA_REQUIRED_VOLUME,
+ PA_ALSA_REQUIRED_ENUMERATION,
+ PA_ALSA_REQUIRED_ANY
+} pa_alsa_required_t;
+
+typedef enum pa_alsa_direction {
+ PA_ALSA_DIRECTION_ANY,
+ PA_ALSA_DIRECTION_OUTPUT,
+ PA_ALSA_DIRECTION_INPUT
+} pa_alsa_direction_t;
+
+
+#include "acp.h"
+#include "device-port.h"
+#include "alsa-util.h"
+#include "alsa-ucm.h"
+#include "card.h"
+
+/* A setting combines a couple of options into a single entity that
+ * may be selected. Only one setting can be active at the same
+ * time. */
+struct pa_alsa_setting {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_setting);
+
+ pa_idxset *options;
+
+ char *name;
+ char *description;
+ unsigned priority;
+};
+
+/* An entry for one ALSA mixer */
+struct pa_alsa_mixer {
+ struct pa_alsa_mixer *alias;
+ snd_mixer_t *mixer_handle;
+ bool used_for_poll:1;
+ bool used_for_probe_only:1;
+};
+
+/* ALSA mixer element identifier */
+struct pa_alsa_mixer_id {
+ char *name;
+ int index;
+};
+
+char *pa_alsa_mixer_id_to_string(char *dst, size_t dst_len, pa_alsa_mixer_id *id);
+
+/* An option belongs to an element and refers to one enumeration item
+ * of the element is an enumeration item, or a switch status if the
+ * element is a switch item. */
+struct pa_alsa_option {
+ pa_alsa_element *element;
+ PA_LLIST_FIELDS(pa_alsa_option);
+
+ char *alsa_name;
+ int alsa_idx;
+
+ char *name;
+ char *description;
+ unsigned priority;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+};
+
+/* An element wraps one specific ALSA element. A series of elements
+ * make up a path (see below). If the element is an enumeration or switch
+ * element it may include a list of options. */
+struct pa_alsa_element {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_element);
+
+ struct pa_alsa_mixer_id alsa_id;
+ pa_alsa_direction_t direction;
+
+ pa_alsa_switch_use_t switch_use;
+ pa_alsa_volume_use_t volume_use;
+ pa_alsa_enumeration_use_t enumeration_use;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+
+ long constant_volume;
+
+ unsigned int override_map;
+ bool direction_try_other:1;
+
+ bool has_dB:1;
+ long min_volume, max_volume;
+ long volume_limit; /* -1 for no configured limit */
+ double min_dB, max_dB;
+
+ pa_channel_position_mask_t masks[SND_MIXER_SCHN_LAST + 1][POSITION_MASK_CHANNELS];
+ unsigned n_channels;
+
+ pa_channel_position_mask_t merged_mask;
+
+ PA_LLIST_HEAD(pa_alsa_option, options);
+
+ pa_alsa_decibel_fix *db_fix;
+};
+
+struct pa_alsa_jack {
+ pa_alsa_path *path;
+ PA_LLIST_FIELDS(pa_alsa_jack);
+
+ snd_mixer_t *mixer_handle;
+ char *mixer_device_name;
+
+ struct pa_alsa_mixer_id alsa_id;
+ char *name; /* E g "Headphone" */
+ bool has_control; /* is the jack itself present? */
+ bool plugged_in; /* is this jack currently plugged in? */
+ snd_mixer_elem_t *melem; /* Jack detection handle */
+ pa_available_t state_unplugged, state_plugged;
+
+ pa_alsa_required_t required;
+ pa_alsa_required_t required_any;
+ pa_alsa_required_t required_absent;
+
+ pa_dynarray *ucm_devices; /* pa_alsa_ucm_device */
+ pa_dynarray *ucm_hw_mute_devices; /* pa_alsa_ucm_device */
+
+ bool append_pcm_to_name;
+};
+
+pa_alsa_jack *pa_alsa_jack_new(pa_alsa_path *path, const char *mixer_device_name, const char *name, int index);
+void pa_alsa_jack_free(pa_alsa_jack *jack);
+void pa_alsa_jack_set_has_control(pa_alsa_jack *jack, bool has_control);
+void pa_alsa_jack_set_plugged_in(pa_alsa_jack *jack, bool plugged_in);
+void pa_alsa_jack_add_ucm_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device);
+void pa_alsa_jack_add_ucm_hw_mute_device(pa_alsa_jack *jack, pa_alsa_ucm_device *device);
+
+/* A path wraps a series of elements into a single entity which can be
+ * used to control it as if it had a single volume slider, a single
+ * mute switch and a single list of selectable options. */
+struct pa_alsa_path {
+ pa_alsa_direction_t direction;
+ pa_device_port* port;
+
+ char *name;
+ char *description_key;
+ char *description;
+ char *availability_group;
+ pa_device_port_type_t device_port_type;
+ unsigned priority;
+ bool autodetect_eld_device;
+ pa_alsa_mixer *eld_mixer_handle;
+ int eld_device;
+ pa_proplist *proplist;
+
+ bool probed:1;
+ bool supported:1;
+ bool has_mute:1;
+ bool has_volume:1;
+ bool has_dB:1;
+ bool mute_during_activation:1;
+ /* These two are used during probing only */
+ bool has_req_any:1;
+ bool req_any_present:1;
+
+ long min_volume, max_volume;
+ double min_dB, max_dB;
+
+ /* This is used during parsing only, as a shortcut so that we
+ * don't have to iterate the list all the time */
+ pa_alsa_element *last_element;
+ pa_alsa_option *last_option;
+ pa_alsa_setting *last_setting;
+ pa_alsa_jack *last_jack;
+
+ PA_LLIST_HEAD(pa_alsa_element, elements);
+ PA_LLIST_HEAD(pa_alsa_setting, settings);
+ PA_LLIST_HEAD(pa_alsa_jack, jacks);
+};
+
+/* A path set is simply a set of paths that are applicable to a
+ * device */
+struct pa_alsa_path_set {
+ pa_hashmap *paths;
+ pa_alsa_direction_t direction;
+};
+
+void pa_alsa_setting_dump(pa_alsa_setting *s);
+
+void pa_alsa_option_dump(pa_alsa_option *o);
+void pa_alsa_jack_dump(pa_alsa_jack *j);
+void pa_alsa_element_dump(pa_alsa_element *e);
+
+pa_alsa_path *pa_alsa_path_new(const char *paths_dir, const char *fname, pa_alsa_direction_t direction);
+pa_alsa_path *pa_alsa_path_synthesize(const char *element, pa_alsa_direction_t direction);
+pa_alsa_element *pa_alsa_element_get(pa_alsa_path *p, const char *section, bool prefixed);
+int pa_alsa_path_probe(pa_alsa_path *p, pa_alsa_mapping *mapping, snd_mixer_t *m, bool ignore_dB);
+void pa_alsa_path_dump(pa_alsa_path *p);
+int pa_alsa_path_get_volume(pa_alsa_path *p, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v);
+int pa_alsa_path_get_mute(pa_alsa_path *path, snd_mixer_t *m, bool *muted);
+int pa_alsa_path_set_volume(pa_alsa_path *path, snd_mixer_t *m, const pa_channel_map *cm, pa_cvolume *v, bool deferred_volume, bool write_to_hw);
+int pa_alsa_path_set_mute(pa_alsa_path *path, snd_mixer_t *m, bool muted);
+int pa_alsa_path_select(pa_alsa_path *p, pa_alsa_setting *s, snd_mixer_t *m, bool device_is_muted);
+void pa_alsa_path_set_callback(pa_alsa_path *p, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata);
+void pa_alsa_path_free(pa_alsa_path *p);
+
+pa_alsa_path_set *pa_alsa_path_set_new(pa_alsa_mapping *m, pa_alsa_direction_t direction, const char *paths_dir);
+void pa_alsa_path_set_dump(pa_alsa_path_set *s);
+void pa_alsa_path_set_set_callback(pa_alsa_path_set *ps, snd_mixer_t *m, snd_mixer_elem_callback_t cb, void *userdata);
+void pa_alsa_path_set_free(pa_alsa_path_set *s);
+int pa_alsa_path_set_is_empty(pa_alsa_path_set *s);
+
+struct pa_alsa_device {
+ struct acp_device device;
+
+ pa_card *card;
+
+ pa_alsa_direction_t direction;
+ pa_proplist *proplist;
+
+ pa_alsa_mapping *mapping;
+ pa_alsa_ucm_mapping_context *ucm_context;
+
+ pa_hashmap *ports;
+ pa_dynarray port_array;
+ pa_device_port *active_port;
+
+ snd_mixer_t *mixer_handle;
+ pa_alsa_path_set *mixer_path_set;
+ pa_alsa_path *mixer_path;
+ snd_pcm_t *pcm_handle;
+
+ unsigned muted:1;
+ unsigned decibel_volume:1;
+ pa_cvolume real_volume;
+ pa_cvolume hardware_volume;
+ pa_cvolume soft_volume;
+
+ pa_volume_t base_volume;
+ unsigned n_volume_steps;
+
+ int (*read_volume)(pa_alsa_device *dev);
+ int (*read_mute)(pa_alsa_device *dev);
+
+ void (*set_volume)(pa_alsa_device *dev, const pa_cvolume *v);
+ void (*set_mute)(pa_alsa_device *dev, bool m);
+};
+
+struct pa_alsa_mapping {
+ pa_alsa_profile_set *profile_set;
+
+ char *name;
+ char *description;
+ char *description_key;
+ unsigned priority;
+ pa_alsa_direction_t direction;
+ /* These are copied over to the resultant sink/source */
+ pa_proplist *proplist;
+
+ pa_sample_spec sample_spec;
+ pa_channel_map channel_map;
+
+ char **device_strings;
+
+ char **input_path_names;
+ char **output_path_names;
+ char **input_element; /* list of fallbacks */
+ char **output_element;
+ pa_alsa_path_set *input_path_set;
+ pa_alsa_path_set *output_path_set;
+
+ unsigned supported;
+ bool exact_channels:1;
+ bool fallback:1;
+
+ /* The "y" in "hw:x,y". This is set to -1 before the device index has been
+ * queried, or if the query failed. */
+ int hw_device_index;
+
+ /* Temporarily used during probing */
+ snd_pcm_t *input_pcm;
+ snd_pcm_t *output_pcm;
+
+ pa_proplist *input_proplist;
+ pa_proplist *output_proplist;
+
+ pa_alsa_device output;
+ pa_alsa_device input;
+
+ /* ucm device context*/
+ pa_alsa_ucm_mapping_context ucm_context;
+};
+
+struct pa_alsa_profile {
+ struct acp_card_profile profile;
+
+ pa_alsa_profile_set *profile_set;
+
+ char *name;
+ char *description;
+ char *description_key;
+ unsigned priority;
+
+ char *input_name;
+ char *output_name;
+
+ bool supported:1;
+ bool fallback_input:1;
+ bool fallback_output:1;
+
+ char **input_mapping_names;
+ char **output_mapping_names;
+
+ pa_idxset *input_mappings;
+ pa_idxset *output_mappings;
+
+ struct {
+ pa_dynarray devices;
+ } out;
+};
+
+struct pa_alsa_decibel_fix {
+ char *key;
+
+ pa_alsa_profile_set *profile_set;
+
+ char *name; /* Alsa volume element name. */
+ int index; /* Alsa volume element index. */
+ long min_step;
+ long max_step;
+
+ /* An array that maps alsa volume element steps to decibels. The steps can
+ * be used as indices to this array, after subtracting min_step from the
+ * real value.
+ *
+ * The values are actually stored as integers representing millibels,
+ * because that's the format the alsa API uses. */
+ long *db_values;
+};
+
+struct pa_alsa_profile_set {
+ pa_hashmap *mappings;
+ pa_hashmap *profiles;
+ pa_hashmap *decibel_fixes;
+ pa_hashmap *input_paths;
+ pa_hashmap *output_paths;
+
+ bool auto_profiles;
+ bool ignore_dB:1;
+ bool probed:1;
+};
+
+void pa_alsa_mapping_dump(pa_alsa_mapping *m);
+void pa_alsa_profile_dump(pa_alsa_profile *p);
+void pa_alsa_decibel_fix_dump(pa_alsa_decibel_fix *db_fix);
+pa_alsa_mapping *pa_alsa_mapping_get(pa_alsa_profile_set *ps, const char *name);
+void pa_alsa_mapping_free (pa_alsa_mapping *m);
+void pa_alsa_profile_free (pa_alsa_profile *p);
+
+pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus);
+void pa_alsa_profile_set_probe(pa_alsa_profile_set *ps, pa_hashmap *mixers, const char *dev_id, const pa_sample_spec *ss, unsigned default_n_fragments, unsigned default_fragment_size_msec);
+void pa_alsa_profile_set_free(pa_alsa_profile_set *s);
+void pa_alsa_profile_set_dump(pa_alsa_profile_set *s);
+void pa_alsa_profile_set_drop_unsupported(pa_alsa_profile_set *s);
+
+void pa_alsa_mixer_use_for_poll(pa_hashmap *mixers, snd_mixer_t *mixer_handle);
+
+#if 0
+pa_alsa_fdlist *pa_alsa_fdlist_new(void);
+void pa_alsa_fdlist_free(pa_alsa_fdlist *fdl);
+int pa_alsa_fdlist_set_handle(pa_alsa_fdlist *fdl, snd_mixer_t *mixer_handle, snd_hctl_t *hctl_handle, pa_mainloop_api* m);
+
+/* Alternative for handling alsa mixer events in io-thread. */
+
+pa_alsa_mixer_pdata *pa_alsa_mixer_pdata_new(void);
+void pa_alsa_mixer_pdata_free(pa_alsa_mixer_pdata *pd);
+int pa_alsa_set_mixer_rtpoll(struct pa_alsa_mixer_pdata *pd, snd_mixer_t *mixer, pa_rtpoll *rtp);
+#endif
+
+/* Data structure for inclusion in pa_device_port for alsa
+ * sinks/sources. This contains nothing that needs to be freed
+ * individually */
+struct pa_alsa_port_data {
+ pa_alsa_path *path;
+ pa_alsa_setting *setting;
+ bool suspend_when_unavailable;
+};
+
+void pa_alsa_add_ports(pa_hashmap *ports, pa_alsa_path_set *ps, pa_card *card);
+void pa_alsa_path_set_add_ports(pa_alsa_path_set *ps, pa_alsa_profile *cp, pa_hashmap *ports, pa_hashmap *extra, pa_core *core);
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c
new file mode 100644
index 0000000..f66b771
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-ucm.c
@@ -0,0 +1,2420 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2011 Wolfson Microelectronics PLC
+ Author Margarita Olaya <magi@slimlogic.co.uk>
+ Copyright 2012 Feng Wei <wei.feng@freescale.com>, Freescale Ltd.
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+***/
+
+#include "config.h"
+
+#include <ctype.h>
+#include <sys/types.h>
+#include <limits.h>
+#include <alsa/asoundlib.h>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+#include <valgrind/memcheck.h>
+#endif
+
+#include "alsa-mixer.h"
+#include "alsa-util.h"
+#include "alsa-ucm.h"
+
+#define PA_UCM_PRE_TAG_OUTPUT "[Out] "
+#define PA_UCM_PRE_TAG_INPUT "[In] "
+
+#define PA_UCM_PLAYBACK_PRIORITY_UNSET(device) ((device)->playback_channels && !(device)->playback_priority)
+#define PA_UCM_CAPTURE_PRIORITY_UNSET(device) ((device)->capture_channels && !(device)->capture_priority)
+#define PA_UCM_DEVICE_PRIORITY_SET(device, priority) \
+ do { \
+ if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) (device)->playback_priority = (priority); \
+ if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) (device)->capture_priority = (priority); \
+ } while (0)
+#define PA_UCM_IS_MODIFIER_MAPPING(m) ((pa_proplist_gets((m)->proplist, PA_ALSA_PROP_UCM_MODIFIER)) != NULL)
+
+#ifdef HAVE_ALSA_UCM
+
+struct ucm_type {
+ const char *prefix;
+ pa_device_port_type_t type;
+};
+
+struct ucm_items {
+ const char *id;
+ const char *property;
+};
+
+struct ucm_info {
+ const char *id;
+ unsigned priority;
+};
+
+static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device);
+static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack);
+static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack);
+
+static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name);
+
+
+static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port,
+ pa_alsa_ucm_device **devices, unsigned n_devices);
+static void ucm_port_data_free(pa_device_port *port);
+static void ucm_port_update_available(pa_alsa_ucm_port_data *port);
+
+static struct ucm_type types[] = {
+ {"None", PA_DEVICE_PORT_TYPE_UNKNOWN},
+ {"Speaker", PA_DEVICE_PORT_TYPE_SPEAKER},
+ {"Line", PA_DEVICE_PORT_TYPE_LINE},
+ {"Mic", PA_DEVICE_PORT_TYPE_MIC},
+ {"Headphones", PA_DEVICE_PORT_TYPE_HEADPHONES},
+ {"Headset", PA_DEVICE_PORT_TYPE_HEADSET},
+ {"Handset", PA_DEVICE_PORT_TYPE_HANDSET},
+ {"Bluetooth", PA_DEVICE_PORT_TYPE_BLUETOOTH},
+ {"Earpiece", PA_DEVICE_PORT_TYPE_EARPIECE},
+ {"SPDIF", PA_DEVICE_PORT_TYPE_SPDIF},
+ {"HDMI", PA_DEVICE_PORT_TYPE_HDMI},
+ {NULL, 0}
+};
+
+static struct ucm_items item[] = {
+ {"PlaybackPCM", PA_ALSA_PROP_UCM_SINK},
+ {"CapturePCM", PA_ALSA_PROP_UCM_SOURCE},
+ {"PlaybackCTL", PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE},
+ {"PlaybackVolume", PA_ALSA_PROP_UCM_PLAYBACK_VOLUME},
+ {"PlaybackSwitch", PA_ALSA_PROP_UCM_PLAYBACK_SWITCH},
+ {"PlaybackMixer", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE},
+ {"PlaybackMixerElem", PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM},
+ {"PlaybackMasterElem", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM},
+ {"PlaybackMasterType", PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE},
+ {"PlaybackPriority", PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY},
+ {"PlaybackRate", PA_ALSA_PROP_UCM_PLAYBACK_RATE},
+ {"PlaybackChannels", PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS},
+ {"CaptureCTL", PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE},
+ {"CaptureVolume", PA_ALSA_PROP_UCM_CAPTURE_VOLUME},
+ {"CaptureSwitch", PA_ALSA_PROP_UCM_CAPTURE_SWITCH},
+ {"CaptureMixer", PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE},
+ {"CaptureMixerElem", PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM},
+ {"CaptureMasterElem", PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM},
+ {"CaptureMasterType", PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE},
+ {"CapturePriority", PA_ALSA_PROP_UCM_CAPTURE_PRIORITY},
+ {"CaptureRate", PA_ALSA_PROP_UCM_CAPTURE_RATE},
+ {"CaptureChannels", PA_ALSA_PROP_UCM_CAPTURE_CHANNELS},
+ {"TQ", PA_ALSA_PROP_UCM_QOS},
+ {"JackCTL", PA_ALSA_PROP_UCM_JACK_DEVICE},
+ {"JackControl", PA_ALSA_PROP_UCM_JACK_CONTROL},
+ {"JackHWMute", PA_ALSA_PROP_UCM_JACK_HW_MUTE},
+ {NULL, NULL},
+};
+
+/* UCM verb info - this should eventually be part of policy management */
+static struct ucm_info verb_info[] = {
+ {SND_USE_CASE_VERB_INACTIVE, 0},
+ {SND_USE_CASE_VERB_HIFI, 8000},
+ {SND_USE_CASE_VERB_HIFI_LOW_POWER, 7000},
+ {SND_USE_CASE_VERB_VOICE, 6000},
+ {SND_USE_CASE_VERB_VOICE_LOW_POWER, 5000},
+ {SND_USE_CASE_VERB_VOICECALL, 4000},
+ {SND_USE_CASE_VERB_IP_VOICECALL, 4000},
+ {SND_USE_CASE_VERB_ANALOG_RADIO, 3000},
+ {SND_USE_CASE_VERB_DIGITAL_RADIO, 3000},
+ {NULL, 0}
+};
+
+/* UCM device info - should be overwritten by ucm property */
+static struct ucm_info dev_info[] = {
+ {SND_USE_CASE_DEV_SPEAKER, 100},
+ {SND_USE_CASE_DEV_LINE, 100},
+ {SND_USE_CASE_DEV_HEADPHONES, 100},
+ {SND_USE_CASE_DEV_HEADSET, 300},
+ {SND_USE_CASE_DEV_HANDSET, 200},
+ {SND_USE_CASE_DEV_BLUETOOTH, 400},
+ {SND_USE_CASE_DEV_EARPIECE, 100},
+ {SND_USE_CASE_DEV_SPDIF, 100},
+ {SND_USE_CASE_DEV_HDMI, 100},
+ {SND_USE_CASE_DEV_NONE, 100},
+ {NULL, 0}
+};
+
+
+static char *ucm_verb_value(
+ snd_use_case_mgr_t *uc_mgr,
+ const char *verb_name,
+ const char *id) {
+
+ const char *value;
+ char *_id = pa_sprintf_malloc("=%s//%s", id, verb_name);
+ int err = snd_use_case_get(uc_mgr, _id, &value);
+ pa_xfree(_id);
+ if (err < 0)
+ return NULL;
+ pa_log_debug("Got %s for verb %s: %s", id, verb_name, value);
+ /* Use the cast here to allow free() call without casting for callers.
+ * The snd_use_case_get() returns mallocated string.
+ * See the Note: in use-case.h for snd_use_case_get().
+ */
+ return (char *)value;
+}
+
+static int ucm_device_exists(pa_idxset *idxset, pa_alsa_ucm_device *dev) {
+ pa_alsa_ucm_device *d;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(d, idxset, idx)
+ if (d == dev)
+ return 1;
+
+ return 0;
+}
+
+static void ucm_add_devices_to_idxset(
+ pa_idxset *idxset,
+ pa_alsa_ucm_device *me,
+ pa_alsa_ucm_device *devices,
+ const char **dev_names,
+ int n) {
+
+ pa_alsa_ucm_device *d;
+
+ PA_LLIST_FOREACH(d, devices) {
+ const char *name;
+ int i;
+
+ if (d == me)
+ continue;
+
+ name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ for (i = 0; i < n; i++)
+ if (pa_streq(dev_names[i], name))
+ pa_idxset_put(idxset, d, NULL);
+ }
+}
+
+/* Split a string into words. Like pa_split_spaces() but handle '' and "". */
+static char *ucm_split_devnames(const char *c, const char **state) {
+ const char *current = *state ? *state : c;
+ char h;
+ size_t l;
+
+ if (!*current || *c == 0)
+ return NULL;
+
+ current += strspn(current, "\n\r \t");
+ h = *current;
+ if (h == '\'' || h =='"') {
+ c = ++current;
+ for (l = 0; *c && *c != h; l++) c++;
+ if (*c != h)
+ return NULL;
+ *state = c + 1;
+ } else {
+ l = strcspn(current, "\n\r \t");
+ *state = current+l;
+ }
+
+ return pa_xstrndup(current, l);
+}
+
+
+static void ucm_volume_free(pa_alsa_ucm_volume *vol) {
+ pa_assert(vol);
+ pa_xfree(vol->mixer_elem);
+ pa_xfree(vol->master_elem);
+ pa_xfree(vol->master_type);
+ pa_xfree(vol);
+}
+
+/* Get the volume identifier */
+static char *ucm_get_mixer_id(
+ pa_alsa_ucm_device *device,
+ const char *mprop,
+ const char *cprop,
+ const char *cid)
+{
+#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */
+ snd_ctl_elem_id_t *ctl;
+ int err;
+#endif
+ const char *value;
+ char *value2;
+ int index;
+
+ /* mixer element as first, if it's found, return it without modifications */
+ value = pa_proplist_gets(device->proplist, mprop);
+ if (value)
+ return pa_xstrdup(value);
+ /* fallback, get the control element identifier */
+ /* and try to do some heuristic to determine the mixer element name */
+ value = pa_proplist_gets(device->proplist, cprop);
+ if (value == NULL)
+ return NULL;
+#if SND_LIB_VERSION >= 0x10201 /* alsa-lib-1.2.1+ check */
+ /* The new parser may return also element index. */
+ snd_ctl_elem_id_alloca(&ctl);
+ err = snd_use_case_parse_ctl_elem_id(ctl, cid, value);
+ if (err < 0)
+ return NULL;
+ value = snd_ctl_elem_id_get_name(ctl);
+ index = snd_ctl_elem_id_get_index(ctl);
+#else
+#warning "Upgrade to alsa-lib 1.2.1!"
+ index = 0;
+#endif
+ if (!(value2 = pa_str_strip_suffix(value, " Playback Volume")))
+ if (!(value2 = pa_str_strip_suffix(value, " Capture Volume")))
+ if (!(value2 = pa_str_strip_suffix(value, " Volume")))
+ value2 = pa_xstrdup(value);
+ if (index > 0) {
+ char *mix = pa_sprintf_malloc("'%s',%d", value2, index);
+ pa_xfree(value2);
+ return mix;
+ }
+ return value2;
+}
+
+/* Get the volume identifier */
+static pa_alsa_ucm_volume *ucm_get_mixer_volume(
+ pa_alsa_ucm_device *device,
+ const char *mprop,
+ const char *cprop,
+ const char *cid,
+ const char *masterid,
+ const char *mastertype)
+{
+ pa_alsa_ucm_volume *vol;
+ char *mixer_elem;
+
+ mixer_elem = ucm_get_mixer_id(device, mprop, cprop, cid);
+ if (mixer_elem == NULL)
+ return NULL;
+ vol = pa_xnew0(pa_alsa_ucm_volume, 1);
+ if (vol == NULL) {
+ pa_xfree(mixer_elem);
+ return NULL;
+ }
+ vol->mixer_elem = mixer_elem;
+ vol->master_elem = pa_xstrdup(pa_proplist_gets(device->proplist, masterid));
+ vol->master_type = pa_xstrdup(pa_proplist_gets(device->proplist, mastertype));
+ return vol;
+}
+
+/* Get the ALSA mixer device for the UCM device */
+static const char *get_mixer_device(pa_alsa_ucm_device *dev, bool is_sink)
+{
+ const char *dev_name;
+
+ if (is_sink) {
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE);
+ if (!dev_name)
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE);
+ } else {
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE);
+ if (!dev_name)
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE);
+ }
+ return dev_name;
+}
+
+/* Get the ALSA mixer device for the UCM jack */
+static const char *get_jack_mixer_device(pa_alsa_ucm_device *dev, bool is_sink) {
+ const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_DEVICE);
+ if (!dev_name)
+ return get_mixer_device(dev, is_sink);
+ return dev_name;
+}
+
+/* Create a property list for this ucm device */
+static int ucm_get_device_property(
+ pa_alsa_ucm_device *device,
+ snd_use_case_mgr_t *uc_mgr,
+ pa_alsa_ucm_verb *verb,
+ const char *device_name) {
+
+ const char *value;
+ const char **devices;
+ char *id, *s;
+ int i;
+ int err;
+ uint32_t ui;
+ int n_confdev, n_suppdev;
+ pa_alsa_ucm_volume *vol;
+
+ /* determine the device type */
+ device->type = PA_DEVICE_PORT_TYPE_UNKNOWN;
+ id = s = pa_xstrdup(device_name);
+ while (s && *s && isalpha(*s)) s++;
+ if (s)
+ *s = '\0';
+ for (i = 0; types[i].prefix; i++)
+ if (pa_streq(id, types[i].prefix)) {
+ device->type = types[i].type;
+ break;
+ }
+ pa_xfree(id);
+
+ /* set properties */
+ for (i = 0; item[i].id; i++) {
+ id = pa_sprintf_malloc("%s/%s", item[i].id, device_name);
+ err = snd_use_case_get(uc_mgr, id, &value);
+ pa_xfree(id);
+ if (err < 0)
+ continue;
+
+ pa_log_debug("Got %s for device %s: %s", item[i].id, device_name, value);
+ pa_proplist_sets(device->proplist, item[i].property, value);
+ free((void*)value);
+ }
+
+ /* get direction and channels */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS);
+ if (value) { /* output */
+ /* get channels */
+ if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui))
+ device->playback_channels = ui;
+ else
+ pa_log("UCM playback channels %s for device %s out of range", value, device_name);
+
+ /* get pcm */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK);
+ if (!value) /* take pcm from verb playback default */
+ pa_log("UCM playback device %s fetch pcm failed", device_name);
+ }
+
+ if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK) &&
+ device->playback_channels == 0) {
+ pa_log_info("UCM file does not specify 'PlaybackChannels' "
+ "for device %s, assuming stereo.", device_name);
+ device->playback_channels = 2;
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS);
+ if (value) { /* input */
+ /* get channels */
+ if (pa_atou(value, &ui) == 0 && pa_channels_valid(ui))
+ device->capture_channels = ui;
+ else
+ pa_log("UCM capture channels %s for device %s out of range", value, device_name);
+
+ /* get pcm */
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE);
+ if (!value) /* take pcm from verb capture default */
+ pa_log("UCM capture device %s fetch pcm failed", device_name);
+ }
+
+ if (pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE) &&
+ device->capture_channels == 0) {
+ pa_log_info("UCM file does not specify 'CaptureChannels' "
+ "for device %s, assuming stereo.", device_name);
+ device->capture_channels = 2;
+ }
+
+ /* get rate and priority of device */
+ if (device->playback_channels) { /* sink device */
+ /* get rate */
+ if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_RATE))) {
+ if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) {
+ pa_log_debug("UCM playback device %s rate %d", device_name, ui);
+ device->playback_rate = ui;
+ } else
+ pa_log_debug("UCM playback device %s has bad rate %s", device_name, value);
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY);
+ if (value) {
+ /* get priority from ucm config */
+ if (pa_atou(value, &ui) == 0)
+ device->playback_priority = ui;
+ else
+ pa_log_debug("UCM playback priority %s for device %s error", value, device_name);
+ }
+
+ vol = ucm_get_mixer_volume(device,
+ PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM,
+ PA_ALSA_PROP_UCM_PLAYBACK_VOLUME,
+ "PlaybackVolume",
+ PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM,
+ PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE);
+ if (vol)
+ pa_hashmap_put(device->playback_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
+ }
+
+ if (device->capture_channels) { /* source device */
+ /* get rate */
+ if ((value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_RATE))) {
+ if (pa_atou(value, &ui) == 0 && pa_sample_rate_valid(ui)) {
+ pa_log_debug("UCM capture device %s rate %d", device_name, ui);
+ device->capture_rate = ui;
+ } else
+ pa_log_debug("UCM capture device %s has bad rate %s", device_name, value);
+ }
+
+ value = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_CAPTURE_PRIORITY);
+ if (value) {
+ /* get priority from ucm config */
+ if (pa_atou(value, &ui) == 0)
+ device->capture_priority = ui;
+ else
+ pa_log_debug("UCM capture priority %s for device %s error", value, device_name);
+ }
+
+ vol = ucm_get_mixer_volume(device,
+ PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM,
+ PA_ALSA_PROP_UCM_CAPTURE_VOLUME,
+ "CaptureVolume",
+ PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM,
+ PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE);
+ if (vol)
+ pa_hashmap_put(device->capture_volumes, pa_xstrdup(pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME)), vol);
+ }
+
+ if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device) || PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
+ /* get priority from static table */
+ for (i = 0; dev_info[i].id; i++) {
+ if (strcasecmp(dev_info[i].id, device_name) == 0) {
+ PA_UCM_DEVICE_PRIORITY_SET(device, dev_info[i].priority);
+ break;
+ }
+ }
+ }
+
+ if (PA_UCM_PLAYBACK_PRIORITY_UNSET(device)) {
+ /* fall through to default priority */
+ device->playback_priority = 100;
+ }
+
+ if (PA_UCM_CAPTURE_PRIORITY_UNSET(device)) {
+ /* fall through to default priority */
+ device->capture_priority = 100;
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", device_name);
+ n_confdev = snd_use_case_get_list(uc_mgr, id, &devices);
+ pa_xfree(id);
+
+ if (n_confdev <= 0)
+ pa_log_debug("No %s for device %s", "_conflictingdevs", device_name);
+ else {
+ device->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ucm_add_devices_to_idxset(device->conflicting_devices, device, verb->devices, devices, n_confdev);
+ snd_use_case_free_list(devices, n_confdev);
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_supporteddevs", device_name);
+ n_suppdev = snd_use_case_get_list(uc_mgr, id, &devices);
+ pa_xfree(id);
+
+ if (n_suppdev <= 0)
+ pa_log_debug("No %s for device %s", "_supporteddevs", device_name);
+ else {
+ device->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ ucm_add_devices_to_idxset(device->supported_devices, device, verb->devices, devices, n_suppdev);
+ snd_use_case_free_list(devices, n_suppdev);
+ }
+
+ return 0;
+};
+
+/* Create a property list for this ucm modifier */
+static int ucm_get_modifier_property(pa_alsa_ucm_modifier *modifier, snd_use_case_mgr_t *uc_mgr, const char *modifier_name) {
+ const char *value;
+ char *id;
+ int i;
+
+ for (i = 0; item[i].id; i++) {
+ int err;
+
+ id = pa_sprintf_malloc("=%s/%s", item[i].id, modifier_name);
+ err = snd_use_case_get(uc_mgr, id, &value);
+ pa_xfree(id);
+ if (err < 0)
+ continue;
+
+ pa_log_debug("Got %s for modifier %s: %s", item[i].id, modifier_name, value);
+ pa_proplist_sets(modifier->proplist, item[i].property, value);
+ free((void*)value);
+ }
+
+ id = pa_sprintf_malloc("%s/%s", "_conflictingdevs", modifier_name);
+ modifier->n_confdev = snd_use_case_get_list(uc_mgr, id, &modifier->conflicting_devices);
+ pa_xfree(id);
+ if (modifier->n_confdev < 0)
+ pa_log_debug("No %s for modifier %s", "_conflictingdevs", modifier_name);
+
+ id = pa_sprintf_malloc("%s/%s", "_supporteddevs", modifier_name);
+ modifier->n_suppdev = snd_use_case_get_list(uc_mgr, id, &modifier->supported_devices);
+ pa_xfree(id);
+ if (modifier->n_suppdev < 0)
+ pa_log_debug("No %s for modifier %s", "_supporteddevs", modifier_name);
+
+ return 0;
+};
+
+/* Create a list of devices for this verb */
+static int ucm_get_devices(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) {
+ const char **dev_list;
+ int num_dev, i;
+
+ num_dev = snd_use_case_get_list(uc_mgr, "_devices", &dev_list);
+ if (num_dev < 0)
+ return num_dev;
+
+ for (i = 0; i < num_dev; i += 2) {
+ pa_alsa_ucm_device *d = pa_xnew0(pa_alsa_ucm_device, 1);
+
+ d->proplist = pa_proplist_new();
+ pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(dev_list[i]));
+ pa_proplist_sets(d->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(dev_list[i + 1]));
+ d->ucm_ports = pa_dynarray_new(NULL);
+ d->hw_mute_jacks = pa_dynarray_new(NULL);
+ d->available = PA_AVAILABLE_UNKNOWN;
+
+ d->playback_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+ (pa_free_cb_t) ucm_volume_free);
+ d->capture_volumes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree,
+ (pa_free_cb_t) ucm_volume_free);
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_device, verb->devices, d);
+ }
+
+ snd_use_case_free_list(dev_list, num_dev);
+
+ return 0;
+};
+
+static int ucm_get_modifiers(pa_alsa_ucm_verb *verb, snd_use_case_mgr_t *uc_mgr) {
+ const char **mod_list;
+ int num_mod, i;
+
+ num_mod = snd_use_case_get_list(uc_mgr, "_modifiers", &mod_list);
+ if (num_mod < 0)
+ return num_mod;
+
+ for (i = 0; i < num_mod; i += 2) {
+ pa_alsa_ucm_modifier *m;
+
+ if (!mod_list[i]) {
+ pa_log_warn("Got a modifier with a null name. Skipping.");
+ continue;
+ }
+
+ m = pa_xnew0(pa_alsa_ucm_modifier, 1);
+ m->proplist = pa_proplist_new();
+
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_NAME, mod_list[i]);
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(mod_list[i + 1]));
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_modifier, verb->modifiers, m);
+ }
+
+ snd_use_case_free_list(mod_list, num_mod);
+
+ return 0;
+};
+
+static void add_role_to_device(pa_alsa_ucm_device *dev, const char *dev_name, const char *role_name, const char *role) {
+ const char *cur = pa_proplist_gets(dev->proplist, role_name);
+
+ if (!cur)
+ pa_proplist_sets(dev->proplist, role_name, role);
+ else if (!pa_str_in_list_spaces(cur, role)) { /* does not exist */
+ char *value = pa_sprintf_malloc("%s %s", cur, role);
+
+ pa_proplist_sets(dev->proplist, role_name, value);
+ pa_xfree(value);
+ }
+
+ pa_log_info("Add role %s to device %s(%s), result %s", role, dev_name, role_name, pa_proplist_gets(dev->proplist,
+ role_name));
+}
+
+static void add_media_role(const char *name, pa_alsa_ucm_device *list, const char *role_name, const char *role, bool is_sink) {
+ pa_alsa_ucm_device *d;
+
+ PA_LLIST_FOREACH(d, list) {
+ const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ if (pa_streq(dev_name, name)) {
+ const char *sink = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SINK);
+ const char *source = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+ if (is_sink && sink)
+ add_role_to_device(d, dev_name, role_name, role);
+ else if (!is_sink && source)
+ add_role_to_device(d, dev_name, role_name, role);
+ break;
+ }
+ }
+}
+
+static char *modifier_name_to_role(const char *mod_name, bool *is_sink) {
+ char *sub = NULL, *tmp;
+
+ *is_sink = false;
+
+ if (pa_startswith(mod_name, "Play")) {
+ *is_sink = true;
+ sub = pa_xstrdup(mod_name + 4);
+ } else if (pa_startswith(mod_name, "Capture"))
+ sub = pa_xstrdup(mod_name + 7);
+
+ if (!sub || !*sub) {
+ pa_xfree(sub);
+ pa_log_warn("Can't match media roles for modifier %s", mod_name);
+ return NULL;
+ }
+
+ tmp = sub;
+
+ do {
+ *tmp = tolower(*tmp);
+ } while (*(++tmp));
+
+ return sub;
+}
+
+static void ucm_set_media_roles(pa_alsa_ucm_modifier *modifier, pa_alsa_ucm_device *list, const char *mod_name) {
+ int i;
+ bool is_sink = false;
+ char *sub = NULL;
+ const char *role_name;
+
+ sub = modifier_name_to_role(mod_name, &is_sink);
+ if (!sub)
+ return;
+
+ modifier->action_direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+ modifier->media_role = sub;
+
+ role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES;
+ for (i = 0; i < modifier->n_suppdev; i++) {
+ /* if modifier has no specific pcm, we add role intent to its supported devices */
+ if (!pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SINK) &&
+ !pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_SOURCE))
+ add_media_role(modifier->supported_devices[i], list, role_name, sub, is_sink);
+ }
+}
+
+static void append_lost_relationship(pa_alsa_ucm_device *dev) {
+ uint32_t idx;
+ pa_alsa_ucm_device *d;
+
+ if (dev->conflicting_devices) {
+ PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) {
+ if (!d->conflicting_devices)
+ d->conflicting_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (pa_idxset_put(d->conflicting_devices, dev, NULL) == 0)
+ pa_log_warn("Add lost conflicting device %s to %s",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME),
+ pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME));
+ }
+ }
+
+ if (dev->supported_devices) {
+ PA_IDXSET_FOREACH(d, dev->supported_devices, idx) {
+ if (!d->supported_devices)
+ d->supported_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ if (pa_idxset_put(d->supported_devices, dev, NULL) == 0)
+ pa_log_warn("Add lost supported device %s to %s",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME),
+ pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME));
+ }
+ }
+}
+
+int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) {
+ char *card_name;
+ const char **verb_list, *value;
+ int num_verbs, i, err = 0;
+
+ /* support multiple card instances, address card directly by index */
+ card_name = pa_sprintf_malloc("hw:%i", card_index);
+ if (card_name == NULL)
+ return -PA_ALSA_ERR_UNSPECIFIED;
+ err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
+ if (err < 0) {
+ /* fallback longname: is UCM available for this card ? */
+ pa_xfree(card_name);
+ err = snd_card_get_name(card_index, &card_name);
+ if (err < 0) {
+ pa_log("Card can't get card_name from card_index %d", card_index);
+ err = -PA_ALSA_ERR_UNSPECIFIED;
+ goto name_fail;
+ }
+
+ err = snd_use_case_mgr_open(&ucm->ucm_mgr, card_name);
+ if (err < 0) {
+ pa_log_info("UCM not available for card %s", card_name);
+ err = -PA_ALSA_ERR_UCM_OPEN;
+ goto ucm_mgr_fail;
+ }
+ }
+
+ err = snd_use_case_get(ucm->ucm_mgr, "=Linked", &value);
+ if (err >= 0) {
+ if (strcasecmp(value, "true") == 0 || strcasecmp(value, "1") == 0) {
+ free((void *)value);
+ pa_log_info("Empty (linked) UCM for card %s", card_name);
+ err = -PA_ALSA_ERR_UCM_LINKED;
+ goto ucm_verb_fail;
+ }
+ free((void *)value);
+ }
+
+ pa_log_info("UCM available for card %s", card_name);
+
+ if (snd_use_case_get(ucm->ucm_mgr, "_alibpref", &value) == 0) {
+ if (value[0]) {
+ ucm->alib_prefix = pa_xstrdup(value);
+ pa_log_debug("UCM _alibpref=%s", ucm->alib_prefix);
+ }
+ free((void *)value);
+ }
+
+ /* get a list of all UCM verbs (profiles) for this card */
+ num_verbs = snd_use_case_verb_list(ucm->ucm_mgr, &verb_list);
+ if (num_verbs < 0) {
+ pa_log("UCM verb list not found for %s", card_name);
+ err = -PA_ALSA_ERR_UNSPECIFIED;
+ goto ucm_verb_fail;
+ }
+
+ /* get the properties of each UCM verb */
+ for (i = 0; i < num_verbs; i += 2) {
+ pa_alsa_ucm_verb *verb;
+
+ /* Get devices and modifiers for each verb */
+ err = pa_alsa_ucm_get_verb(ucm->ucm_mgr, verb_list[i], verb_list[i+1], &verb);
+ if (err < 0) {
+ pa_log("Failed to get the verb %s", verb_list[i]);
+ continue;
+ }
+
+ PA_LLIST_PREPEND(pa_alsa_ucm_verb, ucm->verbs, verb);
+ }
+
+ if (!ucm->verbs) {
+ pa_log("No UCM verb is valid for %s", card_name);
+ err = -PA_ALSA_ERR_UCM_NO_VERB;
+ }
+
+ snd_use_case_free_list(verb_list, num_verbs);
+
+ucm_verb_fail:
+ if (err < 0) {
+ snd_use_case_mgr_close(ucm->ucm_mgr);
+ ucm->ucm_mgr = NULL;
+ }
+
+ucm_mgr_fail:
+ pa_xfree(card_name);
+
+name_fail:
+ return err;
+}
+
+int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) {
+ pa_alsa_ucm_device *d;
+ pa_alsa_ucm_modifier *mod;
+ pa_alsa_ucm_verb *verb;
+ char *value;
+ unsigned ui;
+ int err = 0;
+
+ *p_verb = NULL;
+ pa_log_info("Set UCM verb to %s", verb_name);
+ err = snd_use_case_set(uc_mgr, "_verb", verb_name);
+ if (err < 0)
+ return err;
+
+ verb = pa_xnew0(pa_alsa_ucm_verb, 1);
+ verb->proplist = pa_proplist_new();
+
+ pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_NAME, pa_strnull(verb_name));
+ pa_proplist_sets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION, pa_strna(verb_desc));
+
+ value = ucm_verb_value(uc_mgr, verb_name, "Priority");
+ if (value && !pa_atou(value, &ui))
+ verb->priority = ui > 10000 ? 10000 : ui;
+ free(value);
+
+ err = ucm_get_devices(verb, uc_mgr);
+ if (err < 0)
+ pa_log("No UCM devices for verb %s", verb_name);
+
+ err = ucm_get_modifiers(verb, uc_mgr);
+ if (err < 0)
+ pa_log("No UCM modifiers for verb %s", verb_name);
+
+ PA_LLIST_FOREACH(d, verb->devices) {
+ const char *dev_name = pa_proplist_gets(d->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ /* Devices properties */
+ ucm_get_device_property(d, uc_mgr, verb, dev_name);
+ }
+ /* make conflicting or supported device mutual */
+ PA_LLIST_FOREACH(d, verb->devices)
+ append_lost_relationship(d);
+
+ PA_LLIST_FOREACH(mod, verb->modifiers) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ /* Modifier properties */
+ ucm_get_modifier_property(mod, uc_mgr, mod_name);
+
+ /* Set PA_PROP_DEVICE_INTENDED_ROLES property to devices */
+ pa_log_debug("Set media roles for verb %s, modifier %s", verb_name, mod_name);
+ ucm_set_media_roles(mod, verb->devices, mod_name);
+ }
+
+ *p_verb = verb;
+ return 0;
+}
+
+static int pa_alsa_ucm_device_cmp(const void *a, const void *b) {
+ const pa_alsa_ucm_device *d1 = *(pa_alsa_ucm_device **)a;
+ const pa_alsa_ucm_device *d2 = *(pa_alsa_ucm_device **)b;
+
+ return strcmp(pa_proplist_gets(d1->proplist, PA_ALSA_PROP_UCM_NAME), pa_proplist_gets(d2->proplist, PA_ALSA_PROP_UCM_NAME));
+}
+
+static void set_eld_devices(pa_hashmap *hash)
+{
+ pa_device_port *port;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_device *dev;
+ const char *eld_mixer_device_name;
+ void *state;
+ int idx, eld_device;
+
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+ eld_mixer_device_name = NULL;
+ eld_device = -1;
+ PA_DYNARRAY_FOREACH(dev, data->devices, idx) {
+ if (dev->eld_device >= 0 && dev->eld_mixer_device_name) {
+ if (eld_device >= 0 && eld_device != dev->eld_device) {
+ pa_log_error("The ELD device is already set!");
+ } else if (eld_mixer_device_name && pa_streq(dev->eld_mixer_device_name, eld_mixer_device_name)) {
+ pa_log_error("The ELD mixer device is already set (%s, %s)!", dev->eld_mixer_device_name, dev->eld_mixer_device_name);
+ } else {
+ eld_mixer_device_name = dev->eld_mixer_device_name;
+ eld_device = dev->eld_device;
+ }
+ }
+ }
+ data->eld_device = eld_device;
+ if (data->eld_mixer_device_name)
+ pa_xfree(data->eld_mixer_device_name);
+ data->eld_mixer_device_name = pa_xstrdup(eld_mixer_device_name);
+ }
+}
+
+static void update_mixer_paths(pa_hashmap *ports, const char *profile) {
+ pa_device_port *port;
+ pa_alsa_ucm_port_data *data;
+ void *state;
+
+ /* select volume controls on ports */
+ PA_HASHMAP_FOREACH(port, ports, state) {
+ pa_log_info("Updating mixer path for %s: %s", profile, port->name);
+ data = PA_DEVICE_PORT_DATA(port);
+ data->path = pa_hashmap_get(data->paths, profile);
+ }
+}
+
+static void probe_volumes(pa_hashmap *hash, bool is_sink, snd_pcm_t *pcm_handle, pa_hashmap *mixers, bool ignore_dB) {
+ pa_device_port *port;
+ pa_alsa_path *path;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_device *dev;
+ snd_mixer_t *mixer_handle;
+ const char *profile, *mdev, *mdev2;
+ void *state, *state2;
+ int idx;
+
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+
+ mdev = NULL;
+ PA_DYNARRAY_FOREACH(dev, data->devices, idx) {
+ mdev2 = get_mixer_device(dev, is_sink);
+ if (mdev && mdev2 && !pa_streq(mdev, mdev2)) {
+ pa_log_error("Two mixer device names found ('%s', '%s'), using s/w volume", mdev, mdev2);
+ goto fail;
+ }
+ if (mdev2)
+ mdev = mdev2;
+ }
+
+ if (mdev == NULL || !(mixer_handle = pa_alsa_open_mixer_by_name(mixers, mdev, true))) {
+ pa_log_error("Failed to find a working mixer device (%s).", mdev);
+ goto fail;
+ }
+
+ PA_HASHMAP_FOREACH_KV(profile, path, data->paths, state2) {
+ if (pa_alsa_path_probe(path, NULL, mixer_handle, ignore_dB) < 0) {
+ pa_log_warn("Could not probe path: %s, using s/w volume", path->name);
+ pa_hashmap_remove(data->paths, profile);
+ } else if (!path->has_volume && !path->has_mute) {
+ pa_log_warn("Path %s is not a volume or mute control", path->name);
+ pa_hashmap_remove(data->paths, profile);
+ } else
+ pa_log_debug("Set up h/w %s using '%s' for %s:%s", path->has_volume ? "volume" : "mute",
+ path->name, profile, port->name);
+ }
+ }
+
+ return;
+
+fail:
+ /* We could not probe the paths we created. Free them and revert to software volumes. */
+ PA_HASHMAP_FOREACH(port, hash, state) {
+ data = PA_DEVICE_PORT_DATA(port);
+ pa_hashmap_remove_all(data->paths);
+ }
+}
+
+static void ucm_add_port_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_alsa_ucm_device **pdevices,
+ int num,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_device_port *port;
+ int i;
+ unsigned priority;
+ double prio2;
+ char *name, *desc;
+ const char *dev_name;
+ const char *direction;
+ const char *profile;
+ pa_alsa_ucm_device *sorted[num], *dev;
+ pa_alsa_ucm_port_data *data;
+ pa_alsa_ucm_volume *vol;
+ pa_alsa_jack *jack, *jack2;
+ pa_device_port_type_t type, type2;
+ void *state;
+
+ for (i = 0; i < num; i++)
+ sorted[i] = pdevices[i];
+
+ /* Sort by alphabetical order so as to have a deterministic naming scheme
+ * for combination ports */
+ qsort(&sorted[0], num, sizeof(pa_alsa_ucm_device *), pa_alsa_ucm_device_cmp);
+
+ dev = sorted[0];
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ name = pa_sprintf_malloc("%s%s", is_sink ? PA_UCM_PRE_TAG_OUTPUT : PA_UCM_PRE_TAG_INPUT, dev_name);
+ desc = num == 1 ? pa_xstrdup(pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_DESCRIPTION))
+ : pa_sprintf_malloc("Combination port for %s", dev_name);
+
+ priority = is_sink ? dev->playback_priority : dev->capture_priority;
+ prio2 = (priority == 0 ? 0 : 1.0/priority);
+ jack = ucm_get_jack(context->ucm, dev);
+ type = dev->type;
+
+ for (i = 1; i < num; i++) {
+ char *tmp;
+
+ dev = sorted[i];
+ dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ tmp = pa_sprintf_malloc("%s+%s", name, dev_name);
+ pa_xfree(name);
+ name = tmp;
+
+ tmp = pa_sprintf_malloc("%s,%s", desc, dev_name);
+ pa_xfree(desc);
+ desc = tmp;
+
+ priority = is_sink ? dev->playback_priority : dev->capture_priority;
+ if (priority != 0 && prio2 > 0)
+ prio2 += 1.0/priority;
+
+ jack2 = ucm_get_jack(context->ucm, dev);
+ if (jack2) {
+ if (jack && jack != jack2)
+ pa_log_warn("Multiple jacks per combined device '%s': '%s' '%s'", name, jack->name, jack2->name);
+ jack = jack2;
+ }
+
+ type2 = dev->type;
+ if (type2 != PA_DEVICE_PORT_TYPE_UNKNOWN) {
+ if (type != PA_DEVICE_PORT_TYPE_UNKNOWN && type != type2)
+ pa_log_warn("Multiple device types per combined device '%s': %d %d", name, type, type2);
+ type = type2;
+ }
+ }
+
+ /* Make combination ports always have lower priority, and use the formula
+ 1/p = 1/p1 + 1/p2 + ... 1/pn.
+ This way, the result will always be less than the individual components,
+ yet higher components will lead to higher result. */
+
+ if (num > 1)
+ priority = prio2 > 0 ? 1.0/prio2 : 0;
+
+ port = pa_hashmap_get(ports, name);
+ if (!port) {
+ pa_device_port_new_data port_data;
+
+ pa_device_port_new_data_init(&port_data);
+ pa_device_port_new_data_set_name(&port_data, name);
+ pa_device_port_new_data_set_description(&port_data, desc);
+ pa_device_port_new_data_set_type(&port_data, type);
+ pa_device_port_new_data_set_direction(&port_data, is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT);
+ if (jack)
+ pa_device_port_new_data_set_availability_group(&port_data, jack->name);
+
+ port = pa_device_port_new(core, &port_data, sizeof(pa_alsa_ucm_port_data));
+ pa_device_port_new_data_done(&port_data);
+
+ data = PA_DEVICE_PORT_DATA(port);
+ ucm_port_data_init(data, context->ucm, port, pdevices, num);
+ port->impl_free = ucm_port_data_free;
+
+ pa_hashmap_put(ports, port->name, port);
+ pa_log_debug("Add port %s: %s", port->name, port->description);
+
+ if (num == 1) {
+ /* To keep things simple and not worry about stacking controls, we only support hardware volumes on non-combination
+ * ports. */
+ PA_HASHMAP_FOREACH_KV(profile, vol, is_sink ? dev->playback_volumes : dev->capture_volumes, state) {
+ pa_alsa_path *path = pa_alsa_path_synthesize(vol->mixer_elem,
+ is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT);
+
+ if (!path)
+ pa_log_warn("Failed to set up volume control: %s", vol->mixer_elem);
+ else {
+ if (vol->master_elem) {
+ pa_alsa_element *e = pa_alsa_element_get(path, vol->master_elem, false);
+ e->switch_use = PA_ALSA_SWITCH_MUTE;
+ e->volume_use = PA_ALSA_VOLUME_MERGE;
+ }
+
+ pa_hashmap_put(data->paths, pa_xstrdup(profile), path);
+
+ /* Add path also to already created empty path set */
+ dev = sorted[0];
+ if (is_sink)
+ pa_hashmap_put(dev->playback_mapping->output_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
+ else
+ pa_hashmap_put(dev->capture_mapping->input_path_set->paths, pa_xstrdup(vol->mixer_elem), path);
+ }
+ }
+ }
+ }
+
+ port->priority = priority;
+
+ pa_xfree(name);
+ pa_xfree(desc);
+
+ direction = is_sink ? "output" : "input";
+ pa_log_debug("Port %s direction %s, priority %d", port->name, direction, priority);
+
+ if (cp) {
+ pa_log_debug("Adding profile %s to port %s.", cp->name, port->name);
+ pa_hashmap_put(port->profiles, cp->name, cp);
+ }
+
+ if (hash) {
+ pa_hashmap_put(hash, port->name, port);
+ }
+}
+
+static int ucm_port_contains(const char *port_name, const char *dev_name, bool is_sink) {
+ int ret = 0;
+ const char *r;
+ const char *state = NULL;
+ size_t len;
+
+ if (!port_name || !dev_name)
+ return false;
+
+ port_name += is_sink ? strlen(PA_UCM_PRE_TAG_OUTPUT) : strlen(PA_UCM_PRE_TAG_INPUT);
+
+ while ((r = pa_split_in_place(port_name, "+", &len, &state))) {
+ if (strlen(dev_name) == len && !strncmp(r, dev_name, len)) {
+ ret = 1;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int ucm_check_conformance(
+ pa_alsa_ucm_mapping_context *context,
+ pa_alsa_ucm_device **pdevices,
+ int dev_num,
+ pa_alsa_ucm_device *dev) {
+
+ uint32_t idx;
+ pa_alsa_ucm_device *d;
+ int i;
+
+ pa_assert(dev);
+
+ pa_log_debug("Check device %s conformance with %d other devices",
+ pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME), dev_num);
+ if (dev_num == 0) {
+ pa_log_debug("First device in combination, number 1");
+ return 1;
+ }
+
+ if (dev->conflicting_devices) { /* the device defines conflicting devices */
+ PA_IDXSET_FOREACH(d, dev->conflicting_devices, idx) {
+ for (i = 0; i < dev_num; i++) {
+ if (pdevices[i] == d) {
+ pa_log_debug("Conflicting device found");
+ return 0;
+ }
+ }
+ }
+ } else if (dev->supported_devices) { /* the device defines supported devices */
+ for (i = 0; i < dev_num; i++) {
+ if (!ucm_device_exists(dev->supported_devices, pdevices[i])) {
+ pa_log_debug("Supported device not found");
+ return 0;
+ }
+ }
+ } else { /* not support any other devices */
+ pa_log_debug("Not support any other devices");
+ return 0;
+ }
+
+ pa_log_debug("Device added to combination, number %d", dev_num + 1);
+ return 1;
+}
+
+static inline pa_alsa_ucm_device *get_next_device(pa_idxset *idxset, uint32_t *idx) {
+ pa_alsa_ucm_device *dev;
+
+ if (*idx == PA_IDXSET_INVALID)
+ dev = pa_idxset_first(idxset, idx);
+ else
+ dev = pa_idxset_next(idxset, idx);
+
+ return dev;
+}
+
+static void ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_alsa_ucm_device **pdevices,
+ int dev_num,
+ uint32_t map_index,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_alsa_ucm_device *dev;
+ uint32_t idx = map_index;
+
+ if ((dev = get_next_device(context->ucm_devices, &idx)) == NULL)
+ return;
+
+ /* check if device at map_index can combine with existing devices combination */
+ if (ucm_check_conformance(context, pdevices, dev_num, dev)) {
+ /* add device at map_index to devices combination */
+ pdevices[dev_num] = dev;
+ /* add current devices combination as a new port */
+ ucm_add_port_combination(hash, context, is_sink, pdevices, dev_num + 1, ports, cp, core);
+ /* try more elements combination */
+ ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num + 1, idx, ports, cp, core);
+ }
+
+ /* try other device with current elements number */
+ ucm_add_ports_combination(hash, context, is_sink, pdevices, dev_num, idx, ports, cp, core);
+}
+
+static char* merge_roles(const char *cur, const char *add) {
+ char *r, *ret;
+ const char *state = NULL;
+
+ if (add == NULL)
+ return pa_xstrdup(cur);
+ else if (cur == NULL)
+ return pa_xstrdup(add);
+
+ ret = pa_xstrdup(cur);
+
+ while ((r = pa_split_spaces(add, &state))) {
+ char *value;
+
+ if (!pa_str_in_list_spaces(ret, r))
+ value = pa_sprintf_malloc("%s %s", ret, r);
+ else {
+ pa_xfree(r);
+ continue;
+ }
+
+ pa_xfree(ret);
+ ret = value;
+ pa_xfree(r);
+ }
+
+ return ret;
+}
+
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *p,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+
+ pa_alsa_ucm_device **pdevices;
+
+ pa_assert(context->ucm_devices);
+
+ if (pa_idxset_size(context->ucm_devices) > 0) {
+ pdevices = pa_xnew(pa_alsa_ucm_device *, pa_idxset_size(context->ucm_devices));
+ ucm_add_ports_combination(p, context, is_sink, pdevices, 0, PA_IDXSET_INVALID, ports, cp, core);
+ pa_xfree(pdevices);
+ }
+
+ /* ELD devices */
+ set_eld_devices(ports);
+}
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **p,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB) {
+
+ uint32_t idx;
+ char *merged_roles;
+ const char *role_name = is_sink ? PA_ALSA_PROP_UCM_PLAYBACK_ROLES : PA_ALSA_PROP_UCM_CAPTURE_ROLES;
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ char *tmp;
+
+ pa_assert(p);
+ pa_assert(*p);
+
+ /* add ports first */
+ pa_alsa_ucm_add_ports_combination(*p, context, is_sink, card->ports, NULL, card->core);
+
+ /* now set up volume paths if any */
+ probe_volumes(*p, is_sink, pcm_handle, context->ucm->mixers, ignore_dB);
+
+ /* probe_volumes() removes per-profile paths from ports if probing them
+ * fails. The path for the current profile is cached in
+ * pa_alsa_ucm_port_data.path, which is not cleared by probe_volumes() if
+ * the path gets removed, so we have to call update_mixer_paths() here to
+ * unset the cached path if needed. */
+ if (card->card.active_profile_index < card->card.n_profiles)
+ update_mixer_paths(*p, card->card.profiles[card->card.active_profile_index]->name);
+
+ /* then set property PA_PROP_DEVICE_INTENDED_ROLES */
+ merged_roles = pa_xstrdup(pa_proplist_gets(proplist, PA_PROP_DEVICE_INTENDED_ROLES));
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ const char *roles = pa_proplist_gets(dev->proplist, role_name);
+ tmp = merge_roles(merged_roles, roles);
+ pa_xfree(merged_roles);
+ merged_roles = tmp;
+ }
+
+ if (context->ucm_modifiers)
+ PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) {
+ tmp = merge_roles(merged_roles, mod->media_role);
+ pa_xfree(merged_roles);
+ merged_roles = tmp;
+ }
+
+ if (merged_roles)
+ pa_proplist_sets(proplist, PA_PROP_DEVICE_INTENDED_ROLES, merged_roles);
+
+ pa_log_info("ALSA device %s roles: %s", pa_proplist_gets(proplist, PA_PROP_DEVICE_STRING), pa_strnull(merged_roles));
+ pa_xfree(merged_roles);
+}
+
+/* Change UCM verb and device to match selected card profile */
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
+ int ret = 0;
+ const char *profile;
+ pa_alsa_ucm_verb *verb;
+
+ if (new_profile == old_profile)
+ return ret;
+ else if (new_profile == NULL || old_profile == NULL)
+ profile = new_profile ? new_profile : SND_USE_CASE_VERB_INACTIVE;
+ else if (!pa_streq(new_profile, old_profile))
+ profile = new_profile;
+ else
+ return ret;
+
+ /* change verb */
+ pa_log_info("Set UCM verb to %s", profile);
+ if ((ret = snd_use_case_set(ucm->ucm_mgr, "_verb", profile)) < 0) {
+ pa_log("Failed to set verb %s: %s", profile, snd_strerror(ret));
+ }
+
+ /* find active verb */
+ ucm->active_verb = NULL;
+ PA_LLIST_FOREACH(verb, ucm->verbs) {
+ const char *verb_name;
+ verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
+ if (pa_streq(verb_name, profile)) {
+ ucm->active_verb = verb;
+ break;
+ }
+ }
+
+ update_mixer_paths(card->ports, profile);
+ return ret;
+}
+
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) {
+ int i;
+ int ret = 0;
+ pa_alsa_ucm_config *ucm;
+ const char **enable_devs;
+ int enable_num = 0;
+ uint32_t idx;
+ pa_alsa_ucm_device *dev;
+
+ pa_assert(context && context->ucm);
+
+ ucm = context->ucm;
+ pa_assert(ucm->ucm_mgr);
+
+ enable_devs = pa_xnew(const char *, pa_idxset_size(context->ucm_devices));
+
+ /* first disable then enable */
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ const char *dev_name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ if (ucm_port_contains(port->name, dev_name, is_sink))
+ enable_devs[enable_num++] = dev_name;
+ else {
+ pa_log_debug("Disable ucm device %s", dev_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_disdev", dev_name) > 0) {
+ pa_log("Failed to disable ucm device %s", dev_name);
+ ret = -1;
+ break;
+ }
+ }
+ }
+
+ for (i = 0; i < enable_num; i++) {
+ pa_log_debug("Enable ucm device %s", enable_devs[i]);
+ if (snd_use_case_set(ucm->ucm_mgr, "_enadev", enable_devs[i]) < 0) {
+ pa_log("Failed to enable ucm device %s", enable_devs[i]);
+ ret = -1;
+ break;
+ }
+ }
+
+ pa_xfree(enable_devs);
+
+ return ret;
+}
+
+static void ucm_add_mapping(pa_alsa_profile *p, pa_alsa_mapping *m) {
+
+ pa_alsa_path_set *ps;
+
+ /* create empty path set for the future path additions */
+ ps = pa_xnew0(pa_alsa_path_set, 1);
+ ps->direction = m->direction;
+ ps->paths = pa_hashmap_new_full(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func, pa_xfree,
+ (pa_free_cb_t) pa_alsa_path_free);
+
+ switch (m->direction) {
+ case PA_ALSA_DIRECTION_ANY:
+ pa_idxset_put(p->output_mappings, m, NULL);
+ pa_idxset_put(p->input_mappings, m, NULL);
+ m->output_path_set = ps;
+ m->input_path_set = ps;
+ break;
+ case PA_ALSA_DIRECTION_OUTPUT:
+ pa_idxset_put(p->output_mappings, m, NULL);
+ m->output_path_set = ps;
+ break;
+ case PA_ALSA_DIRECTION_INPUT:
+ pa_idxset_put(p->input_mappings, m, NULL);
+ m->input_path_set = ps;
+ break;
+ }
+}
+
+static void alsa_mapping_add_ucm_device(pa_alsa_mapping *m, pa_alsa_ucm_device *device) {
+ char *cur_desc;
+ const char *new_desc, *mdev;
+ bool is_sink = m->direction == PA_ALSA_DIRECTION_OUTPUT;
+
+ pa_idxset_put(m->ucm_context.ucm_devices, device, NULL);
+
+ new_desc = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ cur_desc = m->description;
+ if (cur_desc)
+ m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc);
+ else
+ m->description = pa_xstrdup(new_desc);
+ pa_xfree(cur_desc);
+
+ /* walk around null case */
+ m->description = m->description ? m->description : pa_xstrdup("");
+
+ /* save mapping to ucm device */
+ if (is_sink)
+ device->playback_mapping = m;
+ else
+ device->capture_mapping = m;
+
+ mdev = get_mixer_device(device, is_sink);
+ if (mdev)
+ pa_proplist_sets(m->proplist, "alsa.mixer_device", mdev);
+}
+
+static void alsa_mapping_add_ucm_modifier(pa_alsa_mapping *m, pa_alsa_ucm_modifier *modifier) {
+ char *cur_desc;
+ const char *new_desc, *mod_name, *channel_str;
+ uint32_t channels = 0;
+
+ pa_idxset_put(m->ucm_context.ucm_modifiers, modifier, NULL);
+
+ new_desc = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ cur_desc = m->description;
+ if (cur_desc)
+ m->description = pa_sprintf_malloc("%s + %s", cur_desc, new_desc);
+ else
+ m->description = pa_xstrdup(new_desc);
+ pa_xfree(cur_desc);
+
+ m->description = m->description ? m->description : pa_xstrdup("");
+
+ /* Modifier sinks should not be routed to by default */
+ m->priority = 0;
+
+ mod_name = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_NAME);
+ pa_proplist_sets(m->proplist, PA_ALSA_PROP_UCM_MODIFIER, mod_name);
+
+ /* save mapping to ucm modifier */
+ if (m->direction == PA_ALSA_DIRECTION_OUTPUT) {
+ modifier->playback_mapping = m;
+ channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS);
+ } else {
+ modifier->capture_mapping = m;
+ channel_str = pa_proplist_gets(modifier->proplist, PA_ALSA_PROP_UCM_CAPTURE_CHANNELS);
+ }
+
+ if (channel_str) {
+ /* FIXME: channel_str is unsanitized input from the UCM configuration,
+ * we should do proper error handling instead of asserting.
+ * https://bugs.freedesktop.org/show_bug.cgi?id=71823 */
+ pa_assert_se(pa_atou(channel_str, &channels) == 0 && pa_channels_valid(channels));
+ pa_log_debug("Got channel count %" PRIu32 " for modifier", channels);
+ }
+
+ if (channels)
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+ else
+ pa_channel_map_init(&m->channel_map);
+}
+
+static pa_alsa_mapping* ucm_alsa_mapping_get(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps, const char *verb_name, const char *device_str, bool is_sink) {
+ pa_alsa_mapping *m;
+ char *mapping_name;
+ size_t ucm_alibpref_len = 0;
+
+ /* find private alsa-lib's configuration device prefix */
+
+ if (ucm->alib_prefix && pa_startswith(device_str, ucm->alib_prefix))
+ ucm_alibpref_len = strlen(ucm->alib_prefix);
+
+ mapping_name = pa_sprintf_malloc("Mapping %s: %s: %s", verb_name, device_str + ucm_alibpref_len, is_sink ? "sink" : "source");
+
+ m = pa_alsa_mapping_get(ps, mapping_name);
+
+ if (!m)
+ pa_log("No mapping for %s", mapping_name);
+
+ pa_xfree(mapping_name);
+
+ return m;
+}
+
+static int ucm_create_mapping_direction(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_device *device,
+ const char *verb_name,
+ const char *device_name,
+ const char *device_str,
+ bool is_sink) {
+
+ pa_alsa_mapping *m;
+ unsigned priority, rate, channels;
+
+ m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink);
+
+ if (!m)
+ return -1;
+
+ pa_log_debug("UCM mapping: %s dev %s", m->name, device_name);
+
+ priority = is_sink ? device->playback_priority : device->capture_priority;
+ rate = is_sink ? device->playback_rate : device->capture_rate;
+ channels = is_sink ? device->playback_channels : device->capture_channels;
+
+ if (!m->ucm_context.ucm_devices) { /* new mapping */
+ m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm = ucm;
+ m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+
+ m->device_strings = pa_xnew0(char*, 2);
+ m->device_strings[0] = pa_xstrdup(device_str);
+ m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT;
+
+ ucm_add_mapping(p, m);
+ if (rate)
+ m->sample_spec.rate = rate;
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+ }
+
+ /* mapping priority is the highest one of ucm devices */
+ if (priority > m->priority)
+ m->priority = priority;
+
+ /* mapping channels is the lowest one of ucm devices */
+ if (channels < m->channel_map.channels)
+ pa_channel_map_init_extend(&m->channel_map, channels, PA_CHANNEL_MAP_ALSA);
+
+ alsa_mapping_add_ucm_device(m, device);
+
+ return 0;
+}
+
+static int ucm_create_mapping_for_modifier(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_modifier *modifier,
+ const char *verb_name,
+ const char *mod_name,
+ const char *device_str,
+ bool is_sink) {
+
+ pa_alsa_mapping *m;
+
+ m = ucm_alsa_mapping_get(ucm, ps, verb_name, device_str, is_sink);
+
+ if (!m)
+ return -1;
+
+ pa_log_info("UCM mapping: %s modifier %s", m->name, mod_name);
+
+ if (!m->ucm_context.ucm_devices && !m->ucm_context.ucm_modifiers) { /* new mapping */
+ m->ucm_context.ucm_devices = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ m->ucm_context.ucm = ucm;
+ m->ucm_context.direction = is_sink ? PA_DIRECTION_OUTPUT : PA_DIRECTION_INPUT;
+
+ m->device_strings = pa_xnew0(char*, 2);
+ m->device_strings[0] = pa_xstrdup(device_str);
+ m->direction = is_sink ? PA_ALSA_DIRECTION_OUTPUT : PA_ALSA_DIRECTION_INPUT;
+ /* Modifier sinks should not be routed to by default */
+ m->priority = 0;
+
+ ucm_add_mapping(p, m);
+ } else if (!m->ucm_context.ucm_modifiers) /* share pcm with device */
+ m->ucm_context.ucm_modifiers = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ alsa_mapping_add_ucm_modifier(m, modifier);
+
+ return 0;
+}
+
+static int ucm_create_mapping(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_profile *p,
+ pa_alsa_ucm_device *device,
+ const char *verb_name,
+ const char *device_name,
+ const char *sink,
+ const char *source) {
+
+ int ret = 0;
+
+ if (!sink && !source) {
+ pa_log("No sink and source at %s: %s", verb_name, device_name);
+ return -1;
+ }
+
+ if (sink)
+ ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, sink, true);
+ if (ret == 0 && source)
+ ret = ucm_create_mapping_direction(ucm, ps, p, device, verb_name, device_name, source, false);
+
+ return ret;
+}
+
+static pa_alsa_jack* ucm_get_jack(pa_alsa_ucm_config *ucm, pa_alsa_ucm_device *device) {
+ pa_alsa_jack *j;
+ const char *device_name;
+ const char *jack_control;
+ const char *mixer_device_name;
+ char *name;
+
+ pa_assert(ucm);
+ pa_assert(device);
+
+ device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ jack_control = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_JACK_CONTROL);
+ if (jack_control) {
+#if SND_LIB_VERSION >= 0x10201
+ snd_ctl_elem_id_t *ctl;
+ int err, index;
+ snd_ctl_elem_id_alloca(&ctl);
+ err = snd_use_case_parse_ctl_elem_id(ctl, "JackControl", jack_control);
+ if (err < 0)
+ return NULL;
+ jack_control = snd_ctl_elem_id_get_name(ctl);
+ index = snd_ctl_elem_id_get_index(ctl);
+ if (index > 0) {
+ pa_log("[%s] Invalid JackControl index value: \"%s\",%d", device_name, jack_control, index);
+ return NULL;
+ }
+#else
+#warning "Upgrade to alsa-lib 1.2.1!"
+#endif
+ if (!pa_endswith(jack_control, " Jack")) {
+ pa_log("[%s] Invalid JackControl value: \"%s\"", device_name, jack_control);
+ return NULL;
+ }
+
+ /* pa_alsa_jack_new() expects a jack name without " Jack" at the
+ * end, so drop the trailing " Jack". */
+ name = pa_xstrndup(jack_control, strlen(jack_control) - 5);
+ } else {
+ /* The jack control hasn't been explicitly configured, fail. */
+ return NULL;
+ }
+
+ PA_LLIST_FOREACH(j, ucm->jacks)
+ if (pa_streq(j->name, name))
+ goto finish;
+
+ mixer_device_name = get_jack_mixer_device(device, true);
+ if (!mixer_device_name)
+ mixer_device_name = get_jack_mixer_device(device, false);
+ if (!mixer_device_name) {
+ pa_log("[%s] No mixer device name for JackControl \"%s\"", device_name, jack_control);
+ j = NULL;
+ goto finish;
+ }
+ j = pa_alsa_jack_new(NULL, mixer_device_name, name, 0);
+ PA_LLIST_PREPEND(pa_alsa_jack, ucm->jacks, j);
+
+finish:
+ pa_xfree(name);
+
+ return j;
+}
+
+static int ucm_create_profile(
+ pa_alsa_ucm_config *ucm,
+ pa_alsa_profile_set *ps,
+ pa_alsa_ucm_verb *verb,
+ const char *verb_name,
+ const char *verb_desc) {
+
+ pa_alsa_profile *p;
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ int i = 0;
+ const char *name, *sink, *source;
+ unsigned int priority;
+
+ pa_assert(ps);
+
+ if (pa_hashmap_get(ps->profiles, verb_name)) {
+ pa_log("Verb %s already exists", verb_name);
+ return -1;
+ }
+
+ p = pa_xnew0(pa_alsa_profile, 1);
+ p->profile_set = ps;
+ p->name = pa_xstrdup(verb_name);
+ p->description = pa_xstrdup(verb_desc);
+
+ p->output_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+ p->input_mappings = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
+
+ p->supported = true;
+ pa_hashmap_put(ps->profiles, p->name, p);
+
+ /* TODO: get profile priority from policy management */
+ priority = verb->priority;
+
+ if (priority == 0) {
+ char *verb_cmp, *c;
+ c = verb_cmp = pa_xstrdup(verb_name);
+ while (*c) {
+ if (*c == '_') *c = ' ';
+ c++;
+ }
+ for (i = 0; verb_info[i].id; i++) {
+ if (strcasecmp(verb_info[i].id, verb_cmp) == 0) {
+ priority = verb_info[i].priority;
+ break;
+ }
+ }
+ pa_xfree(verb_cmp);
+ }
+
+ p->priority = priority;
+
+ PA_LLIST_FOREACH(dev, verb->devices) {
+ pa_alsa_jack *jack;
+ const char *jack_hw_mute;
+
+ name = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ sink = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SINK);
+ source = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+ ucm_create_mapping(ucm, ps, p, dev, verb_name, name, sink, source);
+
+ jack = ucm_get_jack(ucm, dev);
+ if (jack)
+ device_set_jack(dev, jack);
+
+ /* JackHWMute contains a list of device names. Each listed device must
+ * be associated with the jack object that we just created. */
+ jack_hw_mute = pa_proplist_gets(dev->proplist, PA_ALSA_PROP_UCM_JACK_HW_MUTE);
+ if (jack_hw_mute && !jack) {
+ pa_log("[%s] JackHWMute set, but JackControl is missing", name);
+ jack_hw_mute = NULL;
+ }
+ if (jack_hw_mute) {
+ char *hw_mute_device_name;
+ const char *state = NULL;
+
+ while ((hw_mute_device_name = ucm_split_devnames(jack_hw_mute, &state))) {
+ pa_alsa_ucm_verb *verb2;
+ bool device_found = false;
+
+ /* Search the referenced device from all verbs. If there are
+ * multiple verbs that have a device with this name, we add the
+ * hw mute association to each of those devices. */
+ PA_LLIST_FOREACH(verb2, ucm->verbs) {
+ pa_alsa_ucm_device *hw_mute_device;
+
+ hw_mute_device = verb_find_device(verb2, hw_mute_device_name);
+ if (hw_mute_device) {
+ device_found = true;
+ device_add_hw_mute_jack(hw_mute_device, jack);
+ }
+ }
+
+ if (!device_found)
+ pa_log("[%s] JackHWMute references an unknown device: %s", name, hw_mute_device_name);
+
+ pa_xfree(hw_mute_device_name);
+ }
+ }
+ }
+
+ /* Now find modifiers that have their own PlaybackPCM and create
+ * separate sinks for them. */
+ PA_LLIST_FOREACH(mod, verb->modifiers) {
+ name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ sink = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SINK);
+ source = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_SOURCE);
+
+ if (sink)
+ ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, sink, true);
+ else if (source)
+ ucm_create_mapping_for_modifier(ucm, ps, p, mod, verb_name, name, source, false);
+ }
+
+ pa_alsa_profile_dump(p);
+
+ return 0;
+}
+
+static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm)
+{
+ pa_alsa_ucm_mapping_context *context = &m->ucm_context;
+ pa_alsa_ucm_device *dev;
+ uint32_t idx;
+ char *mdev, *alib_prefix;
+ snd_pcm_info_t *info;
+ int pcm_card, pcm_device;
+
+ snd_pcm_info_alloca(&info);
+ if (snd_pcm_info(pcm, info) < 0)
+ return;
+
+ if ((pcm_card = snd_pcm_info_get_card(info)) < 0)
+ return;
+ if ((pcm_device = snd_pcm_info_get_device(info)) < 0)
+ return;
+
+ alib_prefix = context->ucm->alib_prefix;
+
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ mdev = pa_sprintf_malloc("%shw:%i", alib_prefix ? alib_prefix : "", pcm_card);
+ if (mdev == NULL)
+ continue;
+ dev->eld_mixer_device_name = mdev;
+ dev->eld_device = pcm_device;
+ }
+}
+
+static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) {
+ snd_pcm_t* pcm;
+ pa_sample_spec try_ss = ucm->default_sample_spec;
+ pa_channel_map try_map;
+ snd_pcm_uframes_t try_period_size, try_buffer_size;
+ bool exact_channels = m->channel_map.channels > 0;
+
+ if (exact_channels) {
+ try_map = m->channel_map;
+ try_ss.channels = try_map.channels;
+ } else
+ pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_ALSA);
+
+ try_period_size =
+ pa_usec_to_bytes(ucm->default_fragment_size_msec * PA_USEC_PER_MSEC, &try_ss) /
+ pa_frame_size(&try_ss);
+ try_buffer_size = ucm->default_n_fragments * try_period_size;
+
+ pcm = pa_alsa_open_by_device_string(m->device_strings[0], NULL, &try_ss,
+ &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, exact_channels);
+
+ if (pcm) {
+ if (!exact_channels)
+ m->channel_map = try_map;
+ mapping_init_eld(m, pcm);
+ }
+
+ return pcm;
+}
+
+static void profile_finalize_probing(pa_alsa_profile *p) {
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (p->supported)
+ m->supported++;
+
+ if (!m->output_pcm)
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm);
+ pa_alsa_close(&m->output_pcm);
+ }
+
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (p->supported)
+ m->supported++;
+
+ if (!m->input_pcm)
+ continue;
+
+ pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm);
+ pa_alsa_close(&m->input_pcm);
+ }
+}
+
+static void ucm_mapping_jack_probe(pa_alsa_mapping *m, pa_hashmap *mixers) {
+ snd_mixer_t *mixer_handle;
+ pa_alsa_ucm_mapping_context *context = &m->ucm_context;
+ pa_alsa_ucm_device *dev;
+ uint32_t idx;
+
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ bool has_control;
+
+ if (!dev->jack || !dev->jack->mixer_device_name)
+ continue;
+
+ mixer_handle = pa_alsa_open_mixer_by_name(mixers, dev->jack->mixer_device_name, true);
+ if (!mixer_handle) {
+ pa_log_error("Unable to determine open mixer device '%s' for jack %s", dev->jack->mixer_device_name, dev->jack->name);
+ continue;
+ }
+
+ has_control = pa_alsa_mixer_find_card(mixer_handle, &dev->jack->alsa_id, 0) != NULL;
+ pa_alsa_jack_set_has_control(dev->jack, has_control);
+ pa_log_info("UCM jack %s has_control=%d", dev->jack->name, dev->jack->has_control);
+ }
+}
+
+static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set *ps) {
+ void *state;
+ pa_alsa_profile *p;
+ pa_alsa_mapping *m;
+ uint32_t idx;
+
+ PA_HASHMAP_FOREACH(p, ps->profiles, state) {
+ /* change verb */
+ pa_log_info("Set ucm verb to %s", p->name);
+
+ if ((snd_use_case_set(ucm->ucm_mgr, "_verb", p->name)) < 0) {
+ pa_log("Failed to set verb %s", p->name);
+ p->supported = false;
+ continue;
+ }
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx) {
+ if (PA_UCM_IS_MODIFIER_MAPPING(m)) {
+ /* Skip jack probing on modifier PCMs since we expect this to
+ * only be controlled on the main device/verb PCM. */
+ continue;
+ }
+
+ m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK);
+ if (!m->output_pcm) {
+ p->supported = false;
+ break;
+ }
+ }
+
+ if (p->supported) {
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx) {
+ if (PA_UCM_IS_MODIFIER_MAPPING(m)) {
+ /* Skip jack probing on modifier PCMs since we expect this to
+ * only be controlled on the main device/verb PCM. */
+ continue;
+ }
+
+ m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE);
+ if (!m->input_pcm) {
+ p->supported = false;
+ break;
+ }
+ }
+ }
+
+ if (!p->supported) {
+ profile_finalize_probing(p);
+ continue;
+ }
+
+ pa_log_debug("Profile %s supported.", p->name);
+
+ PA_IDXSET_FOREACH(m, p->output_mappings, idx)
+ if (!PA_UCM_IS_MODIFIER_MAPPING(m))
+ ucm_mapping_jack_probe(m, ucm->mixers);
+
+ PA_IDXSET_FOREACH(m, p->input_mappings, idx)
+ if (!PA_UCM_IS_MODIFIER_MAPPING(m))
+ ucm_mapping_jack_probe(m, ucm->mixers);
+
+ profile_finalize_probing(p);
+ }
+
+ /* restore ucm state */
+ snd_use_case_set(ucm->ucm_mgr, "_verb", SND_USE_CASE_VERB_INACTIVE);
+
+ pa_alsa_profile_set_drop_unsupported(ps);
+}
+
+pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) {
+ pa_alsa_ucm_verb *verb;
+ pa_alsa_profile_set *ps;
+
+ ps = pa_xnew0(pa_alsa_profile_set, 1);
+ ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) pa_alsa_mapping_free);
+ ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL,
+ (pa_free_cb_t) pa_alsa_profile_free);
+ ps->decibel_fixes = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+
+ /* create a profile for each verb */
+ PA_LLIST_FOREACH(verb, ucm->verbs) {
+ const char *verb_name;
+ const char *verb_desc;
+
+ verb_name = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_NAME);
+ verb_desc = pa_proplist_gets(verb->proplist, PA_ALSA_PROP_UCM_DESCRIPTION);
+ if (verb_name == NULL) {
+ pa_log("Verb with no name");
+ continue;
+ }
+
+ ucm_create_profile(ucm, ps, verb, verb_name, verb_desc);
+ }
+
+ ucm_probe_profile_set(ucm, ps);
+ ps->probed = true;
+
+ return ps;
+}
+
+static void free_verb(pa_alsa_ucm_verb *verb) {
+ pa_alsa_ucm_device *di, *dn;
+ pa_alsa_ucm_modifier *mi, *mn;
+
+ PA_LLIST_FOREACH_SAFE(di, dn, verb->devices) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_device, verb->devices, di);
+
+ if (di->hw_mute_jacks)
+ pa_dynarray_free(di->hw_mute_jacks);
+
+ if (di->ucm_ports)
+ pa_dynarray_free(di->ucm_ports);
+
+ if (di->playback_volumes)
+ pa_hashmap_free(di->playback_volumes);
+ if (di->capture_volumes)
+ pa_hashmap_free(di->capture_volumes);
+
+ pa_proplist_free(di->proplist);
+
+ if (di->conflicting_devices)
+ pa_idxset_free(di->conflicting_devices, NULL);
+ if (di->supported_devices)
+ pa_idxset_free(di->supported_devices, NULL);
+
+ pa_xfree(di->eld_mixer_device_name);
+
+ pa_xfree(di);
+ }
+
+ PA_LLIST_FOREACH_SAFE(mi, mn, verb->modifiers) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_modifier, verb->modifiers, mi);
+ pa_proplist_free(mi->proplist);
+ if (mi->n_suppdev > 0)
+ snd_use_case_free_list(mi->supported_devices, mi->n_suppdev);
+ if (mi->n_confdev > 0)
+ snd_use_case_free_list(mi->conflicting_devices, mi->n_confdev);
+ pa_xfree(mi->media_role);
+ pa_xfree(mi);
+ }
+ pa_proplist_free(verb->proplist);
+ pa_xfree(verb);
+}
+
+static pa_alsa_ucm_device *verb_find_device(pa_alsa_ucm_verb *verb, const char *device_name) {
+ pa_alsa_ucm_device *device;
+
+ pa_assert(verb);
+ pa_assert(device_name);
+
+ PA_LLIST_FOREACH(device, verb->devices) {
+ const char *name;
+
+ name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME);
+ if (pa_streq(name, device_name))
+ return device;
+ }
+
+ return NULL;
+}
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) {
+ pa_alsa_ucm_verb *vi, *vn;
+ pa_alsa_jack *ji, *jn;
+
+ PA_LLIST_FOREACH_SAFE(vi, vn, ucm->verbs) {
+ PA_LLIST_REMOVE(pa_alsa_ucm_verb, ucm->verbs, vi);
+ free_verb(vi);
+ }
+ PA_LLIST_FOREACH_SAFE(ji, jn, ucm->jacks) {
+ PA_LLIST_REMOVE(pa_alsa_jack, ucm->jacks, ji);
+ pa_alsa_jack_free(ji);
+ }
+ if (ucm->ucm_mgr) {
+ snd_use_case_mgr_close(ucm->ucm_mgr);
+ ucm->ucm_mgr = NULL;
+ }
+ pa_xfree(ucm->alib_prefix);
+ ucm->alib_prefix = NULL;
+}
+
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) {
+ pa_alsa_ucm_device *dev;
+ pa_alsa_ucm_modifier *mod;
+ uint32_t idx;
+
+ if (context->ucm_devices) {
+ /* clear ucm device pointer to mapping */
+ PA_IDXSET_FOREACH(dev, context->ucm_devices, idx) {
+ if (context->direction == PA_DIRECTION_OUTPUT)
+ dev->playback_mapping = NULL;
+ else
+ dev->capture_mapping = NULL;
+ }
+
+ pa_idxset_free(context->ucm_devices, NULL);
+ }
+
+ if (context->ucm_modifiers) {
+ PA_IDXSET_FOREACH(mod, context->ucm_modifiers, idx) {
+ if (context->direction == PA_DIRECTION_OUTPUT)
+ mod->playback_mapping = NULL;
+ else
+ mod->capture_mapping = NULL;
+ }
+
+ pa_idxset_free(context->ucm_modifiers, NULL);
+ }
+}
+
+/* Enable the modifier when the first stream with matched role starts */
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+ pa_alsa_ucm_modifier *mod;
+
+ if (!ucm->active_verb)
+ return;
+
+ PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) {
+ if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) {
+ if (mod->enabled_counter == 0) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ pa_log_info("Enable ucm modifier %s", mod_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_enamod", mod_name) < 0) {
+ pa_log("Failed to enable ucm modifier %s", mod_name);
+ }
+ }
+
+ mod->enabled_counter++;
+ break;
+ }
+ }
+}
+
+/* Disable the modifier when the last stream with matched role ends */
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+ pa_alsa_ucm_modifier *mod;
+
+ if (!ucm->active_verb)
+ return;
+
+ PA_LLIST_FOREACH(mod, ucm->active_verb->modifiers) {
+ if ((mod->action_direction == dir) && (pa_streq(mod->media_role, role))) {
+
+ mod->enabled_counter--;
+ if (mod->enabled_counter == 0) {
+ const char *mod_name = pa_proplist_gets(mod->proplist, PA_ALSA_PROP_UCM_NAME);
+
+ pa_log_info("Disable ucm modifier %s", mod_name);
+ if (snd_use_case_set(ucm->ucm_mgr, "_dismod", mod_name) < 0) {
+ pa_log("Failed to disable ucm modifier %s", mod_name);
+ }
+ }
+
+ break;
+ }
+ }
+}
+
+static void device_add_ucm_port(pa_alsa_ucm_device *device, pa_alsa_ucm_port_data *port) {
+ pa_assert(device);
+ pa_assert(port);
+
+ pa_dynarray_append(device->ucm_ports, port);
+}
+
+static void device_set_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) {
+ pa_assert(device);
+ pa_assert(jack);
+
+ device->jack = jack;
+ pa_alsa_jack_add_ucm_device(jack, device);
+
+ pa_alsa_ucm_device_update_available(device);
+}
+
+static void device_add_hw_mute_jack(pa_alsa_ucm_device *device, pa_alsa_jack *jack) {
+ pa_assert(device);
+ pa_assert(jack);
+
+ pa_dynarray_append(device->hw_mute_jacks, jack);
+ pa_alsa_jack_add_ucm_hw_mute_device(jack, device);
+
+ pa_alsa_ucm_device_update_available(device);
+}
+
+static void device_set_available(pa_alsa_ucm_device *device, pa_available_t available) {
+ pa_alsa_ucm_port_data *port;
+ unsigned idx;
+
+ pa_assert(device);
+
+ if (available == device->available)
+ return;
+
+ device->available = available;
+
+ PA_DYNARRAY_FOREACH(port, device->ucm_ports, idx)
+ ucm_port_update_available(port);
+}
+
+void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device) {
+ pa_available_t available = PA_AVAILABLE_UNKNOWN;
+ pa_alsa_jack *jack;
+ unsigned idx;
+
+ pa_assert(device);
+
+ if (device->jack && device->jack->has_control)
+ available = device->jack->plugged_in ? PA_AVAILABLE_YES : PA_AVAILABLE_NO;
+
+ PA_DYNARRAY_FOREACH(jack, device->hw_mute_jacks, idx) {
+ if (jack->plugged_in) {
+ available = PA_AVAILABLE_NO;
+ break;
+ }
+ }
+
+ device_set_available(device, available);
+}
+
+static void ucm_port_data_init(pa_alsa_ucm_port_data *port, pa_alsa_ucm_config *ucm, pa_device_port *core_port,
+ pa_alsa_ucm_device **devices, unsigned n_devices) {
+ unsigned i;
+
+ pa_assert(ucm);
+ pa_assert(core_port);
+ pa_assert(devices);
+
+ port->ucm = ucm;
+ port->core_port = core_port;
+ port->devices = pa_dynarray_new(NULL);
+ port->eld_device = -1;
+
+ for (i = 0; i < n_devices; i++) {
+ pa_dynarray_append(port->devices, devices[i]);
+ device_add_ucm_port(devices[i], port);
+ }
+
+ port->paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, pa_xfree, NULL);
+
+ ucm_port_update_available(port);
+}
+
+static void ucm_port_data_free(pa_device_port *port) {
+ pa_alsa_ucm_port_data *ucm_port;
+
+ pa_assert(port);
+
+ ucm_port = PA_DEVICE_PORT_DATA(port);
+
+ if (ucm_port->devices)
+ pa_dynarray_free(ucm_port->devices);
+
+ if (ucm_port->paths)
+ pa_hashmap_free(ucm_port->paths);
+
+ pa_xfree(ucm_port->eld_mixer_device_name);
+}
+
+static void ucm_port_update_available(pa_alsa_ucm_port_data *port) {
+ pa_alsa_ucm_device *device;
+ unsigned idx;
+ pa_available_t available = PA_AVAILABLE_YES;
+
+ pa_assert(port);
+
+ PA_DYNARRAY_FOREACH(device, port->devices, idx) {
+ if (device->available == PA_AVAILABLE_UNKNOWN)
+ available = PA_AVAILABLE_UNKNOWN;
+ else if (device->available == PA_AVAILABLE_NO) {
+ available = PA_AVAILABLE_NO;
+ break;
+ }
+ }
+
+ pa_device_port_set_available(port->core_port, available);
+}
+
+#else /* HAVE_ALSA_UCM */
+
+/* Dummy functions for systems without UCM support */
+
+int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index) {
+ pa_log_info("UCM not available.");
+ return -1;
+}
+
+pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map) {
+ return NULL;
+}
+
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile) {
+ return -1;
+}
+
+int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb) {
+ return -1;
+}
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **hash,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB) {
+}
+
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core) {
+}
+
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink) {
+ return -1;
+}
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm) {
+}
+
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context) {
+}
+
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+}
+
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir) {
+}
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h
new file mode 100644
index 0000000..696209e
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-ucm.h
@@ -0,0 +1,301 @@
+#ifndef fooalsaucmhfoo
+#define fooalsaucmhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2011 Wolfson Microelectronics PLC
+ Author Margarita Olaya <magi@slimlogic.co.uk>
+ Copyright 2012 Feng Wei <wei.feng@freescale.com>, Freescale Ltd.
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifdef HAVE_ALSA_UCM
+#include <alsa/use-case.h>
+#else
+typedef void snd_use_case_mgr_t;
+#endif
+
+#include "compat.h"
+
+#include "alsa-mixer.h"
+
+/** For devices: List of verbs, devices or modifiers available */
+#define PA_ALSA_PROP_UCM_NAME "alsa.ucm.name"
+
+/** For devices: List of supported devices per verb*/
+#define PA_ALSA_PROP_UCM_DESCRIPTION "alsa.ucm.description"
+
+/** For devices: Playback device name e.g PlaybackPCM */
+#define PA_ALSA_PROP_UCM_SINK "alsa.ucm.sink"
+
+/** For devices: Capture device name e.g CapturePCM*/
+#define PA_ALSA_PROP_UCM_SOURCE "alsa.ucm.source"
+
+/** For devices: Playback roles */
+#define PA_ALSA_PROP_UCM_PLAYBACK_ROLES "alsa.ucm.playback.roles"
+
+/** For devices: Playback control device name */
+#define PA_ALSA_PROP_UCM_PLAYBACK_CTL_DEVICE "alsa.ucm.playback.ctldev"
+
+/** For devices: Playback control volume ID string. e.g PlaybackVolume */
+#define PA_ALSA_PROP_UCM_PLAYBACK_VOLUME "alsa.ucm.playback.volume"
+
+/** For devices: Playback switch e.g PlaybackSwitch */
+#define PA_ALSA_PROP_UCM_PLAYBACK_SWITCH "alsa.ucm.playback.switch"
+
+/** For devices: Playback mixer device name */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MIXER_DEVICE "alsa.ucm.playback.mixer.device"
+
+/** For devices: Playback mixer identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MIXER_ELEM "alsa.ucm.playback.mixer.element"
+
+/** For devices: Playback mixer master identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ELEM "alsa.ucm.playback.master.element"
+
+/** For devices: Playback mixer master type */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE "alsa.ucm.playback.master.type"
+
+/** For devices: Playback mixer master identifier */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_ID "alsa.ucm.playback.master.id"
+
+/** For devices: Playback mixer master type */
+#define PA_ALSA_PROP_UCM_PLAYBACK_MASTER_TYPE "alsa.ucm.playback.master.type"
+
+/** For devices: Playback priority */
+#define PA_ALSA_PROP_UCM_PLAYBACK_PRIORITY "alsa.ucm.playback.priority"
+
+/** For devices: Playback rate */
+#define PA_ALSA_PROP_UCM_PLAYBACK_RATE "alsa.ucm.playback.rate"
+
+/** For devices: Playback channels */
+#define PA_ALSA_PROP_UCM_PLAYBACK_CHANNELS "alsa.ucm.playback.channels"
+
+/** For devices: Capture roles */
+#define PA_ALSA_PROP_UCM_CAPTURE_ROLES "alsa.ucm.capture.roles"
+
+/** For devices: Capture control device name */
+#define PA_ALSA_PROP_UCM_CAPTURE_CTL_DEVICE "alsa.ucm.capture.ctldev"
+
+/** For devices: Capture controls volume ID string. e.g CaptureVolume */
+#define PA_ALSA_PROP_UCM_CAPTURE_VOLUME "alsa.ucm.capture.volume"
+
+/** For devices: Capture switch e.g CaptureSwitch */
+#define PA_ALSA_PROP_UCM_CAPTURE_SWITCH "alsa.ucm.capture.switch"
+
+/** For devices: Capture mixer device name */
+#define PA_ALSA_PROP_UCM_CAPTURE_MIXER_DEVICE "alsa.ucm.capture.mixer.device"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MIXER_ELEM "alsa.ucm.capture.mixer.element"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_ELEM "alsa.ucm.capture.master.element"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE "alsa.ucm.capture.master.type"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_ID "alsa.ucm.capture.master.id"
+
+/** For devices: Capture mixer identifier */
+#define PA_ALSA_PROP_UCM_CAPTURE_MASTER_TYPE "alsa.ucm.capture.master.type"
+
+/** For devices: Capture priority */
+#define PA_ALSA_PROP_UCM_CAPTURE_PRIORITY "alsa.ucm.capture.priority"
+
+/** For devices: Capture rate */
+#define PA_ALSA_PROP_UCM_CAPTURE_RATE "alsa.ucm.capture.rate"
+
+/** For devices: Capture channels */
+#define PA_ALSA_PROP_UCM_CAPTURE_CHANNELS "alsa.ucm.capture.channels"
+
+/** For devices: Quality of Service */
+#define PA_ALSA_PROP_UCM_QOS "alsa.ucm.qos"
+
+/** For devices: The modifier (if any) that this device corresponds to */
+#define PA_ALSA_PROP_UCM_MODIFIER "alsa.ucm.modifier"
+
+/* Corresponds to the "JackCTL" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_DEVICE "alsa.ucm.jack_device"
+
+/* Corresponds to the "JackControl" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_CONTROL "alsa.ucm.jack_control"
+
+/* Corresponds to the "JackHWMute" UCM value. */
+#define PA_ALSA_PROP_UCM_JACK_HW_MUTE "alsa.ucm.jack_hw_mute"
+
+typedef struct pa_alsa_ucm_verb pa_alsa_ucm_verb;
+typedef struct pa_alsa_ucm_modifier pa_alsa_ucm_modifier;
+typedef struct pa_alsa_ucm_device pa_alsa_ucm_device;
+typedef struct pa_alsa_ucm_config pa_alsa_ucm_config;
+typedef struct pa_alsa_ucm_mapping_context pa_alsa_ucm_mapping_context;
+typedef struct pa_alsa_ucm_port_data pa_alsa_ucm_port_data;
+typedef struct pa_alsa_ucm_volume pa_alsa_ucm_volume;
+
+int pa_alsa_ucm_query_profiles(pa_alsa_ucm_config *ucm, int card_index);
+pa_alsa_profile_set* pa_alsa_ucm_add_profile_set(pa_alsa_ucm_config *ucm, pa_channel_map *default_channel_map);
+int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, const char *new_profile, const char *old_profile);
+
+int pa_alsa_ucm_get_verb(snd_use_case_mgr_t *uc_mgr, const char *verb_name, const char *verb_desc, pa_alsa_ucm_verb **p_verb);
+
+void pa_alsa_ucm_add_ports(
+ pa_hashmap **hash,
+ pa_proplist *proplist,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_card *card,
+ snd_pcm_t *pcm_handle,
+ bool ignore_dB);
+void pa_alsa_ucm_add_ports_combination(
+ pa_hashmap *hash,
+ pa_alsa_ucm_mapping_context *context,
+ bool is_sink,
+ pa_hashmap *ports,
+ pa_card_profile *cp,
+ pa_core *core);
+int pa_alsa_ucm_set_port(pa_alsa_ucm_mapping_context *context, pa_device_port *port, bool is_sink);
+
+void pa_alsa_ucm_free(pa_alsa_ucm_config *ucm);
+void pa_alsa_ucm_mapping_context_free(pa_alsa_ucm_mapping_context *context);
+
+void pa_alsa_ucm_roled_stream_begin(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir);
+void pa_alsa_ucm_roled_stream_end(pa_alsa_ucm_config *ucm, const char *role, pa_direction_t dir);
+
+/* UCM - Use Case Manager is available on some audio cards */
+
+struct pa_alsa_ucm_device {
+ PA_LLIST_FIELDS(pa_alsa_ucm_device);
+
+ pa_proplist *proplist;
+
+ pa_device_port_type_t type;
+
+ unsigned playback_priority;
+ unsigned capture_priority;
+
+ unsigned playback_rate;
+ unsigned capture_rate;
+
+ unsigned playback_channels;
+ unsigned capture_channels;
+
+ /* These may be different per verb, so we store this as a hashmap of verb -> volume_control. We might eventually want to
+ * make this a hashmap of verb -> per-verb-device-properties-struct. */
+ pa_hashmap *playback_volumes;
+ pa_hashmap *capture_volumes;
+
+ pa_alsa_mapping *playback_mapping;
+ pa_alsa_mapping *capture_mapping;
+
+ pa_idxset *conflicting_devices;
+ pa_idxset *supported_devices;
+
+ /* One device may be part of multiple ports, since each device has
+ * a dedicated port, and in addition to that we sometimes generate ports
+ * that represent combinations of devices. */
+ pa_dynarray *ucm_ports; /* struct ucm_port */
+
+ pa_alsa_jack *jack;
+ pa_dynarray *hw_mute_jacks; /* pa_alsa_jack */
+ pa_available_t available;
+
+ char *eld_mixer_device_name;
+ int eld_device;
+};
+
+void pa_alsa_ucm_device_update_available(pa_alsa_ucm_device *device);
+
+struct pa_alsa_ucm_modifier {
+ PA_LLIST_FIELDS(pa_alsa_ucm_modifier);
+
+ pa_proplist *proplist;
+
+ int n_confdev;
+ int n_suppdev;
+
+ const char **conflicting_devices;
+ const char **supported_devices;
+
+ pa_direction_t action_direction;
+
+ char *media_role;
+
+ /* Non-NULL if the modifier has its own PlaybackPCM/CapturePCM */
+ pa_alsa_mapping *playback_mapping;
+ pa_alsa_mapping *capture_mapping;
+
+ /* Count how many role matched streams are running */
+ int enabled_counter;
+};
+
+struct pa_alsa_ucm_verb {
+ PA_LLIST_FIELDS(pa_alsa_ucm_verb);
+
+ pa_proplist *proplist;
+ unsigned priority;
+
+ PA_LLIST_HEAD(pa_alsa_ucm_device, devices);
+ PA_LLIST_HEAD(pa_alsa_ucm_modifier, modifiers);
+};
+
+struct pa_alsa_ucm_config {
+ pa_sample_spec default_sample_spec;
+ pa_channel_map default_channel_map;
+ unsigned default_fragment_size_msec;
+ unsigned default_n_fragments;
+
+ snd_use_case_mgr_t *ucm_mgr;
+ pa_alsa_ucm_verb *active_verb;
+ char *alib_prefix;
+
+ pa_hashmap *mixers;
+ PA_LLIST_HEAD(pa_alsa_ucm_verb, verbs);
+ PA_LLIST_HEAD(pa_alsa_jack, jacks);
+};
+
+struct pa_alsa_ucm_mapping_context {
+ pa_alsa_ucm_config *ucm;
+ pa_direction_t direction;
+
+ pa_idxset *ucm_devices;
+ pa_idxset *ucm_modifiers;
+};
+
+struct pa_alsa_ucm_port_data {
+ pa_alsa_ucm_config *ucm;
+ pa_device_port *core_port;
+
+ /* A single port will be associated with multiple devices if it represents
+ * a combination of devices. */
+ pa_dynarray *devices; /* pa_alsa_ucm_device */
+
+ /* profile name -> pa_alsa_path for volume control */
+ pa_hashmap *paths;
+ /* Current path, set when activating profile */
+ pa_alsa_path *path;
+
+ /* ELD info */
+ char *eld_mixer_device_name;
+ int eld_device; /* PCM device number */
+};
+
+struct pa_alsa_ucm_volume {
+ char *mixer_elem; /* mixer element identifier */
+ char *master_elem; /* master mixer element identifier */
+ char *master_type;
+};
+
+#endif
diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c
new file mode 100644
index 0000000..c76cef3
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-util.c
@@ -0,0 +1,1904 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <alsa/asoundlib.h>
+
+#include "alsa-util.h"
+#include "alsa-mixer.h"
+
+#ifdef HAVE_UDEV
+#include <modules/udev-util.h>
+#endif
+
+static int set_format(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, pa_sample_format_t *f) {
+
+ static const snd_pcm_format_t format_trans[] = {
+ [PA_SAMPLE_U8] = SND_PCM_FORMAT_U8,
+ [PA_SAMPLE_ALAW] = SND_PCM_FORMAT_A_LAW,
+ [PA_SAMPLE_ULAW] = SND_PCM_FORMAT_MU_LAW,
+ [PA_SAMPLE_S16LE] = SND_PCM_FORMAT_S16_LE,
+ [PA_SAMPLE_S16BE] = SND_PCM_FORMAT_S16_BE,
+ [PA_SAMPLE_FLOAT32LE] = SND_PCM_FORMAT_FLOAT_LE,
+ [PA_SAMPLE_FLOAT32BE] = SND_PCM_FORMAT_FLOAT_BE,
+ [PA_SAMPLE_S32LE] = SND_PCM_FORMAT_S32_LE,
+ [PA_SAMPLE_S32BE] = SND_PCM_FORMAT_S32_BE,
+ [PA_SAMPLE_S24LE] = SND_PCM_FORMAT_S24_3LE,
+ [PA_SAMPLE_S24BE] = SND_PCM_FORMAT_S24_3BE,
+ [PA_SAMPLE_S24_32LE] = SND_PCM_FORMAT_S24_LE,
+ [PA_SAMPLE_S24_32BE] = SND_PCM_FORMAT_S24_BE,
+ };
+
+ static const pa_sample_format_t try_order[] = {
+ PA_SAMPLE_FLOAT32NE,
+ PA_SAMPLE_FLOAT32RE,
+ PA_SAMPLE_S32NE,
+ PA_SAMPLE_S32RE,
+ PA_SAMPLE_S24_32NE,
+ PA_SAMPLE_S24_32RE,
+ PA_SAMPLE_S24NE,
+ PA_SAMPLE_S24RE,
+ PA_SAMPLE_S16NE,
+ PA_SAMPLE_S16RE,
+ PA_SAMPLE_ALAW,
+ PA_SAMPLE_ULAW,
+ PA_SAMPLE_U8
+ };
+
+ unsigned i;
+ int ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+ pa_assert(f);
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+
+ if (*f == PA_SAMPLE_FLOAT32BE)
+ *f = PA_SAMPLE_FLOAT32LE;
+ else if (*f == PA_SAMPLE_FLOAT32LE)
+ *f = PA_SAMPLE_FLOAT32BE;
+ else if (*f == PA_SAMPLE_S24BE)
+ *f = PA_SAMPLE_S24LE;
+ else if (*f == PA_SAMPLE_S24LE)
+ *f = PA_SAMPLE_S24BE;
+ else if (*f == PA_SAMPLE_S24_32BE)
+ *f = PA_SAMPLE_S24_32LE;
+ else if (*f == PA_SAMPLE_S24_32LE)
+ *f = PA_SAMPLE_S24_32BE;
+ else if (*f == PA_SAMPLE_S16BE)
+ *f = PA_SAMPLE_S16LE;
+ else if (*f == PA_SAMPLE_S16LE)
+ *f = PA_SAMPLE_S16BE;
+ else if (*f == PA_SAMPLE_S32BE)
+ *f = PA_SAMPLE_S32LE;
+ else if (*f == PA_SAMPLE_S32LE)
+ *f = PA_SAMPLE_S32BE;
+ else
+ goto try_auto;
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+
+try_auto:
+
+ for (i = 0; i < PA_ELEMENTSOF(try_order); i++) {
+ *f = try_order[i];
+
+ if ((ret = snd_pcm_hw_params_set_format(pcm_handle, hwparams, format_trans[*f])) >= 0)
+ return ret;
+
+ pa_log_debug("snd_pcm_hw_params_set_format(%s) failed: %s",
+ snd_pcm_format_description(format_trans[*f]),
+ pa_alsa_strerror(ret));
+ }
+
+ return -1;
+}
+
+static int set_period_size(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, snd_pcm_uframes_t size) {
+ snd_pcm_uframes_t s;
+ int d, ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+
+ s = size;
+ d = 0;
+ if (snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d) < 0) {
+ s = size;
+ d = -1;
+ if (snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d) < 0) {
+ s = size;
+ d = 1;
+ if ((ret = snd_pcm_hw_params_set_period_size_near(pcm_handle, hwparams, &s, &d)) < 0) {
+ pa_log_info("snd_pcm_hw_params_set_period_size_near() failed: %s", pa_alsa_strerror(ret));
+ return ret;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int set_buffer_size(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, snd_pcm_uframes_t size) {
+ int ret;
+
+ pa_assert(pcm_handle);
+ pa_assert(hwparams);
+
+ if ((ret = snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hwparams, &size)) < 0) {
+ pa_log_info("snd_pcm_hw_params_set_buffer_size_near() failed: %s", pa_alsa_strerror(ret));
+ return ret;
+ }
+
+ return 0;
+}
+
+static void check_access(snd_pcm_t *pcm_handle, snd_pcm_hw_params_t *hwparams, bool use_mmap) {
+ if ((use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_INTERLEAVED)) ||
+ !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED))
+ pa_log_error("Weird, PCM claims to support interleaved access, but snd_pcm_hw_params_set_access() failed.");
+
+ if ((use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_NONINTERLEAVED)) ||
+ !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_NONINTERLEAVED))
+ pa_log_debug("PCM seems to support non-interleaved access, but PA doesn't.");
+ else if (use_mmap && !snd_pcm_hw_params_test_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_COMPLEX)) {
+ pa_log_debug("PCM seems to support mmapped complex access, but PA doesn't.");
+ }
+}
+
+/* Set the hardware parameters of the given ALSA device. Returns the
+ * selected fragment settings in *buffer_size and *period_size. Determine
+ * whether mmap and tsched mode can be enabled. */
+int pa_alsa_set_hw_params(
+ snd_pcm_t *pcm_handle,
+ pa_sample_spec *ss,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ int ret = -1;
+ snd_pcm_hw_params_t *hwparams, *hwparams_copy;
+ int dir;
+ snd_pcm_uframes_t _period_size = period_size ? *period_size : 0;
+ snd_pcm_uframes_t _buffer_size = buffer_size ? *buffer_size : 0;
+ bool _use_mmap = use_mmap && *use_mmap;
+ bool _use_tsched = use_tsched && *use_tsched;
+ pa_sample_spec _ss = *ss;
+
+ pa_assert(pcm_handle);
+ pa_assert(ss);
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_hw_params_alloca(&hwparams_copy);
+
+ if ((ret = snd_pcm_hw_params_any(pcm_handle, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if ((ret = snd_pcm_hw_params_set_rate_resample(pcm_handle, hwparams, 0)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_resample() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if (_use_mmap) {
+
+ if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_MMAP_INTERLEAVED) < 0) {
+
+ /* mmap() didn't work, fall back to interleaved */
+
+ if ((ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_access() failed: %s", pa_alsa_strerror(ret));
+ check_access(pcm_handle, hwparams, true);
+ goto finish;
+ }
+
+ _use_mmap = false;
+ }
+
+ } else if ((ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_access() failed: %s", pa_alsa_strerror(ret));
+ check_access(pcm_handle, hwparams, false);
+ goto finish;
+ }
+
+ if (!_use_mmap)
+ _use_tsched = false;
+
+ if (!pa_alsa_pcm_is_hw(pcm_handle))
+ _use_tsched = false;
+
+ /* The PCM pointer is only updated with period granularity */
+ if (snd_pcm_hw_params_is_batch(hwparams)) {
+ bool is_usb = false;
+ const char *id;
+ snd_pcm_info_t* pcm_info;
+ snd_pcm_info_alloca(&pcm_info);
+
+ if (snd_pcm_info(pcm_handle, pcm_info) == 0 &&
+ (id = snd_pcm_info_get_id(pcm_info))) {
+ /* This horrible hack makes sure we don't disable tsched on USB
+ * devices, which have a low enough transfer size for timer-based
+ * scheduling to work. This can go away when the ALSA API supports
+ * querying the block transfer size. */
+ if (pa_streq(id, "USB Audio"))
+ is_usb = true;
+ }
+
+ if (!is_usb) {
+ pa_log_info("Disabling tsched mode since BATCH flag is set");
+ _use_tsched = false;
+ }
+ }
+
+#if (SND_LIB_VERSION >= ((1<<16)|(0<<8)|24)) /* API additions in 1.0.24 */
+ if (_use_tsched) {
+
+ /* try to disable period wakeups if hardware can do so */
+ if (snd_pcm_hw_params_can_disable_period_wakeup(hwparams)) {
+
+ if ((ret = snd_pcm_hw_params_set_period_wakeup(pcm_handle, hwparams, false)) < 0)
+ /* don't bail, keep going with default mode with period wakeups */
+ pa_log_debug("snd_pcm_hw_params_set_period_wakeup() failed: %s", pa_alsa_strerror(ret));
+ else
+ pa_log_info("Trying to disable ALSA period wakeups, using timers only");
+ } else
+ pa_log_info("Cannot disable ALSA period wakeups");
+ }
+#endif
+
+ if ((ret = set_format(pcm_handle, hwparams, &_ss.format)) < 0)
+ goto finish;
+
+ if ((ret = snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, &_ss.rate, NULL)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_near() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ /* We ignore very small sampling rate deviations */
+ if (_ss.rate >= ss->rate*.95 && _ss.rate <= ss->rate*1.05)
+ _ss.rate = ss->rate;
+
+ if (require_exact_channel_number) {
+ if ((ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, _ss.channels)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_channels(%u) failed: %s", _ss.channels, pa_alsa_strerror(ret));
+ goto finish;
+ }
+ } else {
+ unsigned int c = _ss.channels;
+
+ if ((ret = snd_pcm_hw_params_set_channels_near(pcm_handle, hwparams, &c)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_channels_near(%u) failed: %s", _ss.channels, pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ _ss.channels = c;
+ }
+
+ if (_use_tsched && tsched_size > 0) {
+ _buffer_size = (snd_pcm_uframes_t) (((uint64_t) tsched_size * _ss.rate) / ss->rate);
+ _period_size = _buffer_size;
+ } else {
+ _period_size = (snd_pcm_uframes_t) (((uint64_t) _period_size * _ss.rate) / ss->rate);
+ _buffer_size = (snd_pcm_uframes_t) (((uint64_t) _buffer_size * _ss.rate) / ss->rate);
+ }
+
+ if (_buffer_size > 0 || _period_size > 0) {
+ snd_pcm_uframes_t max_frames = 0;
+
+ if ((ret = snd_pcm_hw_params_get_buffer_size_max(hwparams, &max_frames)) < 0)
+ pa_log_warn("snd_pcm_hw_params_get_buffer_size_max() failed: %s", pa_alsa_strerror(ret));
+ else
+ pa_log_debug("Maximum hw buffer size is %lu ms", (long unsigned) (max_frames * PA_MSEC_PER_SEC / _ss.rate));
+
+ /* Some ALSA drivers really don't like if we set the buffer
+ * size first and the number of periods second (which would
+ * make a lot more sense to me). So, try a few combinations
+ * before we give up. */
+
+ if (_buffer_size > 0 && _period_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* First try: set buffer size first, followed by period size */
+ if (set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set buffer size first (to %lu samples), period size second (to %lu samples).", (unsigned long) _buffer_size, (unsigned long) _period_size);
+ goto success;
+ }
+
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+ /* Second try: set period size first, followed by buffer size */
+ if (set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set period size first (to %lu samples), buffer size second (to %lu samples).", (unsigned long) _period_size, (unsigned long) _buffer_size);
+ goto success;
+ }
+ }
+
+ if (_buffer_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* Third try: set only buffer size */
+ if (set_buffer_size(pcm_handle, hwparams_copy, _buffer_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set only buffer size (to %lu samples).", (unsigned long) _buffer_size);
+ goto success;
+ }
+ }
+
+ if (_period_size > 0) {
+ snd_pcm_hw_params_copy(hwparams_copy, hwparams);
+
+ /* Fourth try: set only period size */
+ if (set_period_size(pcm_handle, hwparams_copy, _period_size) >= 0 &&
+ snd_pcm_hw_params(pcm_handle, hwparams_copy) >= 0) {
+ pa_log_debug("Set only period size (to %lu samples).", (unsigned long) _period_size);
+ goto success;
+ }
+ }
+ }
+
+ pa_log_debug("Set neither period nor buffer size.");
+
+ /* Last chance, set nothing */
+ if ((ret = snd_pcm_hw_params(pcm_handle, hwparams)) < 0) {
+ pa_log_info("snd_pcm_hw_params failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+success:
+
+ if (ss->rate != _ss.rate)
+ pa_log_info("Device %s doesn't support %u Hz, changed to %u Hz.", snd_pcm_name(pcm_handle), ss->rate, _ss.rate);
+
+ if (ss->channels != _ss.channels)
+ pa_log_info("Device %s doesn't support %u channels, changed to %u.", snd_pcm_name(pcm_handle), ss->channels, _ss.channels);
+
+ if (ss->format != _ss.format)
+ pa_log_info("Device %s doesn't support sample format %s, changed to %s.", snd_pcm_name(pcm_handle), pa_sample_format_to_string(ss->format), pa_sample_format_to_string(_ss.format));
+
+ if ((ret = snd_pcm_hw_params_current(pcm_handle, hwparams)) < 0) {
+ pa_log_info("snd_pcm_hw_params_current() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+ if ((ret = snd_pcm_hw_params_get_period_size(hwparams, &_period_size, &dir)) < 0 ||
+ (ret = snd_pcm_hw_params_get_buffer_size(hwparams, &_buffer_size)) < 0) {
+ pa_log_info("snd_pcm_hw_params_get_{period|buffer}_size() failed: %s", pa_alsa_strerror(ret));
+ goto finish;
+ }
+
+#if (SND_LIB_VERSION >= ((1<<16)|(0<<8)|24)) /* API additions in 1.0.24 */
+ if (_use_tsched) {
+ unsigned int no_wakeup;
+ /* see if period wakeups were disabled */
+ snd_pcm_hw_params_get_period_wakeup(pcm_handle, hwparams, &no_wakeup);
+ if (no_wakeup == 0)
+ pa_log_info("ALSA period wakeups disabled");
+ else
+ pa_log_info("ALSA period wakeups were not disabled");
+ }
+#endif
+
+ ss->rate = _ss.rate;
+ ss->channels = _ss.channels;
+ ss->format = _ss.format;
+
+ pa_assert(_period_size > 0);
+ pa_assert(_buffer_size > 0);
+
+ if (buffer_size)
+ *buffer_size = _buffer_size;
+
+ if (period_size)
+ *period_size = _period_size;
+
+ if (use_mmap)
+ *use_mmap = _use_mmap;
+
+ if (use_tsched)
+ *use_tsched = _use_tsched;
+
+ ret = 0;
+
+finish:
+
+ return ret;
+}
+
+int pa_alsa_set_sw_params(snd_pcm_t *pcm, snd_pcm_uframes_t avail_min, bool period_event) {
+ snd_pcm_sw_params_t *swparams;
+ snd_pcm_uframes_t boundary;
+ int err;
+
+ pa_assert(pcm);
+
+ snd_pcm_sw_params_alloca(&swparams);
+
+ if ((err = snd_pcm_sw_params_current(pcm, swparams)) < 0) {
+ pa_log_warn("Unable to determine current swparams: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_period_event(pcm, swparams, period_event)) < 0) {
+ pa_log_warn("Unable to disable period event: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_tstamp_mode(pcm, swparams, SND_PCM_TSTAMP_ENABLE)) < 0) {
+ pa_log_warn("Unable to enable time stamping: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_get_boundary(swparams, &boundary)) < 0) {
+ pa_log_warn("Unable to get boundary: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_stop_threshold(pcm, swparams, boundary)) < 0) {
+ pa_log_warn("Unable to set stop threshold: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_start_threshold(pcm, swparams, (snd_pcm_uframes_t) -1)) < 0) {
+ pa_log_warn("Unable to set start threshold: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params_set_avail_min(pcm, swparams, avail_min)) < 0) {
+ pa_log_error("snd_pcm_sw_params_set_avail_min() failed: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ if ((err = snd_pcm_sw_params(pcm, swparams)) < 0) {
+ pa_log_warn("Unable to set sw params: %s", pa_alsa_strerror(err));
+ return err;
+ }
+
+ return 0;
+}
+
+#if 0
+snd_pcm_t *pa_alsa_open_by_device_id_auto(
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping **mapping) {
+
+ char *d;
+ snd_pcm_t *pcm_handle;
+ void *state;
+ pa_alsa_mapping *m;
+
+ pa_assert(dev_id);
+ pa_assert(dev);
+ pa_assert(ss);
+ pa_assert(map);
+ pa_assert(ps);
+
+ /* First we try to find a device string with a superset of the
+ * requested channel map. We iterate through our device table from
+ * top to bottom and take the first that matches. If we didn't
+ * find a working device that way, we iterate backwards, and check
+ * all devices that do not provide a superset of the requested
+ * channel map.*/
+
+ PA_HASHMAP_FOREACH(m, ps->mappings, state) {
+ if (!pa_channel_map_superset(&m->channel_map, map))
+ continue;
+
+ pa_log_debug("Checking for superset %s (%s)", m->name, m->device_strings[0]);
+
+ pcm_handle = pa_alsa_open_by_device_id_mapping(
+ dev_id,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ m);
+
+ if (pcm_handle) {
+ if (mapping)
+ *mapping = m;
+
+ return pcm_handle;
+ }
+ }
+
+ PA_HASHMAP_FOREACH_BACKWARDS(m, ps->mappings, state) {
+ if (pa_channel_map_superset(&m->channel_map, map))
+ continue;
+
+ pa_log_debug("Checking for subset %s (%s)", m->name, m->device_strings[0]);
+
+ pcm_handle = pa_alsa_open_by_device_id_mapping(
+ dev_id,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ m);
+
+ if (pcm_handle) {
+ if (mapping)
+ *mapping = m;
+
+ return pcm_handle;
+ }
+ }
+
+ /* OK, we didn't find any good device, so let's try the raw hw: stuff */
+ d = pa_sprintf_malloc("hw:%s", dev_id);
+ pa_log_debug("Trying %s as last resort...", d);
+ pcm_handle = pa_alsa_open_by_device_string(
+ d,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ false);
+ pa_xfree(d);
+
+ if (pcm_handle && mapping)
+ *mapping = NULL;
+
+ return pcm_handle;
+}
+#endif
+
+snd_pcm_t *pa_alsa_open_by_device_id_mapping(
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ pa_alsa_mapping *m) {
+
+ snd_pcm_t *pcm_handle;
+ pa_sample_spec try_ss;
+ pa_channel_map try_map;
+
+ pa_assert(dev_id);
+ pa_assert(dev);
+ pa_assert(ss);
+ pa_assert(map);
+ pa_assert(m);
+
+ try_ss.channels = m->channel_map.channels;
+ try_ss.rate = ss->rate;
+ try_ss.format = ss->format;
+ try_map = m->channel_map;
+
+ pcm_handle = pa_alsa_open_by_template(
+ m->device_strings,
+ dev_id,
+ dev,
+ &try_ss,
+ &try_map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ pa_channel_map_valid(&m->channel_map) /* Query the channel count if we don't know what we want */);
+
+ if (!pcm_handle)
+ return NULL;
+
+ *ss = try_ss;
+ *map = try_map;
+ pa_assert(map->channels == ss->channels);
+
+ return pcm_handle;
+}
+
+int pa_alsa_close(snd_pcm_t **pcm)
+{
+ int err;
+ pa_assert(pcm);
+ pa_log_info("ALSA device close %p", *pcm);
+ if (*pcm == NULL)
+ return 0;
+ if ((err = snd_pcm_close(*pcm)) < 0) {
+ pa_log_warn("ALSA close failed: %s", snd_strerror(err));
+ }
+ *pcm = NULL;
+ return err;
+}
+
+snd_pcm_t *pa_alsa_open_by_device_string(
+ const char *device,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ int err;
+ char *d;
+ snd_pcm_t *pcm_handle;
+ bool reformat = false;
+
+ pa_assert(device);
+ pa_assert(ss);
+ pa_assert(map);
+
+ d = pa_xstrdup(device);
+
+ for (;;) {
+ pa_log_debug("Trying %s %s SND_PCM_NO_AUTO_FORMAT ...", d, reformat ? "without" : "with");
+
+ if ((err = snd_pcm_open(&pcm_handle, d, mode,
+ SND_PCM_NONBLOCK|
+ SND_PCM_NO_AUTO_RESAMPLE|
+ SND_PCM_NO_AUTO_CHANNELS|
+ (reformat ? 0 : SND_PCM_NO_AUTO_FORMAT))) < 0) {
+ pa_log_info("Error opening PCM device %s: %s", d, pa_alsa_strerror(err));
+ goto fail;
+ }
+ pa_log_info("ALSA device open '%s' %s: %p", d,
+ mode == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", pcm_handle);
+
+ if ((err = pa_alsa_set_hw_params(
+ pcm_handle,
+ ss,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ require_exact_channel_number)) < 0) {
+
+ if (!reformat) {
+ reformat = true;
+
+ pa_alsa_close(&pcm_handle);
+ continue;
+ }
+
+ /* Hmm, some hw is very exotic, so we retry with plug, if without it didn't work */
+ if (!pa_startswith(d, "plug:") && !pa_startswith(d, "plughw:")) {
+ char *t;
+
+ t = pa_sprintf_malloc("plug:SLAVE='%s'", d);
+ pa_xfree(d);
+ d = t;
+
+ reformat = false;
+
+ pa_alsa_close(&pcm_handle);
+ continue;
+ }
+
+ pa_log_info("Failed to set hardware parameters on %s: %s", d, pa_alsa_strerror(err));
+ pa_alsa_close(&pcm_handle);
+
+ goto fail;
+ }
+
+ if (ss->channels > PA_CHANNELS_MAX) {
+ pa_log("Device %s has %u channels, but PulseAudio supports only %u channels. Unable to use the device.",
+ d, ss->channels, PA_CHANNELS_MAX);
+ pa_alsa_close(&pcm_handle);
+ goto fail;
+ }
+
+ if (dev)
+ *dev = d;
+ else
+ pa_xfree(d);
+
+ if (ss->channels != map->channels)
+ pa_channel_map_init_extend(map, ss->channels, PA_CHANNEL_MAP_ALSA);
+
+ return pcm_handle;
+ }
+
+fail:
+ pa_xfree(d);
+
+ return NULL;
+}
+
+snd_pcm_t *pa_alsa_open_by_template(
+ char **template,
+ const char *dev_id,
+ char **dev,
+ pa_sample_spec *ss,
+ pa_channel_map* map,
+ int mode,
+ snd_pcm_uframes_t *period_size,
+ snd_pcm_uframes_t *buffer_size,
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap,
+ bool *use_tsched,
+ bool require_exact_channel_number) {
+
+ snd_pcm_t *pcm_handle;
+ char **i;
+
+ for (i = template; *i; i++) {
+ char *d;
+
+ d = pa_replace(*i, "%f", dev_id);
+
+ pcm_handle = pa_alsa_open_by_device_string(
+ d,
+ dev,
+ ss,
+ map,
+ mode,
+ period_size,
+ buffer_size,
+ tsched_size,
+ use_mmap,
+ use_tsched,
+ require_exact_channel_number);
+
+ pa_xfree(d);
+
+ if (pcm_handle)
+ return pcm_handle;
+ }
+
+ return NULL;
+}
+
+void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm) {
+ int err;
+ snd_output_t *out;
+
+ pa_assert(pcm);
+
+ pa_assert_se(snd_output_buffer_open(&out) == 0);
+
+ if ((err = snd_pcm_dump(pcm, out)) < 0)
+ pa_logl(level, "snd_pcm_dump(): %s", pa_alsa_strerror(err));
+ else {
+ char *s = NULL;
+ snd_output_buffer_string(out, &s);
+ pa_logl(level, "snd_pcm_dump():\n%s", pa_strnull(s));
+ }
+
+ pa_assert_se(snd_output_close(out) == 0);
+}
+
+#if 0
+void pa_alsa_dump_status(snd_pcm_t *pcm) {
+ int err;
+ snd_output_t *out;
+ snd_pcm_status_t *status;
+ char *s = NULL;
+
+ pa_assert(pcm);
+
+ snd_pcm_status_alloca(&status);
+
+ if ((err = snd_output_buffer_open(&out)) < 0) {
+ pa_log_debug("snd_output_buffer_open() failed: %s", pa_cstrerror(err));
+ return;
+ }
+
+ if ((err = snd_pcm_status(pcm, status)) < 0) {
+ pa_log_debug("snd_pcm_status() failed: %s", pa_cstrerror(err));
+ goto finish;
+ }
+
+ if ((err = snd_pcm_status_dump(status, out)) < 0) {
+ pa_log_debug("snd_pcm_status_dump(): %s", pa_alsa_strerror(err));
+ goto finish;
+ }
+
+ snd_output_buffer_string(out, &s);
+ pa_log_debug("snd_pcm_status_dump():\n%s", pa_strnull(s));
+
+finish:
+
+ snd_output_close(out);
+}
+#endif
+
+static PA_PRINTF_FUNC(5,6) void alsa_error_handler(const char *file, int line, const char *function, int err, const char *fmt,...) {
+ va_list ap;
+// char *alsa_file;
+
+// alsa_file = pa_sprintf_malloc("(alsa-lib)%s", file);
+
+ va_start(ap, fmt);
+
+ pa_log_levelv_meta(PA_LOG_INFO, file, line, function, fmt, ap);
+
+ va_end(ap);
+
+// pa_xfree(alsa_file);
+}
+
+static int n_error_handler_installed = 0;
+
+typedef void (*snd_lib2_error_handler_t)(const char *file, int line, const char *function, int err, const char *fmt, ...) PA_PRINTF_FUNC(5,6) /* __attribute__ ((format (printf, 5, 6))) */;
+
+extern int snd_lib_error_set_handler(snd_lib2_error_handler_t handler);
+
+void pa_alsa_refcnt_inc(void) {
+ /* This is not really thread safe, but we do our best */
+ if (n_error_handler_installed++ == 0)
+ snd_lib_error_set_handler(alsa_error_handler);
+}
+
+void pa_alsa_refcnt_dec(void) {
+ int r;
+
+ pa_assert_se((r = n_error_handler_installed--) >= 1);
+
+ if (r == 1) {
+ snd_lib_error_set_handler(NULL);
+ snd_config_update_free_global();
+ }
+}
+
+bool pa_alsa_init_description(pa_proplist *p, pa_card *card) {
+ const char *d, *k;
+ pa_assert(p);
+
+ if (pa_alsa_device_init_description(p, card))
+ return true;
+
+ if (!(d = pa_proplist_gets(p, "alsa.card_name")))
+ d = pa_proplist_gets(p, "alsa.name");
+
+ if (!d)
+ return false;
+
+ k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION);
+
+ if (d && k)
+ pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k);
+ else if (d)
+ pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d);
+
+ return false;
+}
+
+void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card) {
+ char *cn, *lcn, *dn;
+
+ pa_assert(p);
+ pa_assert(card >= 0);
+
+ pa_proplist_setf(p, "alsa.card", "%i", card);
+
+ if (snd_card_get_name(card, &cn) >= 0) {
+ pa_proplist_sets(p, "alsa.card_name", pa_strip(cn));
+ free(cn);
+ }
+
+ if (snd_card_get_longname(card, &lcn) >= 0) {
+ pa_proplist_sets(p, "alsa.long_card_name", pa_strip(lcn));
+ free(lcn);
+ }
+
+ if ((dn = pa_alsa_get_driver_name(card))) {
+ pa_proplist_sets(p, "alsa.driver_name", dn);
+ pa_xfree(dn);
+ }
+
+#ifdef HAVE_UDEV
+ pa_udev_get_info(card, p);
+#endif
+}
+
+void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info) {
+
+ static const char * const alsa_class_table[SND_PCM_CLASS_LAST+1] = {
+ [SND_PCM_CLASS_GENERIC] = "generic",
+ [SND_PCM_CLASS_MULTI] = "multi",
+ [SND_PCM_CLASS_MODEM] = "modem",
+ [SND_PCM_CLASS_DIGITIZER] = "digitizer"
+ };
+ static const char * const class_table[SND_PCM_CLASS_LAST+1] = {
+ [SND_PCM_CLASS_GENERIC] = "sound",
+ [SND_PCM_CLASS_MULTI] = NULL,
+ [SND_PCM_CLASS_MODEM] = "modem",
+ [SND_PCM_CLASS_DIGITIZER] = NULL
+ };
+ static const char * const alsa_subclass_table[SND_PCM_SUBCLASS_LAST+1] = {
+ [SND_PCM_SUBCLASS_GENERIC_MIX] = "generic-mix",
+ [SND_PCM_SUBCLASS_MULTI_MIX] = "multi-mix"
+ };
+
+ snd_pcm_class_t class;
+ snd_pcm_subclass_t subclass;
+ const char *n, *id, *sdn;
+ int card;
+
+ pa_assert(p);
+ pa_assert(pcm_info);
+
+ pa_proplist_sets(p, PA_PROP_DEVICE_API, "alsa");
+
+ if ((class = snd_pcm_info_get_class(pcm_info)) <= SND_PCM_CLASS_LAST) {
+ if (class_table[class])
+ pa_proplist_sets(p, PA_PROP_DEVICE_CLASS, class_table[class]);
+ if (alsa_class_table[class])
+ pa_proplist_sets(p, "alsa.class", alsa_class_table[class]);
+ }
+
+ if ((subclass = snd_pcm_info_get_subclass(pcm_info)) <= SND_PCM_SUBCLASS_LAST)
+ if (alsa_subclass_table[subclass])
+ pa_proplist_sets(p, "alsa.subclass", alsa_subclass_table[subclass]);
+
+ if ((n = snd_pcm_info_get_name(pcm_info))) {
+ char *t = pa_xstrdup(n);
+ pa_proplist_sets(p, "alsa.name", pa_strip(t));
+ pa_xfree(t);
+ }
+
+ if ((id = snd_pcm_info_get_id(pcm_info)))
+ pa_proplist_sets(p, "alsa.id", id);
+
+ pa_proplist_setf(p, "alsa.subdevice", "%u", snd_pcm_info_get_subdevice(pcm_info));
+ if ((sdn = snd_pcm_info_get_subdevice_name(pcm_info)))
+ pa_proplist_sets(p, "alsa.subdevice_name", sdn);
+
+ pa_proplist_setf(p, "alsa.device", "%u", snd_pcm_info_get_device(pcm_info));
+
+ if ((card = snd_pcm_info_get_card(pcm_info)) >= 0)
+ pa_alsa_init_proplist_card(c, p, card);
+}
+
+void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm) {
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_info_t *info;
+ int bits, err;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_info_alloca(&info);
+
+ if ((err = snd_pcm_hw_params_current(pcm, hwparams)) < 0)
+ pa_log_warn("Error fetching hardware parameter info: %s", pa_alsa_strerror(err));
+ else {
+
+ if ((bits = snd_pcm_hw_params_get_sbits(hwparams)) >= 0)
+ pa_proplist_setf(p, "alsa.resolution_bits", "%i", bits);
+ }
+
+ if ((err = snd_pcm_info(pcm, info)) < 0)
+ pa_log_warn("Error fetching PCM info: %s", pa_alsa_strerror(err));
+ else
+ pa_alsa_init_proplist_pcm_info(c, p, info);
+}
+
+#if 0
+void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name) {
+ int err;
+ snd_ctl_t *ctl;
+ snd_ctl_card_info_t *info;
+ const char *t;
+
+ pa_assert(p);
+
+ snd_ctl_card_info_alloca(&info);
+
+ if ((err = snd_ctl_open(&ctl, name, 0)) < 0) {
+ pa_log_warn("Error opening low-level control device '%s': %s", name, snd_strerror(err));
+ return;
+ }
+
+ if ((err = snd_ctl_card_info(ctl, info)) < 0) {
+ pa_log_warn("Control device %s card info: %s", name, snd_strerror(err));
+ snd_ctl_close(ctl);
+ return;
+ }
+
+ if ((t = snd_ctl_card_info_get_mixername(info)) && *t)
+ pa_proplist_sets(p, "alsa.mixer_name", t);
+
+ if ((t = snd_ctl_card_info_get_components(info)) && *t)
+ pa_proplist_sets(p, "alsa.components", t);
+
+ snd_ctl_close(ctl);
+}
+
+int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents) {
+ snd_pcm_state_t state;
+ snd_pcm_hw_params_t *hwparams;
+ int err;
+
+ pa_assert(pcm);
+
+ if (revents & POLLERR)
+ pa_log_debug("Got POLLERR from ALSA");
+ if (revents & POLLNVAL)
+ pa_log_warn("Got POLLNVAL from ALSA");
+ if (revents & POLLHUP)
+ pa_log_warn("Got POLLHUP from ALSA");
+ if (revents & POLLPRI)
+ pa_log_warn("Got POLLPRI from ALSA");
+ if (revents & POLLIN)
+ pa_log_debug("Got POLLIN from ALSA");
+ if (revents & POLLOUT)
+ pa_log_debug("Got POLLOUT from ALSA");
+
+ state = snd_pcm_state(pcm);
+ pa_log_debug("PCM state is %s", snd_pcm_state_name(state));
+
+ /* Try to recover from this error */
+
+ switch (state) {
+
+ case SND_PCM_STATE_DISCONNECTED:
+ /* Do not try to recover */
+ pa_log_info("Device disconnected.");
+ return -1;
+
+ case SND_PCM_STATE_XRUN:
+ if ((err = snd_pcm_recover(pcm, -EPIPE, 1)) != 0) {
+ pa_log_warn("Could not recover from POLLERR|POLLNVAL|POLLHUP and XRUN: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+ break;
+
+ case SND_PCM_STATE_SUSPENDED:
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if (snd_pcm_hw_params_can_resume(hwparams)) {
+ /* Retry resume 3 times before giving up, then fallback to restarting the stream. */
+ for (int i = 0; i < 3; i++) {
+ if ((err = snd_pcm_resume(pcm)) == 0)
+ return 0;
+ if (err != -EAGAIN)
+ break;
+ pa_msleep(25);
+ }
+ pa_log_warn("Could not recover alsa device from SUSPENDED state, trying to restart PCM");
+ }
+ /* Fall through */
+
+ default:
+
+ snd_pcm_drop(pcm);
+ return 1;
+ }
+
+ return 0;
+}
+
+pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll) {
+ int n, err;
+ struct pollfd *pollfd;
+ pa_rtpoll_item *item;
+
+ pa_assert(pcm);
+
+ if ((n = snd_pcm_poll_descriptors_count(pcm)) < 0) {
+ pa_log("snd_pcm_poll_descriptors_count() failed: %s", pa_alsa_strerror(n));
+ return NULL;
+ }
+
+ item = pa_rtpoll_item_new(rtpoll, PA_RTPOLL_NEVER, (unsigned) n);
+ pollfd = pa_rtpoll_item_get_pollfd(item, NULL);
+
+ if ((err = snd_pcm_poll_descriptors(pcm, pollfd, (unsigned) n)) < 0) {
+ pa_log("snd_pcm_poll_descriptors() failed: %s", pa_alsa_strerror(err));
+ pa_rtpoll_item_free(item);
+ return NULL;
+ }
+
+ return item;
+}
+
+snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss) {
+ snd_pcm_sframes_t n;
+ size_t k;
+
+ pa_assert(pcm);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ /* Some ALSA driver expose weird bugs, let's inform the user about
+ * what is going on */
+
+ n = snd_pcm_avail(pcm);
+
+ if (n <= 0)
+ return n;
+
+ k = (size_t) n * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(k >= hwbuf_size * 5 ||
+ k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ n = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ return n;
+}
+
+int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss,
+ bool capture) {
+ ssize_t k;
+ size_t abs_k;
+ int err;
+ snd_pcm_sframes_t avail = 0;
+#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */
+ snd_pcm_audio_tstamp_config_t tstamp_config;
+#endif
+
+ pa_assert(pcm);
+ pa_assert(delay);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ /* Some ALSA driver expose weird bugs, let's inform the user about
+ * what is going on. We're going to get both the avail and delay values so
+ * that we can compare and check them for capture.
+ * This is done with snd_pcm_status() which provides
+ * avail, delay and timestamp values in a single kernel call to improve
+ * timer-based scheduling */
+
+#if (SND_LIB_VERSION >= ((1<<16)|(1<<8)|0)) /* API additions in 1.1.0 */
+
+ /* The time stamp configuration needs to be set so that the
+ * ALSA code will use the internal delay reported by the driver.
+ * The time stamp configuration was introduced in alsa version 1.1.0. */
+ tstamp_config.type_requested = 1; /* ALSA default time stamp type */
+ tstamp_config.report_delay = 1;
+ snd_pcm_status_set_audio_htstamp_config(status, &tstamp_config);
+#endif
+
+ if ((err = snd_pcm_status(pcm, status)) < 0)
+ return err;
+
+ avail = snd_pcm_status_get_avail(status);
+ *delay = snd_pcm_status_get_delay(status);
+
+ k = (ssize_t) *delay * (ssize_t) pa_frame_size(ss);
+
+ abs_k = k >= 0 ? (size_t) k : (size_t) -k;
+
+ if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 ||
+ abs_k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (signed long) k),
+ (signed long) k,
+ k < 0 ? "-" : "",
+ (unsigned long) (pa_bytes_to_usec(abs_k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ if (k < 0)
+ *delay = -(snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ else
+ *delay = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ if (capture) {
+ abs_k = (size_t) avail * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(abs_k >= hwbuf_size * 5 ||
+ abs_k >= pa_bytes_per_second(ss)*10)) {
+
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ /* Mhmm, let's try not to fail completely */
+ avail = (snd_pcm_sframes_t) (hwbuf_size / pa_frame_size(ss));
+ }
+
+ if (PA_UNLIKELY(*delay < avail)) {
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log(_("snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers."),
+ (unsigned long) *delay,
+ (unsigned long) avail,
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_ERROR, pcm);
+ } PA_ONCE_END;
+
+ /* try to fixup */
+ *delay = avail;
+ }
+ }
+
+ return 0;
+}
+
+int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss) {
+ int r;
+ snd_pcm_uframes_t before;
+ size_t k;
+
+ pa_assert(pcm);
+ pa_assert(areas);
+ pa_assert(offset);
+ pa_assert(frames);
+ pa_assert(hwbuf_size > 0);
+ pa_assert(ss);
+
+ before = *frames;
+
+ r = snd_pcm_mmap_begin(pcm, areas, offset, frames);
+
+ if (r < 0)
+ return r;
+
+ k = (size_t) *frames * pa_frame_size(ss);
+
+ if (PA_UNLIKELY(*frames > before ||
+ k >= hwbuf_size * 3 ||
+ k >= pa_bytes_per_second(ss)*10))
+ PA_ONCE_BEGIN {
+ char *dn = pa_alsa_get_driver_name_by_pcm(pcm);
+ pa_log_debug(ngettext("snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n"
+ "Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers.",
+ (unsigned long) k),
+ (unsigned long) k,
+ (unsigned long) (pa_bytes_to_usec(k, ss) / PA_USEC_PER_MSEC),
+ pa_strnull(dn));
+ pa_xfree(dn);
+ pa_alsa_dump(PA_LOG_DEBUG, pcm);
+ } PA_ONCE_END;
+
+ return r;
+}
+#endif
+
+char *pa_alsa_get_driver_name(int card) {
+ char *t, *m, *n;
+
+ pa_assert(card >= 0);
+
+ t = pa_sprintf_malloc("/sys/class/sound/card%i/device/driver/module", card);
+ m = pa_readlink(t);
+ pa_xfree(t);
+
+ if (!m)
+ return NULL;
+
+ n = pa_xstrdup(pa_path_get_filename(m));
+ pa_xfree(m);
+
+ return n;
+}
+
+char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm) {
+ int card;
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return NULL;
+
+ if ((card = snd_pcm_info_get_card(info)) < 0)
+ return NULL;
+
+ return pa_alsa_get_driver_name(card);
+}
+
+#if 0
+char *pa_alsa_get_reserve_name(const char *device) {
+ const char *t;
+ int i;
+
+ pa_assert(device);
+
+ if ((t = strchr(device, ':')))
+ device = t+1;
+
+ if ((i = snd_card_get_index(device)) < 0) {
+ int32_t k;
+
+ if (pa_atoi(device, &k) < 0)
+ return NULL;
+
+ i = (int) k;
+ }
+
+ return pa_sprintf_malloc("Audio%i", i);
+}
+
+unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate) {
+ static unsigned int all_rates[] = { 8000, 11025, 12000,
+ 16000, 22050, 24000,
+ 32000, 44100, 48000,
+ 64000, 88200, 96000,
+ 128000, 176400, 192000,
+ 384000 };
+ bool supported[PA_ELEMENTSOF(all_rates)] = { false, };
+ snd_pcm_hw_params_t *hwparams;
+ unsigned int i, j, n, *rates = NULL;
+ int ret;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ return NULL;
+ }
+
+ for (i = 0, n = 0; i < PA_ELEMENTSOF(all_rates); i++) {
+ if (snd_pcm_hw_params_test_rate(pcm, hwparams, all_rates[i], 0) == 0) {
+ supported[i] = true;
+ n++;
+ }
+ }
+
+ if (n > 0) {
+ rates = pa_xnew(unsigned int, n + 1);
+
+ for (i = 0, j = 0; i < PA_ELEMENTSOF(all_rates); i++) {
+ if (supported[i])
+ rates[j++] = all_rates[i];
+ }
+
+ rates[j] = 0;
+ } else {
+ rates = pa_xnew(unsigned int, 2);
+
+ rates[0] = fallback_rate;
+ if ((ret = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rates[0], NULL)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_rate_near() failed: %s", pa_alsa_strerror(ret));
+ pa_xfree(rates);
+ return NULL;
+ }
+
+ rates[1] = 0;
+ }
+
+ return rates;
+}
+
+pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format) {
+ static const snd_pcm_format_t format_trans_to_pa[] = {
+ [SND_PCM_FORMAT_U8] = PA_SAMPLE_U8,
+ [SND_PCM_FORMAT_A_LAW] = PA_SAMPLE_ALAW,
+ [SND_PCM_FORMAT_MU_LAW] = PA_SAMPLE_ULAW,
+ [SND_PCM_FORMAT_S16_LE] = PA_SAMPLE_S16LE,
+ [SND_PCM_FORMAT_S16_BE] = PA_SAMPLE_S16BE,
+ [SND_PCM_FORMAT_FLOAT_LE] = PA_SAMPLE_FLOAT32LE,
+ [SND_PCM_FORMAT_FLOAT_BE] = PA_SAMPLE_FLOAT32BE,
+ [SND_PCM_FORMAT_S32_LE] = PA_SAMPLE_S32LE,
+ [SND_PCM_FORMAT_S32_BE] = PA_SAMPLE_S32BE,
+ [SND_PCM_FORMAT_S24_3LE] = PA_SAMPLE_S24LE,
+ [SND_PCM_FORMAT_S24_3BE] = PA_SAMPLE_S24BE,
+ [SND_PCM_FORMAT_S24_LE] = PA_SAMPLE_S24_32LE,
+ [SND_PCM_FORMAT_S24_BE] = PA_SAMPLE_S24_32BE,
+ };
+ static const snd_pcm_format_t all_formats[] = {
+ SND_PCM_FORMAT_U8,
+ SND_PCM_FORMAT_A_LAW,
+ SND_PCM_FORMAT_MU_LAW,
+ SND_PCM_FORMAT_S16_LE,
+ SND_PCM_FORMAT_S16_BE,
+ SND_PCM_FORMAT_FLOAT_LE,
+ SND_PCM_FORMAT_FLOAT_BE,
+ SND_PCM_FORMAT_S32_LE,
+ SND_PCM_FORMAT_S32_BE,
+ SND_PCM_FORMAT_S24_3LE,
+ SND_PCM_FORMAT_S24_3BE,
+ SND_PCM_FORMAT_S24_LE,
+ SND_PCM_FORMAT_S24_BE,
+ };
+ bool supported[PA_ELEMENTSOF(all_formats)] = {
+ false,
+ };
+ snd_pcm_hw_params_t *hwparams;
+ unsigned int i, j, n;
+ pa_sample_format_t *formats = NULL;
+ int ret;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+
+ if ((ret = snd_pcm_hw_params_any(pcm, hwparams)) < 0) {
+ pa_log_debug("snd_pcm_hw_params_any() failed: %s", pa_alsa_strerror(ret));
+ return NULL;
+ }
+
+ for (i = 0, n = 0; i < PA_ELEMENTSOF(all_formats); i++) {
+ if (snd_pcm_hw_params_test_format(pcm, hwparams, all_formats[i]) == 0) {
+ supported[i] = true;
+ n++;
+ }
+ }
+
+ if (n > 0) {
+ formats = pa_xnew(pa_sample_format_t, n + 1);
+
+ for (i = 0, j = 0; i < PA_ELEMENTSOF(all_formats); i++) {
+ if (supported[i])
+ formats[j++] = format_trans_to_pa[all_formats[i]];
+ }
+
+ formats[j] = PA_SAMPLE_MAX;
+ } else {
+ formats = pa_xnew(pa_sample_format_t, 2);
+
+ formats[0] = fallback_format;
+ if ((ret = snd_pcm_hw_params_set_format(pcm, hwparams, format_trans_to_pa[formats[0]])) < 0) {
+ pa_log_debug("snd_pcm_hw_params_set_format() failed: %s", pa_alsa_strerror(ret));
+ pa_xfree(formats);
+ return NULL;
+ }
+
+ formats[1] = PA_SAMPLE_MAX;
+ }
+
+ return formats;
+}
+#endif
+
+bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return false;
+
+ return snd_pcm_info_get_card(info) >= 0;
+}
+
+bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) < 0)
+ return false;
+
+ return snd_pcm_info_get_class(info) == SND_PCM_CLASS_MODEM;
+}
+
+const char* pa_alsa_strerror(int errnum) {
+ return snd_strerror(errnum);
+}
+
+#if 0
+bool pa_alsa_may_tsched(bool want) {
+
+ if (!want)
+ return false;
+
+ if (!pa_rtclock_hrtimer()) {
+ /* We cannot depend on being woken up in time when the timers
+ are inaccurate, so let's fallback to classic IO based playback
+ then. */
+ pa_log_notice("Disabling timer-based scheduling because high-resolution timers are not available from the kernel.");
+ return false; }
+
+ if (pa_running_in_vm()) {
+ /* We cannot depend on being woken up when we ask for in a VM,
+ * so let's fallback to classic IO based playback then. */
+ pa_log_notice("Disabling timer-based scheduling because running inside a VM.");
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+#define SND_MIXER_ELEM_PULSEAUDIO (SND_MIXER_ELEM_LAST + 10)
+
+static snd_mixer_elem_t *pa_alsa_mixer_find(snd_mixer_t *mixer,
+ snd_ctl_elem_iface_t iface,
+ const char *name,
+ unsigned int index,
+ unsigned int device) {
+ snd_mixer_elem_t *elem;
+
+ for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
+ snd_hctl_elem_t *helem;
+ if (snd_mixer_elem_get_type(elem) != SND_MIXER_ELEM_PULSEAUDIO)
+ continue;
+ helem = snd_mixer_elem_get_private(elem);
+ if (snd_hctl_elem_get_interface(helem) != iface)
+ continue;
+ if (!pa_streq(snd_hctl_elem_get_name(helem), name))
+ continue;
+ if (snd_hctl_elem_get_index(helem) != index)
+ continue;
+ if (snd_hctl_elem_get_device(helem) != device)
+ continue;
+ return elem;
+ }
+ return NULL;
+}
+
+snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device) {
+ return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_CARD, alsa_id->name, alsa_id->index, device);
+}
+
+snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device) {
+ return pa_alsa_mixer_find(mixer, SND_CTL_ELEM_IFACE_PCM, name, 0, device);
+}
+
+static int mixer_class_compare(const snd_mixer_elem_t *c1, const snd_mixer_elem_t *c2)
+{
+ /* Dummy compare function */
+ return c1 == c2 ? 0 : (c1 > c2 ? 1 : -1);
+}
+
+static int mixer_class_event(snd_mixer_class_t *class, unsigned int mask,
+ snd_hctl_elem_t *helem, snd_mixer_elem_t *melem)
+{
+ int err;
+ const char *name = snd_hctl_elem_get_name(helem);
+ // NOTE: The remove event defined as '~0U`.
+ if (mask == SND_CTL_EVENT_MASK_REMOVE) {
+ // NOTE: unless remove pointer to melem from link-list at private_data of helem, hits
+ // assersion in alsa-lib since the list is not empty.
+ snd_mixer_elem_detach(melem, helem);
+ } else if (mask & SND_CTL_EVENT_MASK_ADD) {
+ snd_ctl_elem_iface_t iface = snd_hctl_elem_get_interface(helem);
+ if (iface == SND_CTL_ELEM_IFACE_CARD || iface == SND_CTL_ELEM_IFACE_PCM) {
+ snd_mixer_elem_t *new_melem;
+
+ /* Put the hctl pointer as our private data - it will be useful for callbacks */
+ if ((err = snd_mixer_elem_new(&new_melem, SND_MIXER_ELEM_PULSEAUDIO, 0, helem, NULL)) < 0) {
+ pa_log_warn("snd_mixer_elem_new failed: %s", pa_alsa_strerror(err));
+ return 0;
+ }
+
+ if ((err = snd_mixer_elem_attach(new_melem, helem)) < 0) {
+ pa_log_warn("snd_mixer_elem_attach failed: %s", pa_alsa_strerror(err));
+ snd_mixer_elem_free(melem);
+ return 0;
+ }
+
+ if ((err = snd_mixer_elem_add(new_melem, class)) < 0) {
+ pa_log_warn("snd_mixer_elem_add failed: %s", pa_alsa_strerror(err));
+ return 0;
+ }
+ }
+ }
+ else if (mask & SND_CTL_EVENT_MASK_VALUE) {
+ snd_mixer_elem_value(melem); /* Calls the element callback */
+ return 0;
+ }
+ else
+ pa_log_info("Got an unknown mixer class event for %s: mask 0x%x", name, mask);
+
+ return 0;
+}
+
+static int prepare_mixer(snd_mixer_t *mixer, const char *dev, snd_hctl_t *hctl) {
+ int err;
+ snd_mixer_class_t *class;
+
+ pa_assert(mixer);
+ pa_assert(dev);
+
+ if ((err = snd_mixer_attach_hctl(mixer, hctl)) < 0) {
+ pa_log_info("Unable to attach to mixer %s: %s", dev, pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if (snd_mixer_class_malloc(&class)) {
+ pa_log_info("Failed to allocate mixer class for %s", dev);
+ return -1;
+ }
+ snd_mixer_class_set_event(class, mixer_class_event);
+ snd_mixer_class_set_compare(class, mixer_class_compare);
+ if ((err = snd_mixer_class_register(class, mixer)) < 0) {
+ pa_log_info("Unable register mixer class for %s: %s", dev, pa_alsa_strerror(err));
+ snd_mixer_class_free(class);
+ return -1;
+ }
+ /* From here on, the mixer class is deallocated by alsa on snd_mixer_close/free. */
+
+ if ((err = snd_mixer_selem_register(mixer, NULL, NULL)) < 0) {
+ pa_log_warn("Unable to register mixer: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ if ((err = snd_mixer_load(mixer)) < 0) {
+ pa_log_warn("Unable to load mixer: %s", pa_alsa_strerror(err));
+ return -1;
+ }
+
+ pa_log_info("Successfully attached to mixer '%s'", dev);
+ return 0;
+}
+
+snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe) {
+ char *md = pa_sprintf_malloc("hw:%i", alsa_card_index);
+ snd_mixer_t *m = pa_alsa_open_mixer_by_name(mixers, md, probe);
+ pa_xfree(md);
+ return m;
+}
+
+pa_alsa_mixer *pa_alsa_create_mixer(pa_hashmap *mixers, const char *dev, snd_mixer_t *m, bool probe) {
+ pa_alsa_mixer *pm;
+
+ pm = pa_xnew0(pa_alsa_mixer, 1);
+ if (pm == NULL)
+ return NULL;
+
+ pm->used_for_probe_only = probe;
+ pm->mixer_handle = m;
+ pa_hashmap_put(mixers, pa_xstrdup(dev), pm);
+ return pm;
+}
+
+snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe) {
+ int err;
+ snd_mixer_t *m;
+ snd_hctl_t *hctl;
+ pa_alsa_mixer *pm;
+
+ pa_assert(mixers);
+ pa_assert(dev);
+
+ pm = pa_hashmap_get(mixers, dev);
+ if (pm) {
+ if (!probe)
+ pm->used_for_probe_only = false;
+ return pm->mixer_handle;
+ }
+
+ if ((err = snd_mixer_open(&m, 0)) < 0) {
+ pa_log("Error opening mixer: %s", pa_alsa_strerror(err));
+ return NULL;
+ }
+
+ err = snd_hctl_open(&hctl, dev, 0);
+ if (err < 0) {
+ pa_log("Error opening hctl device: %s", pa_alsa_strerror(err));
+ goto __close;
+ }
+
+ if (prepare_mixer(m, dev, hctl) >= 0) {
+ /* get the ALSA card number (index) and ID (alias) and create two identical mixers */
+ char *p, *dev2, *dev_idx, *dev_id;
+ snd_ctl_card_info_t *info;
+ snd_ctl_card_info_alloca(&info);
+ err = snd_ctl_card_info(snd_hctl_ctl(hctl), info);
+ if (err < 0)
+ goto __std;
+ dev2 = pa_xstrdup(dev);
+ if (dev2 == NULL)
+ goto __close;
+ p = strchr(dev2, ':');
+ /* sanity check - only hw: devices */
+ if (p == NULL || (p - dev2) < 2 || !pa_strneq(p - 2, "hw:", 3)) {
+ pa_xfree(dev2);
+ goto __std;
+ }
+ *p = '\0';
+ dev_idx = pa_sprintf_malloc("%s:%u", dev2, snd_ctl_card_info_get_card(info));
+ dev_id = pa_sprintf_malloc("%s:%s", dev2, snd_ctl_card_info_get_id(info));
+ pa_log_debug("ALSA alias mixers: %s = %s", dev_idx, dev_id);
+ if (dev_idx && dev_id && (strcmp(dev, dev_idx) == 0 || strcmp(dev, dev_id) == 0)) {
+ pm = pa_alsa_create_mixer(mixers, dev_idx, m, probe);
+ if (pm) {
+ pa_alsa_mixer *pm2;
+ pm2 = pa_alsa_create_mixer(mixers, dev_id, m, probe);
+ if (pm2) {
+ pm->alias = pm2;
+ pm2->alias = pm;
+ }
+ }
+ }
+ pa_xfree(dev_id);
+ pa_xfree(dev_idx);
+ pa_xfree(dev2);
+ __std:
+ if (pm == NULL)
+ pm = pa_alsa_create_mixer(mixers, dev, m, probe);
+ if (pm)
+ return m;
+ }
+
+__close:
+ snd_mixer_close(m);
+ return NULL;
+}
+
+snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe) {
+ snd_pcm_info_t* info;
+ snd_pcm_info_alloca(&info);
+
+ pa_assert(pcm);
+
+ if (snd_pcm_info(pcm, info) >= 0) {
+ int card_idx;
+
+ if ((card_idx = snd_pcm_info_get_card(info)) >= 0)
+ return pa_alsa_open_mixer(mixers, card_idx, probe);
+ }
+
+ return NULL;
+}
+
+#if 0
+void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer_handle, pa_mainloop_api *ml)
+{
+ pa_alsa_mixer *pm;
+ void *state;
+
+ PA_HASHMAP_FOREACH(pm, mixers, state)
+ if (pm->mixer_handle == mixer_handle) {
+ pm->used_for_probe_only = false;
+ if (!pm->fdl) {
+ pm->fdl = pa_alsa_fdlist_new();
+ if (pm->fdl)
+ pa_alsa_fdlist_set_handle(pm->fdl, pm->mixer_handle, NULL, ml);
+ }
+ }
+}
+#endif
+
+void pa_alsa_mixer_free(pa_alsa_mixer *mixer)
+{
+ if (mixer->mixer_handle && mixer->alias == NULL)
+ snd_mixer_close(mixer->mixer_handle);
+ if (mixer->alias)
+ mixer->alias->alias = NULL;
+ pa_xfree(mixer);
+}
+
+int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) {
+
+ /* The ELD format is specific to HDA Intel sound cards and defined in the
+ HDA specification: http://www.intel.com/content/www/us/en/standards/high-definition-audio-specification.html */
+ int err;
+ snd_ctl_elem_info_t *info;
+ snd_ctl_elem_value_t *value;
+ uint8_t *elddata;
+ unsigned int eldsize, mnl;
+ unsigned int device;
+
+ pa_assert(eld != NULL);
+ pa_assert(elem != NULL);
+
+ /* Does it have any contents? */
+ snd_ctl_elem_info_alloca(&info);
+ snd_ctl_elem_value_alloca(&value);
+ if ((err = snd_hctl_elem_info(elem, info)) < 0 ||
+ (err = snd_hctl_elem_read(elem, value)) < 0) {
+ pa_log_warn("Accessing ELD control failed with error %s", snd_strerror(err));
+ return -1;
+ }
+
+ device = snd_hctl_elem_get_device(elem);
+ eldsize = snd_ctl_elem_info_get_count(info);
+ elddata = (unsigned char *) snd_ctl_elem_value_get_bytes(value);
+ if (elddata == NULL || eldsize == 0) {
+ pa_log_debug("ELD info empty (for device=%d)", device);
+ return -1;
+ }
+ if (eldsize < 20 || eldsize > 256) {
+ pa_log_debug("ELD info has wrong size (for device=%d)", device);
+ return -1;
+ }
+
+ /* Try to fetch monitor name */
+ mnl = elddata[4] & 0x1f;
+ if (mnl == 0 || mnl > 16 || 20 + mnl > eldsize) {
+ pa_log_debug("No monitor name in ELD info (for device=%d)", device);
+ mnl = 0;
+ }
+ memcpy(eld->monitor_name, &elddata[20], mnl);
+ eld->monitor_name[mnl] = '\0';
+ if (mnl)
+ pa_log_debug("Monitor name in ELD info is '%s' (for device=%d)", eld->monitor_name, device);
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h
new file mode 100644
index 0000000..b18b98d
--- /dev/null
+++ b/spa/plugins/alsa/acp/alsa-util.h
@@ -0,0 +1,176 @@
+#ifndef fooalsautilhfoo
+#define fooalsautilhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <stdbool.h>
+
+#include <alsa/asoundlib.h>
+
+#include "compat.h"
+
+#include "alsa-mixer.h"
+
+enum {
+ PA_ALSA_ERR_UNSPECIFIED = 1,
+ PA_ALSA_ERR_UCM_OPEN = 1000,
+ PA_ALSA_ERR_UCM_NO_VERB = 1001,
+ PA_ALSA_ERR_UCM_LINKED = 1002
+};
+
+int pa_alsa_set_hw_params(
+ snd_pcm_t *pcm_handle,
+ pa_sample_spec *ss, /* modified at return */
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+int pa_alsa_set_sw_params(
+ snd_pcm_t *pcm,
+ snd_pcm_uframes_t avail_min,
+ bool period_event);
+
+#if 0
+/* Picks a working mapping from the profile set based on the specified ss/map */
+snd_pcm_t *pa_alsa_open_by_device_id_auto(
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ pa_alsa_profile_set *ps,
+ pa_alsa_mapping **mapping); /* modified at return */
+#endif
+
+/* Uses the specified mapping */
+snd_pcm_t *pa_alsa_open_by_device_id_mapping(
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ pa_alsa_mapping *mapping);
+
+/* Opens the explicit ALSA device */
+snd_pcm_t *pa_alsa_open_by_device_string(
+ const char *dir,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+/* Opens the explicit ALSA device with a fallback list */
+snd_pcm_t *pa_alsa_open_by_template(
+ char **template,
+ const char *dev_id,
+ char **dev, /* modified at return */
+ pa_sample_spec *ss, /* modified at return */
+ pa_channel_map* map, /* modified at return */
+ int mode,
+ snd_pcm_uframes_t *period_size, /* modified at return */
+ snd_pcm_uframes_t *buffer_size, /* modified at return */
+ snd_pcm_uframes_t tsched_size,
+ bool *use_mmap, /* modified at return */
+ bool *use_tsched, /* modified at return */
+ bool require_exact_channel_number);
+
+#if 0
+void pa_alsa_dump(pa_log_level_t level, snd_pcm_t *pcm);
+void pa_alsa_dump_status(snd_pcm_t *pcm);
+#endif
+int pa_alsa_close(snd_pcm_t **pcm);
+
+void pa_alsa_refcnt_inc(void);
+void pa_alsa_refcnt_dec(void);
+
+void pa_alsa_init_proplist_pcm_info(pa_core *c, pa_proplist *p, snd_pcm_info_t *pcm_info);
+void pa_alsa_init_proplist_card(pa_core *c, pa_proplist *p, int card);
+void pa_alsa_init_proplist_pcm(pa_core *c, pa_proplist *p, snd_pcm_t *pcm);
+#if 0
+void pa_alsa_init_proplist_ctl(pa_proplist *p, const char *name);
+#endif
+bool pa_alsa_init_description(pa_proplist *p, pa_card *card);
+
+#if 0
+int pa_alsa_recover_from_poll(snd_pcm_t *pcm, int revents);
+
+pa_rtpoll_item* pa_alsa_build_pollfd(snd_pcm_t *pcm, pa_rtpoll *rtpoll);
+
+snd_pcm_sframes_t pa_alsa_safe_avail(snd_pcm_t *pcm, size_t hwbuf_size, const pa_sample_spec *ss);
+int pa_alsa_safe_delay(snd_pcm_t *pcm, snd_pcm_status_t *status, snd_pcm_sframes_t *delay, size_t hwbuf_size, const pa_sample_spec *ss, bool capture);
+int pa_alsa_safe_mmap_begin(snd_pcm_t *pcm, const snd_pcm_channel_area_t **areas, snd_pcm_uframes_t *offset, snd_pcm_uframes_t *frames, size_t hwbuf_size, const pa_sample_spec *ss);
+#endif
+
+char *pa_alsa_get_driver_name(int card);
+char *pa_alsa_get_driver_name_by_pcm(snd_pcm_t *pcm);
+
+char *pa_alsa_get_reserve_name(const char *device);
+
+unsigned int *pa_alsa_get_supported_rates(snd_pcm_t *pcm, unsigned int fallback_rate);
+pa_sample_format_t *pa_alsa_get_supported_formats(snd_pcm_t *pcm, pa_sample_format_t fallback_format);
+
+bool pa_alsa_pcm_is_hw(snd_pcm_t *pcm);
+bool pa_alsa_pcm_is_modem(snd_pcm_t *pcm);
+
+const char* pa_alsa_strerror(int errnum);
+
+#if 0
+bool pa_alsa_may_tsched(bool want);
+#endif
+
+snd_mixer_elem_t *pa_alsa_mixer_find_card(snd_mixer_t *mixer, struct pa_alsa_mixer_id *alsa_id, unsigned int device);
+snd_mixer_elem_t *pa_alsa_mixer_find_pcm(snd_mixer_t *mixer, const char *name, unsigned int device);
+
+snd_mixer_t *pa_alsa_open_mixer(pa_hashmap *mixers, int alsa_card_index, bool probe);
+snd_mixer_t *pa_alsa_open_mixer_by_name(pa_hashmap *mixers, const char *dev, bool probe);
+snd_mixer_t *pa_alsa_open_mixer_for_pcm(pa_hashmap *mixers, snd_pcm_t *pcm, bool probe);
+#if 0
+void pa_alsa_mixer_set_fdlist(pa_hashmap *mixers, snd_mixer_t *mixer, pa_mainloop_api *ml);
+#endif
+void pa_alsa_mixer_free(pa_alsa_mixer *mixer);
+
+typedef struct pa_hdmi_eld pa_hdmi_eld;
+struct pa_hdmi_eld {
+ char monitor_name[17];
+};
+
+int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld);
+
+#endif
diff --git a/spa/plugins/alsa/acp/array.h b/spa/plugins/alsa/acp/array.h
new file mode 100644
index 0000000..8a976ca
--- /dev/null
+++ b/spa/plugins/alsa/acp/array.h
@@ -0,0 +1,150 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_ARRAY_H
+#define PA_ARRAY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <errno.h>
+#include <string.h>
+
+typedef struct pa_array {
+ void *data; /**< pointer to array data */
+ size_t size; /**< length of array in bytes */
+ size_t alloc; /**< number of allocated memory in \a data */
+ size_t extend; /**< number of bytes to extend with */
+} pa_array;
+
+#define PW_ARRAY_INIT(extend) ((struct pa_array) { NULL, 0, 0, (extend) })
+
+#define pa_array_get_len_s(a,s) ((a)->size / (s))
+#define pa_array_get_unchecked_s(a,idx,s,t) (t*)((uint8_t*)(a)->data + (int)((idx)*(s)))
+#define pa_array_check_index_s(a,idx,s) ((idx) < pa_array_get_len_s(a,s))
+
+#define pa_array_get_len(a,t) pa_array_get_len_s(a,sizeof(t))
+#define pa_array_get_unchecked(a,idx,t) pa_array_get_unchecked_s(a,idx,sizeof(t),t)
+#define pa_array_check_index(a,idx,t) pa_array_check_index_s(a,idx,sizeof(t))
+
+#define pa_array_first(a) ((a)->data)
+#define pa_array_end(a) (void*)((uint8_t*)(a)->data + (int)(a)->size)
+#define pa_array_check(a,p) ((void*)((uint8_t*)p + (int)sizeof(*p)) <= pa_array_end(a))
+
+#define pa_array_for_each(pos, array) \
+ for (pos = (__typeof__(pos)) pa_array_first(array); \
+ pa_array_check(array, pos); \
+ (pos)++)
+
+#define pa_array_consume(pos, array) \
+ while (pos = (__typeof__(pos)) pa_array_first(array) && \
+ pa_array_check(array, pos)
+
+#define pa_array_remove(a,p) \
+({ \
+ (a)->size -= sizeof(*(p)); \
+ memmove(p, ((uint8_t*)(p) + (int)sizeof(*(p))), \
+ (uint8_t*)pa_array_end(a) - (uint8_t*)(p)); \
+})
+
+static inline void pa_array_init(pa_array *arr, size_t extend)
+{
+ arr->data = NULL;
+ arr->size = arr->alloc = 0;
+ arr->extend = extend;
+}
+
+static inline void pa_array_clear(pa_array *arr)
+{
+ free(arr->data);
+}
+
+static inline void pa_array_reset(pa_array *arr)
+{
+ arr->size = 0;
+}
+
+static inline int pa_array_ensure_size(pa_array *arr, size_t size)
+{
+ size_t alloc, need;
+
+ alloc = arr->alloc;
+ need = arr->size + size;
+
+ if (alloc < need) {
+ void *data;
+ alloc = alloc > arr->extend ? alloc : arr->extend;
+ while (alloc < need)
+ alloc *= 2;
+ if ((data = realloc(arr->data, alloc)) == NULL)
+ return -errno;
+ arr->data = data;
+ arr->alloc = alloc;
+ }
+ return 0;
+}
+
+static inline void *pa_array_add(pa_array *arr, size_t size)
+{
+ void *p;
+
+ if (pa_array_ensure_size(arr, size) < 0)
+ return NULL;
+
+ p = (void*)((uint8_t*)arr->data + (int)arr->size);
+ arr->size += size;
+
+ return p;
+}
+
+static inline void *pa_array_add_fixed(pa_array *arr, size_t size)
+{
+ void *p;
+ if (arr->alloc < arr->size + size) {
+ errno = ENOSPC;
+ return NULL;
+ }
+ p = ((uint8_t*)arr->data + (int)arr->size);
+ arr->size += size;
+ return p;
+}
+
+#define pa_array_add_ptr(a,p) \
+ *((void**) pa_array_add(a, sizeof(void*))) = (p)
+
+static inline int pa_array_add_data(pa_array *arr, const void *data, size_t size)
+{
+ void *d;
+ if ((d = pa_array_add(arr, size)) == NULL)
+ return -1;
+ memcpy(d, data, size);
+ return size;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_ARRAY_H */
diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h
new file mode 100644
index 0000000..5a94064
--- /dev/null
+++ b/spa/plugins/alsa/acp/card.h
@@ -0,0 +1,75 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+
+#ifndef PULSE_CARD_H
+#define PULSE_CARD_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include "compat.h"
+
+typedef struct pa_card pa_card;
+
+struct pa_card {
+ struct acp_card card;
+
+ pa_core *core;
+
+ char *name;
+ char *driver;
+
+ pa_proplist *proplist;
+
+ bool use_ucm;
+ bool soft_mixer;
+ bool auto_profile;
+ bool auto_port;
+ bool ignore_dB;
+ uint32_t rate;
+
+ pa_alsa_ucm_config ucm;
+ pa_alsa_profile_set *profile_set;
+
+ pa_hashmap *ports;
+ pa_hashmap *profiles;
+ pa_hashmap *jacks;
+
+ struct {
+ pa_dynarray ports;
+ pa_dynarray profiles;
+ pa_dynarray devices;
+ } out;
+
+ const struct acp_card_events *events;
+ void *user_data;
+};
+
+bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_CARD_H */
diff --git a/spa/plugins/alsa/acp/channelmap.h b/spa/plugins/alsa/acp/channelmap.h
new file mode 100644
index 0000000..adb4868
--- /dev/null
+++ b/spa/plugins/alsa/acp/channelmap.h
@@ -0,0 +1,476 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifndef PULSE_CHANNELMAP_H
+#define PULSE_CHANNELMAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "spa/utils/defs.h"
+
+#define PA_CHANNELS_MAX 64
+
+#define PA_CHANNEL_MAP_SNPRINT_MAX (PA_CHANNELS_MAX * 32)
+
+typedef enum pa_channel_map_def {
+ PA_CHANNEL_MAP_AIFF,
+ PA_CHANNEL_MAP_ALSA,
+ PA_CHANNEL_MAP_AUX,
+ PA_CHANNEL_MAP_WAVEEX,
+ PA_CHANNEL_MAP_OSS,
+ PA_CHANNEL_MAP_DEF_MAX,
+ PA_CHANNEL_MAP_DEFAULT = PA_CHANNEL_MAP_AIFF
+} pa_channel_map_def_t;
+
+typedef enum pa_channel_position {
+ PA_CHANNEL_POSITION_INVALID = -1,
+ PA_CHANNEL_POSITION_MONO = 0,
+
+ PA_CHANNEL_POSITION_FRONT_LEFT, /**< Apple, Dolby call this 'Left' */
+ PA_CHANNEL_POSITION_FRONT_RIGHT, /**< Apple, Dolby call this 'Right' */
+ PA_CHANNEL_POSITION_FRONT_CENTER, /**< Apple, Dolby call this 'Center' */
+
+/** \cond fulldocs */
+ PA_CHANNEL_POSITION_LEFT = PA_CHANNEL_POSITION_FRONT_LEFT,
+ PA_CHANNEL_POSITION_RIGHT = PA_CHANNEL_POSITION_FRONT_RIGHT,
+ PA_CHANNEL_POSITION_CENTER = PA_CHANNEL_POSITION_FRONT_CENTER,
+/** \endcond */
+
+ PA_CHANNEL_POSITION_REAR_CENTER, /**< Microsoft calls this 'Back Center', Apple calls this 'Center Surround', Dolby calls this 'Surround Rear Center' */
+ PA_CHANNEL_POSITION_REAR_LEFT, /**< Microsoft calls this 'Back Left', Apple calls this 'Left Surround' (!), Dolby calls this 'Surround Rear Left' */
+ PA_CHANNEL_POSITION_REAR_RIGHT, /**< Microsoft calls this 'Back Right', Apple calls this 'Right Surround' (!), Dolby calls this 'Surround Rear Right' */
+
+ PA_CHANNEL_POSITION_LFE, /**< Microsoft calls this 'Low Frequency', Apple calls this 'LFEScreen' */
+/** \cond fulldocs */
+ PA_CHANNEL_POSITION_SUBWOOFER = PA_CHANNEL_POSITION_LFE,
+/** \endcond */
+
+ PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER, /**< Apple, Dolby call this 'Left Center' */
+ PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER, /**< Apple, Dolby call this 'Right Center */
+
+ PA_CHANNEL_POSITION_SIDE_LEFT, /**< Apple calls this 'Left Surround Direct', Dolby calls this 'Surround Left' (!) */
+ PA_CHANNEL_POSITION_SIDE_RIGHT, /**< Apple calls this 'Right Surround Direct', Dolby calls this 'Surround Right' (!) */
+ PA_CHANNEL_POSITION_AUX0,
+ PA_CHANNEL_POSITION_AUX1,
+ PA_CHANNEL_POSITION_AUX2,
+ PA_CHANNEL_POSITION_AUX3,
+ PA_CHANNEL_POSITION_AUX4,
+ PA_CHANNEL_POSITION_AUX5,
+ PA_CHANNEL_POSITION_AUX6,
+ PA_CHANNEL_POSITION_AUX7,
+ PA_CHANNEL_POSITION_AUX8,
+ PA_CHANNEL_POSITION_AUX9,
+ PA_CHANNEL_POSITION_AUX10,
+ PA_CHANNEL_POSITION_AUX11,
+ PA_CHANNEL_POSITION_AUX12,
+ PA_CHANNEL_POSITION_AUX13,
+ PA_CHANNEL_POSITION_AUX14,
+ PA_CHANNEL_POSITION_AUX15,
+ PA_CHANNEL_POSITION_AUX16,
+ PA_CHANNEL_POSITION_AUX17,
+ PA_CHANNEL_POSITION_AUX18,
+ PA_CHANNEL_POSITION_AUX19,
+ PA_CHANNEL_POSITION_AUX20,
+ PA_CHANNEL_POSITION_AUX21,
+ PA_CHANNEL_POSITION_AUX22,
+ PA_CHANNEL_POSITION_AUX23,
+ PA_CHANNEL_POSITION_AUX24,
+ PA_CHANNEL_POSITION_AUX25,
+ PA_CHANNEL_POSITION_AUX26,
+ PA_CHANNEL_POSITION_AUX27,
+ PA_CHANNEL_POSITION_AUX28,
+ PA_CHANNEL_POSITION_AUX29,
+ PA_CHANNEL_POSITION_AUX30,
+ PA_CHANNEL_POSITION_AUX31,
+
+ PA_CHANNEL_POSITION_TOP_CENTER, /**< Apple calls this 'Top Center Surround' */
+
+ PA_CHANNEL_POSITION_TOP_FRONT_LEFT, /**< Apple calls this 'Vertical Height Left' */
+ PA_CHANNEL_POSITION_TOP_FRONT_RIGHT, /**< Apple calls this 'Vertical Height Right' */
+ PA_CHANNEL_POSITION_TOP_FRONT_CENTER, /**< Apple calls this 'Vertical Height Center' */
+
+ PA_CHANNEL_POSITION_TOP_REAR_LEFT, /**< Microsoft and Apple call this 'Top Back Left' */
+ PA_CHANNEL_POSITION_TOP_REAR_RIGHT, /**< Microsoft and Apple call this 'Top Back Right' */
+ PA_CHANNEL_POSITION_TOP_REAR_CENTER, /**< Microsoft and Apple call this 'Top Back Center' */
+
+ PA_CHANNEL_POSITION_MAX
+} pa_channel_position_t;
+
+typedef struct pa_channel_map {
+ uint8_t channels;
+ pa_channel_position_t map[PA_CHANNELS_MAX];
+} pa_channel_map;
+
+static inline int pa_channels_valid(uint8_t channels)
+{
+ return channels > 0 && channels <= PA_CHANNELS_MAX;
+}
+
+static inline int pa_channel_map_valid(const pa_channel_map *map)
+{
+ unsigned c;
+ if (!pa_channels_valid(map->channels))
+ return 0;
+ for (c = 0; c < map->channels; c++)
+ if (map->map[c] < 0 || map->map[c] >= PA_CHANNEL_POSITION_MAX)
+ return 0;
+ return 1;
+}
+static inline pa_channel_map* pa_channel_map_init(pa_channel_map *m)
+{
+ unsigned c;
+ m->channels = 0;
+ for (c = 0; c < PA_CHANNELS_MAX; c++)
+ m->map[c] = PA_CHANNEL_POSITION_INVALID;
+ return m;
+}
+
+static inline pa_channel_map* pa_channel_map_init_auto(pa_channel_map *m, unsigned channels, pa_channel_map_def_t def)
+{
+ unsigned i;
+
+ pa_assert(m);
+ pa_assert(pa_channels_valid(channels));
+ pa_assert(def < PA_CHANNEL_MAP_DEF_MAX);
+
+ pa_channel_map_init(m);
+
+ m->channels = (uint8_t) channels;
+
+ switch (def) {
+ case PA_CHANNEL_MAP_ALSA:
+ switch (channels) {
+ case 1:
+ m->map[0] = PA_CHANNEL_POSITION_MONO;
+ return m;
+ case 8:
+ m->map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
+ m->map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
+ SPA_FALLTHROUGH;
+ case 6:
+ m->map[5] = PA_CHANNEL_POSITION_LFE;
+ SPA_FALLTHROUGH;
+ case 5:
+ m->map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ SPA_FALLTHROUGH;
+ case 4:
+ m->map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ m->map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ SPA_FALLTHROUGH;
+ case 2:
+ m->map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ m->map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ return m;
+ default:
+ return NULL;
+ }
+ case PA_CHANNEL_MAP_AUX:
+ for (i = 0; i < channels; i++)
+ m->map[i] = PA_CHANNEL_POSITION_AUX0 + (i & 31);
+ return m;
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static inline pa_channel_map* pa_channel_map_init_extend(pa_channel_map *m,
+ unsigned channels, pa_channel_map_def_t def)
+{
+ pa_channel_map *r;
+ if ((r = pa_channel_map_init_auto(m, channels, def)) != NULL)
+ return r;
+ return pa_channel_map_init_auto(m, channels, PA_CHANNEL_MAP_AUX);
+}
+
+typedef uint64_t pa_channel_position_mask_t;
+
+#define PA_CHANNEL_POSITION_MASK(f) ((pa_channel_position_mask_t) (1ULL << (f)))
+
+#define PA_CHANNEL_POSITION_MASK_LEFT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT)) \
+
+#define PA_CHANNEL_POSITION_MASK_RIGHT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT))
+
+#define PA_CHANNEL_POSITION_MASK_CENTER \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_FRONT \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_REAR \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_REAR_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_LFE \
+ PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_LFE)
+
+#define PA_CHANNEL_POSITION_MASK_HFE \
+ (PA_CHANNEL_POSITION_MASK_REAR | PA_CHANNEL_POSITION_MASK_FRONT \
+ | PA_CHANNEL_POSITION_MASK_LEFT | PA_CHANNEL_POSITION_MASK_RIGHT \
+ | PA_CHANNEL_POSITION_MASK_CENTER)
+
+#define PA_CHANNEL_POSITION_MASK_SIDE_OR_TOP_CENTER \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_SIDE_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_TOP \
+ (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_FRONT_CENTER) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_LEFT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_RIGHT) \
+ | PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_TOP_REAR_CENTER))
+
+#define PA_CHANNEL_POSITION_MASK_ALL \
+ ((pa_channel_position_mask_t) (PA_CHANNEL_POSITION_MASK(PA_CHANNEL_POSITION_MAX)-1))
+
+static const char *const pa_position_table[PA_CHANNEL_POSITION_MAX] = {
+ [PA_CHANNEL_POSITION_MONO] = "mono",
+ [PA_CHANNEL_POSITION_FRONT_CENTER] = "front-center",
+ [PA_CHANNEL_POSITION_FRONT_LEFT] = "front-left",
+ [PA_CHANNEL_POSITION_FRONT_RIGHT] = "front-right",
+ [PA_CHANNEL_POSITION_REAR_CENTER] = "rear-center",
+ [PA_CHANNEL_POSITION_REAR_LEFT] = "rear-left",
+ [PA_CHANNEL_POSITION_REAR_RIGHT] = "rear-right",
+ [PA_CHANNEL_POSITION_LFE] = "lfe",
+ [PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER] = "front-left-of-center",
+ [PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER] = "front-right-of-center",
+ [PA_CHANNEL_POSITION_SIDE_LEFT] = "side-left",
+ [PA_CHANNEL_POSITION_SIDE_RIGHT] = "side-right",
+ [PA_CHANNEL_POSITION_AUX0] = "aux0",
+ [PA_CHANNEL_POSITION_AUX1] = "aux1",
+ [PA_CHANNEL_POSITION_AUX2] = "aux2",
+ [PA_CHANNEL_POSITION_AUX3] = "aux3",
+ [PA_CHANNEL_POSITION_AUX4] = "aux4",
+ [PA_CHANNEL_POSITION_AUX5] = "aux5",
+ [PA_CHANNEL_POSITION_AUX6] = "aux6",
+ [PA_CHANNEL_POSITION_AUX7] = "aux7",
+ [PA_CHANNEL_POSITION_AUX8] = "aux8",
+ [PA_CHANNEL_POSITION_AUX9] = "aux9",
+ [PA_CHANNEL_POSITION_AUX10] = "aux10",
+ [PA_CHANNEL_POSITION_AUX11] = "aux11",
+ [PA_CHANNEL_POSITION_AUX12] = "aux12",
+ [PA_CHANNEL_POSITION_AUX13] = "aux13",
+ [PA_CHANNEL_POSITION_AUX14] = "aux14",
+ [PA_CHANNEL_POSITION_AUX15] = "aux15",
+ [PA_CHANNEL_POSITION_AUX16] = "aux16",
+ [PA_CHANNEL_POSITION_AUX17] = "aux17",
+ [PA_CHANNEL_POSITION_AUX18] = "aux18",
+ [PA_CHANNEL_POSITION_AUX19] = "aux19",
+ [PA_CHANNEL_POSITION_AUX20] = "aux20",
+ [PA_CHANNEL_POSITION_AUX21] = "aux21",
+ [PA_CHANNEL_POSITION_AUX22] = "aux22",
+ [PA_CHANNEL_POSITION_AUX23] = "aux23",
+ [PA_CHANNEL_POSITION_AUX24] = "aux24",
+ [PA_CHANNEL_POSITION_AUX25] = "aux25",
+ [PA_CHANNEL_POSITION_AUX26] = "aux26",
+ [PA_CHANNEL_POSITION_AUX27] = "aux27",
+ [PA_CHANNEL_POSITION_AUX28] = "aux28",
+ [PA_CHANNEL_POSITION_AUX29] = "aux29",
+ [PA_CHANNEL_POSITION_AUX30] = "aux30",
+ [PA_CHANNEL_POSITION_AUX31] = "aux31",
+
+ [PA_CHANNEL_POSITION_TOP_CENTER] = "top-center",
+ [PA_CHANNEL_POSITION_TOP_FRONT_CENTER] = "top-front-center",
+ [PA_CHANNEL_POSITION_TOP_FRONT_LEFT] = "top-front-left",
+ [PA_CHANNEL_POSITION_TOP_FRONT_RIGHT] = "top-front-right",
+ [PA_CHANNEL_POSITION_TOP_REAR_CENTER] = "top-rear-center",
+ [PA_CHANNEL_POSITION_TOP_REAR_LEFT] = "top-rear-left",
+ [PA_CHANNEL_POSITION_TOP_REAR_RIGHT] = "top-rear-right"
+};
+
+static inline pa_channel_position_t pa_channel_position_from_string(const char *p)
+{
+ pa_channel_position_t i;
+ /* Some special aliases */
+ if (pa_streq(p, "left"))
+ return PA_CHANNEL_POSITION_LEFT;
+ else if (pa_streq(p, "right"))
+ return PA_CHANNEL_POSITION_RIGHT;
+ else if (pa_streq(p, "center"))
+ return PA_CHANNEL_POSITION_CENTER;
+ else if (pa_streq(p, "subwoofer"))
+ return PA_CHANNEL_POSITION_SUBWOOFER;
+ for (i = 0; i < PA_CHANNEL_POSITION_MAX; i++)
+ if (pa_streq(p, pa_position_table[i]))
+ return i;
+ return PA_CHANNEL_POSITION_INVALID;
+}
+
+static inline pa_channel_map *pa_channel_map_parse(pa_channel_map *rmap, const char *s)
+{
+ const char *state;
+ pa_channel_map map;
+ char *p;
+ pa_channel_map_init(&map);
+ if (pa_streq(s, "stereo")) {
+ map.channels = 2;
+ map.map[0] = PA_CHANNEL_POSITION_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_RIGHT;
+ goto finish;
+ } else if (pa_streq(s, "surround-21")) {
+ map.channels = 3;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-40")) {
+ map.channels = 4;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ goto finish;
+ } else if (pa_streq(s, "surround-41")) {
+ map.channels = 5;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-50")) {
+ map.channels = 5;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ goto finish;
+ } else if (pa_streq(s, "surround-51")) {
+ map.channels = 6;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ map.map[5] = PA_CHANNEL_POSITION_LFE;
+ goto finish;
+ } else if (pa_streq(s, "surround-71")) {
+ map.channels = 8;
+ map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
+ map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
+ map.map[2] = PA_CHANNEL_POSITION_REAR_LEFT;
+ map.map[3] = PA_CHANNEL_POSITION_REAR_RIGHT;
+ map.map[4] = PA_CHANNEL_POSITION_FRONT_CENTER;
+ map.map[5] = PA_CHANNEL_POSITION_LFE;
+ map.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
+ map.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
+ goto finish;
+ }
+ state = NULL;
+ map.channels = 0;
+ while ((p = pa_split(s, ",", &state))) {
+ pa_channel_position_t f;
+
+ if (map.channels >= PA_CHANNELS_MAX) {
+ pa_xfree(p);
+ return NULL;
+ }
+ if ((f = pa_channel_position_from_string(p)) == PA_CHANNEL_POSITION_INVALID) {
+ pa_xfree(p);
+ return NULL;
+ }
+ map.map[map.channels++] = f;
+ pa_xfree(p);
+ }
+finish:
+ if (!pa_channel_map_valid(&map))
+ return NULL;
+ *rmap = map;
+ return rmap;
+}
+
+static inline const char* pa_channel_position_to_string(pa_channel_position_t pos) {
+
+ if (pos < 0 || pos >= PA_CHANNEL_POSITION_MAX)
+ return NULL;
+ return pa_position_table[pos];
+}
+
+static inline int pa_channel_map_equal(const pa_channel_map *a, const pa_channel_map *b)
+{
+ unsigned c;
+ if (PA_UNLIKELY(a == b))
+ return 1;
+ if (a->channels != b->channels)
+ return 0;
+ for (c = 0; c < a->channels; c++)
+ if (a->map[c] != b->map[c])
+ return 0;
+ return 1;
+}
+
+static inline char* pa_channel_map_snprint(char *s, size_t l, const pa_channel_map *map) {
+ unsigned channel;
+ bool first = true;
+ char *e;
+ if (!pa_channel_map_valid(map)) {
+ pa_snprintf(s, l, "%s", _("(invalid)"));
+ return s;
+ }
+ *(e = s) = 0;
+ for (channel = 0; channel < map->channels && l > 1; channel++) {
+ l -= pa_snprintf(e, l, "%s%s",
+ first ? "" : ",",
+ pa_channel_position_to_string(map->map[channel]));
+ e = strchr(e, 0);
+ first = false;
+ }
+
+ return s;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_CHANNELMAP_H */
diff --git a/spa/plugins/alsa/acp/compat.c b/spa/plugins/alsa/acp/compat.c
new file mode 100644
index 0000000..7703444
--- /dev/null
+++ b/spa/plugins/alsa/acp/compat.c
@@ -0,0 +1,210 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2009 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "compat.h"
+#include "device-port.h"
+#include "alsa-mixer.h"
+
+static const char *port_types[] = {
+ [PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown",
+ [PA_DEVICE_PORT_TYPE_AUX] = "aux",
+ [PA_DEVICE_PORT_TYPE_SPEAKER] = "speaker",
+ [PA_DEVICE_PORT_TYPE_HEADPHONES] = "headphones",
+ [PA_DEVICE_PORT_TYPE_LINE] = "line",
+ [PA_DEVICE_PORT_TYPE_MIC] = "mic",
+ [PA_DEVICE_PORT_TYPE_HEADSET] = "headset",
+ [PA_DEVICE_PORT_TYPE_HANDSET] = "handset",
+ [PA_DEVICE_PORT_TYPE_EARPIECE] = "earpiece",
+ [PA_DEVICE_PORT_TYPE_SPDIF] = "spdif",
+ [PA_DEVICE_PORT_TYPE_HDMI] = "hdmi",
+ [PA_DEVICE_PORT_TYPE_TV] = "tv",
+ [PA_DEVICE_PORT_TYPE_RADIO] = "radio",
+ [PA_DEVICE_PORT_TYPE_VIDEO] = "video",
+ [PA_DEVICE_PORT_TYPE_USB] = "usb",
+ [PA_DEVICE_PORT_TYPE_BLUETOOTH] = "bluetooth",
+ [PA_DEVICE_PORT_TYPE_PORTABLE] = "portable",
+ [PA_DEVICE_PORT_TYPE_HANDSFREE] = "handsfree",
+ [PA_DEVICE_PORT_TYPE_CAR] = "car",
+ [PA_DEVICE_PORT_TYPE_HIFI] = "hifi",
+ [PA_DEVICE_PORT_TYPE_PHONE] = "phone",
+ [PA_DEVICE_PORT_TYPE_NETWORK] = "network",
+ [PA_DEVICE_PORT_TYPE_ANALOG] = "analog",
+};
+
+static const char *str_port_type(pa_device_port_type_t type)
+{
+ int idx = (type < PA_ELEMENTSOF(port_types)) ? type : 0;
+ return port_types[idx];
+}
+
+pa_device_port_new_data *pa_device_port_new_data_init(pa_device_port_new_data *data)
+{
+ pa_assert(data);
+ pa_zero(*data);
+ data->type = PA_DEVICE_PORT_TYPE_UNKNOWN;
+ data->available = PA_AVAILABLE_UNKNOWN;
+ return data;
+}
+
+void pa_device_port_new_data_set_name(pa_device_port_new_data *data, const char *name)
+{
+ pa_assert(data);
+ pa_xfree(data->name);
+ data->name = pa_xstrdup(name);
+}
+
+void pa_device_port_new_data_set_description(pa_device_port_new_data *data, const char *description)
+{
+ pa_assert(data);
+ pa_xfree(data->description);
+ data->description = pa_xstrdup(description);
+}
+
+void pa_device_port_new_data_set_available(pa_device_port_new_data *data, pa_available_t available)
+{
+ pa_assert(data);
+ data->available = available;
+}
+
+void pa_device_port_new_data_set_availability_group(pa_device_port_new_data *data, const char *group)
+{
+ pa_assert(data);
+ pa_xfree(data->availability_group);
+ data->availability_group = pa_xstrdup(group);
+}
+
+void pa_device_port_new_data_set_direction(pa_device_port_new_data *data, pa_direction_t direction)
+{
+ pa_assert(data);
+ data->direction = direction;
+}
+
+void pa_device_port_new_data_set_type(pa_device_port_new_data *data, pa_device_port_type_t type)
+{
+ pa_assert(data);
+ data->type = type;
+}
+
+void pa_device_port_new_data_done(pa_device_port_new_data *data)
+{
+ pa_assert(data);
+ pa_xfree(data->name);
+ pa_xfree(data->description);
+ pa_xfree(data->availability_group);
+}
+
+pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, size_t extra)
+{
+ pa_device_port *p;
+
+ pa_assert(data);
+ pa_assert(data->name);
+ pa_assert(data->description);
+ pa_assert(data->direction == PA_DIRECTION_OUTPUT || data->direction == PA_DIRECTION_INPUT);
+
+ p = calloc(1, sizeof(pa_device_port) + extra);
+
+ p->port.name = p->name = data->name;
+ data->name = NULL;
+ p->port.description = p->description = data->description;
+ data->description = NULL;
+ p->priority = p->port.priority = 0;
+ p->available = data->available;
+ p->port.available = (enum acp_available) data->available;
+ p->availability_group = data->availability_group;
+ data->availability_group = NULL;
+ p->profiles = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
+ p->direction = data->direction;
+ p->port.direction = data->direction == PA_DIRECTION_OUTPUT ?
+ ACP_DIRECTION_PLAYBACK : ACP_DIRECTION_CAPTURE;
+ p->type = data->type;
+
+ p->proplist = pa_proplist_new();
+ pa_proplist_sets(p->proplist, ACP_KEY_PORT_TYPE, str_port_type(data->type));
+ if (p->availability_group)
+ pa_proplist_sets(p->proplist, ACP_KEY_PORT_AVAILABILITY_GROUP, p->availability_group);
+
+ p->user_data = (void*)((uint8_t*)p + sizeof(pa_device_port));
+
+ return p;
+}
+
+void pa_device_port_free(pa_device_port *port)
+{
+ pa_xfree(port->name);
+ pa_xfree(port->description);
+ pa_xfree(port->availability_group);
+ pa_hashmap_free(port->profiles);
+ pa_proplist_free(port->proplist);
+ if (port->impl_free)
+ port->impl_free (port);
+ free(port);
+}
+
+void pa_device_port_set_available(pa_device_port *p, pa_available_t status)
+{
+ pa_available_t old = p->available;
+
+ if (old == status)
+ return;
+ p->available = status;
+ p->port.available = (enum acp_available) status;
+
+ if (p->card && p->card->events && p->card->events->port_available)
+ p->card->events->port_available(p->card->user_data, p->port.index,
+ (enum acp_available)old, p->port.available);
+}
+
+bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card) {
+ const char *s, *d = NULL, *k;
+ pa_assert(p);
+
+ if (pa_proplist_contains(p, PA_PROP_DEVICE_DESCRIPTION))
+ return true;
+
+ if (card)
+ if ((s = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_DESCRIPTION)))
+ d = s;
+
+ if (!d)
+ if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_FORM_FACTOR)))
+ if (pa_streq(s, "internal"))
+ d = _("Built-in Audio");
+
+ if (!d)
+ if ((s = pa_proplist_gets(p, PA_PROP_DEVICE_CLASS)))
+ if (pa_streq(s, "modem"))
+ d = _("Modem");
+
+ if (!d)
+ d = pa_proplist_gets(p, PA_PROP_DEVICE_PRODUCT_NAME);
+
+ if (!d)
+ return false;
+
+ k = pa_proplist_gets(p, PA_PROP_DEVICE_PROFILE_DESCRIPTION);
+
+ if (d && k)
+ pa_proplist_setf(p, PA_PROP_DEVICE_DESCRIPTION, "%s %s", d, k);
+ else if (d)
+ pa_proplist_sets(p, PA_PROP_DEVICE_DESCRIPTION, d);
+
+ return true;
+}
diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h
new file mode 100644
index 0000000..46ef1fd
--- /dev/null
+++ b/spa/plugins/alsa/acp/compat.h
@@ -0,0 +1,656 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+
+#ifndef PULSE_COMPAT_H
+#define PULSE_COMPAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+typedef struct pa_core pa_core;
+
+typedef void *(*pa_copy_func_t)(const void *p);
+typedef void (*pa_free_cb_t)(void *p);
+
+#ifdef __GNUC__
+#define PA_LIKELY(x) (__builtin_expect(!!(x),1))
+#define PA_UNLIKELY(x) (__builtin_expect(!!(x),0))
+#define PA_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#else
+#define PA_LIKELY(x) (x)
+#define PA_UNLIKELY(x) (x)
+#define PA_PRINTF_FUNC(fmt, arg1)
+#endif
+
+#define PA_MIN(a,b) \
+({ \
+ __typeof__(a) _a = (a); \
+ __typeof__(b) _b = (b); \
+ PA_LIKELY(_a < _b) ? _a : _b; \
+})
+#define PA_MAX(a,b) \
+({ \
+ __typeof__(a) _a = (a); \
+ __typeof__(b) _b = (b); \
+ PA_LIKELY(_a > _b) ? _a : _b; \
+})
+#define PA_CLAMP_UNLIKELY(v,low,high) \
+({ \
+ __typeof__(v) _v = (v); \
+ __typeof__(low) _low = (low); \
+ __typeof__(high) _high = (high); \
+ PA_MIN(PA_MAX(_v, _low), _high); \
+})
+
+#define PA_PTR_TO_UINT(p) ((unsigned int) ((uintptr_t) (p)))
+#define PA_UINT_TO_PTR(u) ((void*) ((uintptr_t) (u)))
+
+#include "array.h"
+#include "llist.h"
+#include "hashmap.h"
+#include "dynarray.h"
+#include "idxset.h"
+#include "proplist.h"
+
+typedef enum pa_direction {
+ PA_DIRECTION_OUTPUT = 0x0001U, /**< Output direction */
+ PA_DIRECTION_INPUT = 0x0002U /**< Input direction */
+} pa_direction_t;
+
+/* This enum replaces pa_port_available_t (defined in pulse/def.h) for
+ * internal use, so make sure both enum types stay in sync. */
+typedef enum pa_available {
+ PA_AVAILABLE_UNKNOWN = 0,
+ PA_AVAILABLE_NO = 1,
+ PA_AVAILABLE_YES = 2,
+} pa_available_t;
+
+#define PA_RATE_MAX (48000U*8U)
+
+typedef enum pa_sample_format {
+ PA_SAMPLE_U8, /**< Unsigned 8 Bit PCM */
+ PA_SAMPLE_ALAW, /**< 8 Bit a-Law */
+ PA_SAMPLE_ULAW, /**< 8 Bit mu-Law */
+ PA_SAMPLE_S16LE, /**< Signed 16 Bit PCM, little endian (PC) */
+ PA_SAMPLE_S16BE, /**< Signed 16 Bit PCM, big endian */
+ PA_SAMPLE_FLOAT32LE, /**< 32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0 */
+ PA_SAMPLE_FLOAT32BE, /**< 32 Bit IEEE floating point, big endian, range -1.0 to 1.0 */
+ PA_SAMPLE_S32LE, /**< Signed 32 Bit PCM, little endian (PC) */
+ PA_SAMPLE_S32BE, /**< Signed 32 Bit PCM, big endian */
+ PA_SAMPLE_S24LE, /**< Signed 24 Bit PCM packed, little endian (PC). \since 0.9.15 */
+ PA_SAMPLE_S24BE, /**< Signed 24 Bit PCM packed, big endian. \since 0.9.15 */
+ PA_SAMPLE_S24_32LE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC). \since 0.9.15 */
+ PA_SAMPLE_S24_32BE, /**< Signed 24 Bit PCM in LSB of 32 Bit words, big endian. \since 0.9.15 */
+ PA_SAMPLE_MAX, /**< Upper limit of valid sample types */
+ PA_SAMPLE_INVALID = -1 /**< An invalid value */
+} pa_sample_format_t;
+
+static inline int pa_sample_format_valid(unsigned format)
+{
+ return format < PA_SAMPLE_MAX;
+}
+
+#ifdef WORDS_BIGENDIAN
+#define PA_SAMPLE_S16NE PA_SAMPLE_S16BE
+#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32BE
+#define PA_SAMPLE_S32NE PA_SAMPLE_S32BE
+#define PA_SAMPLE_S24NE PA_SAMPLE_S24BE
+#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32BE
+#define PA_SAMPLE_S16RE PA_SAMPLE_S16LE
+#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32LE
+#define PA_SAMPLE_S32RE PA_SAMPLE_S32LE
+#define PA_SAMPLE_S24RE PA_SAMPLE_S24LE
+#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32LE
+#else
+#define PA_SAMPLE_S16NE PA_SAMPLE_S16LE
+#define PA_SAMPLE_FLOAT32NE PA_SAMPLE_FLOAT32LE
+#define PA_SAMPLE_S32NE PA_SAMPLE_S32LE
+#define PA_SAMPLE_S24NE PA_SAMPLE_S24LE
+#define PA_SAMPLE_S24_32NE PA_SAMPLE_S24_32LE
+#define PA_SAMPLE_S16RE PA_SAMPLE_S16BE
+#define PA_SAMPLE_FLOAT32RE PA_SAMPLE_FLOAT32BE
+#define PA_SAMPLE_S32RE PA_SAMPLE_S32BE
+#define PA_SAMPLE_S24RE PA_SAMPLE_S24BE
+#define PA_SAMPLE_S24_32RE PA_SAMPLE_S24_32BE
+#endif
+
+static const size_t pa_sample_size_table[] = {
+ [PA_SAMPLE_U8] = 1,
+ [PA_SAMPLE_ULAW] = 1,
+ [PA_SAMPLE_ALAW] = 1,
+ [PA_SAMPLE_S16LE] = 2,
+ [PA_SAMPLE_S16BE] = 2,
+ [PA_SAMPLE_FLOAT32LE] = 4,
+ [PA_SAMPLE_FLOAT32BE] = 4,
+ [PA_SAMPLE_S32LE] = 4,
+ [PA_SAMPLE_S32BE] = 4,
+ [PA_SAMPLE_S24LE] = 3,
+ [PA_SAMPLE_S24BE] = 3,
+ [PA_SAMPLE_S24_32LE] = 4,
+ [PA_SAMPLE_S24_32BE] = 4
+};
+
+static inline const char *pa_sample_format_to_string(pa_sample_format_t f)
+{
+ static const char* const table[]= {
+ [PA_SAMPLE_U8] = "u8",
+ [PA_SAMPLE_ALAW] = "aLaw",
+ [PA_SAMPLE_ULAW] = "uLaw",
+ [PA_SAMPLE_S16LE] = "s16le",
+ [PA_SAMPLE_S16BE] = "s16be",
+ [PA_SAMPLE_FLOAT32LE] = "float32le",
+ [PA_SAMPLE_FLOAT32BE] = "float32be",
+ [PA_SAMPLE_S32LE] = "s32le",
+ [PA_SAMPLE_S32BE] = "s32be",
+ [PA_SAMPLE_S24LE] = "s24le",
+ [PA_SAMPLE_S24BE] = "s24be",
+ [PA_SAMPLE_S24_32LE] = "s24-32le",
+ [PA_SAMPLE_S24_32BE] = "s24-32be",
+ };
+
+ if (!pa_sample_format_valid(f))
+ return NULL;
+ return table[f];
+}
+
+typedef struct pa_sample_spec {
+ pa_sample_format_t format;
+ uint32_t rate;
+ uint8_t channels;
+} pa_sample_spec;
+
+typedef uint64_t pa_usec_t;
+#define PA_MSEC_PER_SEC ((pa_usec_t) 1000ULL)
+#define PA_USEC_PER_SEC ((pa_usec_t) 1000000ULL)
+#define PA_USEC_PER_MSEC ((pa_usec_t) 1000ULL)
+
+static inline size_t pa_usec_to_bytes(pa_usec_t t, const pa_sample_spec *spec) {
+ return (size_t) (((t * spec->rate) / PA_USEC_PER_SEC)) *
+ (pa_sample_size_table[spec->format] * spec->channels);
+}
+
+static inline int pa_sample_rate_valid(uint32_t rate) {
+ return rate > 0 && rate <= PA_RATE_MAX * 101 / 100;
+}
+
+static inline size_t pa_frame_size(const pa_sample_spec *spec) {
+ return pa_sample_size_table[spec->format] * spec->channels;
+}
+
+typedef enum pa_log_level {
+ PA_LOG_ERROR = 0, /* Error messages */
+ PA_LOG_WARN = 1, /* Warning messages */
+ PA_LOG_NOTICE = 2, /* Notice messages */
+ PA_LOG_INFO = 3, /* Info messages */
+ PA_LOG_DEBUG = 4, /* Debug messages */
+ PA_LOG_LEVEL_MAX
+} pa_log_level_t;
+
+extern int _acp_log_level;
+extern acp_log_func _acp_log_func;
+extern void * _acp_log_data;
+
+#define pa_log_level_enabled(lev) (_acp_log_level >= (int)(lev))
+
+#define pa_log_levelv_meta(lev,f,l,func,fmt,ap) \
+({ \
+ if (pa_log_level_enabled (lev) && _acp_log_func) \
+ _acp_log_func(_acp_log_data,lev,f,l,func,fmt,ap); \
+})
+
+static inline PA_PRINTF_FUNC(5, 6) void pa_log_level_meta(enum pa_log_level level,
+ const char *file, int line, const char *func,
+ const char *fmt, ...)
+{
+ va_list args;
+ va_start(args,fmt);
+ pa_log_levelv_meta(level,file,line,func,fmt,args);
+ va_end(args);
+}
+
+#define pa_logl(lev,fmt,...) pa_log_level_meta(lev,__FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
+#define pa_log_error(fmt,...) pa_logl(PA_LOG_ERROR, fmt, ##__VA_ARGS__)
+#define pa_log_warn(fmt,...) pa_logl(PA_LOG_WARN, fmt, ##__VA_ARGS__)
+#define pa_log_notice(fmt,...) pa_logl(PA_LOG_NOTICE, fmt, ##__VA_ARGS__)
+#define pa_log_info(fmt,...) pa_logl(PA_LOG_INFO, fmt, ##__VA_ARGS__)
+#define pa_log_debug(fmt,...) pa_logl(PA_LOG_DEBUG, fmt, ##__VA_ARGS__)
+#define pa_log pa_log_error
+
+#define pa_assert_se(expr) \
+ do { \
+ if (PA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ abort(); \
+ } \
+ } while (false)
+
+#define pa_assert(expr) \
+ do { \
+ if (PA_UNLIKELY(!(expr))) { \
+ fprintf(stderr, "'%s' failed at %s:%u %s()\n", \
+ #expr , __FILE__, __LINE__, __func__); \
+ abort(); \
+ } \
+ } while (false)
+
+#define pa_assert_not_reached() \
+ do { \
+ fprintf(stderr, "Code should not be reached at %s:%u %s()\n", \
+ __FILE__, __LINE__, __func__); \
+ abort(); \
+ } while (false)
+
+
+#define pa_memzero(x,l) (memset((x), 0, (l)))
+#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))
+
+#define PA_ELEMENTSOF(x) (sizeof(x)/sizeof((x)[0]))
+
+#define pa_streq(a,b) (!strcmp((a),(b)))
+#define pa_strneq(a,b,n) (!strncmp((a),(b),(n)))
+#define pa_strnull(s) ((s) ? (s) : "null")
+#define pa_startswith(s,pfx) (strstr(s, pfx) == s)
+
+PA_PRINTF_FUNC(3, 0)
+static inline size_t pa_vsnprintf(char *str, size_t size, const char *format, va_list ap)
+{
+ int ret;
+
+ pa_assert(str);
+ pa_assert(size > 0);
+ pa_assert(format);
+
+ ret = vsnprintf(str, size, format, ap);
+
+ str[size-1] = 0;
+
+ if (ret < 0)
+ return strlen(str);
+
+ if ((size_t) ret > size-1)
+ return size-1;
+
+ return (size_t) ret;
+}
+
+PA_PRINTF_FUNC(3, 4)
+static inline size_t pa_snprintf(char *str, size_t size, const char *format, ...)
+{
+ size_t ret;
+ va_list ap;
+
+ pa_assert(str);
+ pa_assert(size > 0);
+ pa_assert(format);
+
+ va_start(ap, format);
+ ret = pa_vsnprintf(str, size, format, ap);
+ va_end(ap);
+
+ return ret;
+}
+
+#define pa_xstrdup(s) ((s) != NULL ? strdup(s) : NULL)
+#define pa_xstrndup(s,n) ((s) != NULL ? strndup(s,n) : NULL)
+#define pa_xfree free
+#define pa_xmalloc malloc
+#define pa_xnew0(t,n) calloc(n, sizeof(t))
+#define pa_xnew(t,n) pa_xnew0(t,n)
+#define pa_xrealloc realloc
+#define pa_xrenew(t,p,n) ((t*) realloc(p, (n)*sizeof(t)))
+
+static inline void* pa_xmemdup(const void *p, size_t l) {
+ return memcpy(malloc(l), p, l);
+
+}
+#define pa_xnewdup(t,p,n) ((t*) pa_xmemdup((p), (n)*sizeof(t)))
+
+static inline void pa_xfreev(void**a)
+{
+ int i;
+ for (i = 0; a && a[i]; i++)
+ free(a[i]);
+ free(a);
+}
+static inline void pa_xstrfreev(char **a) {
+ pa_xfreev((void**)a);
+}
+
+
+#define pa_cstrerror strerror
+
+#define PA_PATH_SEP "/"
+#define PA_PATH_SEP_CHAR '/'
+
+#define PA_WHITESPACE "\n\r \t"
+
+static PA_PRINTF_FUNC(1,2) inline char *pa_sprintf_malloc(const char *fmt, ...)
+{
+ char *res;
+ va_list args;
+ va_start(args, fmt);
+ if (vasprintf(&res, fmt, args) < 0)
+ res = NULL;
+ va_end(args);
+ return res;
+}
+
+#define pa_fopen_cloexec(f,m) fopen(f,m"e")
+
+static inline char *pa_path_get_filename(const char *p)
+{
+ char *fn;
+ if (!p)
+ return NULL;
+ if ((fn = strrchr(p, PA_PATH_SEP_CHAR)))
+ return fn+1;
+ return (char*) p;
+}
+
+static inline bool pa_is_path_absolute(const char *fn)
+{
+ return *fn == PA_PATH_SEP_CHAR;
+}
+
+static inline char* pa_maybe_prefix_path(const char *path, const char *prefix)
+{
+ if (pa_is_path_absolute(path))
+ return pa_xstrdup(path);
+ return pa_sprintf_malloc("%s" PA_PATH_SEP "%s", prefix, path);
+}
+
+static inline bool pa_endswith(const char *s, const char *sfx)
+{
+ size_t l1, l2;
+ l1 = strlen(s);
+ l2 = strlen(sfx);
+ return l1 >= l2 && pa_streq(s + l1 - l2, sfx);
+}
+
+static inline char *pa_replace(const char*s, const char*a, const char *b)
+{
+ struct pa_array res;
+ size_t an, bn;
+
+ an = strlen(a);
+ bn = strlen(b);
+ pa_array_init(&res, an);
+
+ for (;;) {
+ const char *p;
+
+ if (!(p = strstr(s, a)))
+ break;
+
+ pa_array_add_data(&res, s, p-s);
+ pa_array_add_data(&res, b, bn);
+ s = p + an;
+ }
+ pa_array_add_data(&res, s, strlen(s) + 1);
+ return res.data;
+}
+
+static inline char *pa_split(const char *c, const char *delimiter, const char**state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current)
+ return NULL;
+ l = strcspn(current, delimiter);
+ *state = current+l;
+ if (**state)
+ (*state)++;
+ return pa_xstrndup(current, l);
+}
+
+static inline char *pa_split_spaces(const char *c, const char **state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current || *c == 0)
+ return NULL;
+ current += strspn(current, PA_WHITESPACE);
+ l = strcspn(current, PA_WHITESPACE);
+ *state = current+l;
+ return pa_xstrndup(current, l);
+}
+
+static inline char **pa_split_spaces_strv(const char *s)
+{
+ char **t, *e;
+ unsigned i = 0, n = 8;
+ const char *state = NULL;
+
+ t = pa_xnew(char*, n);
+ while ((e = pa_split_spaces(s, &state))) {
+ t[i++] = e;
+ if (i >= n) {
+ n *= 2;
+ t = pa_xrenew(char*, t, n);
+ }
+ }
+ if (i <= 0) {
+ pa_xfree(t);
+ return NULL;
+ }
+ t[i] = NULL;
+ return t;
+}
+
+static inline char* pa_str_strip_suffix(const char *str, const char *suffix)
+{
+ size_t str_l, suf_l, prefix;
+ char *ret;
+
+ str_l = strlen(str);
+ suf_l = strlen(suffix);
+
+ if (str_l < suf_l)
+ return NULL;
+ prefix = str_l - suf_l;
+ if (!pa_streq(&str[prefix], suffix))
+ return NULL;
+ ret = pa_xmalloc(prefix + 1);
+ memcpy(ret, str, prefix);
+ ret[prefix] = '\0';
+ return ret;
+}
+
+static inline const char *pa_split_in_place(const char *c, const char *delimiter, size_t *n, const char**state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current)
+ return NULL;
+ l = strcspn(current, delimiter);
+ *state = current+l;
+ if (**state)
+ (*state)++;
+ *n = l;
+ return current;
+}
+
+static inline const char *pa_split_spaces_in_place(const char *c, size_t *n, const char **state)
+{
+ const char *current = *state ? *state : c;
+ size_t l;
+ if (!*current || *c == 0)
+ return NULL;
+ current += strspn(current, PA_WHITESPACE);
+ l = strcspn(current, PA_WHITESPACE);
+ *state = current+l;
+ *n = l;
+ return current;
+}
+
+static inline bool pa_str_in_list_spaces(const char *haystack, const char *needle)
+{
+ const char *s;
+ size_t n;
+ const char *state = NULL;
+
+ if (!haystack || !needle)
+ return false;
+
+ while ((s = pa_split_spaces_in_place(haystack, &n, &state))) {
+ if (pa_strneq(needle, s, n))
+ return true;
+ }
+
+ return false;
+}
+
+static inline char *pa_strip(char *s)
+{
+ char *e, *l = NULL;
+ s += strspn(s, PA_WHITESPACE);
+ for (e = s; *e; e++)
+ if (!strchr(PA_WHITESPACE, *e))
+ l = e;
+ if (l)
+ *(l+1) = 0;
+ else
+ *s = 0;
+ return s;
+}
+
+static inline int pa_atod(const char *s, double *ret_d)
+{
+ if (spa_atod(s, ret_d) && !isnan(*ret_d))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atoi(const char *s, int32_t *ret_i)
+{
+ if (spa_atoi32(s, ret_i, 0))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atou(const char *s, uint32_t *ret_u)
+{
+ if (spa_atou32(s, ret_u, 0))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+static inline int pa_atol(const char *s, long *ret_l)
+{
+ int64_t res;
+ if (spa_atoi64(s, &res, 0)) {
+ *ret_l = res;
+ if (*ret_l == res)
+ return 0;
+ }
+ errno = EINVAL;
+ return -1;
+}
+
+static inline int pa_parse_boolean(const char *v)
+{
+ if (pa_streq(v, "1") || !strcasecmp(v, "y") || !strcasecmp(v, "t")
+ || !strcasecmp(v, "yes") || !strcasecmp(v, "true") || !strcasecmp(v, "on"))
+ return 1;
+ else if (pa_streq(v, "0") || !strcasecmp(v, "n") || !strcasecmp(v, "f")
+ || !strcasecmp(v, "no") || !strcasecmp(v, "false") || !strcasecmp(v, "off"))
+ return 0;
+ errno = EINVAL;
+ return -1;
+}
+
+static inline const char *pa_yes_no(bool b) {
+ return b ? "yes" : "no";
+}
+
+static inline const char *pa_strna(const char *x) {
+ return x ? x : "n/a";
+}
+
+static inline pa_sample_spec* pa_sample_spec_init(pa_sample_spec *spec)
+{
+ spec->format = PA_SAMPLE_INVALID;
+ spec->rate = 0;
+ spec->channels = 0;
+ return spec;
+}
+
+static inline char *pa_readlink(const char *p) {
+#ifdef HAVE_READLINK
+ size_t l = 100;
+
+ for (;;) {
+ char *c;
+ ssize_t n;
+
+ c = pa_xmalloc(l);
+
+ if ((n = readlink(p, c, l-1)) < 0) {
+ pa_xfree(c);
+ return NULL;
+ }
+
+ if ((size_t) n < l-1) {
+ c[n] = 0;
+ return c;
+ }
+
+ pa_xfree(c);
+ l *= 2;
+ }
+#else
+ return NULL;
+#endif
+}
+
+#include <spa/support/i18n.h>
+
+extern struct spa_i18n *acp_i18n;
+
+#define _(String) spa_i18n_text(acp_i18n, String)
+#ifdef gettext_noop
+#define N_(String) gettext_noop(String)
+#else
+#define N_(String) (String)
+#endif
+
+#include "channelmap.h"
+#include "volume.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_COMPAT_H */
diff --git a/spa/plugins/alsa/acp/conf-parser.c b/spa/plugins/alsa/acp/conf-parser.c
new file mode 100644
index 0000000..b4d4d47
--- /dev/null
+++ b/spa/plugins/alsa/acp/conf-parser.c
@@ -0,0 +1,387 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "config.h"
+
+#include <dirent.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "compat.h"
+
+#include "conf-parser.h"
+
+#define WHITESPACE " \t\n"
+#define COMMENTS "#;\n"
+
+/* Run the user supplied parser for an assignment */
+static int normal_assignment(pa_config_parser_state *state) {
+ const pa_config_item *item;
+
+ pa_assert(state);
+
+ for (item = state->item_table; item->parse; item++) {
+
+ if (item->lvalue && !pa_streq(state->lvalue, item->lvalue))
+ continue;
+
+ if (item->section && !state->section)
+ continue;
+
+ if (item->section && !pa_streq(state->section, item->section))
+ continue;
+
+ state->data = item->data;
+
+ return item->parse(state);
+ }
+
+ pa_log("[%s:%u] Unknown lvalue '%s' in section '%s'.", state->filename, state->lineno, state->lvalue, pa_strna(state->section));
+
+ return -1;
+}
+
+/* Parse a proplist entry. */
+static int proplist_assignment(pa_config_parser_state *state) {
+ pa_assert(state);
+ pa_assert(state->proplist);
+
+ if (pa_proplist_sets(state->proplist, state->lvalue, state->rvalue) < 0) {
+ pa_log("[%s:%u] Failed to parse a proplist entry: %s = %s", state->filename, state->lineno, state->lvalue, state->rvalue);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Parse a variable assignment line */
+static int parse_line(pa_config_parser_state *state) {
+ char *c;
+
+ state->lvalue = state->buf + strspn(state->buf, WHITESPACE);
+
+ if ((c = strpbrk(state->lvalue, COMMENTS)))
+ *c = 0;
+
+ if (!*state->lvalue)
+ return 0;
+
+ if (pa_startswith(state->lvalue, ".include ")) {
+ char *path = NULL, *fn;
+ int r;
+
+ fn = pa_strip(state->lvalue + 9);
+ if (!pa_is_path_absolute(fn)) {
+ const char *k;
+ if ((k = strrchr(state->filename, '/'))) {
+ char *dir = pa_xstrndup(state->filename, k - state->filename);
+ fn = path = pa_sprintf_malloc("%s" PA_PATH_SEP "%s", dir, fn);
+ pa_xfree(dir);
+ }
+ }
+
+ r = pa_config_parse(fn, NULL, state->item_table, state->proplist, false, state->userdata);
+ pa_xfree(path);
+ return r;
+ }
+
+ if (*state->lvalue == '[') {
+ size_t k;
+
+ k = strlen(state->lvalue);
+ pa_assert(k > 0);
+
+ if (state->lvalue[k-1] != ']') {
+ pa_log("[%s:%u] Invalid section header.", state->filename, state->lineno);
+ return -1;
+ }
+
+ pa_xfree(state->section);
+ state->section = pa_xstrndup(state->lvalue + 1, k-2);
+
+ if (pa_streq(state->section, "Properties")) {
+ if (!state->proplist) {
+ pa_log("[%s:%u] \"Properties\" section is not allowed in this file.", state->filename, state->lineno);
+ return -1;
+ }
+
+ state->in_proplist = true;
+ } else
+ state->in_proplist = false;
+
+ return 0;
+ }
+
+ if (!(state->rvalue = strchr(state->lvalue, '='))) {
+ pa_log("[%s:%u] Missing '='.", state->filename, state->lineno);
+ return -1;
+ }
+
+ *state->rvalue = 0;
+ state->rvalue++;
+
+ state->lvalue = pa_strip(state->lvalue);
+ state->rvalue = pa_strip(state->rvalue);
+
+ if (state->in_proplist)
+ return proplist_assignment(state);
+ else
+ return normal_assignment(state);
+}
+
+#ifndef OS_IS_WIN32
+static int conf_filter(const struct dirent *entry) {
+ return pa_endswith(entry->d_name, ".conf");
+}
+#endif
+
+/* Go through the file and parse each line */
+int pa_config_parse(const char *filename, FILE *f, const pa_config_item *t, pa_proplist *proplist, bool use_dot_d,
+ void *userdata) {
+ int r = -1;
+ bool do_close = !f;
+ pa_config_parser_state state;
+
+ pa_assert(filename);
+ pa_assert(t);
+
+ pa_zero(state);
+
+ if (!f && !(f = pa_fopen_cloexec(filename, "r"))) {
+ if (errno == ENOENT) {
+ pa_log_debug("Failed to open configuration file '%s': %s", filename, pa_cstrerror(errno));
+ r = 0;
+ goto finish;
+ }
+
+ pa_log_warn("Failed to open configuration file '%s': %s", filename, pa_cstrerror(errno));
+ goto finish;
+ }
+ pa_log_debug("Parsing configuration file '%s'", filename);
+
+ state.filename = filename;
+ state.item_table = t;
+ state.userdata = userdata;
+
+ if (proplist)
+ state.proplist = pa_proplist_new();
+
+ while (!feof(f)) {
+ if (!fgets(state.buf, sizeof(state.buf), f)) {
+ if (feof(f))
+ break;
+
+ pa_log_warn("Failed to read configuration file '%s': %s", filename, pa_cstrerror(errno));
+ goto finish;
+ }
+
+ state.lineno++;
+
+ if (parse_line(&state) < 0)
+ goto finish;
+ }
+
+ if (proplist)
+ pa_proplist_update(proplist, PA_UPDATE_REPLACE, state.proplist);
+
+ r = 0;
+
+finish:
+ if (state.proplist)
+ pa_proplist_free(state.proplist);
+
+ pa_xfree(state.section);
+
+ if (do_close && f)
+ fclose(f);
+
+ if (use_dot_d) {
+#ifdef OS_IS_WIN32
+ char *dir_name = pa_sprintf_malloc("%s.d", filename);
+ char *pattern = pa_sprintf_malloc("%s\\*.conf", dir_name);
+ HANDLE fh;
+ WIN32_FIND_DATA wfd;
+
+ fh = FindFirstFile(pattern, &wfd);
+ if (fh != INVALID_HANDLE_VALUE) {
+ do {
+ if (!(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
+ char *filename2 = pa_sprintf_malloc("%s\\%s", dir_name, wfd.cFileName);
+ pa_config_parse(filename2, NULL, t, proplist, false, userdata);
+ pa_xfree(filename2);
+ }
+ } while (FindNextFile(fh, &wfd));
+ FindClose(fh);
+ } else {
+ DWORD err = GetLastError();
+
+ if (err == ERROR_PATH_NOT_FOUND) {
+ pa_log_debug("Pattern %s did not match any files, ignoring.", pattern);
+ } else {
+ LPVOID msgbuf;
+ DWORD fret = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&msgbuf, 0, NULL);
+
+ if (fret != 0) {
+ pa_log_warn("FindFirstFile(%s) failed with error %ld (%s), ignoring.", pattern, err, (char*)msgbuf);
+ LocalFree(msgbuf);
+ } else {
+ pa_log_warn("FindFirstFile(%s) failed with error %ld, ignoring.", pattern, err);
+ pa_log_warn("FormatMessage failed with error %ld", GetLastError());
+ }
+ }
+ }
+
+ pa_xfree(pattern);
+ pa_xfree(dir_name);
+#else
+ char *dir_name;
+ int n;
+ struct dirent **entries = NULL;
+
+ dir_name = pa_sprintf_malloc("%s.d", filename);
+
+ n = scandir(dir_name, &entries, conf_filter, alphasort);
+ if (n >= 0) {
+ int i;
+
+ for (i = 0; i < n; i++) {
+ char *filename2;
+
+ filename2 = pa_sprintf_malloc("%s" PA_PATH_SEP "%s", dir_name, entries[i]->d_name);
+ pa_config_parse(filename2, NULL, t, proplist, false, userdata);
+ pa_xfree(filename2);
+
+ free(entries[i]);
+ }
+
+ free(entries);
+ } else {
+ if (errno == ENOENT)
+ pa_log_debug("%s does not exist, ignoring.", dir_name);
+ else
+ pa_log_warn("scandir(\"%s\") failed: %s", dir_name, pa_cstrerror(errno));
+ }
+
+ pa_xfree(dir_name);
+#endif
+ }
+
+ return r;
+}
+
+int pa_config_parse_int(pa_config_parser_state *state) {
+ int *i;
+ int32_t k;
+
+ pa_assert(state);
+
+ i = state->data;
+
+ if (pa_atoi(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *i = (int) k;
+ return 0;
+}
+
+int pa_config_parse_unsigned(pa_config_parser_state *state) {
+ unsigned *u;
+ uint32_t k;
+
+ pa_assert(state);
+
+ u = state->data;
+
+ if (pa_atou(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *u = (unsigned) k;
+ return 0;
+}
+
+int pa_config_parse_size(pa_config_parser_state *state) {
+ size_t *i;
+ uint32_t k;
+
+ pa_assert(state);
+
+ i = state->data;
+
+ if (pa_atou(state->rvalue, &k) < 0) {
+ pa_log("[%s:%u] Failed to parse numeric value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *i = (size_t) k;
+ return 0;
+}
+
+int pa_config_parse_bool(pa_config_parser_state *state) {
+ int k;
+ bool *b;
+
+ pa_assert(state);
+
+ b = state->data;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Failed to parse boolean value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *b = !!k;
+
+ return 0;
+}
+
+int pa_config_parse_not_bool(pa_config_parser_state *state) {
+ int k;
+ bool *b;
+
+ pa_assert(state);
+
+ b = state->data;
+
+ if ((k = pa_parse_boolean(state->rvalue)) < 0) {
+ pa_log("[%s:%u] Failed to parse boolean value: %s", state->filename, state->lineno, state->rvalue);
+ return -1;
+ }
+
+ *b = !k;
+
+ return 0;
+}
+
+int pa_config_parse_string(pa_config_parser_state *state) {
+ char **s;
+
+ pa_assert(state);
+
+ s = state->data;
+
+ pa_xfree(*s);
+ *s = *state->rvalue ? pa_xstrdup(state->rvalue) : NULL;
+ return 0;
+}
diff --git a/spa/plugins/alsa/acp/conf-parser.h b/spa/plugins/alsa/acp/conf-parser.h
new file mode 100644
index 0000000..4d19d7b
--- /dev/null
+++ b/spa/plugins/alsa/acp/conf-parser.h
@@ -0,0 +1,88 @@
+#ifndef fooconfparserhfoo
+#define fooconfparserhfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <stdio.h>
+#include <stdbool.h>
+
+#include "compat.h"
+
+/* An abstract parser for simple, line based, shallow configuration
+ * files consisting of variable assignments only. */
+
+typedef struct pa_config_parser_state pa_config_parser_state;
+
+typedef int (*pa_config_parser_cb_t)(pa_config_parser_state *state);
+
+/* Wraps info for parsing a specific configuration variable */
+typedef struct pa_config_item {
+ const char *lvalue; /* name of the variable */
+ pa_config_parser_cb_t parse; /* Function that is called to parse the variable's value */
+ void *data; /* Where to store the variable's data */
+ const char *section;
+} pa_config_item;
+
+struct pa_config_parser_state {
+ const char *filename;
+ unsigned lineno;
+ char *section;
+ char *lvalue;
+ char *rvalue;
+ void *data; /* The data pointer of the current pa_config_item. */
+ void *userdata; /* The pointer that was given to pa_config_parse(). */
+
+ /* Private data to be used only by conf-parser.c. */
+ const pa_config_item *item_table;
+ char buf[4096];
+ pa_proplist *proplist;
+ bool in_proplist;
+};
+
+/* The configuration file parsing routine. Expects a table of
+ * pa_config_items in *t that is terminated by an item where lvalue is
+ * NULL.
+ *
+ * If use_dot_d is true, then after parsing the file named by the filename
+ * argument, the function will parse all files ending with ".conf" in
+ * alphabetical order from a directory whose name is filename + ".d", if such
+ * directory exists.
+ *
+ * Some configuration files may contain a Properties section, which
+ * is a bit special. Normally all accepted lvalues must be predefined
+ * in the pa_config_item table, but in the Properties section the
+ * pa_config_item table is ignored, and all lvalues are accepted (as
+ * long as they are valid proplist keys). If the proplist pointer is
+ * non-NULL, the parser will parse any section named "Properties" as
+ * properties, and those properties will be merged into the given
+ * proplist. If proplist is NULL, then sections named "Properties"
+ * are not allowed at all in the configuration file. */
+int pa_config_parse(const char *filename, FILE *f, const pa_config_item *t, pa_proplist *proplist, bool use_dot_d,
+ void *userdata);
+
+/* Generic parsers for integers, size_t, booleans and strings */
+int pa_config_parse_int(pa_config_parser_state *state);
+int pa_config_parse_unsigned(pa_config_parser_state *state);
+int pa_config_parse_size(pa_config_parser_state *state);
+int pa_config_parse_bool(pa_config_parser_state *state);
+int pa_config_parse_not_bool(pa_config_parser_state *state);
+int pa_config_parse_string(pa_config_parser_state *state);
+
+#endif
diff --git a/spa/plugins/alsa/acp/device-port.h b/spa/plugins/alsa/acp/device-port.h
new file mode 100644
index 0000000..d6bdc22
--- /dev/null
+++ b/spa/plugins/alsa/acp/device-port.h
@@ -0,0 +1,119 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+
+#ifndef PULSE_DEVICE_PORT_H
+#define PULSE_DEVICE_PORT_H
+
+#ifdef __cplusplus
+extern "C" {
+#else
+#include <stdbool.h>
+#endif
+
+#include "compat.h"
+
+typedef struct pa_card pa_card;
+typedef struct pa_device_port pa_device_port;
+
+/** Port type. \since 14.0 */
+typedef enum pa_device_port_type {
+ PA_DEVICE_PORT_TYPE_UNKNOWN = 0,
+ PA_DEVICE_PORT_TYPE_AUX = 1,
+ PA_DEVICE_PORT_TYPE_SPEAKER = 2,
+ PA_DEVICE_PORT_TYPE_HEADPHONES = 3,
+ PA_DEVICE_PORT_TYPE_LINE = 4,
+ PA_DEVICE_PORT_TYPE_MIC = 5,
+ PA_DEVICE_PORT_TYPE_HEADSET = 6,
+ PA_DEVICE_PORT_TYPE_HANDSET = 7,
+ PA_DEVICE_PORT_TYPE_EARPIECE = 8,
+ PA_DEVICE_PORT_TYPE_SPDIF = 9,
+ PA_DEVICE_PORT_TYPE_HDMI = 10,
+ PA_DEVICE_PORT_TYPE_TV = 11,
+ PA_DEVICE_PORT_TYPE_RADIO = 12,
+ PA_DEVICE_PORT_TYPE_VIDEO = 13,
+ PA_DEVICE_PORT_TYPE_USB = 14,
+ PA_DEVICE_PORT_TYPE_BLUETOOTH = 15,
+ PA_DEVICE_PORT_TYPE_PORTABLE = 16,
+ PA_DEVICE_PORT_TYPE_HANDSFREE = 17,
+ PA_DEVICE_PORT_TYPE_CAR = 18,
+ PA_DEVICE_PORT_TYPE_HIFI = 19,
+ PA_DEVICE_PORT_TYPE_PHONE = 20,
+ PA_DEVICE_PORT_TYPE_NETWORK = 21,
+ PA_DEVICE_PORT_TYPE_ANALOG = 22,
+} pa_device_port_type_t;
+
+struct pa_device_port {
+ struct acp_port port;
+
+ pa_card *card;
+
+ char *name;
+ char *description;
+ char *preferred_profile;
+ pa_device_port_type_t type;
+
+ unsigned priority;
+ pa_available_t available; /* PA_AVAILABLE_UNKNOWN, PA_AVAILABLE_NO or PA_AVAILABLE_YES */
+ char *availability_group; /* a string identifier which determine the group of devices handling the available state simultaneously */
+
+ pa_direction_t direction;
+ int64_t latency_offset;
+
+ pa_proplist *proplist;
+ pa_hashmap *profiles;
+ pa_dynarray prof;
+
+ pa_dynarray devices;
+
+ void (*impl_free)(struct pa_device_port *port);
+ void *user_data;
+};
+
+#define PA_DEVICE_PORT_DATA(p) (p->user_data);
+
+typedef struct pa_device_port_new_data {
+ char *name;
+ char *description;
+ pa_available_t available;
+ char *availability_group;
+ pa_direction_t direction;
+ pa_device_port_type_t type;
+} pa_device_port_new_data;
+
+pa_device_port_new_data *pa_device_port_new_data_init(pa_device_port_new_data *data);
+void pa_device_port_new_data_set_name(pa_device_port_new_data *data, const char *name);
+void pa_device_port_new_data_set_description(pa_device_port_new_data *data, const char *description);
+void pa_device_port_new_data_set_available(pa_device_port_new_data *data, pa_available_t available);
+void pa_device_port_new_data_set_availability_group(pa_device_port_new_data *data, const char *group);
+void pa_device_port_new_data_set_direction(pa_device_port_new_data *data, pa_direction_t direction);
+void pa_device_port_new_data_set_type(pa_device_port_new_data *data, pa_device_port_type_t type);
+void pa_device_port_new_data_done(pa_device_port_new_data *data);
+
+pa_device_port *pa_device_port_new(pa_core *c, pa_device_port_new_data *data, size_t extra);
+void pa_device_port_free(pa_device_port *port);
+
+void pa_device_port_set_available(pa_device_port *p, pa_available_t status);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PULSE_DEVICE_PORT_H */
diff --git a/spa/plugins/alsa/acp/dynarray.h b/spa/plugins/alsa/acp/dynarray.h
new file mode 100644
index 0000000..762c84c
--- /dev/null
+++ b/spa/plugins/alsa/acp/dynarray.h
@@ -0,0 +1,159 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_DYNARRAY_H
+#define PA_DYNARRAY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+typedef struct pa_dynarray_item {
+ void *ptr;
+} pa_dynarray_item;
+
+typedef struct pa_dynarray {
+ pa_array array;
+ pa_free_cb_t free_cb;
+} pa_dynarray;
+
+static inline void pa_dynarray_init(pa_dynarray *array, pa_free_cb_t free_cb)
+{
+ pa_array_init(&array->array, 16);
+ array->free_cb = free_cb;
+}
+
+static inline void pa_dynarray_item_free(pa_dynarray *array, pa_dynarray_item *item)
+{
+ if (array->free_cb)
+ array->free_cb(item->ptr);
+}
+
+static inline void pa_dynarray_clear(pa_dynarray *array)
+{
+ pa_dynarray_item *item;
+ pa_array_for_each(item, &array->array)
+ pa_dynarray_item_free(array, item);
+ pa_array_clear(&array->array);
+}
+
+static inline pa_dynarray* pa_dynarray_new(pa_free_cb_t free_cb)
+{
+ pa_dynarray *d = calloc(1, sizeof(*d));
+ pa_dynarray_init(d, free_cb);
+ return d;
+}
+
+static inline void pa_dynarray_free(pa_dynarray *array)
+{
+ pa_dynarray_clear(array);
+ free(array);
+}
+
+static inline void pa_dynarray_append(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item = pa_array_add(&array->array, sizeof(*item));
+ item->ptr = p;
+}
+
+static inline pa_dynarray_item *pa_dynarray_find_item(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item;
+ pa_array_for_each(item, &array->array) {
+ if (item->ptr == p)
+ return item;
+ }
+ return NULL;
+}
+
+static inline pa_dynarray_item *pa_dynarray_get_item(pa_dynarray *array, unsigned i)
+{
+ if (!pa_array_check_index(&array->array, i, pa_dynarray_item))
+ return NULL;
+ return pa_array_get_unchecked(&array->array, i, pa_dynarray_item);
+}
+
+static inline void *pa_dynarray_get(pa_dynarray *array, unsigned i)
+{
+ pa_dynarray_item *item = pa_dynarray_get_item(array, i);
+ if (item == NULL)
+ return NULL;
+ return item->ptr;
+}
+
+static inline int pa_dynarray_insert_by_index(pa_dynarray *array, void *p, unsigned i)
+{
+ unsigned j, len;
+ pa_dynarray_item *item;
+
+ len = pa_array_get_len(&array->array, pa_dynarray_item);
+
+ if (i > len)
+ return -EINVAL;
+
+ item = pa_array_add(&array->array, sizeof(*item));
+ for (j = len; j > i; j--) {
+ item--;
+ item[1].ptr = item[0].ptr;
+ }
+ item->ptr = p;
+ return 0;
+}
+
+static inline int pa_dynarray_remove_by_index(pa_dynarray *array, unsigned i)
+{
+ pa_dynarray_item *item = pa_dynarray_get_item(array, i);
+ if (item == NULL)
+ return -ENOENT;
+ pa_dynarray_item_free(array, item);
+ pa_array_remove(&array->array, item);
+ return 0;
+}
+
+static inline int pa_dynarray_remove_by_data(pa_dynarray *array, void *p)
+{
+ pa_dynarray_item *item = pa_dynarray_find_item(array, p);
+ if (item == NULL)
+ return -ENOENT;
+ pa_dynarray_item_free(array, item);
+ pa_array_remove(&array->array, item);
+ return 0;
+}
+
+static inline unsigned pa_dynarray_size(pa_dynarray *array)
+{
+ return pa_array_get_len(&array->array, pa_dynarray_item);
+}
+
+#define PA_DYNARRAY_FOREACH(elem, array, idx) \
+ for ((idx) = 0; ((elem) = pa_dynarray_get(array, idx)); (idx)++)
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_DYNARRAY_H */
diff --git a/spa/plugins/alsa/acp/hashmap.h b/spa/plugins/alsa/acp/hashmap.h
new file mode 100644
index 0000000..d8dbadb
--- /dev/null
+++ b/spa/plugins/alsa/acp/hashmap.h
@@ -0,0 +1,219 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_HASHMAP_H
+#define PA_HASHMAP_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+typedef unsigned (*pa_hash_func_t)(const void *p);
+typedef int (*pa_compare_func_t)(const void *a, const void *b);
+
+typedef struct pa_hashmap_item {
+ void *key;
+ void *value;
+} pa_hashmap_item;
+
+typedef struct pa_hashmap {
+ pa_array array;
+ pa_hash_func_t hash_func;
+ pa_compare_func_t compare_func;
+ pa_free_cb_t key_free_func;
+ pa_free_cb_t value_free_func;
+} pa_hashmap;
+
+static inline pa_hashmap *pa_hashmap_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func)
+{
+ pa_hashmap *m = calloc(1, sizeof(pa_hashmap));
+ pa_array_init(&m->array, 16);
+ m->hash_func = hash_func;
+ m->compare_func = compare_func;
+ return m;
+}
+
+static inline pa_hashmap *pa_hashmap_new_full(pa_hash_func_t hash_func, pa_compare_func_t compare_func,
+ pa_free_cb_t key_free_func, pa_free_cb_t value_free_func)
+{
+ pa_hashmap *m = pa_hashmap_new(hash_func, compare_func);
+ m->key_free_func = key_free_func;
+ m->value_free_func = value_free_func;
+ return m;
+}
+
+static inline void pa_hashmap_item_free(pa_hashmap *h, pa_hashmap_item *item)
+{
+ if (h->key_free_func && item->key)
+ h->key_free_func(item->key);
+ if (h->value_free_func && item->value)
+ h->value_free_func(item->value);
+}
+
+static inline void pa_hashmap_remove_all(pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ pa_hashmap_item_free(h, item);
+ pa_array_reset(&h->array);
+}
+
+static inline void pa_hashmap_free(pa_hashmap *h)
+{
+ pa_hashmap_remove_all(h);
+ pa_array_clear(&h->array);
+ free(h);
+}
+
+static inline pa_hashmap_item* pa_hashmap_find_free(pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array) {
+ if (item->key == NULL)
+ return item;
+ }
+ return pa_array_add(&h->array, sizeof(*item));
+}
+
+static inline pa_hashmap_item* pa_hashmap_find(const pa_hashmap *h, const void *key)
+{
+ pa_hashmap_item *item = NULL;
+ pa_array_for_each(item, &h->array) {
+ if (item->key != NULL && h->compare_func(item->key, key) == 0)
+ return item;
+ }
+ return NULL;
+}
+
+static inline void* pa_hashmap_get(const pa_hashmap *h, const void *key)
+{
+ const pa_hashmap_item *item = pa_hashmap_find(h, key);
+ if (item == NULL)
+ return NULL;
+ return item->value;
+}
+
+static inline int pa_hashmap_put(pa_hashmap *h, void *key, void *value)
+{
+ pa_hashmap_item *item = pa_hashmap_find(h, key);
+ if (item != NULL)
+ return -1;
+ item = pa_hashmap_find_free(h);
+ item->key = key;
+ item->value = value;
+ return 0;
+}
+
+static inline void* pa_hashmap_remove(pa_hashmap *h, const void *key)
+{
+ pa_hashmap_item *item = pa_hashmap_find(h, key);
+ void *value;
+ if (item == NULL)
+ return NULL;
+ value = item->value;
+ if (h->key_free_func)
+ h->key_free_func(item->key);
+ item->key = NULL;
+ item->value = NULL;
+ return value;
+}
+
+static inline int pa_hashmap_remove_and_free(pa_hashmap *h, const void *key)
+{
+ void *val = pa_hashmap_remove(h, key);
+ if (val && h->value_free_func)
+ h->value_free_func(val);
+ return val ? 0 : -1;
+}
+
+static inline void *pa_hashmap_first(const pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array) {
+ if (item->key != NULL)
+ return item->value;
+ }
+ return NULL;
+}
+
+static inline void *pa_hashmap_iterate(const pa_hashmap *h, void **state, const void **key)
+{
+ pa_hashmap_item *it = *state;
+ if (it == NULL)
+ *state = pa_array_first(&h->array);
+ do {
+ it = *state;
+ if (!pa_array_check(&h->array, it))
+ return NULL;
+ *state = it + 1;
+ } while (it->key == NULL);
+ if (key)
+ *key = it->key;
+ return it->value;
+}
+
+static inline bool pa_hashmap_isempty(const pa_hashmap *h)
+{
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ if (item->key != NULL)
+ return false;
+ return true;
+}
+
+static inline unsigned pa_hashmap_size(const pa_hashmap *h)
+{
+ unsigned count = 0;
+ pa_hashmap_item *item;
+ pa_array_for_each(item, &h->array)
+ if (item->key != NULL)
+ count++;
+ return count;
+}
+
+static inline void pa_hashmap_sort(pa_hashmap *h,
+ int (*compar)(const void *, const void *))
+{
+ qsort((void*)h->array.data,
+ pa_array_get_len(&h->array, pa_hashmap_item),
+ sizeof(pa_hashmap_item), compar);
+}
+
+#define PA_HASHMAP_FOREACH(e, h, state) \
+ for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), NULL); \
+ (e); (e) = pa_hashmap_iterate((h), &(state), NULL))
+
+/* A macro to ease iteration through all key, value pairs */
+#define PA_HASHMAP_FOREACH_KV(k, e, h, state) \
+ for ((state) = NULL, (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k)); \
+ (e); (e) = pa_hashmap_iterate((h), &(state), (const void **) &(k)))
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_HASHMAP_H */
diff --git a/spa/plugins/alsa/acp/idxset.h b/spa/plugins/alsa/acp/idxset.h
new file mode 100644
index 0000000..6e88a84
--- /dev/null
+++ b/spa/plugins/alsa/acp/idxset.h
@@ -0,0 +1,200 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_IDXSET_H
+#define PA_IDXSET_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "array.h"
+
+#define PA_IDXSET_INVALID ((uint32_t) -1)
+
+typedef unsigned (*pa_hash_func_t)(const void *p);
+typedef int (*pa_compare_func_t)(const void *a, const void *b);
+
+typedef struct pa_idxset_item {
+ void *ptr;
+} pa_idxset_item;
+
+typedef struct pa_idxset {
+ pa_array array;
+ pa_hash_func_t hash_func;
+ pa_compare_func_t compare_func;
+} pa_idxset;
+
+static inline unsigned pa_idxset_trivial_hash_func(const void *p)
+{
+ return PA_PTR_TO_UINT(p);
+}
+
+static inline int pa_idxset_trivial_compare_func(const void *a, const void *b)
+{
+ return a < b ? -1 : (a > b ? 1 : 0);
+}
+
+static inline unsigned pa_idxset_string_hash_func(const void *p)
+{
+ unsigned hash = 0;
+ const char *c;
+ for (c = p; *c; c++)
+ hash = 31 * hash + (unsigned) *c;
+ return hash;
+}
+
+static inline int pa_idxset_string_compare_func(const void *a, const void *b)
+{
+ return strcmp(a, b);
+}
+
+static inline pa_idxset *pa_idxset_new(pa_hash_func_t hash_func, pa_compare_func_t compare_func)
+{
+ pa_idxset *s = calloc(1, sizeof(pa_idxset));
+ pa_array_init(&s->array, 16);
+ s->hash_func = hash_func;
+ s->compare_func = compare_func;
+ return s;
+}
+
+static inline void pa_idxset_free(pa_idxset *s, pa_free_cb_t free_cb)
+{
+ if (free_cb) {
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ free_cb(item->ptr);
+ }
+ pa_array_clear(&s->array);
+ free(s);
+}
+
+static inline pa_idxset_item* pa_idxset_find(const pa_idxset *s, const void *ptr)
+{
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array) {
+ if (item->ptr == ptr)
+ return item;
+ }
+ return NULL;
+}
+
+static inline int pa_idxset_put(pa_idxset*s, void *p, uint32_t *idx)
+{
+ pa_idxset_item *item = pa_idxset_find(s, p);
+ int res = item ? -1 : 0;
+ if (item == NULL) {
+ item = pa_idxset_find(s, NULL);
+ if (item == NULL)
+ item = pa_array_add(&s->array, sizeof(*item));
+ item->ptr = p;
+ }
+ if (idx)
+ *idx = item - (pa_idxset_item*)s->array.data;
+ return res;
+}
+
+static inline pa_idxset *pa_idxset_copy(pa_idxset *s, pa_copy_func_t copy_func)
+{
+ pa_idxset_item *item;
+ pa_idxset *copy = pa_idxset_new(s->hash_func, s->compare_func);
+ pa_array_for_each(item, &s->array) {
+ if (item->ptr)
+ pa_idxset_put(copy, copy_func ? copy_func(item->ptr) : item->ptr, NULL);
+ }
+ return copy;
+}
+
+static inline bool pa_idxset_isempty(const pa_idxset *s)
+{
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ if (item->ptr != NULL)
+ return false;
+ return true;
+}
+static inline unsigned pa_idxset_size(pa_idxset*s)
+{
+ unsigned count = 0;
+ pa_idxset_item *item;
+ pa_array_for_each(item, &s->array)
+ if (item->ptr != NULL)
+ count++;
+ return count;
+}
+
+static inline void *pa_idxset_search(pa_idxset *s, uint32_t *idx)
+{
+ pa_idxset_item *item;
+ for (item = pa_array_get_unchecked(&s->array, *idx, pa_idxset_item);
+ pa_array_check(&s->array, item); item++, (*idx)++) {
+ if (item->ptr != NULL)
+ return item->ptr;
+ }
+ *idx = PA_IDXSET_INVALID;
+ return NULL;
+}
+
+static inline void *pa_idxset_next(pa_idxset *s, uint32_t *idx)
+{
+ (*idx)++;;
+ return pa_idxset_search(s, idx);
+}
+
+static inline void* pa_idxset_first(pa_idxset *s, uint32_t *idx)
+{
+ uint32_t i = 0;
+ void *ptr = pa_idxset_search(s, &i);
+ if (idx)
+ *idx = i;
+ return ptr;
+}
+
+static inline void* pa_idxset_get_by_data(pa_idxset*s, const void *p, uint32_t *idx)
+{
+ pa_idxset_item *item = pa_idxset_find(s, p);
+ if (item == NULL)
+ return NULL;
+ if (idx)
+ *idx = item - (pa_idxset_item*)s->array.data;
+ return item->ptr;
+}
+
+static inline void* pa_idxset_get_by_index(pa_idxset*s, uint32_t idx)
+{
+ pa_idxset_item *item;
+ if (!pa_array_check_index(&s->array, idx, pa_idxset_item))
+ return NULL;
+ item = pa_array_get_unchecked(&s->array, idx, pa_idxset_item);
+ return item->ptr;
+}
+
+#define PA_IDXSET_FOREACH(e, s, idx) \
+ for ((e) = pa_idxset_first((s), &(idx)); (e); (e) = pa_idxset_next((s), &(idx)))
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_IDXSET_H */
diff --git a/spa/plugins/alsa/acp/llist.h b/spa/plugins/alsa/acp/llist.h
new file mode 100644
index 0000000..950375f
--- /dev/null
+++ b/spa/plugins/alsa/acp/llist.h
@@ -0,0 +1,109 @@
+#ifndef foollistfoo
+#define foollistfoo
+
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1 of the
+ License, or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+/* Some macros for maintaining doubly linked lists */
+
+/* The head of the linked list. Use this in the structure that shall
+ * contain the head of the linked list */
+#define PA_LLIST_HEAD(t,name) \
+ t *name
+
+/* The pointers in the linked list's items. Use this in the item structure */
+#define PA_LLIST_FIELDS(t) \
+ t *next, *prev
+
+/* Initialize the list's head */
+#define PA_LLIST_HEAD_INIT(t,item) \
+ do { \
+ (item) = (t*) NULL; } \
+ while(0)
+
+/* Initialize a list item */
+#define PA_LLIST_INIT(t,item) \
+ do { \
+ t *_item = (item); \
+ pa_assert(_item); \
+ _item->prev = _item->next = NULL; \
+ } while(0)
+
+/* Prepend an item to the list */
+#define PA_LLIST_PREPEND(t,head,item) \
+ do { \
+ t **_head = &(head), *_item = (item); \
+ pa_assert(_item); \
+ if ((_item->next = *_head)) \
+ _item->next->prev = _item; \
+ _item->prev = NULL; \
+ *_head = _item; \
+ } while (0)
+
+/* Remove an item from the list */
+#define PA_LLIST_REMOVE(t,head,item) \
+ do { \
+ t **_head = &(head), *_item = (item); \
+ pa_assert(_item); \
+ if (_item->next) \
+ _item->next->prev = _item->prev; \
+ if (_item->prev) \
+ _item->prev->next = _item->next; \
+ else { \
+ pa_assert(*_head == _item); \
+ *_head = _item->next; \
+ } \
+ _item->next = _item->prev = NULL; \
+ } while(0)
+
+/* Find the head of the list */
+#define PA_LLIST_FIND_HEAD(t,item,head) \
+ do { \
+ t **_head = (head), *_item = (item); \
+ *_head = _item; \
+ pa_assert(_head); \
+ while ((*_head)->prev) \
+ *_head = (*_head)->prev; \
+ } while (0)
+
+/* Insert an item after another one (a = where, b = what) */
+#define PA_LLIST_INSERT_AFTER(t,head,a,b) \
+ do { \
+ t **_head = &(head), *_a = (a), *_b = (b); \
+ pa_assert(_b); \
+ if (!_a) { \
+ if ((_b->next = *_head)) \
+ _b->next->prev = _b; \
+ _b->prev = NULL; \
+ *_head = _b; \
+ } else { \
+ if ((_b->next = _a->next)) \
+ _b->next->prev = _b; \
+ _b->prev = _a; \
+ _a->next = _b; \
+ } \
+ } while (0)
+
+#define PA_LLIST_FOREACH(i,head) \
+ for (i = (head); i; i = i->next)
+
+#define PA_LLIST_FOREACH_SAFE(i,n,head) \
+ for (i = (head); i && ((n = i->next), 1); i = n)
+
+#endif
diff --git a/spa/plugins/alsa/acp/meson.build b/spa/plugins/alsa/acp/meson.build
new file mode 100644
index 0000000..0ec97e2
--- /dev/null
+++ b/spa/plugins/alsa/acp/meson.build
@@ -0,0 +1,22 @@
+acp_sources = [
+ 'acp.c',
+ 'compat.c',
+ 'alsa-mixer.c',
+ 'alsa-ucm.c',
+ 'alsa-util.c',
+ 'conf-parser.c',
+]
+
+acp_c_args = [
+ '-DHAVE_ALSA_UCM',
+ '-DHAVE_READLINK',
+]
+
+acp_lib = static_library(
+ 'acp',
+ acp_sources,
+ c_args : acp_c_args,
+ include_directories : [configinc, includes_inc ],
+ dependencies : [ spa_dep, alsa_dep, mathlib, ]
+ )
+acp_dep = declare_dependency(link_with: acp_lib)
diff --git a/spa/plugins/alsa/acp/proplist.h b/spa/plugins/alsa/acp/proplist.h
new file mode 100644
index 0000000..ed4cc5d
--- /dev/null
+++ b/spa/plugins/alsa/acp/proplist.h
@@ -0,0 +1,211 @@
+/* ALSA Card Profile
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef PA_PROPLIST_H
+#define PA_PROPLIST_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+#include "array.h"
+#include "acp.h"
+
+#define PA_PROP_DEVICE_DESCRIPTION "device.description"
+
+#define PA_PROP_DEVICE_CLASS "device.class"
+
+#define PA_PROP_DEVICE_FORM_FACTOR "device.form_factor"
+
+#define PA_PROP_DEVICE_INTENDED_ROLES "device.intended_roles"
+
+#define PA_PROP_DEVICE_PROFILE_NAME "device.profile.name"
+
+#define PA_PROP_DEVICE_STRING "device.string"
+
+#define PA_PROP_DEVICE_API "device.api"
+
+#define PA_PROP_DEVICE_PRODUCT_NAME "device.product.name"
+
+#define PA_PROP_DEVICE_PROFILE_DESCRIPTION "device.profile.description"
+
+typedef struct pa_proplist_item {
+ char *key;
+ char *value;
+} pa_proplist_item;
+
+typedef struct pa_proplist {
+ struct pa_array array;
+} pa_proplist;
+
+static inline pa_proplist* pa_proplist_new(void)
+{
+ pa_proplist *p = calloc(1, sizeof(*p));
+ pa_array_init(&p->array, 16);
+ return p;
+}
+static inline pa_proplist_item* pa_proplist_item_find(const pa_proplist *p, const void *key)
+{
+ pa_proplist_item *item;
+ pa_array_for_each(item, &p->array) {
+ if (strcmp(key, item->key) == 0)
+ return item;
+ }
+ return NULL;
+}
+
+static inline void pa_proplist_item_free(pa_proplist_item* it)
+{
+ free(it->key);
+ free(it->value);
+}
+
+static inline void pa_proplist_clear(pa_proplist* p)
+{
+ pa_proplist_item *item;
+ pa_array_for_each(item, &p->array)
+ pa_proplist_item_free(item);
+ pa_array_reset(&p->array);
+}
+
+static inline void pa_proplist_free(pa_proplist* p)
+{
+ pa_proplist_clear(p);
+ pa_array_clear(&p->array);
+ free(p);
+}
+
+static inline unsigned pa_proplist_size(const pa_proplist *p)
+{
+ return pa_array_get_len(&p->array, pa_proplist_item);
+}
+
+static inline int pa_proplist_contains(const pa_proplist *p, const char *key)
+{
+ return pa_proplist_item_find(p, key) ? 1 : 0;
+}
+
+static inline int pa_proplist_sets(pa_proplist *p, const char *key, const char *value)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ if (item != NULL)
+ pa_proplist_item_free(item);
+ else
+ item = pa_array_add(&p->array, sizeof(*item));
+ item->key = strdup(key);
+ item->value = strdup(value);
+ return 0;
+}
+
+static inline int pa_proplist_unset(pa_proplist *p, const char *key)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ if (item == NULL)
+ return -ENOENT;
+ pa_proplist_item_free(item);
+ pa_array_remove(&p->array, item);
+ return 0;
+}
+
+static PA_PRINTF_FUNC(3,4) inline int pa_proplist_setf(pa_proplist *p, const char *key, const char *format, ...)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ va_list args;
+ int res;
+
+ va_start(args, format);
+ if (item != NULL)
+ pa_proplist_item_free(item);
+ else
+ item = pa_array_add(&p->array, sizeof(*item));
+ item->key = strdup(key);
+ if ((res = vasprintf(&item->value, format, args)) < 0)
+ res = -errno;
+ va_end(args);
+ return res;
+}
+
+static inline const char *pa_proplist_gets(const pa_proplist *p, const char *key)
+{
+ pa_proplist_item *item = pa_proplist_item_find(p, key);
+ return item ? item->value : NULL;
+}
+
+typedef enum pa_update_mode {
+ PA_UPDATE_SET
+ /**< Replace the entire property list with the new one. Don't keep
+ * any of the old data around. */,
+ PA_UPDATE_MERGE
+ /**< Merge new property list into the existing one, not replacing
+ * any old entries if they share a common key with the new
+ * property list. */,
+ PA_UPDATE_REPLACE
+ /**< Merge new property list into the existing one, replacing all
+ * old entries that share a common key with the new property
+ * list. */
+} pa_update_mode_t;
+
+
+static inline void pa_proplist_update(pa_proplist *p, pa_update_mode_t mode, const pa_proplist *other)
+{
+ pa_proplist_item *item;
+
+ if (mode == PA_UPDATE_SET)
+ pa_proplist_clear(p);
+
+ pa_array_for_each(item, &other->array) {
+ if (mode == PA_UPDATE_MERGE && pa_proplist_contains(p, item->key))
+ continue;
+ pa_proplist_sets(p, item->key, item->value);
+ }
+}
+
+static inline pa_proplist* pa_proplist_new_dict(const struct acp_dict *dict)
+{
+ pa_proplist *p = pa_proplist_new();
+ if (dict) {
+ const struct acp_dict_item *item;
+ struct acp_dict_item *it;
+ acp_dict_for_each(item, dict) {
+ it = pa_array_add(&p->array, sizeof(*it));
+ it->key = strdup(item->key);
+ it->value = strdup(item->value);
+ }
+ }
+ return p;
+}
+
+static inline void pa_proplist_as_dict(const pa_proplist *p, struct acp_dict *dict)
+{
+ dict->n_items = pa_proplist_size(p);
+ dict->items = p->array.data;
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_PROPLIST_H */
diff --git a/spa/plugins/alsa/acp/volume.h b/spa/plugins/alsa/acp/volume.h
new file mode 100644
index 0000000..b916bd2
--- /dev/null
+++ b/spa/plugins/alsa/acp/volume.h
@@ -0,0 +1,207 @@
+/***
+ This file is part of PulseAudio.
+
+ Copyright 2004-2006 Lennart Poettering
+ Copyright 2006 Pierre Ossman <ossman@cendio.se> for Cendio AB
+
+ PulseAudio is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation; either version 2.1 of the License,
+ or (at your option) any later version.
+
+ PulseAudio is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+***/
+
+#ifndef PA_VOLUME_H
+#define PA_VOLUME_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <math.h>
+
+typedef uint32_t pa_volume_t;
+
+#define PA_VOLUME_MUTED ((pa_volume_t) 0U)
+#define PA_VOLUME_NORM ((pa_volume_t) 0x10000U)
+#define PA_VOLUME_MAX ((pa_volume_t) UINT32_MAX/2)
+
+#ifdef INFINITY
+#define PA_DECIBEL_MININFTY ((double) -INFINITY)
+#else
+#define PA_DECIBEL_MININFTY ((double) -200.0)
+#endif
+
+#define PA_CLAMP_VOLUME(v) (PA_CLAMP_UNLIKELY((v), PA_VOLUME_MUTED, PA_VOLUME_MAX))
+
+typedef struct pa_cvolume {
+ uint32_t channels; /**< Number of channels */
+ pa_volume_t values[PA_CHANNELS_MAX]; /**< Per-channel volume */
+} pa_cvolume;
+
+static inline double pa_volume_linear_to_dB(double v)
+{
+ return 20.0 * log10(v);
+}
+
+static inline double pa_sw_volume_to_linear(pa_volume_t v)
+{
+ double f;
+ if (v <= PA_VOLUME_MUTED)
+ return 0.0;
+ if (v == PA_VOLUME_NORM)
+ return 1.0;
+ f = ((double) v / PA_VOLUME_NORM);
+ return f*f*f;
+}
+
+static inline double pa_sw_volume_to_dB(pa_volume_t v)
+{
+ if (v <= PA_VOLUME_MUTED)
+ return PA_DECIBEL_MININFTY;
+ return pa_volume_linear_to_dB(pa_sw_volume_to_linear(v));
+}
+
+static inline double pa_volume_dB_to_linear(double v)
+{
+ return pow(10.0, v / 20.0);
+}
+
+static inline pa_volume_t pa_sw_volume_from_linear(double v)
+{
+ if (v <= 0.0)
+ return PA_VOLUME_MUTED;
+ return (pa_volume_t) PA_CLAMP_VOLUME((uint64_t) lround(cbrt(v) * PA_VOLUME_NORM));
+}
+
+static inline pa_volume_t pa_sw_volume_from_dB(double dB)
+{
+ if (isinf(dB) < 0 || dB <= PA_DECIBEL_MININFTY)
+ return PA_VOLUME_MUTED;
+ return pa_sw_volume_from_linear(pa_volume_dB_to_linear(dB));
+}
+
+static inline pa_cvolume* pa_cvolume_set(pa_cvolume *a, unsigned channels, pa_volume_t v)
+{
+ uint32_t i;
+ a->channels = (uint8_t) channels;
+ for (i = 0; i < a->channels; i++)
+ a->values[i] = PA_CLAMP_VOLUME(v);
+ return a;
+}
+
+static inline int pa_cvolume_equal(const pa_cvolume *a, const pa_cvolume *b)
+{
+ uint32_t i;
+ if (PA_UNLIKELY(a == b))
+ return 1;
+ if (a->channels != b->channels)
+ return 0;
+ for (i = 0; i < a->channels; i++)
+ if (a->values[i] != b->values[i])
+ return 0;
+ return 1;
+}
+
+static inline pa_volume_t pa_sw_volume_multiply(pa_volume_t a, pa_volume_t b)
+{
+ uint64_t result;
+ result = ((uint64_t) a * (uint64_t) b + (uint64_t) PA_VOLUME_NORM / 2ULL) /
+ (uint64_t) PA_VOLUME_NORM;
+ if (result > (uint64_t)PA_VOLUME_MAX)
+ pa_log_warn("pa_sw_volume_multiply: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings.");
+ return (pa_volume_t) PA_CLAMP_VOLUME(result);
+}
+
+static inline pa_cvolume *pa_sw_cvolume_multiply(pa_cvolume *dest,
+ const pa_cvolume *a, const pa_cvolume *b)
+{
+ unsigned i;
+ dest->channels = PA_MIN(a->channels, b->channels);
+ for (i = 0; i < dest->channels; i++)
+ dest->values[i] = pa_sw_volume_multiply(a->values[i], b->values[i]);
+ return dest;
+}
+
+static inline pa_cvolume *pa_sw_cvolume_multiply_scalar(pa_cvolume *dest,
+ const pa_cvolume *a, pa_volume_t b)
+{
+ unsigned i;
+ for (i = 0; i < a->channels; i++)
+ dest->values[i] = pa_sw_volume_multiply(a->values[i], b);
+ dest->channels = (uint8_t) i;
+ return dest;
+}
+
+static inline pa_volume_t pa_sw_volume_divide(pa_volume_t a, pa_volume_t b)
+{
+ uint64_t result;
+ if (b <= PA_VOLUME_MUTED)
+ return 0;
+ result = ((uint64_t) a * (uint64_t) PA_VOLUME_NORM + (uint64_t) b / 2ULL) / (uint64_t) b;
+ if (result > (uint64_t)PA_VOLUME_MAX)
+ pa_log_warn("pa_sw_volume_divide: Volume exceeds maximum allowed value and will be clipped. Please check your volume settings.");
+ return (pa_volume_t) PA_CLAMP_VOLUME(result);
+}
+
+static inline pa_cvolume *pa_sw_cvolume_divide_scalar(pa_cvolume *dest,
+ const pa_cvolume *a, pa_volume_t b) {
+ unsigned i;
+ for (i = 0; i < a->channels; i++)
+ dest->values[i] = pa_sw_volume_divide(a->values[i], b);
+ dest->channels = (uint8_t) i;
+ return dest;
+}
+
+static inline pa_cvolume *pa_sw_cvolume_divide(pa_cvolume *dest,
+ const pa_cvolume *a, const pa_cvolume *b)
+{
+ unsigned i;
+ dest->channels = PA_MIN(a->channels, b->channels);
+ for (i = 0; i < dest->channels; i++)
+ dest->values[i] = pa_sw_volume_divide(a->values[i], b->values[i]);
+ return dest;
+}
+
+#define pa_cvolume_reset(a, n) pa_cvolume_set((a), (n), PA_VOLUME_NORM)
+#define pa_cvolume_mute(a, n) pa_cvolume_set((a), (n), PA_VOLUME_MUTED)
+
+static inline int pa_cvolume_compatible_with_channel_map(const pa_cvolume *v,
+ const pa_channel_map *cm)
+{
+ return v->channels == cm->channels;
+}
+
+static inline pa_volume_t pa_cvolume_max(const pa_cvolume *a)
+{
+ pa_volume_t m = PA_VOLUME_MUTED;
+ unsigned c;
+ for (c = 0; c < a->channels; c++)
+ if (a->values[c] > m)
+ m = a->values[c];
+ return m;
+}
+
+static inline pa_volume_t pa_cvolume_min(const pa_cvolume *a)
+{
+ pa_volume_t m = PA_VOLUME_MAX;
+ unsigned c;
+ for (c = 0; c < a->channels; c++)
+ if (a->values[c] < m)
+ m = a->values[c];
+ return m;
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PA_VOLUME_H */
diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c
new file mode 100644
index 0000000..bcd7491
--- /dev/null
+++ b/spa/plugins/alsa/alsa-acp-device.c
@@ -0,0 +1,1133 @@
+/* Spa ALSA Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/support/i18n.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/monitor/event.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "alsa.h"
+
+#include "acp/acp.h"
+
+extern struct spa_i18n *acp_i18n;
+
+#define MAX_POLL 16
+
+#define DEFAULT_DEVICE "hw:0"
+#define DEFAULT_AUTO_PROFILE true
+#define DEFAULT_AUTO_PORT true
+
+struct props {
+ char device[64];
+ bool auto_profile;
+ bool auto_port;
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, DEFAULT_DEVICE, 64);
+ props->auto_profile = DEFAULT_AUTO_PROFILE;
+ props->auto_port = DEFAULT_AUTO_PORT;
+}
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *loop;
+
+ uint32_t info_all;
+ struct spa_device_info info;
+#define IDX_EnumProfile 0
+#define IDX_Profile 1
+#define IDX_EnumRoute 2
+#define IDX_Route 3
+ struct spa_param_info params[4];
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+
+ uint32_t profile;
+
+ struct acp_card *card;
+ struct pollfd pfds[MAX_POLL];
+ int n_pfds;
+ struct spa_source sources[MAX_POLL];
+};
+
+static int emit_info(struct impl *this, bool full);
+
+static void handle_acp_poll(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ int i;
+
+ for (i = 0; i < this->n_pfds; i++)
+ this->pfds[i].revents = this->sources[i].rmask;
+ acp_card_handle_events(this->card);
+ for (i = 0; i < this->n_pfds; i++)
+ this->sources[i].rmask = 0;
+ emit_info(this, false);
+}
+
+static void remove_sources(struct impl *this)
+{
+ int i;
+ for (i = 0; i < this->n_pfds; i++) {
+ spa_loop_remove_source(this->loop, &this->sources[i]);
+ }
+ this->n_pfds = 0;
+}
+
+static int setup_sources(struct impl *this)
+{
+ int i;
+
+ remove_sources(this);
+
+ this->n_pfds = acp_card_poll_descriptors(this->card, this->pfds, MAX_POLL);
+
+ for (i = 0; i < this->n_pfds; i++) {
+ this->sources[i].func = handle_acp_poll;
+ this->sources[i].data = this;
+ this->sources[i].fd = this->pfds[i].fd;
+ this->sources[i].mask = this->pfds[i].events;
+ this->sources[i].rmask = 0;
+ spa_loop_add_source(this->loop, &this->sources[i]);
+ }
+ return 0;
+}
+
+static int emit_node(struct impl *this, struct acp_device *dev)
+{
+ struct spa_dict_item *items;
+ const struct acp_dict_item *it;
+ uint32_t n_items, i;
+ char device_name[128], path[180], channels[16], ch[12], routes[16];
+ char card_index[16], *p;
+ char positions[SPA_AUDIO_MAX_CHANNELS * 12];
+ struct spa_device_object_info info;
+ struct acp_card *card = this->card;
+ const char *stream, *devstr;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+
+ if (dev->direction == ACP_DIRECTION_PLAYBACK) {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SINK;
+ stream = "playback";
+ } else {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE;
+ stream = "capture";
+ }
+
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ items = alloca((dev->props.n_items + 8) * sizeof(*items));
+ n_items = 0;
+
+ snprintf(card_index, sizeof(card_index), "%d", card->index);
+
+ devstr = dev->device_strings[0];
+ p = strstr(devstr, "%f");
+ if (p) {
+ snprintf(device_name, sizeof(device_name), "%.*s%d%s",
+ (int)SPA_PTRDIFF(p, devstr), devstr,
+ card->index, p+2);
+ } else {
+ snprintf(device_name, sizeof(device_name), "%s", devstr);
+ }
+ snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", card_index, device_name, stream);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name);
+ if (dev->flags & ACP_DEVICE_UCM_DEVICE)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_OPEN_UCM, "true");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card_index);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, stream);
+
+ snprintf(channels, sizeof(channels), "%d", dev->format.channels);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels);
+
+ p = positions;
+ for (i = 0; i < dev->format.channels; i++) {
+ p += snprintf(p, 12, "%s%s", i == 0 ? "" : ",",
+ acp_channel_str(ch, sizeof(ch), dev->format.map[i]));
+ }
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions);
+
+ snprintf(routes, sizeof(routes), "%d", dev->n_ports);
+ items[n_items++] = SPA_DICT_ITEM_INIT("device.routes", routes);
+
+ acp_dict_for_each(it, &dev->props)
+ items[n_items++] = SPA_DICT_ITEM_INIT(it->key, it->value);
+
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_device_emit_object_info(&this->hooks, dev->index, &info);
+
+ return 0;
+}
+
+static int emit_info(struct impl *this, bool full)
+{
+ int err = 0;
+ struct spa_dict_item *items;
+ uint32_t n_items;
+ const struct acp_dict_item *it;
+ struct acp_card *card = this->card;
+ char path[128];
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ n_items = card->props.n_items + 4;
+ items = alloca(n_items * sizeof(*items));
+
+ n_items = 0;
+#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value)
+ snprintf(path, sizeof(path), "alsa:pcm:%d", card->index);
+ ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+ ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device);
+ acp_dict_for_each(it, &card->props)
+ ADD_ITEM(it->key, it->value);
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+#undef ADD_ITEM
+
+ if (this->info.change_mask & SPA_DEVICE_CHANGE_MASK_PARAMS) {
+ SPA_FOR_EACH_ELEMENT_VAR(this->params, p) {
+ if (p->user > 0) {
+ p->flags ^= SPA_PARAM_INFO_SERIAL;
+ p->user = 0;
+ }
+ }
+ }
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+ return err;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+ struct acp_card *card;
+ struct acp_card_profile *profile;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ card = this->card;
+ if (card->active_profile_index < card->n_profiles)
+ profile = card->profiles[card->active_profile_index];
+ else
+ profile = NULL;
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ emit_info(this, true);
+
+ if (profile) {
+ for (i = 0; i < profile->n_devices; i++)
+ emit_node(this, profile->devices[i]);
+ }
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct spa_pod *build_profile(struct spa_pod_builder *b, uint32_t id,
+ struct acp_card_profile *pr, bool current)
+{
+ struct spa_pod_frame f[2];
+ uint32_t i, n_classes, n_capture = 0, n_playback = 0;
+ uint32_t *capture, *playback;
+
+ capture = alloca(sizeof(uint32_t) * pr->n_devices);
+ playback = alloca(sizeof(uint32_t) * pr->n_devices);
+
+ for (i = 0; i < pr->n_devices; i++) {
+ struct acp_device *dev = pr->devices[i];
+ switch (dev->direction) {
+ case ACP_DIRECTION_PLAYBACK:
+ playback[n_playback++] = dev->index;
+ break;
+ case ACP_DIRECTION_CAPTURE:
+ capture[n_capture++] = dev->index;
+ break;
+ }
+ }
+ n_classes = n_capture > 0 ? 1 : 0;
+ n_classes += n_playback > 0 ? 1 : 0;
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(pr->index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(pr->name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(pr->description),
+ SPA_PARAM_PROFILE_priority, SPA_POD_Int(pr->priority),
+ SPA_PARAM_PROFILE_available, SPA_POD_Id(pr->available),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, n_classes);
+ if (n_capture > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(n_capture),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int,
+ n_capture, capture));
+ }
+ if (n_playback > 0) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(n_playback),
+ SPA_POD_String("card.profile.devices"),
+ SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Int,
+ n_playback, playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ if (current) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_save, 0);
+ spa_pod_builder_bool(b, SPA_FLAG_IS_SET(pr->flags, ACP_PROFILE_SAVE));
+ }
+
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
+ struct acp_port *p, struct acp_device *dev, uint32_t profile)
+{
+ struct spa_pod_frame f[2];
+ const struct acp_dict_item *item;
+ uint32_t i;
+ enum spa_direction direction;
+
+ switch (p->direction) {
+ case ACP_DIRECTION_PLAYBACK:
+ direction = SPA_DIRECTION_OUTPUT;
+ break;
+ case ACP_DIRECTION_CAPTURE:
+ direction = SPA_DIRECTION_INPUT;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamRoute, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(p->index),
+ SPA_PARAM_ROUTE_direction, SPA_POD_Id(direction),
+ SPA_PARAM_ROUTE_name, SPA_POD_String(p->name),
+ SPA_PARAM_ROUTE_description, SPA_POD_String(p->description),
+ SPA_PARAM_ROUTE_priority, SPA_POD_Int(p->priority),
+ SPA_PARAM_ROUTE_available, SPA_POD_Id(p->available),
+ 0);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, SPA_POD_PROP_FLAG_HINT_DICT);
+ spa_pod_builder_push_struct(b, &f[1]);
+ spa_pod_builder_int(b, p->props.n_items + (dev ? 2 : 0));
+ acp_dict_for_each(item, &p->props) {
+ spa_pod_builder_add(b,
+ SPA_POD_String(item->key),
+ SPA_POD_String(item->value),
+ NULL);
+ }
+ if (dev != NULL) {
+ const char *str;
+ str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ? "true" : "false";
+ spa_pod_builder_add(b,
+ SPA_POD_String("route.hw-mute"),
+ SPA_POD_String(str), NULL);
+
+ str = SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ? "true" : "false";
+ spa_pod_builder_add(b,
+ SPA_POD_String("route.hw-volume"),
+ SPA_POD_String(str), NULL);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ for (i = 0; i < p->n_profiles; i++)
+ spa_pod_builder_int(b, p->profiles[i]->index);
+ spa_pod_builder_pop(b, &f[1]);
+ if (dev != NULL) {
+ uint32_t channels = dev->format.channels;
+ float volumes[channels];
+ float soft_volumes[channels];
+ bool mute;
+
+ acp_device_get_mute(dev, &mute);
+ spa_zero(volumes);
+ spa_zero(soft_volumes);
+ acp_device_get_volume(dev, volumes, channels);
+ acp_device_get_soft_volume(dev, soft_volumes, channels);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_device, 0);
+ spa_pod_builder_int(b, dev->index);
+
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_props, 0);
+ spa_pod_builder_push_object(b, &f[1], SPA_TYPE_OBJECT_Props, id);
+
+ spa_pod_builder_prop(b, SPA_PROP_mute,
+ SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_MUTE) ?
+ SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_bool(b, mute);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelVolumes,
+ SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_HW_VOLUME) ?
+ SPA_POD_PROP_FLAG_HARDWARE : 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ channels, volumes);
+
+ spa_pod_builder_prop(b, SPA_PROP_volumeBase, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, dev->base_volume);
+ spa_pod_builder_prop(b, SPA_PROP_volumeStep, SPA_POD_PROP_FLAG_READONLY);
+ spa_pod_builder_float(b, dev->volume_step);
+
+ spa_pod_builder_prop(b, SPA_PROP_channelMap, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ channels, dev->format.map);
+
+ spa_pod_builder_prop(b, SPA_PROP_softVolumes, 0);
+ spa_pod_builder_array(b, sizeof(float), SPA_TYPE_Float,
+ channels, soft_volumes);
+
+ spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0);
+ spa_pod_builder_long(b, dev->latency_ns);
+
+ if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_IEC958)) {
+ spa_pod_builder_prop(b, SPA_PROP_iec958Codecs, 0);
+ spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id,
+ dev->n_codecs, dev->codecs);
+ }
+
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
+ spa_pod_builder_push_array(b, &f[1]);
+ for (i = 0; i < p->n_devices; i++)
+ spa_pod_builder_int(b, p->devices[i]->index);
+ spa_pod_builder_pop(b, &f[1]);
+
+ if (profile != SPA_ID_INVALID) {
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profile, 0);
+ spa_pod_builder_int(b, profile);
+ spa_pod_builder_prop(b, SPA_PARAM_ROUTE_save, 0);
+ spa_pod_builder_bool(b, SPA_FLAG_IS_SET(p->flags, ACP_PORT_SAVE));
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+}
+
+static struct acp_port *find_port_for_device(struct acp_card *card, struct acp_device *dev)
+{
+ uint32_t i;
+ for (i = 0; i < dev->n_ports; i++) {
+ struct acp_port *p = dev->ports[i];
+ if (SPA_FLAG_IS_SET(p->flags, ACP_PORT_ACTIVE))
+ return p;
+ }
+ return NULL;
+}
+
+static int impl_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_device_params result;
+ uint32_t count = 0;
+ struct acp_card *card;
+ struct acp_card_profile *pr;
+ struct acp_port *p;
+ struct acp_device *dev;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ card = this->card;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ if (result.index >= card->n_profiles)
+ return 0;
+
+ pr = card->profiles[result.index];
+ param = build_profile(&b, id, pr, false);
+ break;
+
+ case SPA_PARAM_Profile:
+ if (result.index > 0 || card->active_profile_index >= card->n_profiles)
+ return 0;
+
+ pr = card->profiles[card->active_profile_index];
+ param = build_profile(&b, id, pr, true);
+ break;
+
+ case SPA_PARAM_EnumRoute:
+ if (result.index >= card->n_ports)
+ return 0;
+
+ p = card->ports[result.index];
+ param = build_route(&b, id, p, NULL, SPA_ID_INVALID);
+ break;
+
+ case SPA_PARAM_Route:
+ while (true) {
+ if (result.index >= card->n_devices)
+ return 0;
+
+ dev = card->devices[result.index];
+ if (SPA_FLAG_IS_SET(dev->flags, ACP_DEVICE_ACTIVE) &&
+ (p = find_port_for_device(card, dev)) != NULL)
+ break;
+
+ result.index++;
+ }
+ result.next = result.index + 1;
+ param = build_route(&b, id, p, dev, card->active_profile_index);
+ if (param == NULL)
+ return -errno;
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static void on_latency_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_log_info(this->log, "device %s latency changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(dev->latency_ns));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void on_codecs_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+
+ spa_log_info(this->log, "device %s codecs changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_iec958Codecs, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dev->n_codecs, dev->codecs));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static int apply_device_props(struct impl *this, struct acp_device *dev, struct spa_pod *props)
+{
+ float volume = 0;
+ bool mute = 0;
+ struct spa_pod_prop *prop;
+ struct spa_pod_object *obj = (struct spa_pod_object *) props;
+ int changed = 0;
+ float volumes[ACP_MAX_CHANNELS];
+ uint32_t channels[ACP_MAX_CHANNELS];
+ uint32_t n_volumes = 0;
+
+ if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props))
+ return -EINVAL;
+
+ SPA_POD_OBJECT_FOREACH(obj, prop) {
+ switch (prop->key) {
+ case SPA_PROP_volume:
+ if (spa_pod_get_float(&prop->value, &volume) == 0) {
+ acp_device_set_volume(dev, &volume, 1);
+ changed++;
+ }
+ break;
+ case SPA_PROP_mute:
+ if (spa_pod_get_bool(&prop->value, &mute) == 0) {
+ acp_device_set_mute(dev, mute);
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelVolumes:
+ if ((n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float,
+ volumes, ACP_MAX_CHANNELS)) > 0) {
+ changed++;
+ }
+ break;
+ case SPA_PROP_channelMap:
+ if (spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ channels, ACP_MAX_CHANNELS) > 0) {
+ changed++;
+ }
+ break;
+ case SPA_PROP_latencyOffsetNsec:
+ {
+ int64_t latency_ns;
+ if (spa_pod_get_long(&prop->value, &latency_ns) == 0) {
+ if (dev->latency_ns != latency_ns) {
+ dev->latency_ns = latency_ns;
+ on_latency_changed(this, dev);
+ changed++;
+ }
+ }
+ break;
+ }
+ case SPA_PROP_iec958Codecs:
+ {
+ uint32_t codecs[32], n_codecs;
+
+ n_codecs = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
+ codecs, SPA_N_ELEMENTS(codecs));
+ if (n_codecs != dev->n_codecs ||
+ memcmp(dev->codecs, codecs, n_codecs * sizeof(uint32_t)) != 0) {
+ memcpy(dev->codecs, codecs, n_codecs * sizeof(uint32_t));
+ dev->n_codecs = n_codecs;
+ on_codecs_changed(this, dev);
+ changed++;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ if (n_volumes > 0)
+ acp_device_set_volume(dev, volumes, n_volumes);
+
+ return changed;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx;
+ bool save = false;
+
+ if (param == NULL) {
+ idx = acp_card_find_best_profile_index(this->card, NULL);
+ save = true;
+ } else if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_PROFILE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ acp_card_set_profile(this->card, idx, save ? ACP_PROFILE_SAVE : 0);
+ emit_info(this, false);
+ break;
+ }
+ case SPA_PARAM_Route:
+ {
+ uint32_t idx, device;
+ struct spa_pod *props = NULL;
+ struct acp_device *dev;
+ bool save = false;
+
+ if (param == NULL)
+ return -EINVAL;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamRoute, NULL,
+ SPA_PARAM_ROUTE_index, SPA_POD_Int(&idx),
+ SPA_PARAM_ROUTE_device, SPA_POD_Int(&device),
+ SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props),
+ SPA_PARAM_ROUTE_save, SPA_POD_OPT_Bool(&save))) < 0) {
+ spa_log_warn(this->log, "can't parse route");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+ if (device >= this->card->n_devices)
+ return -EINVAL;
+
+ dev = this->card->devices[device];
+ acp_device_set_port(dev, idx, save ? ACP_PORT_SAVE : 0);
+ if (props)
+ apply_device_props(this, dev, props);
+ emit_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+static void card_props_changed(void *data)
+{
+ struct impl *this = data;
+ spa_log_info(this->log, "card properties changed");
+}
+
+static bool has_device(struct acp_card_profile *pr, uint32_t index)
+{
+ uint32_t i;
+
+ for (i = 0; i < pr->n_devices; i++)
+ if (pr->devices[i]->index == index)
+ return true;
+ return false;
+}
+
+static void card_profile_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_card_profile *op = card->profiles[old_index];
+ struct acp_card_profile *np = card->profiles[new_index];
+ uint32_t i;
+
+ spa_log_info(this->log, "card profile changed from %s to %s",
+ op->name, np->name);
+
+ for (i = 0; i < op->n_devices; i++) {
+ uint32_t index = op->devices[i]->index;
+ if (has_device(np, index))
+ continue;
+ spa_device_emit_object_info(&this->hooks, index, NULL);
+ }
+ for (i = 0; i < np->n_devices; i++) {
+ emit_node(this, np->devices[i]);
+ }
+ setup_sources(this);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Profile].user++;
+ this->params[IDX_Route].user++;
+ this->params[IDX_EnumRoute].user++;
+}
+
+static void card_profile_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_card_profile *p = card->profiles[index];
+
+ spa_log_info(this->log, "card profile %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(available));
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_EnumProfile].user++;
+ this->params[IDX_Profile].user++;
+
+ if (this->props.auto_profile) {
+ uint32_t best = acp_card_find_best_profile_index(card, NULL);
+ acp_card_set_profile(card, best, 0);
+ }
+}
+
+static void card_port_changed(void *data, uint32_t old_index, uint32_t new_index)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_port *op = card->ports[old_index];
+ struct acp_port *np = card->ports[new_index];
+
+ spa_log_info(this->log, "card port changed from %s to %s",
+ op->name, np->name);
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+}
+
+static void card_port_available(void *data, uint32_t index,
+ enum acp_available old, enum acp_available available)
+{
+ struct impl *this = data;
+ struct acp_card *card = this->card;
+ struct acp_port *p = card->ports[index];
+
+ spa_log_info(this->log, "card port %s available %s -> %s", p->name,
+ acp_available_str(old), acp_available_str(available));
+
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_EnumRoute].user++;
+ this->params[IDX_Route].user++;
+
+ if (this->props.auto_port) {
+ uint32_t i;
+
+ for (i = 0; i < p->n_devices; i++) {
+ struct acp_device *d = p->devices[i];
+ uint32_t best;
+
+ if (!(d->flags & ACP_DEVICE_ACTIVE))
+ continue;
+
+ best = acp_device_find_best_port_index(d, NULL);
+ acp_device_set_port(d, best, 0);
+ }
+ }
+}
+
+static void on_volume_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ uint32_t n_volume = dev->format.channels;
+ float volume[n_volume];
+ float soft_volume[n_volume];
+
+ spa_log_info(this->log, "device %s volume changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ spa_zero(volume);
+ spa_zero(soft_volume);
+ acp_device_get_volume(dev, volume, n_volume);
+ acp_device_get_soft_volume(dev, soft_volume, n_volume);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, n_volume, volume),
+ SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t),
+ SPA_TYPE_Id, dev->format.channels,
+ dev->format.map),
+ SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float),
+ SPA_TYPE_Float, n_volume, soft_volume));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static void on_mute_changed(void *data, struct acp_device *dev)
+{
+ struct impl *this = data;
+ struct spa_event *event;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod_frame f[1];
+ bool mute;
+
+ spa_log_info(this->log, "device %s mute changed", dev->name);
+ this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ this->params[IDX_Route].user++;
+
+ acp_device_get_mute(dev, &mute);
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+ spa_pod_builder_push_object(&b, &f[0],
+ SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ObjectConfig);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Object, 0);
+ spa_pod_builder_int(&b, dev->index);
+ spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
+
+ spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props,
+ SPA_PROP_mute, SPA_POD_Bool(mute),
+ SPA_PROP_softMute, SPA_POD_Bool(mute));
+ event = spa_pod_builder_pop(&b, &f[0]);
+
+ spa_device_emit_event(&this->hooks, event);
+}
+
+static const struct acp_card_events card_events = {
+ ACP_VERSION_CARD_EVENTS,
+ .props_changed = card_props_changed,
+ .profile_changed = card_profile_changed,
+ .profile_available = card_profile_available,
+ .port_changed = card_port_changed,
+ .port_available = card_port_available,
+ .volume_changed = on_volume_changed,
+ .mute_changed = on_mute_changed,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static SPA_PRINTF_FUNC(6,0) void impl_acp_log_func(void *data,
+ int level, const char *file, int line, const char *func,
+ const char *fmt, va_list arg)
+{
+ struct spa_log *log = data;
+ spa_log_logv(log, (enum spa_log_level)level, file, line, func, fmt, arg);
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ remove_sources(this);
+ if (this->card) {
+ acp_card_destroy(this->card);
+ this->card = NULL;
+ }
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ const char *str;
+ struct acp_dict_item *items = NULL;
+ const struct spa_dict_item *it;
+ uint32_t n_items = 0;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ acp_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
+ if (this->loop == NULL) {
+ spa_log_error(this->log, "a Loop interface is needed");
+ return -EINVAL;
+ }
+
+ acp_set_log_func(impl_acp_log_func, this->log);
+ acp_set_log_level(6);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH)) != NULL)
+ snprintf(this->props.device, sizeof(this->props.device), "%s", str);
+ if ((str = spa_dict_lookup(info, "api.acp.auto-port")) != NULL)
+ this->props.auto_port = spa_atob(str);
+ if ((str = spa_dict_lookup(info, "api.acp.auto-profile")) != NULL)
+ this->props.auto_profile = spa_atob(str);
+
+ items = alloca((info->n_items) * sizeof(*items));
+ spa_dict_for_each(it, info)
+ items[n_items++] = ACP_DICT_ITEM_INIT(it->key, it->value);
+ }
+
+ spa_log_debug(this->log, "probe card %s", this->props.device);
+ if ((str = strchr(this->props.device, ':')) == NULL)
+ return -EINVAL;
+
+ this->card = acp_card_new(atoi(str+1), &ACP_DICT_INIT(items, n_items));
+ if (this->card == NULL)
+ return -errno;
+
+ setup_sources(this);
+
+ acp_card_add_listener(this->card, &card_events, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_PROPS |
+ SPA_DEVICE_CHANGE_MASK_PARAMS;
+
+ this->params[IDX_EnumProfile] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ this->params[IDX_Profile] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ this->params[IDX_EnumRoute] = SPA_PARAM_INFO(SPA_PARAM_EnumRoute, SPA_PARAM_INFO_READ);
+ this->params[IDX_Route] = SPA_PARAM_INFO(SPA_PARAM_Route, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = 4;
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_acp_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_ACP_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-compress-offload-sink.c b/spa/plugins/alsa/alsa-compress-offload-sink.c
new file mode 100644
index 0000000..966e680
--- /dev/null
+++ b/spa/plugins/alsa/alsa-compress-offload-sink.c
@@ -0,0 +1,1143 @@
+/* Spa ALSA Compress-Offload sink
+ *
+ * Copyright © 2022 Wim Taymans
+ * © 2022 Asymptotic Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <spa/monitor/device.h>
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+#include <spa/support/system.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/json.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/node/keys.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/debug/types.h>
+#include <spa/debug/mem.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/control/control.h>
+
+#include <sound/compress_params.h>
+#include <tinycompress/tinycompress.h>
+
+/*
+ * This creates a PipeWire sink node which uses the tinycompress user space
+ * library to use the ALSA Compress-Offload API for writing compressed data
+ * like MP3, FLAC etc. to an DSP that can handle such data directly.
+ *
+ * These show up under /dev/snd like comprCxDx, as opposed to regular
+ * ALSA PCM devices.
+ *
+ * root@dragonboard-845c:~# ls /dev/snd
+ * by-path comprC0D3 controlC0 pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p pcmC0D2c pcmC0D2p timer
+ *
+ * ## Example configuration
+ *\code{.unparsed}
+ * context.objects = [
+ * { factory = spa-node-factory
+ * args = {
+ * factory.name = api.alsa.compress.offload.sink
+ * node.name = Compress-Offload-Sink
+ * media.class = "Audio/Sink"
+ * api.alsa.path = "hw:0,3"
+ * }
+ * }
+ *]
+ *\endcode
+ *
+ * TODO:
+ * - Clocking
+ * - Implement pause and resume
+ * - Having a better wait mechanism
+ * - Automatic loading using alsa-udev
+ *
+ */
+
+#define NAME "compress-offload-audio-sink"
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_RATE 44100
+#define MAX_BUFFERS 4
+#define MAX_PORTS 1
+#define MAX_CODECS 32 /* See include/sound/compress_params.h */
+
+#define MIN_FRAGMENT_SIZE (4 * 1024)
+#define MAX_FRAGMENT_SIZE (64 * 1024)
+#define MIN_NUM_FRAGMENTS (4)
+#define MAX_NUM_FRAGMENTS (8 * 4)
+
+struct props {
+ uint32_t channels;
+ uint32_t rate;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+ char device[64];
+};
+
+static void reset_props(struct props *props)
+{
+ props->channels = 0;
+ props->rate = 0;
+}
+
+struct buffer {
+ uint32_t id;
+ uint32_t flags;
+ struct spa_buffer *outbuf;
+};
+
+struct impl;
+
+struct port {
+ uint64_t info_all;
+ struct spa_port_info info;
+ struct spa_param_info params[5];
+
+ struct spa_io_buffers *io;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+
+ struct buffer buffers[MAX_BUFFERS];
+ uint32_t n_buffers;
+ uint32_t written;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_node node;
+ struct spa_log *log;
+ struct props props;
+
+ struct spa_node_info info;
+ struct spa_param_info params[1];
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+ struct port port;
+
+ unsigned int started_node:1;
+ unsigned int started_compress:1;
+ uint64_t info_all;
+ uint32_t quantum_limit;
+
+ struct compr_config compr_conf;
+ struct snd_codec codec;
+ struct compress *compress;
+
+ int32_t codecs_supported[MAX_CODECS];
+ uint32_t num_codecs;
+};
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS)
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "alsa" },
+ { SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
+ { SPA_KEY_NODE_DRIVER, "false" },
+ { SPA_KEY_NODE_PAUSE_ON_IDLE, "false" },
+};
+
+static const struct codec_id {
+ uint32_t codec_id;
+} codec_info[] = {
+ { SND_AUDIOCODEC_MP3, },
+ { SND_AUDIOCODEC_AAC, },
+ { SND_AUDIOCODEC_WMA, },
+ { SND_AUDIOCODEC_VORBIS, },
+ { SND_AUDIOCODEC_FLAC, },
+ { SND_AUDIOCODEC_ALAC, },
+ { SND_AUDIOCODEC_APE, },
+ { SND_AUDIOCODEC_REAL, },
+ { SND_AUDIOCODEC_AMR, },
+ { SND_AUDIOCODEC_AMRWB, },
+};
+
+static int
+open_compress(struct impl *this)
+{
+ struct compress *compress;
+
+ compress = compress_open_by_name(this->props.device, COMPRESS_IN, &this->compr_conf);
+ if (!compress || !is_compress_ready(compress)) {
+ spa_log_error(this->log, NAME " %p: Unable to open compress device", this);
+ return -EINVAL;
+ }
+
+ this->compress = compress;
+
+ compress_nonblock(this->compress, 1);
+
+ return 0;
+}
+
+static int
+write_compress(struct impl *this, void *buf, int32_t size)
+{
+ int32_t wrote;
+ int32_t to_write = size;
+ struct port *port = &this->port;
+
+retry:
+ wrote = compress_write(this->compress, buf, to_write);
+ if (wrote < 0) {
+ spa_log_error(this->log, NAME " %p: Error playing sample: %s",
+ this, compress_get_error(this->compress));
+ return wrote;
+ }
+ port->written += wrote;
+
+ spa_log_debug(this->log, NAME " %p: We wrote %d, DSP accepted %d\n", this, size, wrote);
+
+ if (wrote < to_write) {
+ /*
+ * The choice of 20ms as the time to wait is
+ * completely arbitrary.
+ */
+ compress_wait(this->compress, 20);
+ buf = (uint8_t *)buf + wrote;
+ to_write = to_write - wrote;
+ goto retry;
+ }
+
+ /*
+ * One write has to happen before starting the compressed node. Calling
+ * compress_start before writing MIN_NUM_FRAGMENTS * MIN_FRAGMENT_SIZE
+ * will result in a distorted audio playback.
+ */
+ if (!this->started_compress &&
+ (port->written >= (MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS))) {
+ compress_start(this->compress);
+ this->started_compress = true;
+ }
+
+ return size;
+}
+
+static void emit_node_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct impl *this, struct port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumPortConfig:
+ case SPA_PARAM_PortConfig:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamPortConfig, id,
+ SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
+ SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int
+impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ return -ENOTSUP;
+}
+
+static int do_start(struct impl *this)
+{
+ if (this->started_node)
+ return 0;
+
+ spa_log_debug(this->log, "Open compressed device: %s", this->props.device);
+ if (open_compress(this) < 0)
+ return -EINVAL;
+
+ this->started_node = true;
+ this->started_compress = false;
+
+ return 0;
+}
+
+static int do_drain(struct impl *this)
+{
+ if (!this->started_node)
+ return 0;
+
+ if (this->started_compress) {
+ spa_log_debug(this->log, NAME " %p: Issuing drain command", this);
+ compress_drain(this->compress);
+ spa_log_debug(this->log, NAME " %p: Finished drain", this);
+ }
+
+ return 0;
+}
+
+static int do_stop(struct impl *this)
+{
+ if (!this->started_node)
+ return 0;
+
+ compress_stop(this->compress);
+ compress_close(this->compress);
+ spa_log_info(this->log, NAME " %p: Closed compress device", this);
+
+ this->compress = NULL;
+ this->started_node = false;
+ this->started_compress = false;
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ port = &this->port;
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ {
+ if (!port->have_format)
+ return -EIO;
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ do_start(this);
+ break;
+ }
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ do_drain(this);
+ do_stop(this);
+ break;
+
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, &this->port, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+port_enum_formats(struct impl *this,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t index,
+ struct spa_pod **param,
+ struct spa_pod_builder *builder)
+{
+ struct spa_audio_info info;
+ uint32_t codec;
+
+ if (index >= this->num_codecs)
+ return 0;
+
+ codec = this->codecs_supported[index];
+
+ spa_zero(info);
+ info.media_type = SPA_MEDIA_TYPE_audio;
+
+ switch (codec) {
+ case SND_AUDIOCODEC_MP3:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_mp3;
+ info.info.mp3.rate = this->props.rate;
+ info.info.mp3.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_AAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_aac;
+ info.info.aac.rate = this->props.rate;
+ info.info.aac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_WMA:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_wma;
+ info.info.wma.rate = this->props.rate;
+ info.info.wma.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_VORBIS:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_vorbis;
+ info.info.vorbis.rate = this->props.rate;
+ info.info.vorbis.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_FLAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_flac;
+ info.info.flac.rate = this->props.rate;
+ info.info.flac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_ALAC:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_alac;
+ info.info.alac.rate = this->props.rate;
+ info.info.alac.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_APE:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_ape;
+ info.info.ape.rate = this->props.rate;
+ info.info.ape.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_REAL:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_ra;
+ info.info.ra.rate = this->props.rate;
+ info.info.ra.channels = this->props.channels;
+ break;
+ case SND_AUDIOCODEC_AMR:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info.info.amr.rate = this->props.rate;
+ info.info.amr.channels = this->props.channels;
+ info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB;
+ break;
+ case SND_AUDIOCODEC_AMRWB:
+ info.media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info.info.amr.rate = this->props.rate;
+ info.info.amr.channels = this->props.channels;
+ info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ if ((*param = spa_format_audio_build(builder, SPA_PARAM_EnumFormat, &info)) == NULL)
+ return -errno;
+ return 1;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_pod *param;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if ((res = port_enum_formats(this, direction, port_id,
+ result.index, &param, &b)) <= 0)
+ return res;
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_build(&b, id, &port->current_format);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(0),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS,
+ MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS,
+ MAX_FRAGMENT_SIZE),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(0));
+ break;
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct impl *this, struct port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_log_info(this->log, NAME " %p: clear buffers", this);
+ port->n_buffers = 0;
+ this->started_node = false;
+ }
+ return 0;
+}
+
+static int
+compress_setup(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate)
+{
+ struct compr_config *config;
+ struct snd_codec *codec;
+ uint32_t channels, rate;
+
+ memset(&this->codec, 0, sizeof(this->codec));
+ memset(&this->compr_conf, 0, sizeof(this->compr_conf));
+
+ config = &this->compr_conf;
+ codec = &this->codec;
+
+ switch (info->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_vorbis:
+ codec->id = SND_AUDIOCODEC_VORBIS;
+ rate = info->info.vorbis.rate;
+ channels = info->info.vorbis.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_mp3:
+ codec->id = SND_AUDIOCODEC_MP3;
+ rate = info->info.mp3.rate;
+ channels = info->info.mp3.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_aac:
+ codec->id = SND_AUDIOCODEC_AAC;
+ rate = info->info.aac.rate;
+ channels = info->info.aac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_flac:
+ codec->id = SND_AUDIOCODEC_FLAC;
+ /*
+ * Taken from the fcplay utility in tinycompress. Required for
+ * FLAC to work.
+ */
+ codec->options.flac_d.sample_size = 16;
+ codec->options.flac_d.min_blk_size = 16;
+ codec->options.flac_d.max_blk_size = 65535;
+ codec->options.flac_d.min_frame_size = 11;
+ codec->options.flac_d.max_frame_size = 8192 * 4;
+ rate = info->info.flac.rate;
+ channels = info->info.flac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_wma:
+ codec->id = SND_AUDIOCODEC_WMA;
+ /*
+ * WMA does not work with Compress-Offload if codec profile
+ * is not set.
+ */
+ switch (info->info.wma.profile) {
+ case SPA_AUDIO_WMA_PROFILE_WMA9:
+ codec->profile = SND_AUDIOPROFILE_WMA9;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA9_PRO:
+ codec->profile = SND_AUDIOPROFILE_WMA9_PRO;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS:
+ codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA10:
+ codec->profile = SND_AUDIOPROFILE_WMA10;
+ break;
+ case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS:
+ codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS;
+ break;
+ default:
+ spa_log_error(this->log, NAME " %p: Invalid WMA codec profile", this);
+ return -EINVAL;
+ }
+ codec->bit_rate = info->info.wma.bitrate;
+ codec->align = info->info.wma.block_align;
+ rate = info->info.wma.rate;
+ channels = info->info.wma.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_alac:
+ codec->id = SND_AUDIOCODEC_ALAC;
+ rate = info->info.alac.rate;
+ channels = info->info.alac.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_ape:
+ codec->id = SND_AUDIOCODEC_APE;
+ rate = info->info.ape.rate;
+ channels = info->info.ape.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_ra:
+ codec->id = SND_AUDIOCODEC_REAL;
+ rate = info->info.ra.rate;
+ channels = info->info.ra.channels;
+ break;
+ case SPA_MEDIA_SUBTYPE_amr:
+ if (info->info.amr.band_mode == SPA_AUDIO_AMR_BAND_MODE_WB)
+ codec->id = SND_AUDIOCODEC_AMRWB;
+ else
+ codec->id = SND_AUDIOCODEC_AMR;
+ rate = info->info.amr.rate;
+ channels = info->info.amr.channels;
+ break;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ codec->ch_in = channels;
+ codec->ch_out = channels;
+ codec->sample_rate = rate;
+ *out_rate = rate;
+
+ codec->rate_control = 0;
+ codec->level = 0;
+ codec->ch_mode = 0;
+ codec->format = 0;
+
+ spa_log_info(this->log, NAME " %p: Codec info, profile: %d align: %d rate: %d bitrate: %d",
+ this, codec->profile, codec->align, codec->sample_rate, codec->bit_rate);
+
+ if (!is_codec_supported_by_name(this->props.device, 0, codec)) {
+ spa_log_error(this->log, NAME " %p: Requested codec is not supported by DSP", this);
+ return -EINVAL;
+ }
+
+ config->codec = codec;
+ config->fragment_size = MIN_FRAGMENT_SIZE;
+ config->fragments = MIN_NUM_FRAGMENTS;
+
+ return 0;
+}
+
+static int
+port_set_format(struct impl *this,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ int res;
+ struct port *port = &this->port;
+
+ if (format == NULL) {
+ port->have_format = false;
+ clear_buffers(this, port);
+ } else {
+ struct spa_audio_info info = { 0 };
+ uint32_t rate;
+
+ if ((res = spa_format_audio_parse(format, &info)) < 0) {
+ spa_log_error(this->log, NAME " %p: format parse error: %s", this,
+ spa_strerror(res));
+ return res;
+ }
+
+ if ((res = compress_setup(this, &info, &rate)) < 0) {
+ spa_log_error(this->log, NAME " %p: can't setup compress: %s",
+ this, spa_strerror(res));
+ return res;
+ }
+
+ port->current_format = info;
+ port->have_format = true;
+ port->info.rate = SPA_FRACTION(1, rate);
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
+ this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
+ emit_node_info(this, false);
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+
+ if (port->have_format) {
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ return port_set_format(this, direction, port_id, flags, param);
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers,
+ uint32_t n_buffers)
+{
+ struct impl *this = object;
+ struct port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ if (!port->have_format)
+ return -EIO;
+
+ clear_buffers(this, port);
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b;
+ struct spa_data *d = buffers[i]->datas;
+
+ b = &port->buffers[i];
+ b->id = i;
+ b->flags = 0;
+ b->outbuf = buffers[i];
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this,
+ buffers[i]);
+ return -EINVAL;
+ }
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct impl *this = object;
+ struct port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = &this->port;
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct impl *this = object;
+ struct port *port;
+ struct spa_io_buffers *io;
+ struct buffer *b;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ port = &this->port;
+
+ io = port->io;
+ spa_return_val_if_fail(io != NULL, -EIO);
+
+ if (io->status != SPA_STATUS_HAVE_DATA)
+ return io->status;
+
+ if (io->buffer_id >= port->n_buffers) {
+ io->status = -EINVAL;
+ return io->status;
+ }
+
+ b = &port->buffers[io->buffer_id];
+
+ for (i = 0; i < b->outbuf->n_datas; i++) {
+ int32_t offs, size;
+ int32_t wrote;
+ void *buf;
+
+ struct spa_data *d = b->outbuf->datas;
+ d = b->outbuf->datas;
+
+ offs = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->maxsize - offs, d->chunk->size);
+ buf = SPA_PTROFF(d[0].data, offs, void);
+
+ wrote = write_compress(this, buf, size);
+ if (wrote < 0) {
+ spa_log_error(this->log, NAME " %p: Error playing sample: %s",
+ this, compress_get_error(this->compress));
+ io->status = wrote;
+ return SPA_STATUS_STOPPED;
+ }
+ }
+
+ io->status = SPA_STATUS_OK;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .enum_params = impl_node_enum_params,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ struct port *port;
+ const char *str;
+ uint32_t i;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = 0;
+ this->info.flags = SPA_NODE_FLAG_RT |
+ SPA_NODE_FLAG_IN_PORT_CONFIG |
+ SPA_NODE_FLAG_NEED_CONFIGURE;
+ this->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = 1;
+ reset_props(&this->props);
+
+ port = &this->port;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_NO_REF |
+ SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->info.params = port->params;
+ port->info.n_params = 4;
+ port->written = 0;
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, "clock.quantum-limit")) {
+ spa_atou32(s, &this->quantum_limit, 0);
+ } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
+ this->props.channels = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ this->props.rate = atoi(s);
+ }
+ }
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) {
+ if ((str[0] == 'h') || (str[1] == 'w') || (str[2] == ':')) {
+ snprintf(this->props.device, sizeof(this->props.device), "%s", str);
+ } else {
+ spa_log_error(this->log, NAME " %p: Invalid Compress-Offload hw %s", this, str);
+ return -EINVAL;
+ }
+ } else {
+ spa_log_error(this->log, NAME " %p: Invalid compress hw", this);
+ return -EINVAL;
+ }
+
+ /*
+ * TODO:
+ *
+ * Move this to use new compress_get_supported_codecs_by_name API once
+ * merged upstream.
+ *
+ * Right now, we pretend all codecs are supported and then error out
+ * at runtime in port_set_format during compress_setup if not
+ * supported.
+ */
+ this->num_codecs = SPA_N_ELEMENTS (codec_info);
+ for (i = 0; i < this->num_codecs; i++) {
+ this->codecs_supported[i] = codec_info[i].codec_id;
+ }
+
+ spa_log_info(this->log, NAME " %p: Initialized Compress-Offload sink %s",
+ this, this->props.device);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play compressed audio (like MP3 or AAC) with the ALSA Compress-Offload API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<path>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_compress_offload_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-device.c b/spa/plugins/alsa/alsa-pcm-device.c
new file mode 100644
index 0000000..984f1d0
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-device.c
@@ -0,0 +1,581 @@
+/* Spa ALSA Device
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <poll.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/support/log.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+#include <spa/param/param.h>
+#include <spa/pod/filter.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/log.h>
+
+#include "alsa.h"
+
+#define MAX_DEVICES 64
+
+static const char default_device[] = "hw:0";
+
+struct props {
+ char device[64];
+};
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+}
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+
+ struct spa_hook_list hooks;
+
+ struct props props;
+ uint32_t n_nodes;
+ uint32_t n_capture;
+ uint32_t n_playback;
+
+ uint32_t profile;
+};
+
+static const char *get_stream(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_stream(pcminfo)) {
+ case SND_PCM_STREAM_PLAYBACK:
+ return "playback";
+ case SND_PCM_STREAM_CAPTURE:
+ return "capture";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_class(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_class(pcminfo)) {
+ case SND_PCM_CLASS_GENERIC:
+ return "generic";
+ case SND_PCM_CLASS_MULTI:
+ return "multichannel";
+ case SND_PCM_CLASS_MODEM:
+ return "modem";
+ case SND_PCM_CLASS_DIGITIZER:
+ return "digitizer";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_subclass(snd_pcm_info_t *pcminfo)
+{
+ switch (snd_pcm_info_get_subclass(pcminfo)) {
+ case SND_PCM_SUBCLASS_GENERIC_MIX:
+ return "generic-mix";
+ case SND_PCM_SUBCLASS_MULTI_MIX:
+ return "multichannel-mix";
+ default:
+ return "unknown";
+ }
+}
+
+static int emit_node(struct impl *this, snd_ctl_card_info_t *cardinfo, snd_pcm_info_t *pcminfo, uint32_t id)
+{
+ struct spa_dict_item items[12];
+ char device_name[128], path[180];
+ char sync_name[128], dev[16], subdev[16], card[16];
+ struct spa_device_object_info info;
+ snd_pcm_sync_id_t sync_id;
+ const char *stream;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+ info.type = SPA_TYPE_INTERFACE_Node;
+
+ if (snd_pcm_info_get_stream(pcminfo) == SND_PCM_STREAM_PLAYBACK) {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SINK;
+ stream = "playback";
+ } else {
+ info.factory_name = SPA_NAME_API_ALSA_PCM_SOURCE;
+ stream = "capture";
+ }
+
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+
+ snprintf(card, sizeof(card), "%d", snd_pcm_info_get_card(pcminfo));
+ snprintf(dev, sizeof(dev), "%d", snd_pcm_info_get_device(pcminfo));
+ snprintf(subdev, sizeof(subdev), "%d", snd_pcm_info_get_subdevice(pcminfo));
+ snprintf(device_name, sizeof(device_name), "%s,%s", this->props.device, dev);
+ snprintf(path, sizeof(path), "alsa:pcm:%s:%s:%s", snd_ctl_card_info_get_id(cardinfo), dev, stream);
+ items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
+ items[1] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, device_name);
+ items[2] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CARD, card);
+ items[3] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_DEVICE, dev);
+ items[4] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBDEVICE, subdev);
+ items[5] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, get_stream(pcminfo));
+ items[6] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_ID, snd_pcm_info_get_id(pcminfo));
+ items[7] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_NAME, snd_pcm_info_get_name(pcminfo));
+ items[8] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBNAME, snd_pcm_info_get_subdevice_name(pcminfo));
+ items[9] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_CLASS, get_class(pcminfo));
+ items[10] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SUBCLASS, get_subclass(pcminfo));
+ sync_id = snd_pcm_info_get_sync(pcminfo);
+ snprintf(sync_name, sizeof(sync_name), "%08x:%08x:%08x:%08x",
+ sync_id.id32[0], sync_id.id32[1], sync_id.id32[2], sync_id.id32[3]);
+ items[11] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_SYNC_ID, sync_name);
+ info.props = &SPA_DICT_INIT_ARRAY(items);
+
+ spa_device_emit_object_info(&this->hooks, id, &info);
+
+ return 0;
+}
+
+static int activate_profile(struct impl *this, snd_ctl_t *ctl_hndl, uint32_t id)
+{
+ int err = 0, dev;
+ uint32_t i, n_cap, n_play;
+ snd_pcm_info_t *pcminfo;
+ snd_ctl_card_info_t *cardinfo;
+
+ spa_log_debug(this->log, "profile %d", id);
+ this->profile = id;
+
+ snd_ctl_card_info_alloca(&cardinfo);
+ if ((err = snd_ctl_card_info(ctl_hndl, cardinfo)) < 0) {
+ spa_log_error(this->log, "error card info: %s", snd_strerror(err));
+ return err;
+ }
+
+ for (i = 0; i < this->n_nodes; i++)
+ spa_device_emit_object_info(&this->hooks, i, NULL);
+
+ this->n_nodes = this->n_capture = this->n_playback = 0;
+
+ if (id == 0)
+ return 0;
+
+ snd_pcm_info_alloca(&pcminfo);
+ dev = -1;
+ i = n_cap = n_play = 0;
+ while (1) {
+ if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) {
+ spa_log_error(this->log, "error iterating devices: %s", snd_strerror(err));
+ break;
+ }
+ if (dev < 0)
+ break;
+
+ snd_pcm_info_set_device(pcminfo, dev);
+ snd_pcm_info_set_subdevice(pcminfo, 0);
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ spa_log_error(this->log, "error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ n_play++;
+ emit_node(this, cardinfo, pcminfo, i++);
+ }
+
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
+ if ((err = snd_ctl_pcm_info(ctl_hndl, pcminfo)) < 0) {
+ if (err != -ENOENT)
+ spa_log_error(this->log, "error pcm info: %s", snd_strerror(err));
+ }
+ if (err >= 0) {
+ n_cap++;
+ emit_node(this, cardinfo, pcminfo, i++);
+ }
+ }
+ this->n_capture = n_cap;
+ this->n_playback = n_play;
+ this->n_nodes = i;
+ return err;
+}
+
+static int set_profile(struct impl *this, uint32_t id)
+{
+ snd_ctl_t *ctl_hndl;
+ int err;
+
+ spa_log_debug(this->log, "open card %s", this->props.device);
+ if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) {
+ spa_log_error(this->log, "can't open control for card %s: %s",
+ this->props.device, snd_strerror(err));
+ return err;
+ }
+
+ err = activate_profile(this, ctl_hndl, id);
+
+ spa_log_debug(this->log, "close card %s", this->props.device);
+ snd_ctl_close(ctl_hndl);
+
+ return err;
+}
+
+static int emit_info(struct impl *this, bool full)
+{
+ int err = 0;
+ struct spa_dict_item items[20];
+ uint32_t n_items = 0;
+ snd_ctl_t *ctl_hndl;
+ snd_ctl_card_info_t *info;
+ struct spa_device_info dinfo;
+ struct spa_param_info params[2];
+ char path[128];
+
+ spa_log_debug(this->log, "open card %s", this->props.device);
+ if ((err = snd_ctl_open(&ctl_hndl, this->props.device, 0)) < 0) {
+ spa_log_error(this->log, "can't open control for card %s: %s",
+ this->props.device, snd_strerror(err));
+ return err;
+ }
+
+ snd_ctl_card_info_alloca(&info);
+ if ((err = snd_ctl_card_info(ctl_hndl, info)) < 0) {
+ spa_log_error(this->log, "error hardware info: %s", snd_strerror(err));
+ goto exit;
+ }
+
+ dinfo = SPA_DEVICE_INFO_INIT();
+
+ dinfo.change_mask = SPA_DEVICE_CHANGE_MASK_PROPS;
+
+#define ADD_ITEM(key, value) items[n_items++] = SPA_DICT_ITEM_INIT(key, value)
+ snprintf(path, sizeof(path), "alsa:pcm:%s", snd_ctl_card_info_get_id(info));
+ ADD_ITEM(SPA_KEY_OBJECT_PATH, path);
+ ADD_ITEM(SPA_KEY_DEVICE_API, "alsa:pcm");
+ ADD_ITEM(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ ADD_ITEM(SPA_KEY_API_ALSA_PATH, (char *)this->props.device);
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_ID, snd_ctl_card_info_get_id(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_COMPONENTS, snd_ctl_card_info_get_components(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_DRIVER, snd_ctl_card_info_get_driver(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_NAME, snd_ctl_card_info_get_name(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_LONGNAME, snd_ctl_card_info_get_longname(info));
+ ADD_ITEM(SPA_KEY_API_ALSA_CARD_MIXERNAME, snd_ctl_card_info_get_mixername(info));
+ dinfo.props = &SPA_DICT_INIT(items, n_items);
+#undef ADD_ITEM
+
+ dinfo.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS;
+ params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumProfile, SPA_PARAM_INFO_READ);
+ params[1] = SPA_PARAM_INFO(SPA_PARAM_Profile, SPA_PARAM_INFO_READWRITE);
+ dinfo.n_params = SPA_N_ELEMENTS(params);
+ dinfo.params = params;
+
+ spa_device_emit_info(&this->hooks, &dinfo);
+
+ exit:
+ spa_log_debug(this->log, "close card %s", this->props.device);
+ snd_ctl_close(ctl_hndl);
+ return err;
+}
+
+static int impl_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_device_events *events,
+ void *data)
+{
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ if (events->info || events->object_info)
+ emit_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+
+static int impl_sync(void *object, int seq)
+{
+ struct impl *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_device_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder *b,
+ uint32_t id, uint32_t index)
+{
+ struct spa_pod_frame f[2];
+ const char *name, *desc;
+
+ switch (index) {
+ case 0:
+ name = "off";
+ desc = "Off";
+ break;
+ case 1:
+ name = "on";
+ desc = "On";
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamProfile, id);
+ spa_pod_builder_add(b,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(index),
+ SPA_PARAM_PROFILE_name, SPA_POD_String(name),
+ SPA_PARAM_PROFILE_description, SPA_POD_String(desc),
+ 0);
+ if (index == 1) {
+ spa_pod_builder_prop(b, SPA_PARAM_PROFILE_classes, 0);
+ spa_pod_builder_push_struct(b, &f[1]);
+ if (this->n_capture) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Source"),
+ SPA_POD_Int(this->n_capture));
+ }
+ if (this->n_playback) {
+ spa_pod_builder_add_struct(b,
+ SPA_POD_String("Audio/Sink"),
+ SPA_POD_Int(this->n_playback));
+ }
+ spa_pod_builder_pop(b, &f[1]);
+ }
+ return spa_pod_builder_pop(b, &f[0]);
+
+}
+static int impl_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct impl *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_device_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumProfile:
+ {
+ switch (result.index) {
+ case 0:
+ case 1:
+ param = build_profile(this, &b, id, result.index);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Profile:
+ {
+ switch (result.index) {
+ case 0:
+ param = build_profile(this, &b, id, this->profile);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_device_emit_result(&this->hooks, seq, 0,
+ SPA_RESULT_TYPE_DEVICE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_set_param(void *object,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct impl *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Profile:
+ {
+ uint32_t idx;
+
+ if ((res = spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_ParamProfile, NULL,
+ SPA_PARAM_PROFILE_index, SPA_POD_Int(&idx))) < 0) {
+ spa_log_warn(this->log, "can't parse profile");
+ spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param);
+ return res;
+ }
+
+ set_profile(this, idx);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_add_listener,
+ .sync = impl_sync,
+ .enum_params = impl_enum_params,
+ .set_param = impl_set_param,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+ spa_hook_list_init(&this->hooks);
+
+ reset_props(&this->props);
+
+ snd_config_update_free_global();
+
+ if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH)))
+ snprintf(this->props.device, 64, "%s", str);
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_device_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_DEVICE,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-sink.c b/spa/plugins/alsa/alsa-pcm-sink.c
new file mode 100644
index 0000000..a8c40b1
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-sink.c
@@ -0,0 +1,1014 @@
+/* Spa ALSA Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/monitor/device.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "alsa-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0)
+
+static const char default_device[] = "hw:0";
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+ props->use_chmap = DEFAULT_USE_CHMAP;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[7];
+ uint32_t i, n_items = 0;
+ char latency[64], period[64], nperiods[64], headroom[64];
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Sink");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ if (this->have_format) {
+ snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
+ snprintf(period, sizeof(period), "%lu", this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
+ snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
+ snprintf(headroom, sizeof(headroom), "%u", this->headroom);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
+ }
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->port_info.change_mask : 0;
+
+ if (full)
+ this->port_info.change_mask = this->port_info_all;
+ if (this->port_info.change_mask) {
+ uint32_t i;
+
+ if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->port_info.n_params; i++) {
+ if (this->port_params[i].user > 0) {
+ this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->port_params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_INPUT, 0, &this->port_info);
+ this->port_info.change_mask = old;
+ }
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[4096];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ {
+ struct props *p = &this->props;
+
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC));
+ break;
+ case 4:
+ if (!this->is_iec958 && !this->is_hdmi)
+ goto next;
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_iec958Codecs),
+ SPA_PROP_INFO_name, SPA_POD_String("iec958.codecs"),
+ SPA_PROP_INFO_description, SPA_POD_String("Enabled IEC958 (S/PDIF) codecs"),
+ SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_IEC958_CODEC_UNKNOWN),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true),
+ SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array));
+ break;
+ default:
+ param = spa_alsa_enum_propinfo(this, result.index - 5, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod_frame f;
+ uint32_t codecs[16], n_codecs;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)),
+ SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+
+ if (this->is_iec958 || this->is_hdmi) {
+ n_codecs = spa_alsa_get_iec958_codecs(this, codecs, SPA_N_ELEMENTS(codecs));
+ spa_pod_builder_prop(&b, SPA_PROP_iec958Codecs, 0);
+ spa_pod_builder_array(&b, sizeof(uint32_t), SPA_TYPE_Id,
+ n_codecs, codecs);
+ }
+ spa_alsa_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_reassign_follower(this);
+
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *iec958_codecs = NULL, *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_iec958Codecs, SPA_POD_OPT_Pod(&iec958_codecs),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ if ((this->is_iec958 || this->is_hdmi) && iec958_codecs != NULL) {
+ uint32_t i, codecs[16], n_codecs;
+ n_codecs = spa_pod_copy_array(iec958_codecs, SPA_TYPE_Id,
+ codecs, SPA_N_ELEMENTS(codecs));
+ this->iec958_codecs = 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
+ for (i = 0; i < n_codecs; i++)
+ this->iec958_codecs |= 1ULL << codecs[i];
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ this->params[NODE_Props].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_EnumFormat].user++;
+ }
+ spa_alsa_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if (param == NULL)
+ spa_zero(info);
+ else if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ if ((res = spa_alsa_open(this, NULL)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ if (this->have_format)
+ return 0;
+ if ((res = spa_alsa_close(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->have_format)
+ return -EIO;
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if ((res = spa_alsa_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Suspend:
+ case SPA_NODE_COMMAND_Pause:
+ if ((res = spa_alsa_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int
+impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_alsa_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ switch (this->current_format.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ param = spa_format_audio_raw_build(&b, id,
+ &this->current_format.info.raw);
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ param = spa_format_audio_iec958_build(&b, id,
+ &this->current_format.info.iec958);
+ break;
+ case SPA_MEDIA_SUBTYPE_dsd:
+ param = spa_format_audio_dsd_build(&b, id,
+ &this->current_format.info.dsd);
+ break;
+ default:
+ return -EIO;
+ }
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->frame_size * this->frame_scale,
+ 16 * this->frame_size * this->frame_scale,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_INPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this)
+{
+ if (this->n_buffers > 0) {
+ spa_list_init(&this->ready);
+ this->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ const struct spa_pod *format)
+{
+ struct state *this = object;
+ int err = 0;
+
+ if (format == NULL) {
+ if (!this->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ spa_alsa_close(this);
+ clear_buffers(this);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio)
+ return -EINVAL;
+
+ switch (info.media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_SUBTYPE_iec958:
+ if (spa_format_audio_iec958_parse(format, &info.info.iec958) < 0)
+ return -EINVAL;
+ break;
+ case SPA_MEDIA_SUBTYPE_dsd:
+ if (spa_format_audio_dsd_parse(format, &info.info.dsd) < 0)
+ return -EINVAL;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if ((err = spa_alsa_set_format(this, &info, flags)) < 0)
+ return err;
+
+ this->current_format = info;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ this->port_info.rate = SPA_FRACTION(1, this->rate);
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (this->have_format) {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Latency].user++;
+ } else {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, false);
+
+ return err;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, direction, port_id, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if (param == NULL)
+ info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction));
+ else if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+ emit_port_info(this, false);
+ res = 0;
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ uint32_t i;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (this->n_buffers > 0) {
+ spa_alsa_pause(this);
+ if ((res = clear_buffers(this)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !this->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data);
+ }
+ this->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ this->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ this->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct spa_io_buffers *io;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if ((io = this->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status,
+ io->buffer_id, this->n_buffers);
+
+ if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
+ io->status = SPA_STATUS_NEED_DATA;
+ return SPA_STATUS_HAVE_DATA;
+ }
+ if (io->status == SPA_STATUS_HAVE_DATA &&
+ io->buffer_id < this->n_buffers) {
+ struct buffer *b = &this->buffers[io->buffer_id];
+
+ if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_warn(this->log, "%p: buffer %u in use",
+ this, io->buffer_id);
+ io->status = -EINVAL;
+ return -EINVAL;
+ }
+ spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id);
+ spa_list_append(&this->ready, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ io->buffer_id = SPA_ID_INVALID;
+
+ spa_alsa_write(this);
+
+ io->status = SPA_STATUS_OK;
+ }
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_alsa_close(this);
+ spa_alsa_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Node,
+ SPA_VERSION_NODE,
+ &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->stream = SND_PCM_STREAM_PLAYBACK;
+ this->port_direction = SPA_DIRECTION_INPUT;
+ this->latency[this->port_direction] = SPA_LATENCY_INFO(
+ this->port_direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info = SPA_NODE_INFO_INIT();
+ this->info.max_input_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+
+ reset_props(&this->props);
+
+ this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_info = SPA_PORT_INFO_INIT();
+ this->port_info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->port_info.params = this->port_params;
+ this->port_info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&this->ready);
+
+ return spa_alsa_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *info = &impl_interfaces[*index];
+ break;
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Play audio with the alsa API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<path>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_sink_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_SINK,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm-source.c b/spa/plugins/alsa/alsa-pcm-source.c
new file mode 100644
index 0000000..f079bf6
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm-source.c
@@ -0,0 +1,964 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/list.h>
+#include <spa/utils/string.h>
+#include <spa/monitor/device.h>
+#include <spa/param/audio/format.h>
+#include <spa/pod/filter.h>
+
+#include "alsa.h"
+
+#include "alsa-pcm.h"
+
+#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0)
+
+static const char default_device[] = "hw:0";
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, default_device, 64);
+ props->use_chmap = DEFAULT_USE_CHMAP;
+}
+
+static void emit_node_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ struct spa_dict_item items[7];
+ uint32_t i, n_items = 0;
+ char latency[64], period[64], nperiods[64], headroom[64];
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Source");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true");
+ if (this->have_format) {
+ snprintf(latency, sizeof(latency), "%lu/%d", this->buffer_frames / 2, this->rate);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency);
+ snprintf(period, sizeof(period), "%lu", this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-size", period);
+ snprintf(nperiods, sizeof(nperiods), "%lu", this->buffer_frames / this->period_frames);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods);
+ snprintf(headroom, sizeof(headroom), "%u", this->headroom);
+ items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.headroom", headroom);
+ }
+ this->info.props = &SPA_DICT_INIT(items, n_items);
+
+ if (this->info.change_mask & SPA_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->info.n_params; i++) {
+ if (this->params[i].user > 0) {
+ this->params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void emit_port_info(struct state *this, bool full)
+{
+ uint64_t old = full ? this->port_info.change_mask : 0;
+ if (full)
+ this->port_info.change_mask = this->port_info_all;
+ if (this->port_info.change_mask) {
+ uint32_t i;
+
+ if (this->port_info.change_mask & SPA_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < this->port_info.n_params; i++) {
+ if (this->port_params[i].user > 0) {
+ this->port_params[i].flags ^= SPA_PARAM_INFO_SERIAL;
+ this->port_params[i].user = 0;
+ }
+ }
+ }
+ spa_node_emit_port_info(&this->hooks,
+ SPA_DIRECTION_OUTPUT, 0, &this->port_info);
+ this->port_info.change_mask = old;
+ }
+}
+
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct props *p;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ p = &this->props;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_cardName),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA card name"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)));
+ break;
+ case 3:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec),
+ SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, 0LL, 2 * SPA_NSEC_PER_SEC));
+ break;
+ default:
+ param = spa_alsa_enum_propinfo(this, result.index - 4, &b);
+ if (param == NULL)
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Props:
+ {
+ struct spa_pod_frame f;
+
+ switch (result.index) {
+ case 0:
+ spa_pod_builder_push_object(&b, &f,
+ SPA_TYPE_OBJECT_Props, id);
+ spa_pod_builder_add(&b,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_deviceName, SPA_POD_Stringn(p->device_name, sizeof(p->device_name)),
+ SPA_PROP_cardName, SPA_POD_Stringn(p->card_name, sizeof(p->card_name)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_Long(this->process_latency.ns),
+ 0);
+ spa_alsa_add_prop_params(this, &b);
+ param = spa_pod_builder_pop(&b, &f);
+ break;
+ default:
+ return 0;
+ }
+ break;
+ }
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_ProcessLatency:
+ switch (result.index) {
+ case 0:
+ param = spa_process_latency_build(&b, id, &this->process_latency);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ if (size > 0 && size < sizeof(struct spa_io_clock))
+ return -EINVAL;
+ this->clock = data;
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_reassign_follower(this);
+ return 0;
+}
+
+static void handle_process_latency(struct state *this,
+ const struct spa_process_latency_info *info)
+{
+ bool ns_changed = this->process_latency.ns != info->ns;
+
+ if (this->process_latency.quantum == info->quantum &&
+ this->process_latency.rate == info->rate &&
+ !ns_changed)
+ return;
+
+ this->process_latency = *info;
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ if (ns_changed)
+ this->params[NODE_Props].user++;
+ this->params[NODE_ProcessLatency].user++;
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+ struct spa_pod *params = NULL;
+ int64_t lat_ns = -1;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)),
+ SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&lat_ns),
+ SPA_PROP_params, SPA_POD_OPT_Pod(&params));
+
+ spa_alsa_parse_prop_params(this, params);
+ if (lat_ns != -1) {
+ struct spa_process_latency_info info;
+ info = this->process_latency;
+ info.ns = lat_ns;
+ handle_process_latency(this, &info);
+ }
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ case SPA_PARAM_ProcessLatency:
+ {
+ struct spa_process_latency_info info;
+ if (param == NULL)
+ spa_zero(info);
+ else if ((res = spa_process_latency_parse(param, &info)) < 0)
+ return res;
+
+ handle_process_latency(this, &info);
+
+ emit_node_info(this, false);
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_ParamBegin:
+ if ((res = spa_alsa_open(this, NULL)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_ParamEnd:
+ if (this->have_format)
+ return 0;
+ if ((res = spa_alsa_close(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Start:
+ if (!this->have_format)
+ return -EIO;
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if ((res = spa_alsa_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_alsa_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_port_info(this, true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct state *this = object;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ return spa_alsa_enum_format(this, seq, start, num, filter);
+
+ case SPA_PARAM_Format:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_format_audio_raw_build(&b, id, &this->current_format.info.raw);
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!this->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ this->quantum_limit * this->frame_size,
+ 16 * this->frame_size,
+ INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ {
+ struct spa_latency_info latency = this->latency[result.index];
+ if (latency.direction == SPA_DIRECTION_OUTPUT)
+ spa_process_latency_info_add(&this->process_latency, &latency);
+ param = spa_latency_build(&b, id, &latency);
+ break;
+ }
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct state *this)
+{
+ if (this->n_buffers > 0) {
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+ this->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct state *this = object;
+ int err = 0;
+
+ if (format == NULL) {
+ if (!this->have_format)
+ return 0;
+
+ spa_log_debug(this->log, "clear format");
+ spa_alsa_close(this);
+ clear_buffers(this);
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+
+ if (spa_format_audio_raw_parse(format, &info.info.raw) < 0)
+ return -EINVAL;
+
+ if ((err = spa_alsa_set_format(this, &info, flags)) < 0)
+ return err;
+
+ this->current_format = info;
+ }
+
+ this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS;
+ emit_node_info(this, false);
+
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ this->port_info.rate = SPA_FRACTION(1, this->rate);
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (this->have_format) {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Latency].user++;
+ } else {
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, false);
+
+ return err;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, direction, port_id, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if (param == NULL)
+ info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction));
+ else if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ this->latency[info.direction] = info;
+ this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_params[PORT_Latency].user++;
+ emit_port_info(this, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct state *this = object;
+ int res;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers);
+
+ if (this->n_buffers > 0) {
+ spa_alsa_pause(this);
+ if ((res = clear_buffers(this)) < 0)
+ return res;
+ }
+ if (n_buffers > 0 && !this->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = 0;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ spa_list_append(&this->free, &b->link);
+ }
+ this->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ this->io = data;
+ break;
+ case SPA_IO_RateMatch:
+ this->rate_match = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(port_id == 0, -EINVAL);
+
+ if (this->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= this->n_buffers)
+ return -EINVAL;
+
+ spa_alsa_recycle_buffer(this, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct state *this = object;
+ struct spa_io_buffers *io;
+ struct buffer *b;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ if ((io = this->io) == NULL)
+ return -EIO;
+
+ spa_log_trace_fp(this->log, "%p; status %d", this, io->status);
+
+ if (io->status == SPA_STATUS_HAVE_DATA)
+ return SPA_STATUS_HAVE_DATA;
+
+ if (io->buffer_id < this->n_buffers) {
+ spa_alsa_recycle_buffer(this, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+
+ if (spa_list_is_empty(&this->ready) && this->following) {
+ if (this->freewheel)
+ spa_alsa_skip(this);
+ else
+ spa_alsa_read(this);
+ }
+ if (spa_list_is_empty(&this->ready) || !this->following)
+ return SPA_STATUS_OK;
+
+ b = spa_list_first(&this->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+
+ return SPA_STATUS_HAVE_DATA;
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct state *this;
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ this = (struct state *) handle;
+ spa_alsa_close(this);
+ spa_alsa_clear(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct state *this;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "%p: a data loop is needed", this);
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "%p: a data system is needed", this);
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+ this->stream = SND_PCM_STREAM_CAPTURE;
+ this->port_direction = SPA_DIRECTION_OUTPUT;
+ this->latency[this->port_direction] = SPA_LATENCY_INFO(
+ this->port_direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info.max_output_ports = 1;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+ reset_props(&this->props);
+
+ this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ this->port_info = SPA_PORT_INFO_INIT();
+ this->port_info.flags = SPA_PORT_FLAG_LIVE |
+ SPA_PORT_FLAG_PHYSICAL |
+ SPA_PORT_FLAG_TERMINAL;
+ this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ this->port_info.params = this->port_params;
+ this->port_info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+
+ return spa_alsa_init(this, info);
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Record audio with the alsa API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<device>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_source_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_PCM_SOURCE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c
new file mode 100644
index 0000000..012b460
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm.c
@@ -0,0 +1,2696 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+
+#include <spa/pod/filter.h>
+#include <spa/utils/string.h>
+#include <spa/utils/result.h>
+#include <spa/support/system.h>
+#include <spa/utils/keys.h>
+
+#include "alsa-pcm.h"
+
+static struct spa_list cards = SPA_LIST_INIT(&cards);
+
+static struct card *find_card(uint32_t index)
+{
+ struct card *c;
+ spa_list_for_each(c, &cards, link) {
+ if (c->index == index) {
+ c->ref++;
+ return c;
+ }
+ }
+ return NULL;
+}
+
+static struct card *ensure_card(uint32_t index, bool ucm)
+{
+ struct card *c;
+ char card_name[64];
+ const char *alibpref = NULL;
+ int err;
+
+ if ((c = find_card(index)) != NULL)
+ return c;
+
+ c = calloc(1, sizeof(*c));
+ c->ref = 1;
+ c->index = index;
+
+ if (ucm) {
+ snprintf(card_name, sizeof(card_name), "hw:%i", index);
+ err = snd_use_case_mgr_open(&c->ucm, card_name);
+ if (err < 0) {
+ char *name;
+ err = snd_card_get_name(index, &name);
+ if (err < 0)
+ goto error;
+
+ snprintf(card_name, sizeof(card_name), "%s", name);
+ free(name);
+
+ err = snd_use_case_mgr_open(&c->ucm, card_name);
+ if (err < 0)
+ goto error;
+ }
+ if ((snd_use_case_get(c->ucm, "_alibpref", &alibpref) != 0))
+ alibpref = NULL;
+ c->ucm_prefix = (char*)alibpref;
+ }
+ spa_list_append(&cards, &c->link);
+
+ return c;
+error:
+ free(c);
+ errno = -err;
+ return NULL;
+}
+
+static void release_card(struct card *c)
+{
+ spa_assert(c->ref > 0);
+
+ if (--c->ref > 0)
+ return;
+
+ spa_list_remove(&c->link);
+ if (c->ucm) {
+ free(c->ucm_prefix);
+ snd_use_case_mgr_close(c->ucm);
+ }
+ free(c);
+}
+
+static int alsa_set_param(struct state *state, const char *k, const char *s)
+{
+ int fmt_change = 0;
+ if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) {
+ state->default_channels = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) {
+ state->default_rate = atoi(s);
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_FORMAT)) {
+ state->default_format = spa_alsa_format_from_name(s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) {
+ spa_alsa_parse_position(&state->default_pos, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) {
+ state->n_allowed_rates = spa_alsa_parse_rates(state->allowed_rates,
+ MAX_RATES, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, "iec958.codecs")) {
+ spa_alsa_parse_iec958_codecs(&state->iec958_codecs, s, strlen(s));
+ fmt_change++;
+ } else if (spa_streq(k, "api.alsa.period-size")) {
+ state->default_period_size = atoi(s);
+ } else if (spa_streq(k, "api.alsa.period-num")) {
+ state->default_period_num = atoi(s);
+ } else if (spa_streq(k, "api.alsa.headroom")) {
+ state->default_headroom = atoi(s);
+ } else if (spa_streq(k, "api.alsa.start-delay")) {
+ state->default_start_delay = atoi(s);
+ } else if (spa_streq(k, "api.alsa.disable-mmap")) {
+ state->disable_mmap = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.disable-batch")) {
+ state->disable_batch = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.use-chmap")) {
+ state->props.use_chmap = spa_atob(s);
+ } else if (spa_streq(k, "api.alsa.multi-rate")) {
+ state->multi_rate = spa_atob(s);
+ } else if (spa_streq(k, "latency.internal.rate")) {
+ state->process_latency.rate = atoi(s);
+ } else if (spa_streq(k, "latency.internal.ns")) {
+ state->process_latency.ns = atoi(s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(state->clock_name,
+ sizeof(state->clock_name), "%s", s);
+ } else
+ return 0;
+
+ if (fmt_change > 0) {
+ state->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ state->port_params[PORT_EnumFormat].user++;
+ }
+ return 1;
+}
+
+static int position_to_string(struct channel_map *map, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < map->channels; i++) {
+ r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ",
+ spa_debug_type_find_short_name(spa_type_audio_channel,
+ map->pos[i]));
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len)
+{
+ uint32_t i, o = 0;
+ int r;
+ o += snprintf(val, len, "[ ");
+ for (i = 0; i < n_vals; i++) {
+ r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]);
+ if (r < 0 || o + r >= len)
+ return -ENOSPC;
+ o += r;
+ }
+ if (len > o)
+ o += snprintf(val+o, len-o, " ]");
+ return 0;
+}
+
+struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b)
+{
+ struct spa_pod *param;
+
+ switch (idx) {
+ case 0:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_CHANNELS),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Channels"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_channels),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_RATE),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Rate"),
+ SPA_PROP_INFO_type, SPA_POD_Int(state->default_rate),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 2:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_FORMAT),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Format"),
+ SPA_PROP_INFO_type, SPA_POD_String(
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format)),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 3:
+ {
+ char buf[1024];
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_POSITION),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Position"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 4:
+ {
+ char buf[1024];
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates, buf, sizeof(buf));
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_AUDIO_ALLOWED_RATES),
+ SPA_PROP_INFO_description, SPA_POD_String("Audio Allowed Rates"),
+ SPA_PROP_INFO_type, SPA_POD_String(buf),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ }
+ case 5:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-size"),
+ SPA_PROP_INFO_description, SPA_POD_String("Period Size"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_size, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 6:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.period-num"),
+ SPA_PROP_INFO_description, SPA_POD_String("Number of Periods"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_period_num, 0, 1024),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 7:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.headroom"),
+ SPA_PROP_INFO_description, SPA_POD_String("Headroom"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_headroom, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 8:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.start-delay"),
+ SPA_PROP_INFO_description, SPA_POD_String("Start Delay"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->default_start_delay, 0, 8192),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 9:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-mmap"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable MMAP"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_mmap),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 10:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.disable-batch"),
+ SPA_PROP_INFO_description, SPA_POD_String("Disable Batch"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->disable_batch),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 11:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.use-chmap"),
+ SPA_PROP_INFO_description, SPA_POD_String("Use the driver channelmap"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->props.use_chmap),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 12:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("api.alsa.multi-rate"),
+ SPA_PROP_INFO_description, SPA_POD_String("Support multiple rates"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(state->multi_rate),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 13:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.rate"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in samples"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(state->process_latency.rate,
+ 0, 65536),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 14:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("latency.internal.ns"),
+ SPA_PROP_INFO_description, SPA_POD_String("Internal latency in nanoseconds"),
+ SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(state->process_latency.ns,
+ 0LL, 2 * SPA_NSEC_PER_SEC),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ case 15:
+ param = spa_pod_builder_add_object(b,
+ SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
+ SPA_PROP_INFO_name, SPA_POD_String("clock.name"),
+ SPA_PROP_INFO_description, SPA_POD_String("The name of the clock"),
+ SPA_PROP_INFO_type, SPA_POD_String(state->clock_name),
+ SPA_PROP_INFO_params, SPA_POD_Bool(true));
+ break;
+ default:
+ return NULL;
+ }
+ return param;
+}
+
+int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ char buf[1024];
+
+ spa_pod_builder_prop(b, SPA_PROP_params, 0);
+ spa_pod_builder_push_struct(b, &f[0]);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_CHANNELS);
+ spa_pod_builder_int(b, state->default_channels);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_RATE);
+ spa_pod_builder_int(b, state->default_rate);
+
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_FORMAT);
+ spa_pod_builder_string(b,
+ spa_debug_type_find_short_name(spa_type_audio_format,
+ state->default_format));
+
+ position_to_string(&state->default_pos, buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_POSITION);
+ spa_pod_builder_string(b, buf);
+
+ uint32_array_to_string(state->allowed_rates, state->n_allowed_rates,
+ buf, sizeof(buf));
+ spa_pod_builder_string(b, SPA_KEY_AUDIO_ALLOWED_RATES);
+ spa_pod_builder_string(b, buf);
+
+ spa_pod_builder_string(b, "api.alsa.period-size");
+ spa_pod_builder_int(b, state->default_period_size);
+
+ spa_pod_builder_string(b, "api.alsa.period-num");
+ spa_pod_builder_int(b, state->default_period_num);
+
+ spa_pod_builder_string(b, "api.alsa.headroom");
+ spa_pod_builder_int(b, state->default_headroom);
+
+ spa_pod_builder_string(b, "api.alsa.start-delay");
+ spa_pod_builder_int(b, state->default_start_delay);
+
+ spa_pod_builder_string(b, "api.alsa.disable-mmap");
+ spa_pod_builder_bool(b, state->disable_mmap);
+
+ spa_pod_builder_string(b, "api.alsa.disable-batch");
+ spa_pod_builder_bool(b, state->disable_batch);
+
+ spa_pod_builder_string(b, "api.alsa.use-chmap");
+ spa_pod_builder_bool(b, state->props.use_chmap);
+
+ spa_pod_builder_string(b, "api.alsa.multi-rate");
+ spa_pod_builder_bool(b, state->multi_rate);
+
+ spa_pod_builder_string(b, "latency.internal.rate");
+ spa_pod_builder_int(b, state->process_latency.rate);
+
+ spa_pod_builder_string(b, "latency.internal.ns");
+ spa_pod_builder_long(b, state->process_latency.ns);
+
+ spa_pod_builder_string(b, "clock.name");
+ spa_pod_builder_string(b, state->clock_name);
+
+ spa_pod_builder_pop(b, &f[0]);
+ return 0;
+}
+
+int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params)
+{
+ struct spa_pod_parser prs;
+ struct spa_pod_frame f;
+ int changed = 0;
+
+ if (params == NULL)
+ return 0;
+
+ spa_pod_parser_pod(&prs, params);
+ if (spa_pod_parser_push_struct(&prs, &f) < 0)
+ return 0;
+
+ while (true) {
+ const char *name;
+ struct spa_pod *pod;
+ char value[512];
+
+ if (spa_pod_parser_get_string(&prs, &name) < 0)
+ break;
+
+ if (spa_pod_parser_get_pod(&prs, &pod) < 0)
+ break;
+ if (spa_pod_is_string(pod)) {
+ spa_pod_copy_string(pod, sizeof(value), value);
+ } else if (spa_pod_is_int(pod)) {
+ snprintf(value, sizeof(value), "%d",
+ SPA_POD_VALUE(struct spa_pod_int, pod));
+ } else if (spa_pod_is_long(pod)) {
+ snprintf(value, sizeof(value), "%"PRIi64,
+ SPA_POD_VALUE(struct spa_pod_long, pod));
+ } else if (spa_pod_is_bool(pod)) {
+ snprintf(value, sizeof(value), "%s",
+ SPA_POD_VALUE(struct spa_pod_bool, pod) ?
+ "true" : "false");
+ } else
+ continue;
+
+ spa_log_info(state->log, "key:'%s' val:'%s'", name, value);
+ alsa_set_param(state, name, value);
+ changed++;
+ }
+ if (changed > 0) {
+ state->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS;
+ state->params[NODE_Props].user++;
+ }
+ return changed;
+}
+
+#define CHECK(s,msg,...) if ((err = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(err)); return err; }
+
+static ssize_t log_write(void *cookie, const char *buf, size_t size)
+{
+ struct state *state = cookie;
+ int len;
+
+ while (size > 0) {
+ len = strcspn(buf, "\n");
+ if (len > 0)
+ spa_log_debug(state->log, "%.*s", (int)len, buf);
+ buf += len + 1;
+ size -= len + 1;
+ }
+ return size;
+}
+
+static cookie_io_functions_t io_funcs = {
+ .write = log_write,
+};
+
+int spa_alsa_init(struct state *state, const struct spa_dict *info)
+{
+ uint32_t i;
+ int err;
+
+ snd_config_update_free_global();
+
+ state->multi_rate = true;
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) {
+ snprintf(state->props.device, 63, "%s", s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_PCM_CARD)) {
+ state->card_index = atoi(s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_OPEN_UCM)) {
+ state->open_ucm = spa_atob(s);
+ } else if (spa_streq(k, "clock.quantum-limit")) {
+ spa_atou32(s, &state->quantum_limit, 0);
+ } else {
+ alsa_set_param(state, k, s);
+ }
+ }
+ if (state->clock_name[0] == '\0')
+ snprintf(state->clock_name, sizeof(state->clock_name),
+ "api.alsa.%s-%u",
+ state->stream == SND_PCM_STREAM_PLAYBACK ? "p" : "c",
+ state->card_index);
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK) {
+ state->is_iec958 = spa_strstartswith(state->props.device, "iec958");
+ state->is_hdmi = spa_strstartswith(state->props.device, "hdmi");
+ state->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM;
+ }
+
+ state->card = ensure_card(state->card_index, state->open_ucm);
+ if (state->card == NULL) {
+ spa_log_error(state->log, "can't create card %u", state->card_index);
+ return -errno;
+ }
+ state->log_file = fopencookie(state, "w", io_funcs);
+ if (state->log_file == NULL) {
+ spa_log_error(state->log, "can't create log file");
+ return -errno;
+ }
+ CHECK(snd_output_stdio_attach(&state->output, state->log_file, 0), "attach failed");
+
+ state->rate_limit.interval = 2 * SPA_NSEC_PER_SEC;
+ state->rate_limit.burst = 1;
+
+ return 0;
+}
+
+int spa_alsa_clear(struct state *state)
+{
+ int err;
+
+ release_card(state->card);
+
+ state->card = NULL;
+ state->card_index = SPA_ID_INVALID;
+
+ if ((err = snd_output_close(state->output)) < 0)
+ spa_log_warn(state->log, "output close failed: %s", snd_strerror(err));
+ fclose(state->log_file);
+
+ return err;
+}
+
+int spa_alsa_open(struct state *state, const char *params)
+{
+ int err;
+ struct props *props = &state->props;
+ char device_name[256];
+
+ if (state->opened)
+ return 0;
+
+ spa_scnprintf(device_name, sizeof(device_name), "%s%s%s",
+ state->card->ucm_prefix ? state->card->ucm_prefix : "",
+ props->device, params ? params : "");
+
+ spa_log_info(state->log, "%p: ALSA device open '%s' %s", state, device_name,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback");
+ CHECK(snd_pcm_open(&state->hndl,
+ device_name,
+ state->stream,
+ SND_PCM_NONBLOCK |
+ SND_PCM_NO_AUTO_RESAMPLE |
+ SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_FORMAT), "'%s': %s open failed",
+ device_name,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback");
+
+ if ((err = spa_system_timerfd_create(state->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_exit_close;
+
+ state->timerfd = err;
+
+ if (state->clock)
+ spa_scnprintf(state->clock->name, sizeof(state->clock->name),
+ "%s", state->clock_name);
+ state->opened = true;
+ state->sample_count = 0;
+ state->sample_time = 0;
+
+ return 0;
+
+error_exit_close:
+ spa_log_info(state->log, "%p: Device '%s' closing: %s", state, state->props.device,
+ spa_strerror(err));
+ snd_pcm_close(state->hndl);
+ return err;
+}
+
+int spa_alsa_close(struct state *state)
+{
+ int err = 0;
+
+ if (!state->opened)
+ return 0;
+
+ spa_alsa_pause(state);
+
+ spa_log_info(state->log, "%p: Device '%s' closing", state, state->props.device);
+ if ((err = snd_pcm_close(state->hndl)) < 0)
+ spa_log_warn(state->log, "%s: close failed: %s", state->props.device,
+ snd_strerror(err));
+
+ spa_system_close(state->data_system, state->timerfd);
+
+ if (state->have_format)
+ state->card->format_ref--;
+
+ state->have_format = false;
+ state->opened = false;
+
+ return err;
+}
+
+struct format_info {
+ uint32_t spa_format;
+ uint32_t spa_pformat;
+ snd_pcm_format_t format;
+};
+
+static const struct format_info format_info[] = {
+ { SPA_AUDIO_FORMAT_UNKNOWN, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_UNKNOWN},
+ { SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_LE},
+ { SPA_AUDIO_FORMAT_F32_BE, SPA_AUDIO_FORMAT_F32P, SND_PCM_FORMAT_FLOAT_BE},
+ { SPA_AUDIO_FORMAT_S32_LE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_LE},
+ { SPA_AUDIO_FORMAT_S32_BE, SPA_AUDIO_FORMAT_S32P, SND_PCM_FORMAT_S32_BE},
+ { SPA_AUDIO_FORMAT_S24_32_LE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_LE},
+ { SPA_AUDIO_FORMAT_S24_32_BE, SPA_AUDIO_FORMAT_S24_32P, SND_PCM_FORMAT_S24_BE},
+ { SPA_AUDIO_FORMAT_S24_LE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3LE},
+ { SPA_AUDIO_FORMAT_S24_BE, SPA_AUDIO_FORMAT_S24P, SND_PCM_FORMAT_S24_3BE},
+ { SPA_AUDIO_FORMAT_S16_LE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_LE},
+ { SPA_AUDIO_FORMAT_S16_BE, SPA_AUDIO_FORMAT_S16P, SND_PCM_FORMAT_S16_BE},
+ { SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_S8},
+ { SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_U8P, SND_PCM_FORMAT_U8},
+ { SPA_AUDIO_FORMAT_U16_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_LE},
+ { SPA_AUDIO_FORMAT_U16_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U16_BE},
+ { SPA_AUDIO_FORMAT_U24_32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_LE},
+ { SPA_AUDIO_FORMAT_U24_32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_BE},
+ { SPA_AUDIO_FORMAT_U24_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3LE},
+ { SPA_AUDIO_FORMAT_U24_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U24_3BE},
+ { SPA_AUDIO_FORMAT_U32_LE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_LE},
+ { SPA_AUDIO_FORMAT_U32_BE, SPA_AUDIO_FORMAT_UNKNOWN, SND_PCM_FORMAT_U32_BE},
+ { SPA_AUDIO_FORMAT_F64_LE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_LE},
+ { SPA_AUDIO_FORMAT_F64_BE, SPA_AUDIO_FORMAT_F64P, SND_PCM_FORMAT_FLOAT64_BE},
+};
+
+static snd_pcm_format_t spa_format_to_alsa(uint32_t format, bool *planar)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i) {
+ *planar = i->spa_pformat == format;
+ if (i->spa_format == format || *planar)
+ return i->format;
+ }
+ return SND_PCM_FORMAT_UNKNOWN;
+}
+
+struct chmap_info {
+ enum snd_pcm_chmap_position pos;
+ enum spa_audio_channel channel;
+};
+
+static const struct chmap_info chmap_info[] = {
+ [SND_CHMAP_UNKNOWN] = { SND_CHMAP_UNKNOWN, SPA_AUDIO_CHANNEL_UNKNOWN },
+ [SND_CHMAP_NA] = { SND_CHMAP_NA, SPA_AUDIO_CHANNEL_NA },
+ [SND_CHMAP_MONO] = { SND_CHMAP_MONO, SPA_AUDIO_CHANNEL_MONO },
+ [SND_CHMAP_FL] = { SND_CHMAP_FL, SPA_AUDIO_CHANNEL_FL },
+ [SND_CHMAP_FR] = { SND_CHMAP_FR, SPA_AUDIO_CHANNEL_FR },
+ [SND_CHMAP_RL] = { SND_CHMAP_RL, SPA_AUDIO_CHANNEL_RL },
+ [SND_CHMAP_RR] = { SND_CHMAP_RR, SPA_AUDIO_CHANNEL_RR },
+ [SND_CHMAP_FC] = { SND_CHMAP_FC, SPA_AUDIO_CHANNEL_FC },
+ [SND_CHMAP_LFE] = { SND_CHMAP_LFE, SPA_AUDIO_CHANNEL_LFE },
+ [SND_CHMAP_SL] = { SND_CHMAP_SL, SPA_AUDIO_CHANNEL_SL },
+ [SND_CHMAP_SR] = { SND_CHMAP_SR, SPA_AUDIO_CHANNEL_SR },
+ [SND_CHMAP_RC] = { SND_CHMAP_RC, SPA_AUDIO_CHANNEL_RC },
+ [SND_CHMAP_FLC] = { SND_CHMAP_FLC, SPA_AUDIO_CHANNEL_FLC },
+ [SND_CHMAP_FRC] = { SND_CHMAP_FRC, SPA_AUDIO_CHANNEL_FRC },
+ [SND_CHMAP_RLC] = { SND_CHMAP_RLC, SPA_AUDIO_CHANNEL_RLC },
+ [SND_CHMAP_RRC] = { SND_CHMAP_RRC, SPA_AUDIO_CHANNEL_RRC },
+ [SND_CHMAP_FLW] = { SND_CHMAP_FLW, SPA_AUDIO_CHANNEL_FLW },
+ [SND_CHMAP_FRW] = { SND_CHMAP_FRW, SPA_AUDIO_CHANNEL_FRW },
+ [SND_CHMAP_FLH] = { SND_CHMAP_FLH, SPA_AUDIO_CHANNEL_FLH },
+ [SND_CHMAP_FCH] = { SND_CHMAP_FCH, SPA_AUDIO_CHANNEL_FCH },
+ [SND_CHMAP_FRH] = { SND_CHMAP_FRH, SPA_AUDIO_CHANNEL_FRH },
+ [SND_CHMAP_TC] = { SND_CHMAP_TC, SPA_AUDIO_CHANNEL_TC },
+ [SND_CHMAP_TFL] = { SND_CHMAP_TFL, SPA_AUDIO_CHANNEL_TFL },
+ [SND_CHMAP_TFR] = { SND_CHMAP_TFR, SPA_AUDIO_CHANNEL_TFR },
+ [SND_CHMAP_TFC] = { SND_CHMAP_TFC, SPA_AUDIO_CHANNEL_TFC },
+ [SND_CHMAP_TRL] = { SND_CHMAP_TRL, SPA_AUDIO_CHANNEL_TRL },
+ [SND_CHMAP_TRR] = { SND_CHMAP_TRR, SPA_AUDIO_CHANNEL_TRR },
+ [SND_CHMAP_TRC] = { SND_CHMAP_TRC, SPA_AUDIO_CHANNEL_TRC },
+ [SND_CHMAP_TFLC] = { SND_CHMAP_TFLC, SPA_AUDIO_CHANNEL_TFLC },
+ [SND_CHMAP_TFRC] = { SND_CHMAP_TFRC, SPA_AUDIO_CHANNEL_TFRC },
+ [SND_CHMAP_TSL] = { SND_CHMAP_TSL, SPA_AUDIO_CHANNEL_TSL },
+ [SND_CHMAP_TSR] = { SND_CHMAP_TSR, SPA_AUDIO_CHANNEL_TSR },
+ [SND_CHMAP_LLFE] = { SND_CHMAP_LLFE, SPA_AUDIO_CHANNEL_LLFE },
+ [SND_CHMAP_RLFE] = { SND_CHMAP_RLFE, SPA_AUDIO_CHANNEL_RLFE },
+ [SND_CHMAP_BC] = { SND_CHMAP_BC, SPA_AUDIO_CHANNEL_BC },
+ [SND_CHMAP_BLC] = { SND_CHMAP_BLC, SPA_AUDIO_CHANNEL_BLC },
+ [SND_CHMAP_BRC] = { SND_CHMAP_BRC, SPA_AUDIO_CHANNEL_BRC },
+};
+
+#define _M(ch) (1LL << SND_CHMAP_ ##ch)
+
+struct def_mask {
+ int channels;
+ uint64_t mask;
+};
+
+static const struct def_mask default_layouts[] = {
+ { 0, 0 },
+ { 1, _M(MONO) },
+ { 2, _M(FL) | _M(FR) },
+ { 3, _M(FL) | _M(FR) | _M(LFE) },
+ { 4, _M(FL) | _M(FR) | _M(RL) |_M(RR) },
+ { 5, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) },
+ { 6, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(FC) | _M(LFE) },
+ { 7, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) },
+ { 8, _M(FL) | _M(FR) | _M(RL) |_M(RR) | _M(SL) | _M(SR) | _M(FC) | _M(LFE) },
+};
+
+#define _C(ch) (SPA_AUDIO_CHANNEL_ ##ch)
+
+static const struct channel_map default_map[] = {
+ { 0, { 0, } } ,
+ { 1, { _C(MONO), } },
+ { 2, { _C(FL), _C(FR), } },
+ { 3, { _C(FL), _C(FR), _C(LFE) } },
+ { 4, { _C(FL), _C(FR), _C(RL), _C(RR), } },
+ { 5, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC) } },
+ { 6, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), } },
+ { 7, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(SL), _C(SR), } },
+ { 8, { _C(FL), _C(FR), _C(RL), _C(RR), _C(FC), _C(LFE), _C(SL), _C(SR), } },
+};
+
+static enum spa_audio_channel chmap_position_to_channel(enum snd_pcm_chmap_position pos)
+{
+ return chmap_info[pos].channel;
+}
+
+static void sanitize_map(snd_pcm_chmap_t* map)
+{
+ uint64_t mask = 0, p, dup = 0;
+ const struct def_mask *def;
+ uint32_t i, j, pos;
+
+ for (i = 0; i < map->channels; i++) {
+ if (map->pos[i] > SND_CHMAP_LAST)
+ map->pos[i] = SND_CHMAP_UNKNOWN;
+
+ p = 1LL << map->pos[i];
+ if (mask & p) {
+ /* duplicate channel */
+ for (j = 0; j <= i; j++)
+ if (map->pos[j] == map->pos[i])
+ map->pos[j] = SND_CHMAP_UNKNOWN;
+ dup |= p;
+ p = 1LL << SND_CHMAP_UNKNOWN;
+ }
+ mask |= p;
+ }
+ if ((mask & (1LL << SND_CHMAP_UNKNOWN)) == 0)
+ return;
+
+ def = &default_layouts[map->channels];
+
+ /* remove duplicates */
+ mask &= ~dup;
+ /* keep unassigned channels */
+ mask = def->mask & ~mask;
+
+ pos = 0;
+ for (i = 0; i < map->channels; i++) {
+ if (map->pos[i] == SND_CHMAP_UNKNOWN) {
+ do {
+ mask >>= 1;
+ pos++;
+ }
+ while (mask != 0 && (mask & 1) == 0);
+ map->pos[i] = mask ? pos : 0;
+ }
+
+ }
+}
+
+static bool uint32_array_contains(uint32_t *vals, uint32_t n_vals, uint32_t val)
+{
+ uint32_t i;
+ for (i = 0; i < n_vals; i++)
+ if (vals[i] == val)
+ return true;
+ return false;
+}
+
+static int add_rate(struct state *state, uint32_t scale, uint32_t interleave, bool all, uint32_t index, uint32_t *next,
+ uint32_t min_allowed_rate, snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ int err, dir;
+ unsigned int min, max;
+ struct spa_pod_choice *choice;
+ uint32_t rate;
+
+ CHECK(snd_pcm_hw_params_get_rate_min(params, &min, &dir), "get_rate_min");
+ CHECK(snd_pcm_hw_params_get_rate_max(params, &max, &dir), "get_rate_max");
+
+ spa_log_debug(state->log, "min:%u max:%u min-allowed:%u scale:%u interleave:%u all:%d",
+ min, max, min_allowed_rate, scale, interleave, all);
+
+ min = SPA_MAX(min_allowed_rate * scale / interleave, min) * interleave / scale;
+ max = max * interleave / scale;
+ if (max < min)
+ return 0;
+
+ if (!state->multi_rate && state->card->format_ref > 0)
+ rate = state->card->rate;
+ else
+ rate = state->default_rate;
+
+ if (rate < min || rate > max)
+ rate = 0;
+
+ if (rate != 0 && !all)
+ min = max = rate;
+
+ if (rate == 0)
+ rate = state->position ? state->position->clock.rate.denom : DEFAULT_RATE;
+
+ rate = SPA_CLAMP(rate, min, max);
+
+ spa_log_debug(state->log, "rate:%u multi:%d card:%d def:%d",
+ rate, state->multi_rate, state->card->rate, state->default_rate);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0);
+
+ spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]);
+
+ if (state->n_allowed_rates > 0) {
+ uint32_t i, v, last = 0, count = 0;
+
+ if (uint32_array_contains(state->allowed_rates, state->n_allowed_rates, rate)) {
+ spa_pod_builder_int(b, rate * scale);
+ count++;
+ }
+ for (i = 0; i < state->n_allowed_rates; i++) {
+ v = SPA_CLAMP(state->allowed_rates[i], min, max);
+ if (v != last &&
+ uint32_array_contains(state->allowed_rates, state->n_allowed_rates, v)) {
+ spa_pod_builder_int(b, v * scale);
+ if (count == 0)
+ spa_pod_builder_int(b, v * scale);
+ count++;
+ }
+ last = v;
+ }
+ if (count > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ } else {
+ spa_pod_builder_int(b, rate * scale);
+
+ if (min != max) {
+ spa_pod_builder_int(b, min * scale);
+ spa_pod_builder_int(b, max * scale);
+ choice->body.type = SPA_CHOICE_Range;
+ }
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ return 1;
+}
+
+static int add_channels(struct state *state, bool all, uint32_t index, uint32_t *next,
+ snd_pcm_hw_params_t *params, struct spa_pod_builder *b)
+{
+ struct spa_pod_frame f[1];
+ size_t i;
+ int err;
+ snd_pcm_t *hndl = state->hndl;
+ snd_pcm_chmap_query_t **maps;
+ unsigned int min, max;
+
+ CHECK(snd_pcm_hw_params_get_channels_min(params, &min), "get_channels_min");
+ CHECK(snd_pcm_hw_params_get_channels_max(params, &max), "get_channels_max");
+ spa_log_debug(state->log, "channels (%d %d) default:%d all:%d",
+ min, max, state->default_channels, all);
+
+ if (state->default_channels != 0 && !all) {
+ if (min < state->default_channels)
+ min = state->default_channels;
+ if (max > state->default_channels)
+ max = state->default_channels;
+ }
+ min = SPA_MIN(min, SPA_AUDIO_MAX_CHANNELS);
+ max = SPA_MIN(max, SPA_AUDIO_MAX_CHANNELS);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_channels, 0);
+
+ if (state->props.use_chmap && (maps = snd_pcm_query_chmaps(hndl)) != NULL) {
+ uint32_t channel;
+ snd_pcm_chmap_t* map;
+
+skip_channels:
+ if (maps[index] == NULL) {
+ snd_pcm_free_chmaps(maps);
+ return 0;
+ }
+ map = &maps[index]->map;
+
+ spa_log_debug(state->log, "map %d channels (%d %d)", map->channels, min, max);
+
+ if (map->channels < min || map->channels > max) {
+ index = (*next)++;
+ goto skip_channels;
+ }
+
+ sanitize_map(map);
+ spa_pod_builder_int(b, map->channels);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0);
+ spa_pod_builder_push_array(b, &f[0]);
+ for (i = 0; i < map->channels; i++) {
+ spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]);
+ channel = chmap_position_to_channel(map->pos[i]);
+ spa_pod_builder_id(b, channel);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ snd_pcm_free_chmaps(maps);
+ }
+ else {
+ const struct channel_map *map = NULL;
+ struct spa_pod_choice *choice;
+
+ if (index > 0)
+ return 0;
+
+ spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]);
+ spa_pod_builder_int(b, max);
+ if (min != max) {
+ spa_pod_builder_int(b, min);
+ spa_pod_builder_int(b, max);
+ choice->body.type = SPA_CHOICE_Range;
+ }
+ spa_pod_builder_pop(b, &f[0]);
+
+ if (min == max) {
+ if (state->default_pos.channels == min)
+ map = &state->default_pos;
+ else if (min == max && min <= 8)
+ map = &default_map[min];
+ }
+ if (map) {
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0);
+ spa_pod_builder_push_array(b, &f[0]);
+ for (i = 0; i < map->channels; i++) {
+ spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]);
+ spa_pod_builder_id(b, map->pos[i]);
+ }
+ spa_pod_builder_pop(b, &f[0]);
+ }
+ }
+ return 1;
+}
+
+static void debug_hw_params(struct state *state, const char *prefix, snd_pcm_hw_params_t *params)
+{
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+ spa_log_debug(state->log, "%s:", prefix);
+ snd_pcm_hw_params_dump(params, state->output);
+ fflush(state->log_file);
+ }
+}
+static int enum_pcm_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err;
+ size_t j;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ struct spa_pod_frame f[2];
+ snd_pcm_format_mask_t *fmask;
+ snd_pcm_access_mask_t *amask;
+ unsigned int rrate, rchannels;
+ struct spa_pod_choice *choice;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ if (state->default_channels != 0) {
+ rchannels = state->default_channels;
+ CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &rchannels), "set_channels");
+ if (state->default_channels != rchannels) {
+ spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)",
+ state->props.device, state->default_channels, rchannels);
+ }
+ }
+ if (state->default_rate != 0) {
+ rrate = state->default_rate;
+ CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &rrate, 0), "set_rate_near");
+ if (state->default_rate != rrate) {
+ spa_log_warn(state->log, "%s: Rate doesn't match (requested %u, got %u)",
+ state->props.device, state->default_rate, rrate);
+ }
+ }
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+ 0);
+
+ snd_pcm_format_mask_alloca(&fmask);
+ snd_pcm_hw_params_get_format_mask(params, fmask);
+
+ snd_pcm_access_mask_alloca(&amask);
+ snd_pcm_hw_params_get_access_mask(params, amask);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_format, 0);
+
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0);
+ choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]);
+
+ j = 0;
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) {
+ if (fi->format == SND_PCM_FORMAT_UNKNOWN)
+ continue;
+
+ if (snd_pcm_format_mask_test(fmask, fi->format)) {
+ if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED) ||
+ snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_NONINTERLEAVED)) &&
+ fi->spa_pformat != SPA_AUDIO_FORMAT_UNKNOWN &&
+ (state->default_format == 0 || state->default_format == fi->spa_pformat)) {
+ if (j++ == 0)
+ spa_pod_builder_id(b, fi->spa_pformat);
+ spa_pod_builder_id(b, fi->spa_pformat);
+ }
+ if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_INTERLEAVED) ||
+ snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_INTERLEAVED)) &&
+ (state->default_format == 0 || state->default_format == fi->spa_format)) {
+ if (j++ == 0)
+ spa_pod_builder_id(b, fi->spa_format);
+ spa_pod_builder_id(b, fi->spa_format);
+ }
+ }
+ }
+ if (j == 0) {
+ char buf[1024];
+ int i, r, offs;
+
+ for (i = 0, offs = 0; i <= SND_PCM_FORMAT_LAST; i++) {
+ if (snd_pcm_format_mask_test(fmask, (snd_pcm_format_t)i)) {
+ r = snprintf(&buf[offs], sizeof(buf) - offs,
+ "%s ", snd_pcm_format_name((snd_pcm_format_t)i));
+ if (r < 0 || r + offs >= (int)sizeof(buf))
+ return -ENOSPC;
+ offs += r;
+ }
+ }
+ spa_log_warn(state->log, "%s: no format found (def:%d) formats:%s",
+ state->props.device, state->default_format, buf);
+
+ for (i = 0, offs = 0; i <= SND_PCM_ACCESS_LAST; i++) {
+ if (snd_pcm_access_mask_test(amask, (snd_pcm_access_t)i)) {
+ r = snprintf(&buf[offs], sizeof(buf) - offs,
+ "%s ", snd_pcm_access_name((snd_pcm_access_t)i));
+ if (r < 0 || r + offs >= (int)sizeof(buf))
+ return -ENOSPC;
+ offs += r;
+ }
+ }
+ spa_log_warn(state->log, "%s: access:%s", state->props.device, buf);
+ return -ENOTSUP;
+ }
+ if (j > 1)
+ choice->body.type = SPA_CHOICE_Enum;
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((res = add_rate(state, 1, 1, false, index & 0xffff, next, 0, params, b)) != 1)
+ return res;
+
+ if ((res = add_channels(state, false, index & 0xffff, next, params, b)) != 1)
+ return res;
+
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+static bool codec_supported(uint32_t codec, unsigned int chmax, unsigned int rmax)
+{
+ switch (codec) {
+ case SPA_AUDIO_IEC958_CODEC_PCM:
+ case SPA_AUDIO_IEC958_CODEC_DTS:
+ case SPA_AUDIO_IEC958_CODEC_AC3:
+ case SPA_AUDIO_IEC958_CODEC_MPEG:
+ case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC:
+ if (chmax >= 2)
+ return true;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_EAC3:
+ if (rmax >= 48000 * 4 && chmax >= 2)
+ return true;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ if (chmax >= 8)
+ return true;
+ break;
+ }
+ return false;
+}
+
+static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err, dir;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ struct spa_pod_frame f[2];
+ unsigned int rmin, rmax;
+ unsigned int chmin, chmax;
+ uint32_t i, c, codecs[16], n_codecs;
+
+ if ((index & 0xffff) > 0)
+ return 0;
+
+ if (!(state->is_iec958 || state->is_hdmi))
+ return 0;
+ if (state->iec958_codecs == 0)
+ return 0;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_iec958),
+ 0);
+
+ CHECK(snd_pcm_hw_params_get_channels_min(params, &chmin), "get_channels_min");
+ CHECK(snd_pcm_hw_params_get_channels_max(params, &chmax), "get_channels_max");
+ spa_log_debug(state->log, "channels (%d %d)", chmin, chmax);
+
+ CHECK(snd_pcm_hw_params_get_rate_min(params, &rmin, &dir), "get_rate_min");
+ CHECK(snd_pcm_hw_params_get_rate_max(params, &rmax, &dir), "get_rate_max");
+ spa_log_debug(state->log, "rate (%d %d)", rmin, rmax);
+
+ if (state->default_rate != 0) {
+ if (rmin < state->default_rate)
+ rmin = state->default_rate;
+ if (rmax > state->default_rate)
+ rmax = state->default_rate;
+ }
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_iec958Codec, 0);
+ spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0);
+
+ n_codecs = spa_alsa_get_iec958_codecs(state, codecs, SPA_N_ELEMENTS(codecs));
+ for (i = 0, c = 0; i < n_codecs; i++) {
+ if (!codec_supported(codecs[i], chmax, rmax))
+ continue;
+ if (c++ == 0)
+ spa_pod_builder_id(b, codecs[i]);
+ spa_pod_builder_id(b, codecs[i]);
+ }
+ spa_pod_builder_pop(b, &f[1]);
+
+ if ((res = add_rate(state, 1, 1, true, index & 0xffff, next, 0, params, b)) != 1)
+ return res;
+
+ (*next)++;
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next,
+ struct spa_pod **result, struct spa_pod_builder *b)
+{
+ int res, err;
+ snd_pcm_t *hndl;
+ snd_pcm_hw_params_t *params;
+ snd_pcm_format_mask_t *fmask;
+ struct spa_pod_frame f[2];
+ int32_t interleave;
+
+ if ((index & 0xffff) > 0)
+ return 0;
+
+ hndl = state->hndl;
+ snd_pcm_hw_params_alloca(&params);
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ snd_pcm_format_mask_alloca(&fmask);
+ snd_pcm_hw_params_get_format_mask(params, fmask);
+
+ if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_BE))
+ interleave = 4;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_LE))
+ interleave = -4;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_BE))
+ interleave = 2;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_LE))
+ interleave = -2;
+ else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U8))
+ interleave = 1;
+ else
+ return 0;
+
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
+ spa_pod_builder_add(b,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsd),
+ 0);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_bitorder, 0);
+ spa_pod_builder_id(b, SPA_PARAM_BITORDER_msb);
+
+ spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_interleave, 0);
+ spa_pod_builder_int(b, interleave);
+
+ /* Use a lower rate limit of 352800 (= 44100 * 64 / 8). This is because in
+ * PipeWire, DSD rates are given in bytes, not bits, so 352800 corresponds
+ * to the bit rate of DSD64. (The "64" in DSD64 means "64 times the rate
+ * of 44.1 kHz".) Some hardware may report rates lower than that, for example
+ * 176400. This would correspond to "DSD32" (which does not exist). Trying
+ * to use such a rate with DSD hardware does not work and may cause undefined
+ * behavior in said hardware. */
+ if ((res = add_rate(state, 8, SPA_ABS(interleave), true, index & 0xffff,
+ next, 44100, params, b)) != 1)
+ return res;
+
+ if ((res = add_channels(state, true, index & 0xffff, next, params, b)) != 1)
+ return res;
+
+ *result = spa_pod_builder_pop(b, &f[0]);
+ return 1;
+}
+
+int
+spa_alsa_enum_format(struct state *state, int seq, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ uint8_t buffer[4096];
+ struct spa_pod_builder b = { 0 };
+ struct spa_pod *fmt;
+ int err, res;
+ bool opened;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+ state->have_format, state->started);
+
+ opened = state->opened;
+ if (!state->started && state->have_format)
+ spa_alsa_close(state);
+ if ((err = spa_alsa_open(state, NULL)) < 0)
+ return err;
+
+ result.id = SPA_PARAM_EnumFormat;
+ result.next = start;
+
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ if (result.index < 0x10000) {
+ if ((res = enum_pcm_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x10000;
+ goto next;
+ }
+ }
+ else if (result.index < 0x20000) {
+ if ((res = enum_iec958_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x20000;
+ goto next;
+ }
+ }
+ else if (result.index < 0x30000) {
+ if ((res = enum_dsd_formats(state, result.index, &result.next, &fmt, &b)) != 1) {
+ result.next = 0x30000;
+ goto next;
+ }
+ }
+ else
+ goto enum_end;
+
+ if (spa_pod_filter(&b, &result.param, fmt, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ enum_end:
+ res = 0;
+ if (!opened)
+ spa_alsa_close(state);
+ return res;
+}
+
+int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags)
+{
+ unsigned int rrate, rchannels, val, rscale = 1;
+ snd_pcm_uframes_t period_size;
+ int err, dir;
+ snd_pcm_hw_params_t *params;
+ snd_pcm_format_t rformat;
+ snd_pcm_access_mask_t *amask;
+ snd_pcm_t *hndl;
+ unsigned int periods;
+ bool match = true, planar = false, is_batch;
+ char spdif_params[128] = "";
+
+ spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened,
+ state->have_format, state->started);
+
+ state->use_mmap = !state->disable_mmap;
+
+ switch (fmt->media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ struct spa_audio_info_raw *f = &fmt->info.raw;
+ rrate = f->rate;
+ rchannels = f->channels;
+ rformat = spa_format_to_alsa(f->format, &planar);
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 *f = &fmt->info.iec958;
+ unsigned aes3;
+
+ spa_log_info(state->log, "using IEC958 Codec:%s rate:%d",
+ spa_debug_type_find_short_name(spa_type_audio_iec958_codec, f->codec),
+ f->rate);
+
+ rformat = SND_PCM_FORMAT_S16_LE;
+ rchannels = 2;
+ rrate = f->rate;
+
+ switch (f->codec) {
+ case SPA_AUDIO_IEC958_CODEC_PCM:
+ case SPA_AUDIO_IEC958_CODEC_DTS:
+ case SPA_AUDIO_IEC958_CODEC_AC3:
+ case SPA_AUDIO_IEC958_CODEC_MPEG:
+ case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC:
+ break;
+ case SPA_AUDIO_IEC958_CODEC_EAC3:
+ /* EAC3 has 3 rates, 32, 44.1 and 48KHz. We need to
+ * open the device in 4x that rate. Some clients
+ * already multiply (mpv,..) others don't (vlc). */
+ if (rrate <= 48000)
+ rrate *= 4;
+ break;
+ case SPA_AUDIO_IEC958_CODEC_TRUEHD:
+ case SPA_AUDIO_IEC958_CODEC_DTSHD:
+ rchannels = 8;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ switch (rrate) {
+ case 22050: aes3 = IEC958_AES3_CON_FS_22050; break;
+ case 24000: aes3 = IEC958_AES3_CON_FS_24000; break;
+ case 32000: aes3 = IEC958_AES3_CON_FS_32000; break;
+ case 44100: aes3 = IEC958_AES3_CON_FS_44100; break;
+ case 48000: aes3 = IEC958_AES3_CON_FS_48000; break;
+ case 88200: aes3 = IEC958_AES3_CON_FS_88200; break;
+ case 96000: aes3 = IEC958_AES3_CON_FS_96000; break;
+ case 176400: aes3 = IEC958_AES3_CON_FS_176400; break;
+ case 192000: aes3 = IEC958_AES3_CON_FS_192000; break;
+ case 768000: aes3 = IEC958_AES3_CON_FS_768000; break;
+ default: aes3 = IEC958_AES3_CON_FS_NOTID; break;
+ }
+ spa_scnprintf(spdif_params, sizeof(spdif_params),
+ ",AES0=0x%x,AES1=0x%x,AES2=0x%x,AES3=0x%x",
+ IEC958_AES0_CON_EMPHASIS_NONE | IEC958_AES0_NONAUDIO,
+ IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER,
+ 0, aes3);
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_dsd:
+ {
+ struct spa_audio_info_dsd *f = &fmt->info.dsd;
+
+ rrate = f->rate;
+ rchannels = f->channels;
+
+ switch (f->interleave) {
+ case 4:
+ rformat = SND_PCM_FORMAT_DSD_U32_BE;
+ rrate /= 4;
+ rscale = 4;
+ break;
+ case -4:
+ rformat = SND_PCM_FORMAT_DSD_U32_LE;
+ rrate /= 4;
+ rscale = 4;
+ break;
+ case 2:
+ rformat = SND_PCM_FORMAT_DSD_U16_BE;
+ rrate /= 2;
+ rscale = 2;
+ break;
+ case -2:
+ rformat = SND_PCM_FORMAT_DSD_U16_LE;
+ rrate /= 2;
+ rscale = 2;
+ break;
+ case 1:
+ rformat = SND_PCM_FORMAT_DSD_U8;
+ rscale = 1;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ break;
+ }
+ default:
+ return -ENOTSUP;
+ }
+
+ if (rformat == SND_PCM_FORMAT_UNKNOWN) {
+ spa_log_warn(state->log, "%s: unknown format",
+ state->props.device);
+ return -EINVAL;
+ }
+
+ if (!state->started && state->have_format)
+ spa_alsa_close(state);
+ if ((err = spa_alsa_open(state, spdif_params)) < 0)
+ return err;
+
+ hndl = state->hndl;
+
+ snd_pcm_hw_params_alloca(&params);
+ /* choose all parameters */
+ CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration for playback: no configurations available");
+
+ debug_hw_params(state, __func__, params);
+
+ /* set hardware resampling, no resample */
+ CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample");
+
+ /* set the interleaved/planar read/write format */
+ snd_pcm_access_mask_alloca(&amask);
+ snd_pcm_hw_params_get_access_mask(params, amask);
+
+ if (state->use_mmap) {
+ if ((err = snd_pcm_hw_params_set_access(hndl, params,
+ planar ? SND_PCM_ACCESS_MMAP_NONINTERLEAVED
+ : SND_PCM_ACCESS_MMAP_INTERLEAVED)) < 0) {
+ spa_log_debug(state->log, "%p: MMAP not possible: %s", state,
+ snd_strerror(err));
+ state->use_mmap = false;
+ }
+ }
+ if (!state->use_mmap) {
+ if ((err = snd_pcm_hw_params_set_access(hndl, params,
+ planar ? SND_PCM_ACCESS_RW_NONINTERLEAVED
+ : SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ spa_log_error(state->log, "%s: RW not possible: %s",
+ state->props.device, snd_strerror(err));
+ return err;
+ }
+ }
+
+ /* set the sample format */
+ spa_log_debug(state->log, "%p: Stream parameters are %iHz fmt:%s access:%s-%s channels:%i",
+ state, rrate, snd_pcm_format_name(rformat),
+ state->use_mmap ? "mmap" : "rw",
+ planar ? "planar" : "interleaved", rchannels);
+ CHECK(snd_pcm_hw_params_set_format(hndl, params, rformat), "set_format");
+
+ /* set the count of channels */
+ val = rchannels;
+ CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &val), "set_channels");
+ if (rchannels != val) {
+ spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)",
+ state->props.device, rchannels, val);
+ if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
+ return -EINVAL;
+ if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+ rchannels = val;
+ fmt->info.raw.channels = rchannels;
+ match = false;
+ }
+
+ if (!state->multi_rate &&
+ state->card->format_ref > 0 &&
+ state->card->rate != rrate) {
+ spa_log_error(state->log, "%p: card already opened at rate:%i",
+ state, state->card->rate);
+ return -EINVAL;
+ }
+
+ /* set the stream rate */
+ val = rrate;
+ CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &val, 0), "set_rate_near");
+ if (rrate != val) {
+ spa_log_warn(state->log, "%s: Rate doesn't match (requested %iHz, got %iHz)",
+ state->props.device, rrate, val);
+ if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST))
+ return -EINVAL;
+ if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+ return -EINVAL;
+ rrate = val;
+ fmt->info.raw.rate = rrate;
+ match = false;
+ }
+ if (rchannels == 0 || rrate == 0) {
+ spa_log_error(state->log, "%s: invalid channels:%d or rate:%d",
+ state->props.device, rchannels, rrate);
+ return -EIO;
+ }
+
+ state->format = rformat;
+ state->channels = rchannels;
+ state->rate = rrate;
+ state->frame_size = snd_pcm_format_physical_width(rformat) / 8;
+ state->frame_scale = rscale;
+ state->planar = planar;
+ state->blocks = 1;
+ if (planar)
+ state->blocks *= rchannels;
+ else
+ state->frame_size *= rchannels;
+
+ state->have_format = true;
+ if (state->card->format_ref++ == 0)
+ state->card->rate = rrate;
+
+ dir = 0;
+ period_size = state->default_period_size;
+ is_batch = snd_pcm_hw_params_is_batch(params) &&
+ !state->disable_batch;
+
+ if (is_batch) {
+ if (period_size == 0)
+ period_size = state->position ? state->position->clock.duration : DEFAULT_PERIOD;
+ if (period_size == 0)
+ period_size = DEFAULT_PERIOD;
+ /* batch devices get their hw pointers updated every period. Make
+ * the period smaller and add one period of headroom. Limit the
+ * period size to our default so that we don't create too much
+ * headroom. */
+ period_size = SPA_MIN(period_size, DEFAULT_PERIOD) / 2;
+ spa_log_info(state->log, "%s: batch mode, period_size:%ld",
+ state->props.device, period_size);
+ } else {
+ if (period_size == 0)
+ period_size = DEFAULT_PERIOD;
+ /* disable ALSA wakeups, we use a timer */
+ if (snd_pcm_hw_params_can_disable_period_wakeup(params))
+ CHECK(snd_pcm_hw_params_set_period_wakeup(hndl, params, 0), "set_period_wakeup");
+ }
+
+ CHECK(snd_pcm_hw_params_set_period_size_near(hndl, params, &period_size, &dir), "set_period_size_near");
+
+ if (period_size == 0) {
+ spa_log_error(state->log, "%s: invalid period_size 0 (driver error?)", state->props.device);
+ return -EIO;
+ }
+
+ state->period_frames = period_size;
+
+ if (state->default_period_num != 0) {
+ periods = state->default_period_num;
+ CHECK(snd_pcm_hw_params_set_periods_near(hndl, params, &periods, &dir), "set_periods");
+ state->buffer_frames = period_size * periods;
+ } else {
+ CHECK(snd_pcm_hw_params_get_buffer_size_max(params, &state->buffer_frames), "get_buffer_size_max");
+
+ state->buffer_frames = SPA_MIN(state->buffer_frames, state->quantum_limit * 4)* state->frame_scale;
+
+ CHECK(snd_pcm_hw_params_set_buffer_size_min(hndl, params, &state->buffer_frames), "set_buffer_size_min");
+ CHECK(snd_pcm_hw_params_set_buffer_size_near(hndl, params, &state->buffer_frames), "set_buffer_size_near");
+ periods = state->buffer_frames / period_size;
+ }
+ if (state->buffer_frames == 0) {
+ spa_log_error(state->log, "%s: invalid buffer_frames 0 (driver error?)", state->props.device);
+ return -EIO;
+ }
+
+ state->headroom = state->default_headroom;
+ if (is_batch)
+ state->headroom += period_size;
+
+ if (spa_strstartswith(state->props.device, "a52") ||
+ spa_strstartswith(state->props.device, "dca"))
+ state->min_delay = SPA_MIN(2048u, state->buffer_frames);
+ else
+ state->min_delay = 0;
+
+ state->headroom = SPA_MIN(state->headroom, state->buffer_frames);
+ state->start_delay = state->default_start_delay;
+
+ state->latency[state->port_direction].min_rate =
+ state->latency[state->port_direction].max_rate =
+ SPA_MAX(state->min_delay, state->headroom);
+
+ spa_log_info(state->log, "%s (%s): format:%s access:%s-%s rate:%d channels:%d "
+ "buffer frames %lu, period frames %lu, periods %u, frame_size %zd "
+ "headroom %u start-delay:%u",
+ state->props.device,
+ state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback",
+ snd_pcm_format_name(state->format),
+ state->use_mmap ? "mmap" : "rw",
+ planar ? "planar" : "interleaved",
+ state->rate, state->channels, state->buffer_frames, state->period_frames,
+ periods, state->frame_size, state->headroom, state->start_delay);
+
+ /* write the parameters to device */
+ CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params");
+
+ return match ? 0 : 1;
+}
+
+static int set_swparams(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ int err = 0;
+ snd_pcm_sw_params_t *params;
+
+ snd_pcm_sw_params_alloca(&params);
+
+ /* get the current params */
+ CHECK(snd_pcm_sw_params_current(hndl, params), "sw_params_current");
+
+ CHECK(snd_pcm_sw_params_set_tstamp_mode(hndl, params, SND_PCM_TSTAMP_ENABLE),
+ "sw_params_set_tstamp_mode");
+ CHECK(snd_pcm_sw_params_set_tstamp_type(hndl, params, SND_PCM_TSTAMP_TYPE_MONOTONIC),
+ "sw_params_set_tstamp_type");
+#if 0
+ snd_pcm_uframes_t boundary;
+ CHECK(snd_pcm_sw_params_get_boundary(params, &boundary), "get_boundary");
+
+ CHECK(snd_pcm_sw_params_set_stop_threshold(hndl, params, boundary), "set_stop_threshold");
+#endif
+
+ /* start the transfer */
+ CHECK(snd_pcm_sw_params_set_start_threshold(hndl, params, LONG_MAX), "set_start_threshold");
+
+ CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event");
+
+ /* write the parameters to the playback device */
+ CHECK(snd_pcm_sw_params(hndl, params), "sw_params");
+
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) {
+ spa_log_debug(state->log, "state after sw_params:");
+ snd_pcm_dump(hndl, state->output);
+ fflush(state->log_file);
+ }
+
+ return 0;
+}
+
+static int set_timeout(struct state *state, uint64_t time)
+{
+ struct itimerspec ts;
+
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system,
+ state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+ return 0;
+}
+
+int spa_alsa_silence(struct state *state, snd_pcm_uframes_t silence)
+{
+ snd_pcm_t *hndl = state->hndl;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t frames, offset;
+ int i, res;
+
+ if (state->use_mmap) {
+ frames = state->buffer_frames;
+
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ silence = SPA_MIN(silence, frames);
+
+ spa_log_trace_fp(state->log, "%p: frames:%ld offset:%ld silence %ld",
+ state, frames, offset, silence);
+ snd_pcm_areas_silence(my_areas, offset, state->channels, silence, state->format);
+
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_commit(hndl, offset, silence)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ } else {
+ uint8_t buffer[silence * state->frame_size];
+ memset(buffer, 0, silence * state->frame_size);
+
+ if (state->planar) {
+ void *bufs[state->channels];
+ for (i = 0; i < state->channels; i++)
+ bufs[i] = buffer;
+ snd_pcm_writen(hndl, bufs, silence);
+ } else {
+ snd_pcm_writei(hndl, buffer, silence);
+ }
+ }
+ return 0;
+}
+
+static inline int do_start(struct state *state)
+{
+ int res;
+ if (SPA_UNLIKELY(!state->alsa_started)) {
+ spa_log_trace(state->log, "%p: snd_pcm_start", state);
+ if ((res = snd_pcm_start(state->hndl)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_start: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ state->alsa_started = true;
+ }
+ return 0;
+}
+
+static int alsa_recover(struct state *state, int err)
+{
+ int res, st;
+ snd_pcm_status_t *status;
+
+ snd_pcm_status_alloca(&status);
+ if (SPA_UNLIKELY((res = snd_pcm_status(state->hndl, status)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_status error: %s",
+ state->props.device, snd_strerror(res));
+ goto recover;
+ }
+
+ st = snd_pcm_status_get_state(status);
+ switch (st) {
+ case SND_PCM_STATE_XRUN:
+ {
+ struct timeval now, trigger, diff;
+ uint64_t delay, missing;
+
+ snd_pcm_status_get_tstamp (status, &now);
+ snd_pcm_status_get_trigger_tstamp (status, &trigger);
+ timersub(&now, &trigger, &diff);
+
+ delay = SPA_TIMEVAL_TO_USEC(&diff);
+ missing = delay * state->rate / SPA_USEC_PER_SEC;
+
+ spa_log_trace(state->log, "%p: xrun of %"PRIu64" usec %"PRIu64,
+ state, delay, missing);
+
+ spa_node_call_xrun(&state->callbacks,
+ SPA_TIMEVAL_TO_USEC(&trigger), delay, NULL);
+
+ state->sample_count += missing ? missing : state->threshold;
+ break;
+ }
+ case SND_PCM_STATE_SUSPENDED:
+ spa_log_info(state->log, "%s: recover from state %s",
+ state->props.device, snd_pcm_state_name(st));
+ res = snd_pcm_resume(state->hndl);
+ if (res >= 0)
+ return res;
+ err = -ESTRPIPE;
+ break;
+ default:
+ spa_log_error(state->log, "%s: recover from error state %s",
+ state->props.device, snd_pcm_state_name(st));
+ break;
+ }
+
+recover:
+ if (SPA_UNLIKELY((res = snd_pcm_recover(state->hndl, err, true)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_recover error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_dll_init(&state->dll);
+ state->alsa_recovering = true;
+ state->alsa_started = false;
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
+
+ return do_start(state);
+}
+
+static int get_avail(struct state *state, uint64_t current_time)
+{
+ int res, missed;
+ snd_pcm_sframes_t avail;
+
+ if (SPA_UNLIKELY((avail = snd_pcm_avail(state->hndl)) < 0)) {
+ if ((res = alsa_recover(state, avail)) < 0)
+ return res;
+ if ((avail = snd_pcm_avail(state->hndl)) < 0) {
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_warn(state->log, "%s: (%d missed) snd_pcm_avail after recover: %s",
+ state->props.device, missed, snd_strerror(avail));
+ }
+ avail = state->threshold * 2;
+ }
+ } else {
+ state->alsa_recovering = false;
+ }
+ return avail;
+}
+
+#if 0
+static int get_avail_htimestamp(struct state *state, uint64_t current_time)
+{
+ int res, missed;
+ snd_pcm_uframes_t avail;
+ snd_htimestamp_t tstamp;
+ uint64_t then;
+
+ if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) {
+ if ((res = alsa_recover(state, avail)) < 0)
+ return res;
+ if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) {
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_warn(state->log, "%s: (%d missed) snd_pcm_htimestamp error: %s",
+ state->props.device, missed, snd_strerror(res));
+ }
+ avail = state->threshold * 2;
+ }
+ } else {
+ state->alsa_recovering = false;
+ }
+
+ if ((then = SPA_TIMESPEC_TO_NSEC(&tstamp)) != 0) {
+ if (then < current_time)
+ avail += (current_time - then) * state->rate / SPA_NSEC_PER_SEC;
+ else
+ avail -= (then - current_time) * state->rate / SPA_NSEC_PER_SEC;
+ }
+ return SPA_MIN(avail, state->buffer_frames);
+}
+#endif
+
+static int get_status(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t *delay, snd_pcm_uframes_t *target)
+{
+ int avail;
+
+ if ((avail = get_avail(state, current_time)) < 0)
+ return avail;
+
+ avail = SPA_MIN(avail, (int)state->buffer_frames);
+
+ *target = state->threshold + state->headroom;
+
+ if (state->resample && state->rate_match) {
+ state->delay = state->rate_match->delay;
+ state->read_size = state->rate_match->size;
+ } else {
+ state->delay = 0;
+ state->read_size = state->threshold;
+ }
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK) {
+ *delay = state->buffer_frames - avail;
+ } else {
+ *delay = avail;
+ *target = SPA_MAX(*target, state->read_size);
+ }
+ *target = SPA_CLAMP(*target, state->min_delay, state->buffer_frames);
+ return 0;
+}
+
+static int update_time(struct state *state, uint64_t current_time, snd_pcm_sframes_t delay,
+ snd_pcm_sframes_t target, bool follower)
+{
+ double err, corr;
+ int32_t diff;
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ err = delay - target;
+ else
+ err = target - delay;
+
+ if (SPA_UNLIKELY(state->dll.bw == 0.0)) {
+ spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, state->rate);
+ state->next_time = current_time;
+ state->base_time = current_time;
+ }
+ diff = (int32_t) (state->last_threshold - state->threshold);
+
+ if (SPA_UNLIKELY(diff != 0)) {
+ err -= diff;
+ spa_log_trace(state->log, "%p: follower:%d quantum change %d -> %d (%d) %f",
+ state, follower, state->last_threshold, state->threshold, diff, err);
+ state->last_threshold = state->threshold;
+ state->alsa_sync = true;
+ state->alsa_sync_warning = false;
+ }
+ if (err > state->max_error) {
+ err = state->max_error;
+ state->alsa_sync = true;
+ } else if (err < -state->max_error) {
+ err = -state->max_error;
+ state->alsa_sync = true;
+ }
+
+ if (!follower || state->matching)
+ corr = spa_dll_update(&state->dll, err);
+ else
+ corr = 1.0;
+
+ if (diff < 0)
+ state->next_time += diff / corr * 1e9 / state->rate;
+
+ if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) {
+ state->base_time = state->next_time;
+
+ spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f "
+ "bw:%f thr:%u del:%ld target:%ld err:%f max:%f",
+ state->props.device, follower, state->matching,
+ corr, state->dll.bw, state->threshold, delay, target,
+ err, state->max_error);
+ }
+
+ if (state->rate_match) {
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ state->rate_match->rate = corr;
+ else
+ state->rate_match->rate = 1.0/corr;
+
+ SPA_FLAG_UPDATE(state->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, state->matching);
+ }
+
+ state->next_time += state->threshold / corr * 1e9 / state->rate;
+
+ if (SPA_LIKELY(!follower && state->clock)) {
+ state->clock->nsec = current_time;
+ state->clock->position += state->duration;
+ state->clock->duration = state->duration;
+ state->clock->delay = delay + state->delay;
+ state->clock->rate_diff = corr;
+ state->clock->next_nsec = state->next_time;
+ }
+
+ spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %f %ld %f %f %u",
+ state, follower, current_time, corr, delay, err, state->threshold * corr,
+ state->threshold);
+
+ return 0;
+}
+
+static inline bool is_following(struct state *state)
+{
+ return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+static int setup_matching(struct state *state)
+{
+ state->matching = state->following;
+
+ if (state->position == NULL)
+ return -ENOTSUP;
+
+ spa_log_debug(state->log, "driver clock:'%s' our clock:'%s'",
+ state->position->clock.name, state->clock_name);
+
+ if (spa_streq(state->position->clock.name, state->clock_name))
+ state->matching = false;
+
+ state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching;
+
+ spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d",
+ state->position->clock.name, state->rate_denom,
+ state->clock_name, state->rate,
+ state->matching, state->resample);
+ return 0;
+}
+
+static inline void check_position_config(struct state *state)
+{
+ if (SPA_UNLIKELY(state->position == NULL))
+ return;
+
+ if (SPA_UNLIKELY((state->duration != state->position->clock.duration) ||
+ (state->rate_denom != state->position->clock.rate.denom))) {
+ state->duration = state->position->clock.duration;
+ state->rate_denom = state->position->clock.rate.denom;
+ state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom);
+ state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
+ state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching;
+ state->alsa_sync = true;
+ }
+}
+
+int spa_alsa_write(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write;
+ snd_pcm_sframes_t commitres;
+ int res = 0, missed;
+ size_t frame_size = state->frame_size;
+
+ check_position_config(state);
+
+ max_write = state->buffer_frames;
+
+ if (state->following && state->alsa_started) {
+ uint64_t current_time;
+ snd_pcm_uframes_t delay, target;
+
+ current_time = state->position->clock.nsec;
+
+ if (SPA_UNLIKELY((res = get_status(state, current_time, &delay, &target)) < 0))
+ return res;
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0))
+ return res;
+
+ if (SPA_UNLIKELY(state->alsa_sync)) {
+ enum spa_log_level lev;
+
+ if (SPA_UNLIKELY(state->alsa_sync_warning))
+ lev = SPA_LOG_LEVEL_WARN;
+ else
+ lev = SPA_LOG_LEVEL_INFO;
+
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+ "resync (%d missed)", state->props.device, delay,
+ target, state->threshold, missed);
+ }
+
+ if (delay > target)
+ snd_pcm_rewind(state->hndl, delay - target);
+ else if (delay < target)
+ spa_alsa_silence(state, target - delay);
+ delay = target;
+ state->alsa_sync = false;
+ } else
+ state->alsa_sync_warning = true;
+ }
+
+ total_written = 0;
+again:
+
+ frames = max_write;
+ if (state->use_mmap && frames > 0) {
+ if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_log_trace_fp(state->log, "%p: begin %ld %ld %d",
+ state, offset, frames, state->threshold);
+ off = offset;
+ } else {
+ off = 0;
+ }
+
+ to_write = frames;
+ written = 0;
+
+ while (!spa_list_is_empty(&state->ready) && to_write > 0) {
+ size_t n_bytes, n_frames;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, offs, size, last_offset;
+
+ b = spa_list_first(&state->ready, struct buffer, link);
+ d = b->buf->datas;
+
+ offs = d[0].chunk->offset + state->ready_offset;
+ last_offset = d[0].chunk->size;
+ size = last_offset - state->ready_offset;
+
+ offs = SPA_MIN(offs, d[0].maxsize);
+ size = SPA_MIN(d[0].maxsize - offs, size);
+
+ n_frames = SPA_MIN(size / frame_size, to_write);
+ n_bytes = n_frames * frame_size;
+
+ if (SPA_LIKELY(state->use_mmap)) {
+ for (i = 0; i < b->buf->n_datas; i++) {
+ spa_memcpy(SPA_PTROFF(my_areas[i].addr, off * frame_size, void),
+ SPA_PTROFF(d[i].data, offs, void), n_bytes);
+ }
+ } else {
+ void *bufs[b->buf->n_datas];
+ for (i = 0; i < b->buf->n_datas; i++)
+ bufs[i] = SPA_PTROFF(d[i].data, offs, void);
+
+ if (state->planar)
+ snd_pcm_writen(hndl, bufs, n_frames);
+ else
+ snd_pcm_writei(hndl, bufs[0], n_frames);
+ }
+
+ state->ready_offset += n_bytes;
+
+ if (state->ready_offset >= last_offset) {
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ state->io->buffer_id = b->id;
+ spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id);
+
+ spa_node_call_reuse_buffer(&state->callbacks, 0, b->id);
+
+ state->ready_offset = 0;
+ }
+ written += n_frames;
+ off += n_frames;
+ to_write -= n_frames;
+ }
+
+ spa_log_trace_fp(state->log, "%p: commit %ld %ld %"PRIi64,
+ state, offset, written, state->sample_count);
+ total_written += written;
+
+ if (state->use_mmap && written > 0) {
+ if (SPA_UNLIKELY((commitres = snd_pcm_mmap_commit(hndl, offset, written)) < 0)) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s",
+ state->props.device, snd_strerror(commitres));
+ if (commitres != -EPIPE && commitres != -ESTRPIPE)
+ return res;
+ }
+ if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) {
+ spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld",
+ state->props.device, commitres, written);
+ }
+ }
+
+ if (!spa_list_is_empty(&state->ready) && written > 0)
+ goto again;
+
+ state->sample_count += total_written;
+
+ if (SPA_UNLIKELY(!state->alsa_started && (total_written > 0 || frames == 0)))
+ do_start(state);
+
+ return 0;
+}
+
+void spa_alsa_recycle_buffer(struct state *this, uint32_t buffer_id)
+{
+ struct buffer *b = &this->buffers[buffer_id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id);
+ spa_list_append(&this->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+}
+
+static snd_pcm_uframes_t
+push_frames(struct state *state,
+ const snd_pcm_channel_area_t *my_areas,
+ snd_pcm_uframes_t offset,
+ snd_pcm_uframes_t frames)
+{
+ snd_pcm_uframes_t total_frames = 0;
+
+ if (spa_list_is_empty(&state->free)) {
+ spa_log_warn(state->log, "%s: no more buffers", state->props.device);
+ total_frames = frames;
+ } else {
+ size_t n_bytes, left, frame_size = state->frame_size;
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, avail, l0, l1;
+
+ b = spa_list_first(&state->free, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ if (b->h) {
+ b->h->seq = state->sample_count;
+ b->h->pts = state->next_time;
+ b->h->dts_offset = 0;
+ }
+
+ d = b->buf->datas;
+
+ avail = d[0].maxsize / frame_size;
+ total_frames = SPA_MIN(avail, frames);
+ n_bytes = total_frames * frame_size;
+
+ if (my_areas) {
+ left = state->buffer_frames - offset;
+ l0 = SPA_MIN(n_bytes, left * frame_size);
+ l1 = n_bytes - l0;
+
+ for (i = 0; i < b->buf->n_datas; i++) {
+ spa_memcpy(d[i].data,
+ SPA_PTROFF(my_areas[i].addr, offset * frame_size, void),
+ l0);
+ if (SPA_UNLIKELY(l1 > 0))
+ spa_memcpy(SPA_PTROFF(d[i].data, l0, void),
+ my_areas[i].addr,
+ l1);
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = frame_size;
+ }
+ } else {
+ void *bufs[b->buf->n_datas];
+ for (i = 0; i < b->buf->n_datas; i++) {
+ bufs[i] = d[i].data;
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = frame_size;
+ }
+ if (state->planar) {
+ snd_pcm_readn(state->hndl, bufs, total_frames);
+ } else {
+ snd_pcm_readi(state->hndl, bufs[0], total_frames);
+ }
+ }
+ spa_log_trace_fp(state->log, "%p: wrote %ld frames into buffer %d",
+ state, total_frames, b->id);
+
+ spa_list_append(&state->ready, &b->link);
+ }
+ return total_frames;
+}
+
+
+int spa_alsa_read(struct state *state)
+{
+ snd_pcm_t *hndl = state->hndl;
+ snd_pcm_uframes_t total_read = 0, to_read, max_read;
+ const snd_pcm_channel_area_t *my_areas;
+ snd_pcm_uframes_t read, frames, offset;
+ snd_pcm_sframes_t commitres;
+ int res = 0, missed;
+
+ check_position_config(state);
+
+ max_read = state->buffer_frames;
+
+ if (state->following && state->alsa_started) {
+ uint64_t current_time;
+ snd_pcm_uframes_t avail, delay, target;
+
+ current_time = state->position->clock.nsec;
+
+ if ((res = get_status(state, current_time, &delay, &target)) < 0)
+ return res;
+
+ avail = delay;
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0))
+ return res;
+
+ if (state->alsa_sync) {
+ enum spa_log_level lev;
+
+ if (SPA_UNLIKELY(state->alsa_sync_warning))
+ lev = SPA_LOG_LEVEL_WARN;
+ else
+ lev = SPA_LOG_LEVEL_INFO;
+
+ if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) {
+ spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, "
+ "resync (%d missed)", state->props.device, delay,
+ target, state->threshold, missed);
+ }
+
+ if (delay < target)
+ max_read = target - delay;
+ else if (delay > target)
+ snd_pcm_forward(state->hndl, delay - target);
+ delay = target;
+ state->alsa_sync = false;
+ } else
+ state->alsa_sync_warning = true;
+
+ if (avail < state->read_size)
+ max_read = 0;
+ }
+
+ frames = SPA_MIN(max_read, state->read_size);
+
+ if (state->use_mmap) {
+ to_read = state->buffer_frames;
+ if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &to_read)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s",
+ state->props.device, snd_strerror(res));
+ return res;
+ }
+ spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld to_read:%ld thres:%d", state,
+ offset, frames, to_read, state->threshold);
+ } else {
+ my_areas = NULL;
+ offset = 0;
+ }
+
+ if (frames > 0) {
+ read = push_frames(state, my_areas, offset, frames);
+ total_read += read;
+ } else {
+ spa_alsa_skip(state);
+ total_read += state->read_size;
+ read = 0;
+ }
+
+ if (state->use_mmap && read > 0) {
+ spa_log_trace_fp(state->log, "%p: commit offs:%ld read:%ld count:%"PRIi64, state,
+ offset, read, state->sample_count);
+ if ((commitres = snd_pcm_mmap_commit(hndl, offset, read)) < 0) {
+ spa_log_error(state->log, "%s: snd_pcm_mmap_commit error %lu %lu: %s",
+ state->props.device, frames, read, snd_strerror(commitres));
+ if (commitres != -EPIPE && commitres != -ESTRPIPE)
+ return res;
+ }
+ if (commitres > 0 && read != (snd_pcm_uframes_t) commitres) {
+ spa_log_warn(state->log, "%s: mmap_commit read %ld instead of %ld",
+ state->props.device, commitres, read);
+ }
+ }
+
+ state->sample_count += total_read;
+
+ return 0;
+}
+
+int spa_alsa_skip(struct state *state)
+{
+ struct buffer *b;
+ struct spa_data *d;
+ uint32_t i, avail, total_frames, n_bytes, frames;
+
+ if (spa_list_is_empty(&state->free)) {
+ spa_log_warn(state->log, "%s: no more buffers", state->props.device);
+ return -EPIPE;
+ }
+
+ frames = state->read_size;
+
+ b = spa_list_first(&state->free, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ d = b->buf->datas;
+
+ avail = d[0].maxsize / state->frame_size;
+ total_frames = SPA_MIN(avail, frames);
+ n_bytes = total_frames * state->frame_size;
+
+ for (i = 0; i < b->buf->n_datas; i++) {
+ memset(d[i].data, 0, n_bytes);
+ d[i].chunk->offset = 0;
+ d[i].chunk->size = n_bytes;
+ d[i].chunk->stride = state->frame_size;
+ }
+ spa_list_append(&state->ready, &b->link);
+
+ return 0;
+}
+
+
+static int handle_play(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t delay, snd_pcm_uframes_t target)
+{
+ int res;
+
+ if (state->alsa_started && SPA_UNLIKELY(delay > target + state->max_error)) {
+ spa_log_trace(state->log, "%p: early wakeup %lu %lu", state, delay, target);
+ if (delay > target * 3)
+ delay = target * 3;
+ state->next_time = current_time + (delay - target) * SPA_NSEC_PER_SEC / state->rate;
+ return -EAGAIN;
+ }
+
+ if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, false)) < 0))
+ return res;
+
+ if (spa_list_is_empty(&state->ready)) {
+ struct spa_io_buffers *io = state->io;
+
+ spa_log_trace_fp(state->log, "%p: %d", state, io->status);
+
+ io->status = SPA_STATUS_NEED_DATA;
+
+ res = spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA);
+ }
+ else {
+ res = spa_alsa_write(state);
+ }
+ return res;
+}
+
+static int handle_capture(struct state *state, uint64_t current_time,
+ snd_pcm_uframes_t delay, snd_pcm_uframes_t target)
+{
+ int res;
+ struct spa_io_buffers *io;
+
+ if (SPA_UNLIKELY(delay < target)) {
+ spa_log_trace(state->log, "%p: early wakeup %ld %ld", state, delay, target);
+ state->next_time = current_time + (target - delay) * SPA_NSEC_PER_SEC /
+ state->rate;
+ return -EAGAIN;
+ }
+
+ if (SPA_UNLIKELY(res = update_time(state, current_time, delay, target, false)) < 0)
+ return res;
+
+ if ((res = spa_alsa_read(state)) < 0)
+ return res;
+
+ if (spa_list_is_empty(&state->ready))
+ return 0;
+
+ io = state->io;
+ if (io != NULL &&
+ (io->status != SPA_STATUS_HAVE_DATA || state->rate_match != NULL)) {
+ struct buffer *b;
+
+ if (io->buffer_id < state->n_buffers)
+ spa_alsa_recycle_buffer(state, io->buffer_id);
+
+ b = spa_list_first(&state->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id);
+ }
+ spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA);
+ return 0;
+}
+
+static void alsa_on_timeout_event(struct spa_source *source)
+{
+ struct state *state = source->data;
+ snd_pcm_uframes_t delay, target;
+ uint64_t expire, current_time;
+ int res;
+
+ if (SPA_LIKELY(state->started)) {
+ if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system,
+ state->timerfd, &expire)) < 0)) {
+ /* we can get here when the timer is changed since the last
+ * timerfd wakeup, for example by do_reassign_follower() executed
+ * in the same epoll wakeup cycle */
+ if (res != -EAGAIN)
+ spa_log_warn(state->log, "%p: error reading timerfd: %s",
+ state, spa_strerror(res));
+ return;
+ }
+ }
+
+ check_position_config(state);
+
+ current_time = state->next_time;
+
+ if (SPA_UNLIKELY(get_status(state, current_time, &delay, &target) < 0)) {
+ spa_log_error(state->log, "get_status error");
+ state->next_time += state->threshold * 1e9 / state->rate;
+ goto done;
+ }
+
+#ifndef FASTPATH
+ if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) {
+ struct timespec now;
+ uint64_t nsec;
+ if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0)
+ return;
+ nsec = SPA_TIMESPEC_TO_NSEC(&now);
+ spa_log_trace_fp(state->log, "%p: timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64
+ " %d %"PRIi64, state, delay, target, nsec, nsec,
+ nsec - current_time, state->threshold, state->sample_count);
+ }
+#endif
+
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ handle_play(state, current_time, delay, target);
+ else
+ handle_capture(state, current_time, delay, target);
+
+done:
+ if (state->next_time > current_time + SPA_NSEC_PER_SEC ||
+ current_time > state->next_time + SPA_NSEC_PER_SEC) {
+ spa_log_error(state->log, "%s: impossible timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64
+ " %d %"PRIi64, state->props.device, delay, target, current_time, state->next_time,
+ state->next_time - current_time, state->threshold, state->sample_count);
+ state->next_time = current_time + state->threshold * 1e9 / state->rate;
+ }
+ set_timeout(state, state->next_time);
+}
+
+static void reset_buffers(struct state *this)
+{
+ uint32_t i;
+
+ spa_list_init(&this->free);
+ spa_list_init(&this->ready);
+
+ for (i = 0; i < this->n_buffers; i++) {
+ struct buffer *b = &this->buffers[i];
+ if (this->stream == SND_PCM_STREAM_PLAYBACK) {
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
+ } else {
+ spa_list_append(&this->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ }
+}
+
+static int set_timers(struct state *state)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+ state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+
+ if (state->following) {
+ set_timeout(state, 0);
+ } else {
+ set_timeout(state, state->next_time);
+ }
+ return 0;
+}
+
+int spa_alsa_start(struct state *state)
+{
+ int err;
+
+ if (state->started)
+ return 0;
+
+ if (state->position) {
+ state->duration = state->position->clock.duration;
+ state->rate_denom = state->position->clock.rate.denom;
+ }
+ else {
+ spa_log_warn(state->log, "%s: no position set, using defaults",
+ state->props.device);
+ state->duration = 1024;
+ state->rate_denom = state->rate;
+ }
+ if (state->rate_denom == 0) {
+ spa_log_error(state->log, "%s: unset rate_denom", state->props.device);
+ return -EIO;
+ }
+ if (state->duration == 0) {
+ spa_log_error(state->log, "%s: unset duration", state->props.device);
+ return -EIO;
+ }
+
+ state->following = is_following(state);
+ setup_matching(state);
+
+ spa_dll_init(&state->dll);
+ state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom);
+ state->last_threshold = state->threshold;
+ state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f);
+
+ spa_log_debug(state->log, "%p: start %d duration:%d rate:%d follower:%d match:%d resample:%d",
+ state, state->threshold, state->duration, state->rate_denom,
+ state->following, state->matching, state->resample);
+
+ CHECK(set_swparams(state), "swparams");
+
+ if ((err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) {
+ spa_log_error(state->log, "%s: snd_pcm_prepare error: %s",
+ state->props.device, snd_strerror(err));
+ return err;
+ }
+
+ state->source.func = alsa_on_timeout_event;
+ state->source.data = state;
+ state->source.fd = state->timerfd;
+ state->source.mask = SPA_IO_IN;
+ state->source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->source);
+
+ reset_buffers(state);
+ state->alsa_sync = true;
+ state->alsa_sync_warning = false;
+ state->alsa_recovering = false;
+ state->alsa_started = false;
+
+ /* start capture now, playback will start after first write */
+ if (state->stream == SND_PCM_STREAM_PLAYBACK)
+ spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom);
+ else if ((err = do_start(state)) < 0)
+ return err;
+
+ set_timers(state);
+
+ state->started = true;
+
+ return 0;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+ set_timers(state);
+ spa_dll_init(&state->dll);
+ return 0;
+}
+
+int spa_alsa_reassign_follower(struct state *state)
+{
+ bool following, freewheel;
+
+ if (!state->started)
+ return 0;
+
+ following = is_following(state);
+ if (following != state->following) {
+ spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following);
+ state->following = following;
+ spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+ }
+ setup_matching(state);
+
+ freewheel = state->position &&
+ SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
+
+ if (state->freewheel != freewheel) {
+ spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel);
+ state->freewheel = freewheel;
+ if (freewheel)
+ snd_pcm_pause(state->hndl, 1);
+ else
+ snd_pcm_pause(state->hndl, 0);
+ }
+
+ state->alsa_sync_warning = false;
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct state *state = user_data;
+ struct itimerspec ts;
+
+ spa_loop_remove_source(state->data_loop, &state->source);
+ ts.it_value.tv_sec = 0;
+ ts.it_value.tv_nsec = 0;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system, state->timerfd, 0, &ts, NULL);
+
+ return 0;
+}
+
+int spa_alsa_pause(struct state *state)
+{
+ int err;
+
+ if (!state->started)
+ return 0;
+
+ spa_log_debug(state->log, "%p: pause", state);
+
+ spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+ if ((err = snd_pcm_drop(state->hndl)) < 0)
+ spa_log_error(state->log, "%s: snd_pcm_drop %s", state->props.device,
+ snd_strerror(err));
+
+ state->started = false;
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h
new file mode 100644
index 0000000..9c4a868
--- /dev/null
+++ b/spa/plugins/alsa/alsa-pcm.h
@@ -0,0 +1,374 @@
+/* Spa ALSA Sink
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_UTILS_H
+#define SPA_ALSA_UTILS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+#include <alsa/use-case.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/json.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/debug/types.h>
+#include <spa/param/param.h>
+#include <spa/param/latency-utils.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "alsa.h"
+
+
+#define MAX_RATES 16
+
+#define DEFAULT_PERIOD 1024u
+#define DEFAULT_RATE 48000u
+#define DEFAULT_CHANNELS 2u
+#define DEFAULT_USE_CHMAP false
+
+struct props {
+ char device[64];
+ char device_name[128];
+ char card_name[128];
+ bool use_chmap;
+};
+
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+#define BW_MAX 0.128
+#define BW_MED 0.064
+#define BW_MIN 0.016
+#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
+
+struct channel_map {
+ uint32_t channels;
+ uint32_t pos[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct card {
+ struct spa_list link;
+ int ref;
+ uint32_t index;
+ snd_use_case_mgr_t *ucm;
+ char *ucm_prefix;
+ int format_ref;
+ uint32_t rate;
+};
+
+struct ratelimit {
+ uint64_t interval;
+ uint64_t begin;
+ unsigned burst;
+ unsigned n_printed, n_missed;
+};
+
+struct state {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_system *data_system;
+ struct spa_loop *data_loop;
+
+ FILE *log_file;
+ struct ratelimit rate_limit;
+
+ uint32_t card_index;
+ struct card *card;
+ snd_pcm_stream_t stream;
+ snd_output_t *output;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_IO 2
+#define NODE_ProcessLatency 3
+#define N_NODE_PARAMS 4
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ bool opened;
+ snd_pcm_t *hndl;
+
+ bool have_format;
+ struct spa_audio_info current_format;
+
+ uint32_t default_period_size;
+ uint32_t default_period_num;
+ uint32_t default_headroom;
+ uint32_t default_start_delay;
+ uint32_t default_format;
+ unsigned int default_channels;
+ unsigned int default_rate;
+ uint32_t allowed_rates[MAX_RATES];
+ uint32_t n_allowed_rates;
+ struct channel_map default_pos;
+ unsigned int disable_mmap;
+ unsigned int disable_batch;
+ char clock_name[64];
+ uint32_t quantum_limit;
+
+ snd_pcm_uframes_t buffer_frames;
+ snd_pcm_uframes_t period_frames;
+ snd_pcm_format_t format;
+ int rate;
+ int channels;
+ size_t frame_size;
+ size_t frame_scale;
+ int blocks;
+ uint32_t rate_denom;
+ uint32_t delay;
+ uint32_t read_size;
+
+ uint64_t port_info_all;
+ struct spa_port_info port_info;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info port_params[N_PORT_PARAMS];
+ enum spa_direction port_direction;
+ struct spa_io_buffers *io;
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+ struct spa_io_rate_match *rate_match;
+
+ struct buffer buffers[MAX_BUFFERS];
+ unsigned int n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ size_t ready_offset;
+
+ bool started;
+ struct spa_source source;
+ int timerfd;
+ uint32_t threshold;
+ uint32_t last_threshold;
+ uint32_t headroom;
+ uint32_t start_delay;
+ uint32_t min_delay;
+
+ uint32_t duration;
+ unsigned int alsa_started:1;
+ unsigned int alsa_sync:1;
+ unsigned int alsa_sync_warning:1;
+ unsigned int alsa_recovering:1;
+ unsigned int following:1;
+ unsigned int matching:1;
+ unsigned int resample:1;
+ unsigned int use_mmap:1;
+ unsigned int planar:1;
+ unsigned int freewheel:1;
+ unsigned int open_ucm:1;
+ unsigned int is_iec958:1;
+ unsigned int is_hdmi:1;
+ unsigned int multi_rate:1;
+
+ uint64_t iec958_codecs;
+
+ int64_t sample_count;
+
+ int64_t sample_time;
+ uint64_t next_time;
+ uint64_t base_time;
+
+ uint64_t underrun;
+
+ struct spa_dll dll;
+ double max_error;
+
+ struct spa_latency_info latency[2];
+ struct spa_process_latency_info process_latency;
+};
+
+struct spa_pod *spa_alsa_enum_propinfo(struct state *state,
+ uint32_t idx, struct spa_pod_builder *b);
+int spa_alsa_add_prop_params(struct state *state, struct spa_pod_builder *b);
+int spa_alsa_parse_prop_params(struct state *state, struct spa_pod *params);
+
+int spa_alsa_enum_format(struct state *state, int seq,
+ uint32_t start, uint32_t num,
+ const struct spa_pod *filter);
+
+int spa_alsa_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags);
+
+int spa_alsa_init(struct state *state, const struct spa_dict *info);
+int spa_alsa_clear(struct state *state);
+
+int spa_alsa_open(struct state *state, const char *params);
+int spa_alsa_start(struct state *state);
+int spa_alsa_reassign_follower(struct state *state);
+int spa_alsa_pause(struct state *state);
+int spa_alsa_close(struct state *state);
+
+int spa_alsa_write(struct state *state);
+int spa_alsa_read(struct state *state);
+int spa_alsa_skip(struct state *state);
+
+void spa_alsa_recycle_buffer(struct state *state, uint32_t buffer_id);
+
+static inline uint32_t spa_alsa_format_from_name(const char *name, size_t len)
+{
+ int i;
+ for (i = 0; spa_type_audio_format[i].name; i++) {
+ if (strncmp(name, spa_debug_type_short_name(spa_type_audio_format[i].name), len) == 0)
+ return spa_type_audio_format[i].type;
+ }
+ return SPA_AUDIO_FORMAT_UNKNOWN;
+}
+
+static inline uint32_t spa_alsa_channel_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (strcmp(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)) == 0)
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static inline void spa_alsa_parse_position(struct channel_map *map, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ map->channels = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 &&
+ map->channels < SPA_AUDIO_MAX_CHANNELS) {
+ map->pos[map->channels++] = spa_alsa_channel_from_name(v);
+ }
+}
+
+static inline uint32_t spa_alsa_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+ uint32_t count;
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ count = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0 && count < max)
+ rates[count++] = atoi(v);
+ return count;
+}
+
+static inline uint32_t spa_alsa_iec958_codec_from_name(const char *name)
+{
+ int i;
+ for (i = 0; spa_type_audio_iec958_codec[i].name; i++) {
+ if (strcmp(name, spa_debug_type_short_name(spa_type_audio_iec958_codec[i].name)) == 0)
+ return spa_type_audio_iec958_codec[i].type;
+ }
+ return SPA_AUDIO_IEC958_CODEC_UNKNOWN;
+}
+
+static inline void spa_alsa_parse_iec958_codecs(uint64_t *codecs, const char *val, size_t len)
+{
+ struct spa_json it[2];
+ char v[256];
+
+ spa_json_init(&it[0], val, len);
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0)
+ spa_json_init(&it[1], val, len);
+
+ *codecs = 0;
+ while (spa_json_get_string(&it[1], v, sizeof(v)) > 0)
+ *codecs |= 1ULL << spa_alsa_iec958_codec_from_name(v);
+}
+
+static inline uint32_t spa_alsa_get_iec958_codecs(struct state *state, uint32_t *codecs,
+ uint32_t max_codecs)
+{
+ uint64_t mask = state->iec958_codecs;
+ uint32_t i = 0, j = 0;
+ if (!(state->is_iec958 || state->is_hdmi))
+ return 0;
+ while (mask && i < max_codecs) {
+ if (mask & 1)
+ codecs[i++] = j;
+ mask >>= 1;
+ j++;
+ }
+ return i;
+}
+
+static inline int ratelimit_test(struct ratelimit *r, uint64_t now)
+{
+ unsigned missed = 0;
+ if (r->begin + r->interval < now) {
+ missed = r->n_missed;
+ r->begin = now;
+ r->n_printed = 0;
+ r->n_missed = 0;
+ } else if (r->n_printed >= r->burst) {
+ r->n_missed++;
+ return -1;
+ }
+ r->n_printed++;
+ return missed;
+}
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_ALSA_UTILS_H */
diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c
new file mode 100644
index 0000000..ee50163
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq-bridge.c
@@ -0,0 +1,1006 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <ctype.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/keys.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/string.h>
+#include <spa/utils/list.h>
+#include <spa/monitor/device.h>
+#include <spa/param/audio/format.h>
+#include <spa/param/latency-utils.h>
+#include <spa/pod/filter.h>
+
+#include "alsa-seq.h"
+
+#define DEFAULT_DEVICE "default"
+#define DEFAULT_CLOCK_NAME "clock.system.monotonic"
+
+static void reset_props(struct props *props)
+{
+ strncpy(props->device, DEFAULT_DEVICE, sizeof(props->device));
+ strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name));
+ props->disable_longname = 0;
+}
+
+static int impl_node_enum_params(void *object, int seq,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct seq_state *this = object;
+ struct spa_pod *param;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = { 0 };
+ struct props *p;
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ p = &this->props;
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_PropInfo:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_PropInfo, id,
+ SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
+ SPA_PROP_INFO_description, SPA_POD_String("The ALSA device"),
+ SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Props:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Props, id,
+ SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
+ break;
+ case 1:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_IO_Clock:
+ this->clock = data;
+ if (this->clock != NULL)
+ spa_scnprintf(this->clock->name, sizeof(this->clock->name),
+ "%s", this->props.clock_name);
+ break;
+ case SPA_IO_Position:
+ this->position = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ spa_alsa_seq_reassign_follower(this);
+ return 0;
+}
+
+static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ switch (id) {
+ case SPA_PARAM_Props:
+ {
+ struct props *p = &this->props;
+
+ if (param == NULL) {
+ reset_props(p);
+ return 0;
+ }
+ spa_pod_parse_object(param,
+ SPA_TYPE_OBJECT_Props, NULL,
+ SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device)));
+ break;
+ }
+ default:
+ return -ENOENT;
+ }
+
+ return 0;
+}
+
+static int impl_node_send_command(void *object, const struct spa_command *command)
+{
+ struct seq_state *this = object;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(command != NULL, -EINVAL);
+
+ switch (SPA_NODE_COMMAND_ID(command)) {
+ case SPA_NODE_COMMAND_Start:
+ if ((res = spa_alsa_seq_start(this)) < 0)
+ return res;
+ break;
+ case SPA_NODE_COMMAND_Pause:
+ case SPA_NODE_COMMAND_Suspend:
+ if ((res = spa_alsa_seq_pause(this)) < 0)
+ return res;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+ return 0;
+}
+
+static const struct spa_dict_item node_info_items[] = {
+ { SPA_KEY_DEVICE_API, "alsa" },
+ { SPA_KEY_MEDIA_CLASS, "Midi/Bridge" },
+ { SPA_KEY_NODE_DRIVER, "true" },
+};
+
+static void emit_node_info(struct seq_state *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
+ spa_node_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static inline void clean_name(char *name)
+{
+ char *c;
+ for (c = name; *c; ++c) {
+ if (!isalnum(*c) && strchr(" /_:()[]", *c) == NULL)
+ *c = '-';
+ }
+}
+
+static void emit_port_info(struct seq_state *this, struct seq_port *port, bool full)
+{
+ uint64_t old = full ? port->info.change_mask : 0;
+ if (full)
+ port->info.change_mask = port->info_all;
+ if (port->info.change_mask) {
+ struct spa_dict_item items[5];
+ uint32_t n_items = 0;
+ int id;
+ snd_seq_port_info_t *info;
+ snd_seq_client_info_t *client_info;
+ char card[8];
+ char name[256];
+ char path[128];
+ char alias[128];
+
+ snd_seq_port_info_alloca(&info);
+ snd_seq_get_any_port_info(this->sys.hndl,
+ port->addr.client, port->addr.port, info);
+
+ snd_seq_client_info_alloca(&client_info);
+ snd_seq_get_any_client_info(this->sys.hndl,
+ port->addr.client, client_info);
+
+ int card_id;
+
+ // Failed to obtain card number (software device) or disabled
+ if (this->props.disable_longname || (card_id = snd_seq_client_info_get_card(client_info)) < 0) {
+ snprintf(name, sizeof(name), "%s:(%s_%d) %s",
+ snd_seq_client_info_get_name(client_info),
+ port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
+ port->addr.port,
+ snd_seq_port_info_get_name(info));
+ } else {
+ char *longname;
+ if (snd_card_get_longname(card_id, &longname) == 0) {
+ snprintf(name, sizeof(name), "%s:(%s_%d) %s",
+ longname,
+ port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
+ port->addr.port,
+ snd_seq_port_info_get_name(info));
+ free(longname);
+ } else {
+ // At least add card number to be distinct
+ snprintf(name, sizeof(name), "%s %d:(%s_%d) %s",
+ snd_seq_client_info_get_name(client_info),
+ card_id,
+ port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
+ port->addr.port,
+ snd_seq_port_info_get_name(info));
+ }
+ }
+ clean_name(name);
+
+ snprintf(path, sizeof(path), "alsa:seq:%s:client_%d:%s_%d",
+ this->props.device,
+ port->addr.client,
+ port->direction == SPA_DIRECTION_OUTPUT ? "capture" : "playback",
+ port->addr.port);
+ clean_name(path);
+
+ snprintf(alias, sizeof(alias), "%s:%s",
+ snd_seq_client_info_get_name(client_info),
+ snd_seq_port_info_get_name(info));
+ clean_name(alias);
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias);
+ if ((id = snd_seq_client_info_get_card(client_info)) != -1) {
+ snprintf(card, sizeof(card), "%d", id);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card);
+ }
+ port->info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_node_emit_port_info(&this->hooks,
+ port->direction, port->id, &port->info);
+ port->info.change_mask = old;
+ }
+}
+
+static void emit_stream_info(struct seq_state *this, struct seq_stream *stream, bool full)
+{
+ uint32_t i;
+
+ for (i = 0; i < MAX_PORTS; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid)
+ emit_port_info(this, port, full);
+ }
+}
+
+static int
+impl_node_add_listener(void *object,
+ struct spa_hook *listener,
+ const struct spa_node_events *events,
+ void *data)
+{
+ struct seq_state *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_node_info(this, true);
+ emit_stream_info(this, &this->streams[SPA_DIRECTION_INPUT], true);
+ emit_stream_info(this, &this->streams[SPA_DIRECTION_OUTPUT], true);
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ return 0;
+}
+
+static int
+impl_node_set_callbacks(void *object,
+ const struct spa_node_callbacks *callbacks,
+ void *data)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
+
+ return 0;
+}
+
+static int impl_node_sync(void *object, int seq)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
+
+ return 0;
+}
+
+static struct seq_port *find_port(struct seq_state *state,
+ struct seq_stream *stream, const snd_seq_addr_t *addr)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid &&
+ port->addr.client == addr->client &&
+ port->addr.port == addr->port)
+ return port;
+ }
+ return NULL;
+}
+
+static struct seq_port *alloc_port(struct seq_state *state, struct seq_stream *stream)
+{
+ uint32_t i;
+ for (i = 0; i < MAX_PORTS; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (!port->valid) {
+ port->id = i;
+ port->direction = stream->direction;
+ port->valid = true;
+ if (stream->last_port < i + 1)
+ stream->last_port = i + 1;
+ return port;
+ }
+ }
+ return NULL;
+}
+
+static void free_port(struct seq_state *state, struct seq_stream *stream, struct seq_port *port)
+{
+ port->valid = false;
+
+ if (port->id + 1 == stream->last_port) {
+ int i;
+ for (i = stream->last_port - 1; i >= 0; i--)
+ if (stream->ports[i].valid)
+ break;
+ stream->last_port = i + 1;
+ }
+
+ spa_node_emit_port_info(&state->hooks,
+ port->direction, port->id, NULL);
+ spa_zero(*port);
+}
+
+static void init_port(struct seq_state *state, struct seq_port *port, const snd_seq_addr_t *addr,
+ unsigned int type)
+{
+ enum spa_direction reverse = SPA_DIRECTION_REVERSE(port->direction);
+
+ port->addr = *addr;
+ port->info_all = SPA_PORT_CHANGE_MASK_FLAGS |
+ SPA_PORT_CHANGE_MASK_PROPS |
+ SPA_PORT_CHANGE_MASK_PARAMS;
+ port->info = SPA_PORT_INFO_INIT();
+ port->info.flags = SPA_PORT_FLAG_LIVE;
+ if (type & (SND_SEQ_PORT_TYPE_HARDWARE|SND_SEQ_PORT_TYPE_PORT|SND_SEQ_PORT_TYPE_SPECIFIC))
+ port->info.flags |= SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL;
+ port->params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
+ port->params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ);
+ port->params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ port->params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE);
+ port->info.params = port->params;
+ port->info.n_params = N_PORT_PARAMS;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ port->latency[port->direction] = SPA_LATENCY_INFO(
+ port->direction,
+ .min_quantum = 1.0f,
+ .max_quantum = 1.0f);
+ port->latency[reverse] = SPA_LATENCY_INFO(reverse);
+
+ spa_alsa_seq_activate_port(state, port, true);
+
+ emit_port_info(state, port, true);
+}
+
+static void update_stream_port(struct seq_state *state, struct seq_stream *stream,
+ const snd_seq_addr_t *addr, unsigned int caps, const snd_seq_port_info_t *info)
+{
+ struct seq_port *port = find_port(state, stream, addr);
+
+ if (info == NULL) {
+ spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port);
+ if (port)
+ free_port(state, stream, port);
+ } else {
+ if (port == NULL && (caps & stream->caps) == stream->caps) {
+ spa_log_debug(state->log, "new port %d.%d", addr->client, addr->port);
+ port = alloc_port(state, stream);
+ if (port == NULL)
+ return;
+ init_port(state, port, addr, snd_seq_port_info_get_type(info));
+ } else if (port != NULL) {
+ if ((caps & stream->caps) != stream->caps) {
+ spa_log_debug(state->log, "free port %d.%d", addr->client, addr->port);
+ free_port(state, stream, port);
+ }
+ else {
+ spa_log_debug(state->log, "update port %d.%d", addr->client, addr->port);
+ port->info.change_mask = SPA_PORT_CHANGE_MASK_PROPS;
+ emit_port_info(state, port, false);
+ }
+ }
+ }
+}
+
+static int on_port_info(void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info)
+{
+ struct seq_state *state = data;
+
+ if (info == NULL) {
+ update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, 0, info);
+ update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, 0, info);
+ } else {
+ unsigned int caps = snd_seq_port_info_get_capability(info);
+
+ if (caps & SND_SEQ_PORT_CAP_NO_EXPORT)
+ return 0;
+
+ update_stream_port(state, &state->streams[SPA_DIRECTION_INPUT], addr, caps, info);
+ update_stream_port(state, &state->streams[SPA_DIRECTION_OUTPUT], addr, caps, info);
+ }
+ return 0;
+}
+
+static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
+ const struct spa_dict *props)
+{
+ return -ENOTSUP;
+}
+
+static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
+{
+ return -ENOTSUP;
+}
+
+static int
+impl_node_port_enum_params(void *object, int seq,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t start, uint32_t num,
+ const struct spa_pod *filter)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ struct spa_pod *param;
+ struct spa_pod_builder b = { 0 };
+ uint8_t buffer[1024];
+ struct spa_result_node_params result;
+ uint32_t count = 0;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(num != 0, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ result.id = id;
+ result.next = start;
+ next:
+ result.index = result.next++;
+
+ spa_pod_builder_init(&b, buffer, sizeof(buffer));
+
+ switch (id) {
+ case SPA_PARAM_EnumFormat:
+ if (result.index > 0)
+ return 0;
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ break;
+
+ case SPA_PARAM_Format:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+ break;
+
+ case SPA_PARAM_Buffers:
+ if (!port->have_format)
+ return -EIO;
+ if (result.index > 0)
+ return 0;
+
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamBuffers, id,
+ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS),
+ SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
+ SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
+ 4096, 4096, INT32_MAX),
+ SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1));
+ break;
+
+ case SPA_PARAM_Meta:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamMeta, id,
+ SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
+ SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_IO:
+ switch (result.index) {
+ case 0:
+ param = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_ParamIO, id,
+ SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
+ SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ case SPA_PARAM_Latency:
+ switch (result.index) {
+ case 0: case 1:
+ param = spa_latency_build(&b, id, &port->latency[result.index]);
+ break;
+ default:
+ return 0;
+ }
+ break;
+
+ default:
+ return -ENOENT;
+ }
+
+ if (spa_pod_filter(&b, &result.param, param, filter) < 0)
+ goto next;
+
+ spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
+
+ if (++count != num)
+ goto next;
+
+ return 0;
+}
+
+static int clear_buffers(struct seq_state *this, struct seq_port *port)
+{
+ if (port->n_buffers > 0) {
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+ port->n_buffers = 0;
+ }
+ return 0;
+}
+
+static int port_set_format(void *object, struct seq_port *port,
+ uint32_t flags, const struct spa_pod *format)
+{
+ struct seq_state *this = object;
+ int err;
+
+ if (format == NULL) {
+ if (!port->have_format)
+ return 0;
+
+ clear_buffers(this, port);
+ port->have_format = false;
+ } else {
+ struct spa_audio_info info = { 0 };
+
+ if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0)
+ return err;
+
+ if (info.media_type != SPA_MEDIA_TYPE_application ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_control)
+ return -EINVAL;
+
+ port->current_format = info;
+ port->have_format = true;
+ }
+
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
+ port->info.rate = SPA_FRACTION(1, 1);
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ if (port->have_format) {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
+ } else {
+ port->params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
+ port->params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
+ }
+ emit_port_info(this, port, false);
+
+ return 0;
+}
+
+static int
+impl_node_port_set_param(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t id, uint32_t flags,
+ const struct spa_pod *param)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ int res;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ res = port_set_format(this, port, flags, param);
+ break;
+ case SPA_PARAM_Latency:
+ {
+ struct spa_latency_info info;
+ if ((res = spa_latency_parse(param, &info)) < 0)
+ return res;
+ if (direction == info.direction)
+ return -EINVAL;
+
+ port->latency[info.direction] = info;
+ port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
+ port->params[PORT_Latency].flags ^= SPA_PARAM_INFO_SERIAL;
+ emit_port_info(this, port, false);
+ break;
+ }
+ default:
+ res = -ENOENT;
+ break;
+ }
+ return res;
+}
+
+static int
+impl_node_port_use_buffers(void *object,
+ enum spa_direction direction, uint32_t port_id,
+ uint32_t flags,
+ struct spa_buffer **buffers, uint32_t n_buffers)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+ uint32_t i;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: port %d.%d buffers:%d format:%d", this,
+ direction, port_id, n_buffers, port->have_format);
+
+ clear_buffers(this, port);
+
+ if (n_buffers > 0 && !port->have_format)
+ return -EIO;
+ if (n_buffers > MAX_BUFFERS)
+ return -ENOSPC;
+
+ for (i = 0; i < n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ struct spa_data *d = buffers[i]->datas;
+
+ b->buf = buffers[i];
+ b->id = i;
+ b->flags = BUFFER_FLAG_OUT;
+
+ b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h));
+
+ if (d[0].data == NULL) {
+ spa_log_error(this->log, "%p: need mapped memory", this);
+ return -EINVAL;
+ }
+ if (direction == SPA_DIRECTION_OUTPUT)
+ spa_alsa_seq_recycle_buffer(this, port, i);
+ }
+ port->n_buffers = n_buffers;
+
+ return 0;
+}
+
+static int
+impl_node_port_set_io(void *object,
+ enum spa_direction direction,
+ uint32_t port_id,
+ uint32_t id,
+ void *data, size_t size)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
+
+ port = GET_PORT(this, direction, port_id);
+
+ spa_log_debug(this->log, "%p: io %d.%d %d %p %zd", this,
+ direction, port_id, id, data, size);
+
+ switch (id) {
+ case SPA_IO_Buffers:
+ port->io = data;
+ break;
+ default:
+ return -ENOENT;
+ }
+ return 0;
+}
+
+static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
+{
+ struct seq_state *this = object;
+ struct seq_port *port;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ spa_return_val_if_fail(!CHECK_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL);
+
+ port = GET_PORT(this, SPA_DIRECTION_OUTPUT, port_id);
+
+ if (port->n_buffers == 0)
+ return -EIO;
+
+ if (buffer_id >= port->n_buffers)
+ return -EINVAL;
+
+ spa_alsa_seq_recycle_buffer(this, port, buffer_id);
+
+ return 0;
+}
+
+static int impl_node_process(void *object)
+{
+ struct seq_state *this = object;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+
+ return spa_alsa_seq_process(this);
+}
+
+static const struct spa_node_methods impl_node = {
+ SPA_VERSION_NODE_METHODS,
+ .add_listener = impl_node_add_listener,
+ .set_callbacks = impl_node_set_callbacks,
+ .sync = impl_node_sync,
+ .enum_params = impl_node_enum_params,
+ .set_param = impl_node_set_param,
+ .set_io = impl_node_set_io,
+ .send_command = impl_node_send_command,
+ .add_port = impl_node_add_port,
+ .remove_port = impl_node_remove_port,
+ .port_enum_params = impl_node_port_enum_params,
+ .port_set_param = impl_node_port_set_param,
+ .port_use_buffers = impl_node_port_use_buffers,
+ .port_set_io = impl_node_port_set_io,
+ .port_reuse_buffer = impl_node_port_reuse_buffer,
+ .process = impl_node_process,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct seq_state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct seq_state *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
+ *interface = &this->node;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct seq_state *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ this = (struct seq_state *) handle;
+
+ spa_alsa_seq_close(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct seq_state);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct seq_state *this;
+ uint32_t i;
+ int res;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct seq_state *) handle;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+
+ this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
+ this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+
+ if (this->data_loop == NULL) {
+ spa_log_error(this->log, "a data loop is needed");
+ return -EINVAL;
+ }
+ if (this->data_system == NULL) {
+ spa_log_error(this->log, "a data system is needed");
+ return -EINVAL;
+ }
+
+ this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this);
+
+ spa_hook_list_init(&this->hooks);
+
+ this->info_all = SPA_NODE_CHANGE_MASK_FLAGS |
+ SPA_NODE_CHANGE_MASK_PROPS |
+ SPA_NODE_CHANGE_MASK_PARAMS;
+ this->info.max_input_ports = MAX_PORTS;
+ this->info.max_output_ports = MAX_PORTS;
+ this->info.flags = SPA_NODE_FLAG_RT;
+ this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
+ this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
+ this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
+ this->info.params = this->params;
+ this->info.n_params = N_NODE_PARAMS;
+ reset_props(&this->props);
+
+ for (i = 0; info && i < info->n_items; i++) {
+ const char *k = info->items[i].key;
+ const char *s = info->items[i].value;
+ if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) {
+ spa_scnprintf(this->props.device,
+ sizeof(this->props.device), "%s", s);
+ } else if (spa_streq(k, "clock.name")) {
+ spa_scnprintf(this->props.clock_name,
+ sizeof(this->props.clock_name), "%s", s);
+ } else if (spa_streq(k, SPA_KEY_API_ALSA_DISABLE_LONGNAME)) {
+ this->props.disable_longname = spa_atob(s);
+ }
+ }
+
+ this->port_info = on_port_info;
+ this->port_info_data = this;
+
+ if ((res = spa_alsa_seq_open(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Node,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+
+ return 1;
+}
+
+static const struct spa_dict_item info_items[] = {
+ { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
+ { SPA_KEY_FACTORY_DESCRIPTION, "Bridge midi ports with the alsa sequencer API" },
+ { SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<device>]" },
+};
+
+static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
+
+const struct spa_handle_factory spa_alsa_seq_bridge_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_SEQ_BRIDGE,
+ &info,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c
new file mode 100644
index 0000000..9cec44d
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq.c
@@ -0,0 +1,983 @@
+/* Spa ALSA Sequencer
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sched.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+#include <math.h>
+#include <limits.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/filter.h>
+#include <spa/support/system.h>
+#include <spa/control/control.h>
+
+#include "alsa.h"
+
+#include "alsa-seq.h"
+
+#define CHECK(s,msg,...) if ((res = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(res)); return res; }
+
+static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue)
+{
+ struct props *props = &state->props;
+ int res;
+
+ spa_log_debug(state->log, "%p: ALSA seq open '%s' duplex", state, props->device);
+
+ if ((res = snd_seq_open(&conn->hndl,
+ props->device,
+ SND_SEQ_OPEN_DUPLEX,
+ 0)) < 0) {
+ return res;
+ }
+ return 0;
+}
+
+static int seq_init(struct seq_state *state, struct seq_conn *conn, bool with_queue)
+{
+ struct pollfd pfd;
+ snd_seq_port_info_t *pinfo;
+ int res;
+
+ /* client id */
+ if ((res = snd_seq_client_id(conn->hndl)) < 0) {
+ spa_log_error(state->log, "failed to get client id: %d", res);
+ goto error_exit_close;
+ }
+ conn->addr.client = res;
+
+ /* queue */
+ if (with_queue) {
+ if ((res = snd_seq_alloc_queue(conn->hndl)) < 0) {
+ spa_log_error(state->log, "failed to create queue: %d", res);
+ goto error_exit_close;
+ }
+ conn->queue_id = res;
+ } else {
+ conn->queue_id = -1;
+ }
+
+ if ((res = snd_seq_nonblock(conn->hndl, 1)) < 0)
+ spa_log_warn(state->log, "can't set nonblock mode: %s", snd_strerror(res));
+
+ /* port for receiving */
+ snd_seq_port_info_alloca(&pinfo);
+ snd_seq_port_info_set_name(pinfo, "input");
+ snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
+ snd_seq_port_info_set_capability(pinfo,
+ SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_READ);
+ /* Enable timestamping for events sent by external subscribers. */
+ snd_seq_port_info_set_timestamping(pinfo, 1);
+ snd_seq_port_info_set_timestamp_real(pinfo, 1);
+ if (with_queue)
+ snd_seq_port_info_set_timestamp_queue(pinfo, conn->queue_id);
+
+ if ((res = snd_seq_create_port(conn->hndl, pinfo)) < 0) {
+ spa_log_error(state->log, "failed to create port: %s", snd_strerror(res));
+ goto error_exit_close;
+ }
+ conn->addr.port = snd_seq_port_info_get_port(pinfo);
+
+ spa_log_debug(state->log, "queue:%d client:%d port:%d",
+ conn->queue_id, conn->addr.client, conn->addr.port);
+
+ snd_seq_poll_descriptors(conn->hndl, &pfd, 1, POLLIN);
+ conn->source.fd = pfd.fd;
+ conn->source.mask = SPA_IO_IN;
+
+ return 0;
+
+error_exit_close:
+ snd_seq_close(conn->hndl);
+ return res;
+}
+
+static int seq_close(struct seq_state *state, struct seq_conn *conn)
+{
+ int res;
+ spa_log_debug(state->log, "%p: Device '%s' closing", state, state->props.device);
+ if ((res = snd_seq_close(conn->hndl)) < 0) {
+ spa_log_warn(state->log, "close failed: %s", snd_strerror(res));
+ }
+ return res;
+}
+
+static int init_stream(struct seq_state *state, enum spa_direction direction)
+{
+ struct seq_stream *stream = &state->streams[direction];
+ int res;
+ stream->direction = direction;
+ if (direction == SPA_DIRECTION_INPUT) {
+ stream->caps = SND_SEQ_PORT_CAP_SUBS_WRITE;
+ } else {
+ stream->caps = SND_SEQ_PORT_CAP_SUBS_READ;
+ }
+ if ((res = snd_midi_event_new(MAX_EVENT_SIZE, &stream->codec)) < 0) {
+ spa_log_error(state->log, "can make event decoder: %s",
+ snd_strerror(res));
+ return res;
+ }
+ snd_midi_event_no_status(stream->codec, 1);
+ memset(stream->ports, 0, sizeof(stream->ports));
+ return 0;
+}
+
+static int uninit_stream(struct seq_state *state, enum spa_direction direction)
+{
+ struct seq_stream *stream = &state->streams[direction];
+ if (stream->codec)
+ snd_midi_event_free(stream->codec);
+ stream->codec = NULL;
+ return 0;
+}
+
+static void init_ports(struct seq_state *state)
+{
+ snd_seq_addr_t addr;
+ snd_seq_client_info_t *client_info;
+ snd_seq_port_info_t *port_info;
+
+ snd_seq_client_info_alloca(&client_info);
+ snd_seq_port_info_alloca(&port_info);
+ snd_seq_client_info_set_client(client_info, -1);
+
+ while (snd_seq_query_next_client(state->sys.hndl, client_info) >= 0) {
+
+ addr.client = snd_seq_client_info_get_client(client_info);
+ if (addr.client == SND_SEQ_CLIENT_SYSTEM ||
+ addr.client == state->sys.addr.client ||
+ addr.client == state->event.addr.client)
+ continue;
+
+ snd_seq_port_info_set_client(port_info, addr.client);
+ snd_seq_port_info_set_port(port_info, -1);
+ while (snd_seq_query_next_port(state->sys.hndl, port_info) >= 0) {
+ addr.port = snd_seq_port_info_get_port(port_info);
+ state->port_info(state->port_info_data, &addr, port_info);
+ }
+ }
+}
+
+static void debug_event(struct seq_state *state, snd_seq_event_t *ev)
+{
+ if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE)))
+ return;
+
+ spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags);
+ switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) {
+ case SND_SEQ_TIME_STAMP_TICK:
+ spa_log_trace(state->log, " time: %d ticks", ev->time.tick);
+ break;
+ case SND_SEQ_TIME_STAMP_REAL:
+ spa_log_trace(state->log, " time = %d.%09d",
+ (int)ev->time.time.tv_sec,
+ (int)ev->time.time.tv_nsec);
+ break;
+ }
+ spa_log_trace(state->log, " source:%d.%d dest:%d.%d queue:%d",
+ ev->source.client,
+ ev->source.port,
+ ev->dest.client,
+ ev->dest.port,
+ ev->queue);
+}
+
+static void alsa_seq_on_sys(struct spa_source *source)
+{
+ struct seq_state *state = source->data;
+ snd_seq_event_t *ev;
+ int res;
+
+ while (snd_seq_event_input(state->sys.hndl, &ev) > 0) {
+ const snd_seq_addr_t *addr = &ev->data.addr;
+
+ if (addr->client == state->event.addr.client)
+ continue;
+
+ debug_event(state, ev);
+
+ switch (ev->type) {
+ case SND_SEQ_EVENT_CLIENT_START:
+ case SND_SEQ_EVENT_CLIENT_CHANGE:
+ spa_log_info(state->log, "client add/change %d", addr->client);
+ break;
+ case SND_SEQ_EVENT_CLIENT_EXIT:
+ spa_log_info(state->log, "client exit %d", addr->client);
+ break;
+
+ case SND_SEQ_EVENT_PORT_START:
+ case SND_SEQ_EVENT_PORT_CHANGE:
+ {
+ snd_seq_port_info_t *info;
+
+ snd_seq_port_info_alloca(&info);
+
+ if ((res = snd_seq_get_any_port_info(state->sys.hndl,
+ addr->client, addr->port, info)) < 0) {
+ spa_log_warn(state->log, "can't get port info %d.%d: %s",
+ addr->client, addr->port, snd_strerror(res));
+ } else {
+ spa_log_info(state->log, "port add/change %d:%d",
+ addr->client, addr->port);
+ state->port_info(state->port_info_data, addr, info);
+ }
+ break;
+ }
+ case SND_SEQ_EVENT_PORT_EXIT:
+ spa_log_info(state->log, "port_event: del %d:%d",
+ addr->client, addr->port);
+ state->port_info(state->port_info_data, addr, NULL);
+ break;
+ default:
+ spa_log_info(state->log, "unhandled event %d: %d:%d",
+ ev->type, addr->client, addr->port);
+ break;
+
+ }
+ snd_seq_free_event(ev);
+ }
+}
+
+int spa_alsa_seq_open(struct seq_state *state)
+{
+ int n, i, res;
+ snd_seq_port_subscribe_t *sub;
+ snd_seq_addr_t addr;
+ snd_seq_queue_timer_t *timer;
+ struct seq_conn reserve[16];
+
+ if (state->opened)
+ return 0;
+
+ init_stream(state, SPA_DIRECTION_INPUT);
+ init_stream(state, SPA_DIRECTION_OUTPUT);
+
+ spa_zero(reserve);
+ for (i = 0; i < 16; i++) {
+ spa_log_debug(state->log, "close %d", i);
+ if ((res = seq_open(state, &reserve[i], false)) < 0)
+ break;
+ }
+ if (i >= 2) {
+ state->event = reserve[--i];
+ state->sys = reserve[--i];
+ res = 0;
+ }
+ for (n = --i; n >= 0; n--) {
+ spa_log_debug(state->log, "close %d", n);
+ seq_close(state, &reserve[n]);
+ }
+ if (res < 0) {
+ spa_log_error(state->log, "open failed: %s", snd_strerror(res));
+ return res;
+ }
+
+ if ((res = seq_init(state, &state->sys, false)) < 0)
+ goto error_close;
+
+ snd_seq_set_client_name(state->sys.hndl, "PipeWire-System");
+
+ if ((res = seq_init(state, &state->event, true)) < 0)
+ goto error_close;
+
+ snd_seq_set_client_name(state->event.hndl, "PipeWire-RT-Event");
+
+ /* connect to system announce */
+ snd_seq_port_subscribe_alloca(&sub);
+ addr.client = SND_SEQ_CLIENT_SYSTEM;
+ addr.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE;
+ snd_seq_port_subscribe_set_sender(sub, &addr);
+ snd_seq_port_subscribe_set_dest(sub, &state->sys.addr);
+ if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "failed to connect announce port: %s", snd_strerror(res));
+ }
+
+ addr.client = SND_SEQ_CLIENT_SYSTEM;
+ addr.port = SND_SEQ_PORT_SYSTEM_TIMER;
+ snd_seq_port_subscribe_set_sender(sub, &addr);
+ if ((res = snd_seq_subscribe_port(state->sys.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "failed to connect timer port: %s", snd_strerror(res));
+ }
+
+ state->sys.source.func = alsa_seq_on_sys;
+ state->sys.source.data = state;
+ spa_loop_add_source(state->main_loop, &state->sys.source);
+
+ /* increase event queue timer resolution */
+ snd_seq_queue_timer_alloca(&timer);
+ if ((res = snd_seq_get_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) {
+ spa_log_warn(state->log, "failed to get queue timer: %s", snd_strerror(res));
+ }
+ snd_seq_queue_timer_set_resolution(timer, INT_MAX);
+ if ((res = snd_seq_set_queue_timer(state->event.hndl, state->event.queue_id, timer)) < 0) {
+ spa_log_warn(state->log, "failed to set queue timer: %s", snd_strerror(res));
+ }
+
+ init_ports(state);
+
+ if ((res = spa_system_timerfd_create(state->data_system,
+ CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK)) < 0)
+ goto error_close;
+
+ state->timerfd = res;
+
+ state->opened = true;
+
+ return 0;
+
+error_close:
+ seq_close(state, &state->event);
+ seq_close(state, &state->sys);
+ return res;
+}
+
+int spa_alsa_seq_close(struct seq_state *state)
+{
+ int res = 0;
+
+ if (!state->opened)
+ return 0;
+
+ spa_loop_remove_source(state->main_loop, &state->sys.source);
+
+ seq_close(state, &state->sys);
+ seq_close(state, &state->event);
+
+ uninit_stream(state, SPA_DIRECTION_INPUT);
+ uninit_stream(state, SPA_DIRECTION_OUTPUT);
+
+ spa_system_close(state->data_system, state->timerfd);
+ state->opened = false;
+
+ return res;
+}
+
+static int set_timeout(struct seq_state *state, uint64_t time)
+{
+ struct itimerspec ts;
+
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ spa_system_timerfd_settime(state->data_system,
+ state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL);
+ return 0;
+}
+
+static struct seq_port *find_port(struct seq_state *state,
+ struct seq_stream *stream, const snd_seq_addr_t *addr)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid &&
+ port->addr.client == addr->client &&
+ port->addr.port == addr->port)
+ return port;
+ }
+ return NULL;
+}
+
+int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active)
+{
+ int res;
+ snd_seq_port_subscribe_t* sub;
+
+ spa_log_debug(state->log, "activate: %d.%d: started:%d active:%d wanted:%d",
+ port->addr.client, port->addr.port, state->started, port->active, active);
+
+ if (active && !state->started)
+ return 0;
+ if (port->active == active)
+ return 0;
+
+ snd_seq_port_subscribe_alloca(&sub);
+ if (port->direction == SPA_DIRECTION_OUTPUT) {
+ snd_seq_port_subscribe_set_sender(sub, &port->addr);
+ snd_seq_port_subscribe_set_dest(sub, &state->event.addr);
+ } else {
+ snd_seq_port_subscribe_set_sender(sub, &state->event.addr);
+ snd_seq_port_subscribe_set_dest(sub, &port->addr);
+ }
+
+ if (active) {
+ snd_seq_port_subscribe_set_time_update(sub, 1);
+ snd_seq_port_subscribe_set_time_real(sub, 1);
+ snd_seq_port_subscribe_set_queue(sub, state->event.queue_id);
+ if ((res = snd_seq_subscribe_port(state->event.hndl, sub)) < 0) {
+ spa_log_error(state->log, "can't subscribe to %d:%d - %s",
+ port->addr.client, port->addr.port, snd_strerror(res));
+ active = false;
+ }
+ spa_log_info(state->log, "subscribe: %s port %d.%d",
+ port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input",
+ port->addr.client, port->addr.port);
+ } else {
+ if ((res = snd_seq_unsubscribe_port(state->event.hndl, sub)) < 0) {
+ spa_log_warn(state->log, "can't unsubscribe from %d:%d - %s",
+ port->addr.client, port->addr.port, snd_strerror(res));
+ }
+ spa_log_info(state->log, "unsubscribe: %s port %d.%d",
+ port->direction == SPA_DIRECTION_OUTPUT ? "output" : "input",
+ port->addr.client, port->addr.port);
+ }
+ port->active = active;
+ return res;
+}
+
+static struct buffer *peek_buffer(struct seq_state *state,
+ struct seq_port *port)
+{
+ if (spa_list_is_empty(&port->free))
+ return NULL;
+ return spa_list_first(&port->free, struct buffer, link);
+}
+
+int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id)
+{
+ struct buffer *b = &port->buffers[buffer_id];
+
+ if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) {
+ spa_log_trace_fp(state->log, "%p: recycle buffer port:%p buffer-id:%u",
+ state, port, buffer_id);
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ return 0;
+}
+
+static int prepare_buffer(struct seq_state *state, struct seq_port *port)
+{
+ if (port->buffer != NULL)
+ return 0;
+
+ if ((port->buffer = peek_buffer(state, port)) == NULL)
+ return -EPIPE;
+
+ spa_pod_builder_init(&port->builder,
+ port->buffer->buf->datas[0].data,
+ port->buffer->buf->datas[0].maxsize);
+ spa_pod_builder_push_sequence(&port->builder, &port->frame, 0);
+
+ return 0;
+}
+
+static int process_recycle(struct seq_state *state)
+{
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT];
+ uint32_t i;
+
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (io->status != SPA_STATUS_HAVE_DATA &&
+ io->buffer_id < port->n_buffers) {
+ spa_alsa_seq_recycle_buffer(state, port, io->buffer_id);
+ io->buffer_id = SPA_ID_INVALID;
+ }
+ }
+ return 0;
+}
+
+#define NSEC_TO_CLOCK(r,n) (((n) * (r)->denom) / ((r)->num * SPA_NSEC_PER_SEC))
+#define NSEC_FROM_CLOCK(r,n) (((n) * (r)->num * SPA_NSEC_PER_SEC) / (r)->denom)
+
+static int process_read(struct seq_state *state)
+{
+ snd_seq_event_t *ev;
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT];
+ uint32_t i;
+ long size;
+ uint8_t data[MAX_EVENT_SIZE];
+ int res;
+
+ /* copy all new midi events into their port buffers */
+ while (snd_seq_event_input(state->event.hndl, &ev) > 0) {
+ const snd_seq_addr_t *addr = &ev->source;
+ struct seq_port *port;
+ uint64_t ev_time, diff;
+ uint32_t offset;
+
+ debug_event(state, ev);
+
+ if ((port = find_port(state, stream, addr)) == NULL) {
+ spa_log_debug(state->log, "unknown port %d.%d",
+ addr->client, addr->port);
+ continue;
+ }
+ if (port->io == NULL || port->n_buffers == 0)
+ continue;
+
+ if ((res = prepare_buffer(state, port)) < 0) {
+ spa_log_debug(state->log, "can't prepare buffer port:%p %d.%d: %s",
+ port, addr->client, addr->port, spa_strerror(res));
+ continue;
+ }
+
+ snd_midi_event_reset_decode(stream->codec);
+ if ((size = snd_midi_event_decode(stream->codec, data, MAX_EVENT_SIZE, ev)) < 0) {
+ spa_log_warn(state->log, "decode failed: %s", snd_strerror(size));
+ continue;
+ }
+
+ /* queue_time is the estimated current time of the queue as calculated by
+ * the DLL. Calculate the age of the event. */
+ ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time);
+ if (state->queue_time > ev_time)
+ diff = state->queue_time - ev_time;
+ else
+ diff = 0;
+
+ /* convert the age to samples and convert to an offset */
+ offset = NSEC_TO_CLOCK(&state->rate, diff);
+ if (state->duration > offset)
+ offset = state->duration - 1 - offset;
+ else
+ offset = 0;
+
+ spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%ld port:%d.%d",
+ ev_time, offset, size, addr->client, addr->port);
+
+ spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi);
+ spa_pod_builder_bytes(&port->builder, data, size);
+
+ snd_seq_free_event(ev);
+ }
+
+ /* prepare a buffer on each port, some ports might have their
+ * buffer filled above */
+ res = 0;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (prepare_buffer(state, port) >= 0) {
+ spa_pod_builder_pop(&port->builder, &port->frame);
+
+ port->buffer->buf->datas[0].chunk->offset = 0;
+ port->buffer->buf->datas[0].chunk->size = port->builder.state.offset;
+
+ /* move buffer to ready queue */
+ spa_list_remove(&port->buffer->link);
+ SPA_FLAG_SET(port->buffer->flags, BUFFER_FLAG_OUT);
+ spa_list_append(&port->ready, &port->buffer->link);
+ port->buffer = NULL;
+ }
+
+ /* if there is already data, continue */
+ if (io->status == SPA_STATUS_HAVE_DATA) {
+ res |= SPA_STATUS_HAVE_DATA;
+ continue;
+ }
+
+ if (io->buffer_id < port->n_buffers)
+ spa_alsa_seq_recycle_buffer(state, port, io->buffer_id);
+
+ if (spa_list_is_empty(&port->ready)) {
+ /* we have no ready buffers */
+ io->buffer_id = SPA_ID_INVALID;
+ io->status = -EPIPE;
+ } else {
+ struct buffer *b = spa_list_first(&port->ready, struct buffer, link);
+ spa_list_remove(&b->link);
+
+ /* dequeue ready buffer */
+ io->buffer_id = b->id;
+ io->status = SPA_STATUS_HAVE_DATA;
+ res |= SPA_STATUS_HAVE_DATA;
+ }
+ }
+ return res;
+}
+
+static int process_write(struct seq_state *state)
+{
+ struct seq_stream *stream = &state->streams[SPA_DIRECTION_INPUT];
+ uint32_t i;
+ int err, res = 0;
+
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ struct spa_io_buffers *io = port->io;
+ struct buffer *buffer;
+ struct spa_pod_sequence *pod;
+ struct spa_data *d;
+ struct spa_pod_control *c;
+ snd_seq_event_t ev;
+ uint64_t out_time;
+ snd_seq_real_time_t out_rt;
+
+ if (!port->valid || io == NULL)
+ continue;
+
+ if (io->status != SPA_STATUS_HAVE_DATA ||
+ io->buffer_id >= port->n_buffers)
+ continue;
+
+ buffer = &port->buffers[io->buffer_id];
+ d = &buffer->buf->datas[0];
+
+ io->status = SPA_STATUS_NEED_DATA;
+ spa_node_call_reuse_buffer(&state->callbacks, i, io->buffer_id);
+ res |= SPA_STATUS_NEED_DATA;
+
+ pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
+ if (pod == NULL) {
+ spa_log_warn(state->log, "invalid sequence in buffer max:%u offset:%u size:%u",
+ d->maxsize, d->chunk->offset, d->chunk->size);
+ continue;
+ }
+
+ SPA_POD_SEQUENCE_FOREACH(pod, c) {
+ long size;
+
+ if (c->type != SPA_CONTROL_Midi)
+ continue;
+
+ snd_seq_ev_clear(&ev);
+
+ snd_midi_event_reset_encode(stream->codec);
+ if ((size = snd_midi_event_encode(stream->codec,
+ SPA_POD_BODY(&c->value),
+ SPA_POD_BODY_SIZE(&c->value), &ev)) <= 0) {
+ spa_log_warn(state->log, "failed to encode event: %s",
+ snd_strerror(size));
+ continue;
+ }
+
+ snd_seq_ev_set_source(&ev, state->event.addr.port);
+ snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port);
+
+ out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset);
+
+ out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC;
+ out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC;
+ snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt);
+
+ spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%ld port:%d.%d",
+ out_time, c->offset, size, port->addr.client, port->addr.port);
+
+ if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) {
+ spa_log_warn(state->log, "failed to output event: %s",
+ snd_strerror(err));
+ }
+ }
+ }
+ snd_seq_drain_output(state->event.hndl);
+
+ return res;
+}
+
+static void update_position(struct seq_state *state)
+{
+ if (state->position) {
+ struct spa_io_clock *clock = &state->position->clock;
+ state->rate = clock->rate;
+ if (state->rate.num == 0 || state->rate.denom == 0)
+ state->rate = SPA_FRACTION(1, 48000);
+ state->duration = clock->duration;
+ } else {
+ state->rate = SPA_FRACTION(1, 48000);
+ state->duration = 1024;
+ }
+ state->threshold = state->duration;
+}
+
+static int update_time(struct seq_state *state, uint64_t nsec, bool follower)
+{
+ snd_seq_queue_status_t *status;
+ const snd_seq_real_time_t* queue_time;
+ uint64_t queue_real;
+ double err, corr;
+ uint64_t queue_elapsed;
+
+ corr = 1.0 - (state->dll.z2 + state->dll.z3);
+
+ /* take queue time */
+ snd_seq_queue_status_alloca(&status);
+ snd_seq_get_queue_status(state->event.hndl, state->event.queue_id, status);
+ queue_time = snd_seq_queue_status_get_real_time(status);
+ queue_real = SPA_TIMESPEC_TO_NSEC(queue_time);
+
+ if (state->queue_time == 0)
+ queue_elapsed = 0;
+ else
+ queue_elapsed = (queue_real - state->queue_time) / corr;
+
+ state->queue_time = queue_real;
+
+ queue_elapsed = NSEC_TO_CLOCK(&state->rate, queue_elapsed);
+
+ err = ((int64_t)state->threshold - (int64_t) queue_elapsed);
+ err = SPA_CLAMP(err, -64, 64);
+
+ if (state->dll.bw == 0.0) {
+ spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold,
+ state->rate.denom);
+ state->next_time = nsec;
+ state->base_time = nsec;
+ }
+ corr = spa_dll_update(&state->dll, err);
+
+ if ((state->next_time - state->base_time) > BW_PERIOD) {
+ state->base_time = state->next_time;
+ spa_log_debug(state->log, "%p: follower:%d rate:%f bw:%f err:%f (%f %f %f)",
+ state, follower, corr, state->dll.bw, err,
+ state->dll.z1, state->dll.z2, state->dll.z3);
+ }
+
+ state->next_time += state->threshold / corr * 1e9 / state->rate.denom;
+
+ if (!follower && state->clock) {
+ state->clock->nsec = nsec;
+ state->clock->position += state->duration;
+ state->clock->duration = state->duration;
+ state->clock->delay = state->duration * corr;
+ state->clock->rate_diff = corr;
+ state->clock->next_nsec = state->next_time;
+ }
+
+ spa_log_trace_fp(state->log, "now:%"PRIu64" queue:%"PRIu64" err:%f corr:%f next:%"PRIu64" thr:%d",
+ nsec, queue_real, err, corr, state->next_time, state->threshold);
+
+ return 0;
+}
+
+int spa_alsa_seq_process(struct seq_state *state)
+{
+ int res;
+
+ update_position(state);
+
+ res = process_recycle(state);
+
+ if (state->following && state->position) {
+ update_time(state, state->position->clock.nsec, true);
+ res |= process_read(state);
+ }
+ res |= process_write(state);
+
+ return res;
+}
+
+static void alsa_on_timeout_event(struct spa_source *source)
+{
+ struct seq_state *state = source->data;
+ uint64_t expire;
+ int res;
+
+ if (state->started) {
+ if ((res = spa_system_timerfd_read(state->data_system, state->timerfd, &expire)) < 0) {
+ if (res != -EAGAIN)
+ spa_log_warn(state->log, "%p: error reading timerfd: %s",
+ state, spa_strerror(res));
+ return;
+ }
+ }
+
+ state->current_time = state->next_time;
+
+ spa_log_trace(state->log, "timeout %"PRIu64, state->current_time);
+
+ update_position(state);
+
+ update_time(state, state->current_time, false);
+
+ res = process_read(state);
+ if (res >= 0)
+ spa_node_call_ready(&state->callbacks, res | SPA_STATUS_NEED_DATA);
+
+ set_timeout(state, state->next_time);
+}
+
+static void reset_buffers(struct seq_state *this, struct seq_port *port)
+{
+ uint32_t i;
+
+ spa_list_init(&port->free);
+ spa_list_init(&port->ready);
+
+ for (i = 0; i < port->n_buffers; i++) {
+ struct buffer *b = &port->buffers[i];
+ if (port->direction == SPA_DIRECTION_INPUT) {
+ SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT);
+ } else {
+ spa_list_append(&port->free, &b->link);
+ SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT);
+ }
+ }
+}
+static void reset_stream(struct seq_state *this, struct seq_stream *stream, bool active)
+{
+ uint32_t i;
+ for (i = 0; i < stream->last_port; i++) {
+ struct seq_port *port = &stream->ports[i];
+ if (port->valid) {
+ reset_buffers(this, port);
+ spa_alsa_seq_activate_port(this, port, active);
+ }
+ }
+}
+
+static int set_timers(struct seq_state *state)
+{
+ struct timespec now;
+ int res;
+
+ if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0)
+ return res;
+
+ state->next_time = SPA_TIMESPEC_TO_NSEC(&now);
+ if (state->following) {
+ set_timeout(state, 0);
+ } else {
+ set_timeout(state, state->next_time);
+ }
+ return 0;
+}
+
+static inline bool is_following(struct seq_state *state)
+{
+ return state->position && state->clock && state->position->clock.id != state->clock->id;
+}
+
+int spa_alsa_seq_start(struct seq_state *state)
+{
+ int res;
+
+ if (state->started)
+ return 0;
+
+ state->following = is_following(state);
+
+ spa_log_debug(state->log, "alsa %p: start follower:%d", state, state->following);
+
+ if ((res = snd_seq_start_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) {
+ spa_log_error(state->log, "failed to start queue: %s", snd_strerror(res));
+ return res;
+ }
+ while (snd_seq_drain_output(state->event.hndl) > 0)
+ sleep(1);
+
+ update_position(state);
+
+ state->started = true;
+
+ reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], true);
+ reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], true);
+
+ state->source.func = alsa_on_timeout_event;
+ state->source.data = state;
+ state->source.fd = state->timerfd;
+ state->source.mask = SPA_IO_IN;
+ state->source.rmask = 0;
+ spa_loop_add_source(state->data_loop, &state->source);
+
+ state->queue_time = 0;
+ spa_dll_init(&state->dll);
+ set_timers(state);
+
+ return 0;
+}
+
+static int do_reassign_follower(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct seq_state *state = user_data;
+ set_timers(state);
+ return 0;
+}
+
+int spa_alsa_seq_reassign_follower(struct seq_state *state)
+{
+ bool following;
+
+ if (!state->started)
+ return 0;
+
+ following = is_following(state);
+ if (following != state->following) {
+ spa_log_debug(state->log, "alsa %p: reassign follower %d->%d", state, state->following, following);
+ state->following = following;
+ spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state);
+ }
+ return 0;
+}
+
+static int do_remove_source(struct spa_loop *loop,
+ bool async,
+ uint32_t seq,
+ const void *data,
+ size_t size,
+ void *user_data)
+{
+ struct seq_state *state = user_data;
+
+ spa_loop_remove_source(state->data_loop, &state->source);
+ set_timeout(state, 0);
+
+ return 0;
+}
+
+int spa_alsa_seq_pause(struct seq_state *state)
+{
+ int res;
+
+ if (!state->started)
+ return 0;
+
+ spa_log_debug(state->log, "alsa %p: pause", state);
+
+ spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state);
+
+ if ((res = snd_seq_stop_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) {
+ spa_log_warn(state->log, "failed to stop queue: %s", snd_strerror(res));
+ }
+ while (snd_seq_drain_output(state->event.hndl) > 0)
+ sleep(1);
+
+ state->started = false;
+
+ reset_stream(state, &state->streams[SPA_DIRECTION_INPUT], false);
+ reset_stream(state, &state->streams[SPA_DIRECTION_OUTPUT], false);
+
+ return 0;
+}
diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h
new file mode 100644
index 0000000..5d5ed51
--- /dev/null
+++ b/spa/plugins/alsa/alsa-seq.h
@@ -0,0 +1,199 @@
+/* Spa ALSA Sequencer
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_SEQ_H
+#define SPA_ALSA_SEQ_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/loop.h>
+#include <spa/utils/list.h>
+#include <spa/utils/dll.h>
+
+#include <spa/node/node.h>
+#include <spa/node/utils.h>
+#include <spa/node/io.h>
+#include <spa/param/param.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/latency-utils.h>
+
+#include "alsa.h"
+
+
+struct props {
+ char device[64];
+ char clock_name[64];
+ bool disable_longname;
+};
+
+#define MAX_EVENT_SIZE 1024
+#define MAX_PORTS 256
+#define MAX_BUFFERS 32
+
+struct buffer {
+ uint32_t id;
+#define BUFFER_FLAG_OUT (1<<0)
+ uint32_t flags;
+ struct spa_buffer *buf;
+ struct spa_meta_header *h;
+ struct spa_list link;
+};
+
+struct seq_port {
+ uint32_t id;
+ enum spa_direction direction;
+ snd_seq_addr_t addr;
+
+ uint64_t info_all;
+ struct spa_port_info info;
+#define PORT_EnumFormat 0
+#define PORT_Meta 1
+#define PORT_IO 2
+#define PORT_Format 3
+#define PORT_Buffers 4
+#define PORT_Latency 5
+#define N_PORT_PARAMS 6
+ struct spa_param_info params[N_PORT_PARAMS];
+
+ struct spa_io_buffers *io;
+
+ struct buffer buffers[MAX_BUFFERS];
+ unsigned int n_buffers;
+
+ struct spa_list free;
+ struct spa_list ready;
+
+ struct buffer *buffer;
+ struct spa_pod_builder builder;
+ struct spa_pod_frame frame;
+
+ struct spa_audio_info current_format;
+ unsigned int have_format:1;
+ unsigned int valid:1;
+ unsigned int active:1;
+
+ struct spa_latency_info latency[2];
+};
+
+struct seq_stream {
+ enum spa_direction direction;
+ unsigned int caps;
+ snd_midi_event_t *codec;
+ struct seq_port ports[MAX_PORTS];
+ uint32_t last_port;
+};
+
+struct seq_conn {
+ snd_seq_t *hndl;
+ snd_seq_addr_t addr;
+ int queue_id;
+ int fd;
+ struct spa_source source;
+};
+
+#define BW_PERIOD (3 * SPA_NSEC_PER_SEC)
+
+struct seq_state {
+ struct spa_handle handle;
+ struct spa_node node;
+
+ struct spa_log *log;
+ struct spa_system *data_system;
+ struct spa_loop *data_loop;
+ struct spa_loop *main_loop;
+
+ struct seq_conn sys;
+ struct seq_conn event;
+ int (*port_info) (void *data, const snd_seq_addr_t *addr, const snd_seq_port_info_t *info);
+ void *port_info_data;
+
+ struct spa_hook_list hooks;
+ struct spa_callbacks callbacks;
+
+ uint64_t info_all;
+ struct spa_node_info info;
+#define NODE_PropInfo 0
+#define NODE_Props 1
+#define NODE_IO 2
+#define N_NODE_PARAMS 3
+ struct spa_param_info params[N_NODE_PARAMS];
+ struct props props;
+
+ struct spa_io_clock *clock;
+ struct spa_io_position *position;
+
+ int rate_denom;
+ uint32_t duration;
+ uint32_t threshold;
+ struct spa_fraction rate;
+
+ struct spa_source source;
+ int timerfd;
+ uint64_t current_time;
+ uint64_t next_time;
+ uint64_t base_time;
+ uint64_t queue_time;
+
+ unsigned int opened:1;
+ unsigned int started:1;
+ unsigned int following:1;
+
+ struct seq_stream streams[2];
+
+ struct spa_dll dll;
+};
+
+#define VALID_DIRECTION(this,d) ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT)
+#define VALID_PORT(this,d,p) ((p) < MAX_PORTS && this->streams[d].ports[p].id == (p))
+#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && VALID_PORT(this,d,p))
+#define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && VALID_PORT(this,d,p))
+#define CHECK_PORT(this,d,p) (VALID_DIRECTION(this,d) && VALID_PORT(this,d,p))
+
+#define GET_PORT(this,d,p) (&this->streams[d].ports[p])
+
+int spa_alsa_seq_open(struct seq_state *state);
+int spa_alsa_seq_close(struct seq_state *state);
+
+int spa_alsa_seq_start(struct seq_state *state);
+int spa_alsa_seq_pause(struct seq_state *state);
+int spa_alsa_seq_reassign_follower(struct seq_state *state);
+
+int spa_alsa_seq_activate_port(struct seq_state *state, struct seq_port *port, bool active);
+int spa_alsa_seq_recycle_buffer(struct seq_state *state, struct seq_port *port, uint32_t buffer_id);
+
+int spa_alsa_seq_process(struct seq_state *state);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* SPA_ALSA_SEQ_H */
diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c
new file mode 100644
index 0000000..f89d863
--- /dev/null
+++ b/spa/plugins/alsa/alsa-udev.c
@@ -0,0 +1,1014 @@
+/* Spa ALSA udev
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/inotify.h>
+#include <fcntl.h>
+#include <dirent.h>
+
+#include <libudev.h>
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/type.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/names.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/support/loop.h>
+#include <spa/support/plugin.h>
+#include <spa/monitor/device.h>
+#include <spa/monitor/utils.h>
+
+#include "alsa.h"
+
+#define MAX_DEVICES 64
+
+#define ACTION_ADD 0
+#define ACTION_REMOVE 1
+#define ACTION_DISABLE 2
+
+struct device {
+ uint32_t id;
+ struct udev_device *dev;
+ unsigned int unavailable:1;
+ unsigned int accessible:1;
+ unsigned int ignored:1;
+ unsigned int emitted:1;
+};
+
+struct impl {
+ struct spa_handle handle;
+ struct spa_device device;
+
+ struct spa_log *log;
+ struct spa_loop *main_loop;
+ struct spa_system *main_system;
+
+ struct spa_hook_list hooks;
+
+ uint64_t info_all;
+ struct spa_device_info info;
+
+ struct udev *udev;
+ struct udev_monitor *umonitor;
+
+ struct device devices[MAX_DEVICES];
+ uint32_t n_devices;
+
+ struct spa_source source;
+ struct spa_source notify;
+ unsigned int use_acp:1;
+};
+
+static int impl_udev_open(struct impl *this)
+{
+ if (this->udev == NULL) {
+ this->udev = udev_new();
+ if (this->udev == NULL)
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static int impl_udev_close(struct impl *this)
+{
+ if (this->udev != NULL)
+ udev_unref(this->udev);
+ this->udev = NULL;
+ return 0;
+}
+
+static struct device *add_device(struct impl *this, uint32_t id, struct udev_device *dev)
+{
+ struct device *device;
+
+ if (this->n_devices >= MAX_DEVICES)
+ return NULL;
+ device = &this->devices[this->n_devices++];
+ spa_zero(*device);
+ device->id = id;
+ udev_device_ref(dev);
+ device->dev = dev;
+ return device;
+}
+
+static struct device *find_device(struct impl *this, uint32_t id)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++) {
+ if (this->devices[i].id == id)
+ return &this->devices[i];
+ }
+ return NULL;
+}
+
+static void remove_device(struct impl *this, struct device *device)
+{
+ udev_device_unref(device->dev);
+ *device = this->devices[--this->n_devices];
+}
+
+static void clear_devices(struct impl *this)
+{
+ uint32_t i;
+ for (i = 0; i < this->n_devices; i++)
+ udev_device_unref(this->devices[i].dev);
+ this->n_devices = 0;
+}
+
+static uint32_t get_card_id(struct impl *this, struct udev_device *dev)
+{
+ const char *e, *str;
+
+ if (udev_device_get_property_value(dev, "ACP_IGNORE"))
+ return SPA_ID_INVALID;
+
+ if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && spa_streq(str, "modem"))
+ return SPA_ID_INVALID;
+
+ if (udev_device_get_property_value(dev, "SOUND_INITIALIZED") == NULL)
+ return SPA_ID_INVALID;
+
+ if ((str = udev_device_get_property_value(dev, "DEVPATH")) == NULL)
+ return SPA_ID_INVALID;
+
+ if ((e = strrchr(str, '/')) == NULL)
+ return SPA_ID_INVALID;
+
+ if (strlen(e) <= 5 || strncmp(e, "/card", 5) != 0)
+ return SPA_ID_INVALID;
+
+ return atoi(e + 5);
+}
+
+static int dehex(char x)
+{
+ if (x >= '0' && x <= '9')
+ return x - '0';
+ if (x >= 'A' && x <= 'F')
+ return x - 'A' + 10;
+ if (x >= 'a' && x <= 'f')
+ return x - 'a' + 10;
+ return -1;
+}
+
+static void unescape(const char *src, char *dst)
+{
+ const char *s;
+ char *d;
+ int h1 = 0, h2 = 0;
+ enum { TEXT, BACKSLASH, EX, FIRST } state = TEXT;
+
+ for (s = src, d = dst; *s; s++) {
+ switch (state) {
+ case TEXT:
+ if (*s == '\\')
+ state = BACKSLASH;
+ else
+ *(d++) = *s;
+ break;
+
+ case BACKSLASH:
+ if (*s == 'x')
+ state = EX;
+ else {
+ *(d++) = '\\';
+ *(d++) = *s;
+ state = TEXT;
+ }
+ break;
+
+ case EX:
+ h1 = dehex(*s);
+ if (h1 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *s;
+ state = TEXT;
+ } else
+ state = FIRST;
+ break;
+
+ case FIRST:
+ h2 = dehex(*s);
+ if (h2 < 0) {
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ *(d++) = *s;
+ } else
+ *(d++) = (char) (h1 << 4) | h2;
+ state = TEXT;
+ break;
+ }
+ }
+ switch (state) {
+ case TEXT:
+ break;
+ case BACKSLASH:
+ *(d++) = '\\';
+ break;
+ case EX:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ break;
+ case FIRST:
+ *(d++) = '\\';
+ *(d++) = 'x';
+ *(d++) = *(s-1);
+ break;
+ }
+ *d = 0;
+}
+
+static int check_device_pcm_class(const char *devname)
+{
+ FILE *f;
+ char path[PATH_MAX];
+ char buf[16];
+ size_t sz;
+
+ /* Check device class */
+ spa_scnprintf(path, sizeof(path), "/sys/class/sound/%s/pcm_class",
+ devname);
+ f = fopen(path, "re");
+ if (f == NULL)
+ return -errno;
+ sz = fread(buf, 1, sizeof(buf) - 1, f);
+ buf[sz] = '\0';
+ fclose(f);
+ return spa_strstartswith(buf, "modem") ? -ENXIO : 0;
+}
+
+static int get_num_pcm_devices(unsigned int card_id)
+{
+ char prefix[32];
+ DIR *snd = NULL;
+ struct dirent *entry;
+ int num_dev = 0;
+ int res;
+
+ /* Check if card has PCM devices, without opening them */
+
+ spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", card_id);
+
+ if ((snd = opendir("/dev/snd")) == NULL)
+ return -errno;
+
+ while ((errno = 0, entry = readdir(snd)) != NULL) {
+ if (!(entry->d_type == DT_CHR &&
+ spa_strstartswith(entry->d_name, prefix)))
+ continue;
+
+ res = check_device_pcm_class(entry->d_name);
+ if (res != -ENXIO) {
+ /* count device also if sysfs status file not accessible */
+ ++num_dev;
+ }
+ }
+ if (errno != 0)
+ res = -errno;
+ else
+ res = num_dev;
+
+ closedir(snd);
+ return res;
+}
+
+static int check_device_available(struct impl *this, struct device *device, int *num_pcm)
+{
+ char path[PATH_MAX];
+ DIR *card = NULL, *pcm = NULL;
+ FILE *f;
+ char buf[16];
+ size_t sz;
+ struct dirent *entry, *entry_pcm;
+ int res;
+
+ res = get_num_pcm_devices(device->id);
+ if (res < 0) {
+ spa_log_error(this->log, "Error finding PCM devices for ALSA card %u: %s",
+ (unsigned int)device->id, spa_strerror(res));
+ return res;
+ }
+ *num_pcm = res;
+
+ spa_log_debug(this->log, "card %u has %d pcm device(s)", (unsigned int)device->id, *num_pcm);
+
+ /*
+ * Check if some pcm devices of the card are busy. Check it via /proc, as we
+ * don't want to actually open any devices using alsa-lib (generates uncontrolled
+ * number of inotify events), or replicate its subdevice logic.
+ *
+ * The /proc/asound directory might not exist if kernel is compiled with
+ * CONFIG_SND_PROCFS=n, and the pcmXX directories may be missing if compiled
+ * with CONFIG_SND_VERBOSE_PROCFS=n. In those cases, the busy check always succeeds.
+ */
+
+ res = 0;
+
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u", (unsigned int)device->id);
+
+ if ((card = opendir(path)) == NULL)
+ goto done;
+
+ while ((errno = 0, entry = readdir(card)) != NULL) {
+ if (!(entry->d_type == DT_DIR &&
+ spa_strstartswith(entry->d_name, "pcm")))
+ continue;
+
+ spa_scnprintf(path, sizeof(path), "pcmC%uD%s",
+ (unsigned int)device->id, entry->d_name+3);
+ if (check_device_pcm_class(path) < 0)
+ continue;
+
+ /* Check busy status */
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s",
+ (unsigned int)device->id, entry->d_name);
+ if ((pcm = opendir(path)) == NULL)
+ goto done;
+
+ while ((errno = 0, entry_pcm = readdir(pcm)) != NULL) {
+ if (!(entry_pcm->d_type == DT_DIR &&
+ spa_strstartswith(entry_pcm->d_name, "sub")))
+ continue;
+
+ spa_scnprintf(path, sizeof(path), "/proc/asound/card%u/%s/%s/status",
+ (unsigned int)device->id, entry->d_name, entry_pcm->d_name);
+
+ f = fopen(path, "re");
+ if (f == NULL)
+ goto done;
+ sz = fread(buf, 1, 6, f);
+ buf[sz] = '\0';
+ fclose(f);
+
+ if (!spa_strstartswith(buf, "closed")) {
+ spa_log_debug(this->log, "card %u pcm device %s busy",
+ (unsigned int)device->id, entry->d_name);
+ res = -EBUSY;
+ goto done;
+ }
+ spa_log_debug(this->log, "card %u pcm device %s free",
+ (unsigned int)device->id, entry->d_name);
+ }
+ if (errno != 0)
+ goto done;
+
+ closedir(pcm);
+ pcm = NULL;
+ }
+ if (errno != 0)
+ goto done;
+
+done:
+ if (errno != 0) {
+ spa_log_info(this->log, "card %u: failed to find busy status (%s)",
+ (unsigned int)device->id, spa_strerror(-errno));
+ }
+ if (card)
+ closedir(card);
+ if (pcm)
+ closedir(pcm);
+ return res;
+}
+
+static int emit_object_info(struct impl *this, struct device *device)
+{
+ struct spa_device_object_info info;
+ uint32_t id = device->id;
+ struct udev_device *dev = device->dev;
+ const char *str;
+ char path[32], *cn = NULL, *cln = NULL;
+ struct spa_dict_item items[25];
+ uint32_t n_items = 0;
+ int res, pcm;
+
+ /*
+ * inotify close events under /dev/snd must not be emitted, except after setting
+ * device->emitted to true. alsalib functions can be used after that.
+ */
+
+ snprintf(path, sizeof(path), "hw:%u", id);
+
+ if ((res = check_device_available(this, device, &pcm)) < 0)
+ return res;
+ if (pcm == 0) {
+ spa_log_debug(this->log, "no pcm devices for %s", path);
+ device->ignored = true;
+ return -ENODEV;
+ }
+
+ spa_log_debug(this->log, "emitting card %s", path);
+ device->emitted = true;
+
+ info = SPA_DEVICE_OBJECT_INFO_INIT();
+
+ info.type = SPA_TYPE_INTERFACE_Device;
+ info.factory_name = this->use_acp ?
+ SPA_NAME_API_ALSA_ACP_DEVICE :
+ SPA_NAME_API_ALSA_PCM_DEVICE;
+ info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS;
+ info.flags = 0;
+
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API, "udev");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device");
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, path);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, path+3);
+ if (snd_card_get_name(id, &cn) >= 0)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_NAME, cn);
+ if (snd_card_get_longname(id, &cln) >= 0)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD_LONGNAME, cln);
+
+ if ((str = udev_device_get_property_value(dev, "ACP_NAME")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_NAME, str);
+
+ if ((str = udev_device_get_property_value(dev, "ACP_PROFILE_SET")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PROFILE_SET, str);
+
+ if ((str = udev_device_get_property_value(dev, "SOUND_CLASS")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CLASS, str);
+
+ if ((str = udev_device_get_property_value(dev, "USEC_INITIALIZED")) && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PLUGGED_USEC, str);
+
+ str = udev_device_get_property_value(dev, "ID_PATH");
+ if (!(str && *str))
+ str = udev_device_get_syspath(dev);
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_PATH, str);
+ }
+ if ((str = udev_device_get_devpath(dev)) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SYSFS_PATH, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_ID")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS_ID, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_BUS")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "SUBSYSTEM")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SUBSYSTEM, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_VENDOR_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffffffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, dec);
+ }
+ }
+ str = udev_device_get_property_value(dev, "ID_VENDOR_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_VENDOR");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_NAME, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "ID_MODEL_ID")) && *str) {
+ int32_t val;
+ if (spa_atoi32(str, &val, 16)) {
+ char *dec = alloca(12); /* 0xffffffff is max */
+ snprintf(dec, 12, "0x%04x", val);
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, dec);
+ }
+ }
+ str = udev_device_get_property_value(dev, "ID_MODEL_FROM_DATABASE");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL_ENC");
+ if (!(str && *str)) {
+ str = udev_device_get_property_value(dev, "ID_MODEL");
+ } else {
+ char *t = alloca(strlen(str) + 1);
+ unescape(str, t);
+ str = t;
+ }
+ }
+ if (str && *str)
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_NAME, str);
+
+ if ((str = udev_device_get_property_value(dev, "ID_SERIAL")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_SERIAL, str);
+ }
+ if ((str = udev_device_get_property_value(dev, "SOUND_FORM_FACTOR")) && *str) {
+ items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, str);
+ }
+ info.props = &SPA_DICT_INIT(items, n_items);
+
+ spa_device_emit_object_info(&this->hooks, id, &info);
+ free(cn);
+ free(cln);
+
+ return 1;
+}
+
+static bool check_access(struct impl *this, struct device *device)
+{
+ char path[128], prefix[32];
+ DIR *snd = NULL;
+ struct dirent *entry;
+ bool accessible = false;
+
+ snprintf(path, sizeof(path), "/dev/snd/controlC%u", device->id);
+ if (access(path, R_OK|W_OK) >= 0 && (snd = opendir("/dev/snd"))) {
+ /*
+ * It's possible that controlCX is accessible before pcmCX* or
+ * the other way around. Return true only if all devices are
+ * accessible.
+ */
+
+ accessible = true;
+ spa_scnprintf(prefix, sizeof(prefix), "pcmC%uD", device->id);
+ while ((entry = readdir(snd)) != NULL) {
+ if (!(entry->d_type == DT_CHR &&
+ spa_strstartswith(entry->d_name, prefix)))
+ continue;
+
+ snprintf(path, sizeof(path), "/dev/snd/%.32s", entry->d_name);
+ if (access(path, R_OK|W_OK) < 0) {
+ accessible = false;
+ break;
+ }
+ }
+ closedir(snd);
+ }
+
+ if (accessible != device->accessible)
+ spa_log_debug(this->log, "%s accessible:%u", path, accessible);
+ device->accessible = accessible;
+
+ return device->accessible;
+}
+
+static void process_device(struct impl *this, uint32_t action, struct udev_device *dev)
+{
+ uint32_t id;
+ struct device *device;
+ bool emitted;
+ int res;
+
+ if ((id = get_card_id(this, dev)) == SPA_ID_INVALID)
+ return;
+
+ device = find_device(this, id);
+ if (device && device->ignored)
+ return;
+
+ switch (action) {
+ case ACTION_ADD:
+ if (device == NULL)
+ device = add_device(this, id, dev);
+ if (device == NULL)
+ return;
+ if (!check_access(this, device))
+ return;
+ res = emit_object_info(this, device);
+ if (res < 0) {
+ if (device->ignored)
+ spa_log_info(this->log, "ALSA card %u unavailable (%s): it is ignored",
+ device->id, spa_strerror(res));
+ else if (!device->unavailable)
+ spa_log_info(this->log, "ALSA card %u unavailable (%s): wait for it",
+ device->id, spa_strerror(res));
+ else
+ spa_log_debug(this->log, "ALSA card %u still unavailable (%s)",
+ device->id, spa_strerror(res));
+ device->unavailable = true;
+ } else {
+ if (device->unavailable)
+ spa_log_info(this->log, "ALSA card %u now available",
+ device->id);
+ device->unavailable = false;
+ }
+ break;
+
+ case ACTION_REMOVE:
+ if (device == NULL)
+ return;
+ emitted = device->emitted;
+ remove_device(this, device);
+ if (emitted)
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ break;
+
+ case ACTION_DISABLE:
+ if (device == NULL)
+ return;
+ if (device->emitted) {
+ device->emitted = false;
+ spa_device_emit_object_info(&this->hooks, id, NULL);
+ }
+ break;
+ }
+}
+
+static int stop_inotify(struct impl *this)
+{
+ if (this->notify.fd == -1)
+ return 0;
+ spa_log_info(this->log, "stop inotify");
+ spa_loop_remove_source(this->main_loop, &this->notify);
+ close(this->notify.fd);
+ this->notify.fd = -1;
+ return 0;
+}
+
+static void impl_on_notify_events(struct spa_source *source)
+{
+ bool deleted = false;
+ struct impl *this = source->data;
+ union {
+ struct inotify_event e;
+ char name[NAME_MAX+1+sizeof(struct inotify_event)];
+ } buf;
+
+ while (true) {
+ ssize_t len;
+ const struct inotify_event *event;
+ void *p, *e;
+
+ len = read(source->fd, &buf, sizeof(buf));
+ if (len < 0 && errno != EAGAIN)
+ break;
+ if (len <= 0)
+ break;
+
+ e = SPA_PTROFF(&buf, len, void);
+
+ for (p = &buf; p < e;
+ p = SPA_PTROFF(p, sizeof(struct inotify_event) + event->len, void)) {
+ unsigned int id;
+ struct device *device;
+
+ event = (const struct inotify_event *) p;
+ spa_assert_se(SPA_PTRDIFF(e, p) >= (ptrdiff_t)sizeof(struct inotify_event) &&
+ SPA_PTRDIFF(e, p) - sizeof(struct inotify_event) >= event->len &&
+ "bad event from kernel");
+
+ /* Device becomes accessible or not busy */
+ if ((event->mask & (IN_ATTRIB | IN_CLOSE_WRITE))) {
+ bool access;
+ if (sscanf(event->name, "controlC%u", &id) != 1 &&
+ sscanf(event->name, "pcmC%uD", &id) != 1)
+ continue;
+ if ((device = find_device(this, id)) == NULL)
+ continue;
+
+ access = check_access(this, device);
+ if (access && !device->emitted)
+ process_device(this, ACTION_ADD, device->dev);
+ else if (!access && device->emitted)
+ process_device(this, ACTION_DISABLE, device->dev);
+ }
+ /* /dev/snd/ might have been removed */
+ if ((event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)))
+ deleted = true;
+ }
+ }
+ if (deleted)
+ stop_inotify(this);
+}
+
+static int start_inotify(struct impl *this)
+{
+ int res, notify_fd;
+
+ if (this->notify.fd != -1)
+ return 0;
+
+ if ((notify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) < 0)
+ return -errno;
+
+ res = inotify_add_watch(notify_fd, "/dev/snd",
+ IN_ATTRIB | IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF);
+ if (res < 0) {
+ res = -errno;
+ close(notify_fd);
+
+ if (res == -ENOENT) {
+ spa_log_debug(this->log, "/dev/snd/ does not exist yet");
+ return 0;
+ }
+ spa_log_error(this->log, "inotify_add_watch() failed: %s", spa_strerror(res));
+ return res;
+ }
+ spa_log_info(this->log, "start inotify");
+ this->notify.func = impl_on_notify_events;
+ this->notify.data = this;
+ this->notify.fd = notify_fd;
+ this->notify.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_loop_add_source(this->main_loop, &this->notify);
+
+ return 0;
+}
+
+static void impl_on_fd_events(struct spa_source *source)
+{
+ struct impl *this = source->data;
+ struct udev_device *dev;
+ const char *action;
+
+ dev = udev_monitor_receive_device(this->umonitor);
+ if (dev == NULL)
+ return;
+
+ if ((action = udev_device_get_action(dev)) == NULL)
+ action = "change";
+
+ spa_log_debug(this->log, "action %s", action);
+
+ start_inotify(this);
+
+ if (spa_streq(action, "change")) {
+ process_device(this, ACTION_ADD, dev);
+ } else if (spa_streq(action, "remove")) {
+ process_device(this, ACTION_REMOVE, dev);
+ }
+ udev_device_unref(dev);
+}
+
+static int start_monitor(struct impl *this)
+{
+ int res;
+
+ if (this->umonitor != NULL)
+ return 0;
+
+ this->umonitor = udev_monitor_new_from_netlink(this->udev, "udev");
+ if (this->umonitor == NULL)
+ return -ENOMEM;
+
+ udev_monitor_filter_add_match_subsystem_devtype(this->umonitor,
+ "sound", NULL);
+ udev_monitor_enable_receiving(this->umonitor);
+
+ this->source.func = impl_on_fd_events;
+ this->source.data = this;
+ this->source.fd = udev_monitor_get_fd(this->umonitor);
+ this->source.mask = SPA_IO_IN | SPA_IO_ERR;
+
+ spa_log_debug(this->log, "monitor %p", this->umonitor);
+ spa_loop_add_source(this->main_loop, &this->source);
+
+ if ((res = start_inotify(this)) < 0)
+ return res;
+
+ return 0;
+}
+
+static int stop_monitor(struct impl *this)
+{
+ if (this->umonitor == NULL)
+ return 0;
+
+ clear_devices (this);
+
+ spa_loop_remove_source(this->main_loop, &this->source);
+ udev_monitor_unref(this->umonitor);
+ this->umonitor = NULL;
+
+ stop_inotify(this);
+
+ return 0;
+}
+
+static int enum_devices(struct impl *this)
+{
+ struct udev_enumerate *enumerate;
+ struct udev_list_entry *devices;
+
+ enumerate = udev_enumerate_new(this->udev);
+ if (enumerate == NULL)
+ return -ENOMEM;
+
+ udev_enumerate_add_match_subsystem(enumerate, "sound");
+ udev_enumerate_scan_devices(enumerate);
+
+ for (devices = udev_enumerate_get_list_entry(enumerate); devices;
+ devices = udev_list_entry_get_next(devices)) {
+ struct udev_device *dev;
+
+ dev = udev_device_new_from_syspath(this->udev, udev_list_entry_get_name(devices));
+ if (dev == NULL)
+ continue;
+
+ process_device(this, ACTION_ADD, dev);
+
+ udev_device_unref(dev);
+ }
+ udev_enumerate_unref(enumerate);
+
+ return 0;
+}
+
+static const struct spa_dict_item device_info_items[] = {
+ { SPA_KEY_DEVICE_API, "udev" },
+ { SPA_KEY_DEVICE_NICK, "alsa-udev" },
+ { SPA_KEY_API_UDEV_MATCH, "sound" },
+};
+
+static void emit_device_info(struct impl *this, bool full)
+{
+ uint64_t old = full ? this->info.change_mask : 0;
+ if (full)
+ this->info.change_mask = this->info_all;
+ if (this->info.change_mask) {
+ this->info.props = &SPA_DICT_INIT_ARRAY(device_info_items);
+ spa_device_emit_info(&this->hooks, &this->info);
+ this->info.change_mask = old;
+ }
+}
+
+static void impl_hook_removed(struct spa_hook *hook)
+{
+ struct impl *this = hook->priv;
+ if (spa_hook_list_is_empty(&this->hooks)) {
+ stop_monitor(this);
+ impl_udev_close(this);
+ }
+}
+
+static int
+impl_device_add_listener(void *object, struct spa_hook *listener,
+ const struct spa_device_events *events, void *data)
+{
+ int res;
+ struct impl *this = object;
+ struct spa_hook_list save;
+
+ spa_return_val_if_fail(this != NULL, -EINVAL);
+ spa_return_val_if_fail(events != NULL, -EINVAL);
+
+ if ((res = impl_udev_open(this)) < 0)
+ return res;
+
+ spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
+
+ emit_device_info(this, true);
+
+ if ((res = start_monitor(this)) < 0)
+ return res;
+
+ if ((res = enum_devices(this)) < 0)
+ return res;
+
+ spa_hook_list_join(&this->hooks, &save);
+
+ listener->removed = impl_hook_removed;
+ listener->priv = this;
+
+ return 0;
+}
+
+static const struct spa_device_methods impl_device = {
+ SPA_VERSION_DEVICE_METHODS,
+ .add_listener = impl_device_add_listener,
+};
+
+static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
+{
+ struct impl *this;
+
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+ spa_return_val_if_fail(interface != NULL, -EINVAL);
+
+ this = (struct impl *) handle;
+
+ if (spa_streq(type, SPA_TYPE_INTERFACE_Device))
+ *interface = &this->device;
+ else
+ return -ENOENT;
+
+ return 0;
+}
+
+static int impl_clear(struct spa_handle *handle)
+{
+ struct impl *this = (struct impl *) handle;
+ stop_monitor(this);
+ impl_udev_close(this);
+ return 0;
+}
+
+static size_t
+impl_get_size(const struct spa_handle_factory *factory,
+ const struct spa_dict *params)
+{
+ return sizeof(struct impl);
+}
+
+static int
+impl_init(const struct spa_handle_factory *factory,
+ struct spa_handle *handle,
+ const struct spa_dict *info,
+ const struct spa_support *support,
+ uint32_t n_support)
+{
+ struct impl *this;
+ const char *str;
+
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(handle != NULL, -EINVAL);
+
+ handle->get_interface = impl_get_interface;
+ handle->clear = impl_clear;
+
+ this = (struct impl *) handle;
+ this->notify.fd = -1;
+
+ this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
+ alsa_log_topic_init(this->log);
+ this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
+ this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
+
+ if (this->main_loop == NULL) {
+ spa_log_error(this->log, "a main-loop is needed");
+ return -EINVAL;
+ }
+ if (this->main_system == NULL) {
+ spa_log_error(this->log, "a main-system is needed");
+ return -EINVAL;
+ }
+ spa_hook_list_init(&this->hooks);
+
+ this->device.iface = SPA_INTERFACE_INIT(
+ SPA_TYPE_INTERFACE_Device,
+ SPA_VERSION_DEVICE,
+ &impl_device, this);
+
+ this->info = SPA_DEVICE_INFO_INIT();
+ this->info_all = SPA_DEVICE_CHANGE_MASK_FLAGS |
+ SPA_DEVICE_CHANGE_MASK_PROPS;
+ this->info.flags = 0;
+
+ if (info) {
+ if ((str = spa_dict_lookup(info, "alsa.use-acp")) != NULL)
+ this->use_acp = spa_atob(str);
+ }
+
+ return 0;
+}
+
+static const struct spa_interface_info impl_interfaces[] = {
+ {SPA_TYPE_INTERFACE_Device,},
+};
+
+static int
+impl_enum_interface_info(const struct spa_handle_factory *factory,
+ const struct spa_interface_info **info,
+ uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(info != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ if (*index >= SPA_N_ELEMENTS(impl_interfaces))
+ return 0;
+
+ *info = &impl_interfaces[(*index)++];
+ return 1;
+}
+
+const struct spa_handle_factory spa_alsa_udev_factory = {
+ SPA_VERSION_HANDLE_FACTORY,
+ SPA_NAME_API_ALSA_ENUM_UDEV,
+ NULL,
+ impl_get_size,
+ impl_init,
+ impl_enum_interface_info,
+};
diff --git a/spa/plugins/alsa/alsa.c b/spa/plugins/alsa/alsa.c
new file mode 100644
index 0000000..ab4aa3a
--- /dev/null
+++ b/spa/plugins/alsa/alsa.c
@@ -0,0 +1,80 @@
+/* Spa ALSA support
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+
+#include <spa/support/plugin.h>
+#include <spa/support/log.h>
+
+extern const struct spa_handle_factory spa_alsa_source_factory;
+extern const struct spa_handle_factory spa_alsa_sink_factory;
+extern const struct spa_handle_factory spa_alsa_udev_factory;
+extern const struct spa_handle_factory spa_alsa_device_factory;
+extern const struct spa_handle_factory spa_alsa_seq_bridge_factory;
+extern const struct spa_handle_factory spa_alsa_acp_device_factory;
+#ifdef HAVE_ALSA_COMPRESS_OFFLOAD
+extern const struct spa_handle_factory spa_alsa_compress_offload_sink_factory;
+#endif
+
+struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.alsa");
+struct spa_log_topic *alsa_log_topic = &log_topic;
+
+SPA_EXPORT
+int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
+{
+ spa_return_val_if_fail(factory != NULL, -EINVAL);
+ spa_return_val_if_fail(index != NULL, -EINVAL);
+
+ switch (*index) {
+ case 0:
+ *factory = &spa_alsa_source_factory;
+ break;
+ case 1:
+ *factory = &spa_alsa_sink_factory;
+ break;
+ case 2:
+ *factory = &spa_alsa_udev_factory;
+ break;
+ case 3:
+ *factory = &spa_alsa_device_factory;
+ break;
+ case 4:
+ *factory = &spa_alsa_seq_bridge_factory;
+ break;
+ case 5:
+ *factory = &spa_alsa_acp_device_factory;
+ break;
+#ifdef HAVE_ALSA_COMPRESS_OFFLOAD
+ case 6:
+ *factory = &spa_alsa_compress_offload_sink_factory;
+ break;
+#endif
+ default:
+ return 0;
+ }
+ (*index)++;
+ return 1;
+}
diff --git a/spa/plugins/alsa/alsa.h b/spa/plugins/alsa/alsa.h
new file mode 100644
index 0000000..ee18929
--- /dev/null
+++ b/spa/plugins/alsa/alsa.h
@@ -0,0 +1,39 @@
+/* Spa ALSA Source
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef SPA_ALSA_H
+#define SPA_ALSA_H
+
+#include <spa/support/log.h>
+
+#undef SPA_LOG_TOPIC_DEFAULT
+#define SPA_LOG_TOPIC_DEFAULT alsa_log_topic
+extern struct spa_log_topic *alsa_log_topic;
+
+static inline void alsa_log_topic_init(struct spa_log *log)
+{
+ spa_log_topic_init(log, alsa_log_topic);
+}
+
+#endif /* SPA_ALSA_H */
diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build
new file mode 100644
index 0000000..7dedc87
--- /dev/null
+++ b/spa/plugins/alsa/meson.build
@@ -0,0 +1,60 @@
+subdir('acp')
+subdir('mixer')
+
+spa_alsa_dependencies = [ spa_dep, alsa_dep, libudev_dep, mathlib, epoll_shim_dep, libinotify_dep ]
+
+spa_alsa_sources = ['alsa.c',
+ 'alsa.h',
+ 'alsa-udev.c',
+ 'alsa-acp-device.c',
+ 'alsa-pcm-device.c',
+ 'alsa-pcm-sink.c',
+ 'alsa-pcm-source.c',
+ 'alsa-pcm.c',
+ 'alsa-seq-bridge.c',
+ 'alsa-seq.c']
+
+if tinycompress_dep.found()
+ spa_alsa_sources += [ 'alsa-compress-offload-sink.c' ]
+ spa_alsa_dependencies += tinycompress_dep
+endif
+
+spa_alsa = shared_library(
+ 'spa-alsa',
+ [ spa_alsa_sources ],
+ c_args : acp_c_args,
+ include_directories : [configinc],
+ dependencies : spa_alsa_dependencies,
+ link_with : [ acp_lib ],
+ install : true,
+ install_dir : spa_plugindir / 'alsa'
+)
+
+alsa_udevrules = [
+ '90-pipewire-alsa.rules',
+]
+
+executable('spa-acp-tool',
+ [ 'acp-tool.c' ],
+ c_args : acp_c_args,
+ dependencies : [ spa_dep, alsa_dep, mathlib, acp_dep ],
+ install : true,
+)
+
+executable('test-timer',
+ [ 'test-timer.c' ],
+ dependencies : [ spa_dep, alsa_dep, mathlib, epoll_shim_dep ],
+ install : false,
+)
+
+executable('test-hw-params',
+ [ 'test-hw-params.c' ],
+ dependencies : [ spa_dep, alsa_dep, mathlib ],
+ install : false,
+)
+
+if libudev_dep.found()
+ install_data(alsa_udevrules,
+ install_dir : udevrulesdir,
+ )
+endif
diff --git a/spa/plugins/alsa/mixer/meson.build b/spa/plugins/alsa/mixer/meson.build
new file mode 100644
index 0000000..d4327b8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/meson.build
@@ -0,0 +1,7 @@
+install_subdir('paths',
+ install_dir : alsadatadir
+)
+
+install_subdir('profile-sets',
+ install_dir : alsadatadir
+)
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-aux.conf b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf
new file mode 100644
index 0000000..47e22c5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-aux.conf
@@ -0,0 +1,65 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where an 'Aux' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 80
+description-key = analog-input
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf
new file mode 100644
index 0000000..96861e7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-dock-mic.conf
@@ -0,0 +1,104 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Dock Mic' or 'Dock Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 78
+description-key = analog-input-microphone-dock
+
+[Jack Dock Mic]
+required-any = any
+
+[Jack Dock Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Dock Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Dock Mic Boost:on]
+name = input-boost-on
+
+[Option Dock Mic Boost:off]
+name = input-boost-off
+
+[Element Dock Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Dock Mic]
+name = analog-input-microphone-dock
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Dock Mic]
+name = analog-input-microphone-dock
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-fm.conf b/spa/plugins/alsa/mixer/paths/analog-input-fm.conf
new file mode 100644
index 0000000..d3501a8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-fm.conf
@@ -0,0 +1,65 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where an 'FM' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+description-key = analog-input-radio
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf
new file mode 100644
index 0000000..6e7775c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-front-mic.conf
@@ -0,0 +1,104 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Front Mic' or 'Front Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 85
+description-key = analog-input-microphone-front
+
+[Jack Front Mic]
+required-any = any
+
+[Jack Front Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Front Mic Boost:on]
+name = input-boost-on
+
+[Option Front Mic Boost:off]
+name = input-boost-off
+
+[Element Front Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Front Mic]
+name = analog-input-microphone-front
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Front Mic]
+name = analog-input-microphone-front
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf
new file mode 100644
index 0000000..eb5740a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-headphone-mic.conf
@@ -0,0 +1,102 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For some ASUS netbooks that have one jack that can be either a Headphone
+; *or* a mic. This path will be active only when it is used as a mic.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 87
+description-key = analog-input-microphone
+
+[Jack Headphone Mic]
+required-any = any
+state.plugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Headphone Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Headphone Mic]
+name = analog-input-microphone
+required-any = any
+
+; Make sure the internal speakers are not auto-muted when you plug a mic in
+[Element Auto-Mute Mode]
+enumeration = select
+
+[Option Auto-Mute Mode:Disabled]
+name = analog-input-microphone
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf
new file mode 100644
index 0000000..579db6b
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-headset-mic.conf
@@ -0,0 +1,114 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Headset Mic' or 'Headset Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 88
+description-key = analog-input-microphone-headset
+
+[Jack Headset Mic]
+required-any = any
+
+[Jack Headset Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Headphone]
+state.plugged = unknown
+
+[Jack Front Headphone]
+state.plugged = unknown
+
+[Jack Headphone Mic]
+state.plugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headset]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Headset Mic]
+name = Headset Microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Headset Mic]
+name = Headset Microphone
+required-any = any
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf
new file mode 100644
index 0000000..9e22008
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic-always.conf
@@ -0,0 +1,133 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Internal Mic' or 'Internal Mic Boost' element exists
+; 'Int Mic' and 'Int Mic Boost' are for compatibility with kernels < 2.6.38
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 89
+description-key = analog-input-microphone-internal
+
+[Jack Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Internal Mic Boost]
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Internal Mic Boost:on]
+name = input-boost-on
+
+[Option Internal Mic Boost:off]
+name = input-boost-off
+
+[Element Int Mic Boost]
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Int Mic Boost:on]
+name = input-boost-on
+
+[Option Int Mic Boost:off]
+name = input-boost-off
+
+[Element Internal Mic]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Int Mic]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Internal Mic]
+name = analog-input-microphone-internal
+
+[Option Input Source:Int Mic]
+name = analog-input-microphone-internal
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Internal Mic]
+name = analog-input-microphone-internal
+
+[Option Capture Source:Int Mic]
+name = analog-input-microphone-internal
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf
new file mode 100644
index 0000000..898410a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-internal-mic.conf
@@ -0,0 +1,154 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Internal Mic' or 'Internal Mic Boost' element exists
+; 'Int Mic' and 'Int Mic Boost' are for compatibility with kernels < 2.6.38
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 89
+description-key = analog-input-microphone-internal
+
+[Jack Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Mic]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Internal Mic Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Internal Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Internal Mic Boost:on]
+name = input-boost-on
+
+[Option Internal Mic Boost:off]
+name = input-boost-off
+
+[Element Int Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Int Mic Boost:on]
+name = input-boost-on
+
+[Option Int Mic Boost:off]
+name = input-boost-off
+
+[Element Internal Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Int Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Internal Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Option Input Source:Int Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Internal Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Option Capture Source:Int Mic]
+name = analog-input-microphone-internal
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Headphone Mic]
+switch = off
+volume = off
+
+[Element Headphone Mic Boost]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-linein.conf b/spa/plugins/alsa/mixer/paths/analog-input-linein.conf
new file mode 100644
index 0000000..cf20790
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-linein.conf
@@ -0,0 +1,144 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Line' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 81
+
+[Jack Line]
+required-any = any
+
+[Jack Line Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Line - Input]
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line Boost]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:Line]
+name = analog-input-linein
+required-any = any
+
+[Option PCM Capture Source:Line In]
+name = analog-input-linein
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Line In]
+priority = 19
+name = input-linein
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf b/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf
new file mode 100644
index 0000000..7147d20
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic-line.conf
@@ -0,0 +1,66 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Mic/Line' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 85
+description-key = analog-input
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf
new file mode 100644
index 0000000..53c03c8
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf
@@ -0,0 +1,141 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Mic' or 'Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 87
+description-key = analog-input-microphone
+
+[Jack Mic]
+required-any = any
+
+[Jack Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Mic - Input]
+required-any = any
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Mic Boost:on]
+name = input-boost-on
+
+[Option Mic Boost:off]
+name = input-boost-off
+
+[Element Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:Mic]
+name = analog-input-microphone
+required-any = any
+
+[Option PCM Capture Source:Mic-In/Mic Array]
+name = analog-input-microphone
+required-any = any
+
+;;; Some AC'97s have "Mic Select" and "Mic Boost (+20dB)"
+
+[Element Mic Select]
+enumeration = select
+
+[Option Mic Select:Mic1]
+name = input-microphone
+priority = 20
+
+[Option Mic Select:Mic2]
+name = input-microphone
+priority = 19
+
+[Element Mic Boost (+20dB)]
+switch = select
+volume = merge
+
+[Option Mic Boost (+20dB):on]
+name = input-boost-on
+
+[Option Mic Boost (+20dB):off]
+name = input-boost-off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Rear Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+[Element Rear Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common
new file mode 100644
index 0000000..e5ced21
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-mic.conf.common
@@ -0,0 +1,60 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Common element for all microphone inputs
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[Properties]
+device.icon_name = audio-input-microphone
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Line Boost]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+[Element Inverted Internal Mic]
+switch = off
+volume = off
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Mic In]
+priority = 19
+name = input-microphone
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf b/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf
new file mode 100644
index 0000000..a92f9d1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-rear-mic.conf
@@ -0,0 +1,107 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Rear Mic' or 'Rear Mic Boost' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 82
+description-key = analog-input-microphone-rear
+
+[Jack Rear Mic]
+required-any = any
+
+[Jack Rear Mic - Input]
+required-any = any
+
+[Jack Rear Mic Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Rear Mic Boost]
+required-any = any
+switch = select
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Option Rear Mic Boost:on]
+name = input-boost-on
+
+[Option Rear Mic Boost:off]
+name = input-boost-off
+
+[Element Rear Mic]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Rear Mic]
+name = analog-input-microphone-rear
+required-any = any
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:Rear Mic]
+name = analog-input-microphone-rear
+required-any = any
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Front Mic]
+switch = off
+volume = off
+
+[Element Dock Mic]
+switch = off
+volume = off
+
+[Element Mic Boost]
+switch = off
+volume = off
+
+[Element Dock Mic Boost]
+switch = off
+volume = off
+
+[Element Internal Mic Boost]
+switch = off
+volume = off
+
+[Element Front Mic Boost]
+switch = off
+volume = off
+
+.include analog-input-mic.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf b/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf
new file mode 100644
index 0000000..99d1d79
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-tvtuner.conf
@@ -0,0 +1,65 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'TV Tuner' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+description-key = analog-input-video
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+switch = off
+volume = off
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input-video.conf b/spa/plugins/alsa/mixer/paths/analog-input-video.conf
new file mode 100644
index 0000000..50c999e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input-video.conf
@@ -0,0 +1,64 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; For devices where a 'Video' element exists
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 70
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+switch = off
+volume = off
+
+[Element Internal Mic]
+switch = off
+volume = off
+
+[Element Line]
+switch = off
+volume = off
+
+[Element Aux]
+switch = off
+volume = off
+
+[Element Video]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic/Line]
+switch = off
+volume = off
+
+[Element TV Tuner]
+switch = off
+volume = off
+
+[Element FM]
+switch = off
+volume = off
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input.conf b/spa/plugins/alsa/mixer/paths/analog-input.conf
new file mode 100644
index 0000000..c9db677
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input.conf
@@ -0,0 +1,102 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; A fallback for devices that lack separate Mic/Line/Aux/Video/TV
+; Tuner/FM elements
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+
+[Element Capture]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Mic]
+required-absent = any
+
+[Element Mic Boost]
+required-absent = any
+
+[Element Dock Mic]
+required-absent = any
+
+[Element Dock Mic Boost]
+required-absent = any
+
+[Element Front Mic]
+required-absent = any
+
+[Element Front Mic Boost]
+required-absent = any
+
+[Element Int Mic]
+required-absent = any
+
+[Element Int Mic Boost]
+required-absent = any
+
+[Element Internal Mic]
+required-absent = any
+
+[Element Internal Mic Boost]
+required-absent = any
+
+[Element Rear Mic]
+required-absent = any
+
+[Element Rear Mic Boost]
+required-absent = any
+
+[Element Headset]
+required-absent = any
+
+[Element Headset Mic]
+required-absent = any
+
+[Element Headset Mic Boost]
+required-absent = any
+
+[Element Headphone Mic]
+required-absent = any
+
+[Element Headphone Mic Boost]
+required-absent = any
+
+[Element Line]
+required-absent = any
+
+[Element Line Boost]
+required-absent = any
+
+[Element Aux]
+required-absent = any
+
+[Element Video]
+required-absent = any
+
+[Element Mic/Line]
+required-absent = any
+
+[Element TV Tuner]
+required-absent = any
+
+[Element FM]
+required-absent = any
+
+.include analog-input.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-input.conf.common b/spa/plugins/alsa/mixer/paths/analog-input.conf.common
new file mode 100644
index 0000000..201087e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-input.conf.common
@@ -0,0 +1,289 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Mixer path for PulseAudio's ALSA backend, common elements for all
+; input paths. If multiple options by the same id are discovered they
+; will be suffixed with a number to distinguish them, in the same
+; order they appear here.
+;
+; Source selection should use the following names:
+;
+; input -- If we don't know the exact kind of input
+; input-microphone
+; input-microphone-internal
+; input-microphone-external
+; input-linein
+; input-video
+; input-radio
+; input-docking-microphone
+; input-docking-linein
+; input-docking
+;
+; We explicitly don't want to wrap the following sources:
+;
+; CD
+; Synth/MIDI
+; Phone
+; Mix
+; Digital/SPDIF
+; Master
+; PC Speaker
+;
+; See analog-output.conf.common for an explanation on the directives
+
+;;; 'Input Source Select'
+
+[Element Input Source Select]
+enumeration = select
+
+[Option Input Source Select:Input1]
+name = input
+priority = 10
+
+[Option Input Source Select:Input2]
+name = input
+priority = 5
+
+;;; 'Input Source'
+
+[Element Input Source]
+enumeration = select
+
+[Option Input Source:Digital Mic]
+name = input-microphone
+priority = 20
+
+[Option Input Source:Microphone]
+name = input-microphone
+priority = 20
+
+[Option Input Source:Front Microphone]
+name = input-microphone
+priority = 19
+
+[Option Input Source:Internal Mic 1]
+name = input-microphone
+priority = 19
+
+[Option Input Source:Line-In]
+name = input-linein
+priority = 18
+
+[Option Input Source:Line In]
+name = input-linein
+priority = 18
+
+[Option Input Source:Docking-Station]
+name = input-docking
+priority = 17
+
+[Option Input Source:AUX IN]
+name = input
+priority = 10
+
+;;; 'Capture Source'
+
+[Element Capture Source]
+enumeration = select
+
+[Option Capture Source:TV Tuner]
+name = input-video
+
+[Option Capture Source:FM]
+name = input-radio
+
+[Option Capture Source:Mic/Line]
+name = input
+
+[Option Capture Source:Line/Mic]
+name = input
+
+[Option Capture Source:Microphone]
+name = input-microphone
+
+[Option Capture Source:Int DMic]
+name = input-microphone-internal
+
+[Option Capture Source:iMic]
+name = input-microphone-internal
+
+[Option Capture Source:i-Mic]
+name = input-microphone-internal
+
+[Option Capture Source:Internal Microphone]
+name = input-microphone-internal
+
+[Option Capture Source:Front Microphone]
+name = input-microphone
+
+[Option Capture Source:Mic1]
+name = input-microphone
+
+[Option Capture Source:Mic2]
+name = input-microphone
+
+[Option Capture Source:D-Mic]
+name = input-microphone
+
+[Option Capture Source:IntMic]
+name = input-microphone-internal
+
+[Option Capture Source:ExtMic]
+name = input-microphone-external
+
+[Option Capture Source:Ext Mic]
+name = input-microphone-external
+
+[Option Capture Source:E-Mic]
+name = input-microphone-external
+
+[Option Capture Source:e-Mic]
+name = input-microphone-external
+
+[Option Capture Source:LineIn]
+name = input-linein
+
+[Option Capture Source:Analog]
+name = input
+
+[Option Capture Source:Line-In]
+name = input-linein
+
+[Option Capture Source:Line In]
+name = input-linein
+
+[Option Capture Source:Video]
+name = input-video
+
+[Option Capture Source:Aux]
+name = input
+
+[Option Capture Source:Aux0]
+name = input
+
+[Option Capture Source:Aux1]
+name = input
+
+[Option Capture Source:Aux2]
+name = input
+
+[Option Capture Source:Aux3]
+name = input
+
+[Option Capture Source:AUX IN]
+name = input
+
+[Option Capture Source:Aux In]
+name = input
+
+[Option Capture Source:AOUT]
+name = input
+
+[Option Capture Source:AUX]
+name = input
+
+[Option Capture Source:Cam Mic]
+name = input-microphone
+
+[Option Capture Source:Digital Mic]
+name = input-microphone
+
+[Option Capture Source:Digital Mic 1]
+name = input-microphone
+
+[Option Capture Source:Digital Mic 2]
+name = input-microphone
+
+[Option Capture Source:Analog Inputs]
+name = input
+
+[Option Capture Source:Unknown1]
+name = input
+
+[Option Capture Source:Unknown2]
+name = input
+
+[Option Capture Source:Docking-Station]
+name = input-docking
+
+;;; 'Mic Jack Mode'
+
+[Element Mic Jack Mode]
+enumeration = select
+
+[Option Mic Jack Mode:Mic In]
+name = input-microphone
+
+[Option Mic Jack Mode:Line In]
+name = input-linein
+
+;;; 'Digital Input Source'
+
+[Element Digital Input Source]
+enumeration = select
+
+[Option Digital Input Source:Digital Mic 1]
+name = input-microphone
+
+[Option Digital Input Source:Analog Inputs]
+name = input
+
+[Option Digital Input Source:Digital Mic 2]
+name = input-microphone
+
+;;; 'Analog Source'
+
+[Element Analog Source]
+enumeration = select
+
+[Option Analog Source:Mic]
+name = input-microphone
+
+[Option Analog Source:Line in]
+name = input-linein
+
+[Option Analog Source:Aux]
+name = input
+
+;;; 'Shared Mic/Line in'
+
+[Element Shared Mic/Line in]
+enumeration = select
+
+[Option Shared Mic/Line in:Mic in]
+name = input-microphone
+
+[Option Shared Mic/Line in:Line in]
+name = input-linein
+
+;;; Various Boosts
+
+[Element Capture Boost]
+switch = select
+
+[Option Capture Boost:on]
+name = input-boost-on
+
+[Option Capture Boost:off]
+name = input-boost-off
+
+[Element Auto Gain Control]
+switch = select
+
+[Option Auto Gain Control:on]
+name = input-agc-on
+
+[Option Auto Gain Control:off]
+name = input-agc-off
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-chat.conf b/spa/plugins/alsa/mixer/paths/analog-output-chat.conf
new file mode 100644
index 0000000..360a1fc
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-chat.conf
@@ -0,0 +1,5 @@
+; Some gaming devices have a separate "chat" device, this is for voice chat
+; while playing games. This device is just a fairly standard analog mono
+; device, but it's nicer to make it clear that this is the "chat" device
+; as is mentioned in the marketing info and manual.
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf b/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf
new file mode 100644
index 0000000..bda137d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-headphones-2.conf
@@ -0,0 +1,118 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Path for the second headphone output on dual-headphone machines.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 98
+
+[Properties]
+device.icon_name = audio-headphones
+
+; HP EliteDesk 800 SFF Headphone
+[Jack Front Headphone,1]
+required-any = any
+
+; HP EliteDesk 800 DM Headphone
+[Jack Front Headphone Surround]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; This profile path is intended to control the second headphones, not
+; the first headphones. But it should not hurt if we leave the
+; headphone jack enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone+LO]
+switch = mute
+volume = zero
+
+[Element Speaker+LO]
+switch = off
+volume = off
+
+[Element Headphone2]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+; On some machines, the Front Volume Control is shared by Headphone and Lineout,
+; or Headphone and Speaker, but they have independent Volume Switch. Here only
+; use switch to mute Lineout or Speaker.
+[Element Front]
+switch = off
+volume = zero
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf b/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf
new file mode 100644
index 0000000..3c62c5e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-headphones.conf
@@ -0,0 +1,180 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Path for mixers that have a 'Headphone' control
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 99
+description-key = analog-output-headphones
+
+[Properties]
+device.icon_name = audio-headphones
+
+[Jack Dock Headphone]
+required-any = any
+
+[Jack Dock Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Front Headphone]
+required-any = any
+
+; HP EliteDesk 800 DM Headset
+[Jack Front Headphone Front]
+required-any = any
+
+[Jack Front Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Headphone]
+required-any = any
+
+[Jack Headphone Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+# This jack can be either a headphone *or* a mic. Used on some ASUS netbooks.
+[Jack Headphone Mic]
+required-any = any
+
+[Jack Headphone - Output]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+switch = off
+volume = off
+
+[Element Headphone+LO]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Headphone]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+; This path is intended to control the first headphones, not
+; the second headphones. But it should not hurt if we leave the second
+; headphone jack enabled nonetheless.
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headset]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Line HP Swap]
+switch = on
+required-any = any
+
+; This profile path is intended to control the first headphones, not
+; the second headphones. But it should not hurt if we leave the second
+; headphone jack enabled nonetheless.
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+; On some machines, the Front Volume Control is shared by Headphone and Lineout,
+; or Headphone and Speaker, but they have independent Volume Switch. Here only
+; use switch to mute Lineout or Speaker.
+[Element Front]
+switch = off
+volume = zero
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+[Element Speaker Front]
+switch = off
+volume = off
+
+[Element Speaker Surround]
+switch = off
+volume = off
+
+[Element Speaker Side]
+switch = off
+volume = off
+
+[Element Speaker CLFE]
+switch = off
+volume = off
+
+[Element Speaker Center/LFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf b/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf
new file mode 100644
index 0000000..1ffce22
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-lineout.conf
@@ -0,0 +1,214 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+[General]
+priority = 90
+description-key = analog-output-lineout
+
+[Jack Line Out]
+required-any = any
+
+[Jack Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Front Line Out]
+required-any = any
+
+[Jack Front Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Rear Line Out]
+required-any = any
+
+[Jack Rear Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Front]
+required-any = any
+
+[Jack Line Out Front Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out CLFE]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out CLFE Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Surround]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Surround Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Side]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Line Out Side Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Jack Dock Line Out]
+required-any = any
+
+[Jack Dock Line Out Phantom]
+state.plugged = unknown
+state.unplugged = unknown
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+required-any = any
+
+[Element Headphone+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+required-any = any
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Line HP Swap]
+switch = off
+required-any = any
+
+; This profile path is intended to control line out, let's mute headphones
+; else there will be a spike when plugging in headphones
+[Element Headphone]
+switch = off
+volume = off
+
+[Element Headphone,1]
+switch = off
+volume = off
+
+[Element Headphone2]
+switch = off
+volume = off
+
+[Element Speaker]
+switch = off
+volume = off
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Bass Speaker]
+switch = off
+volume = off
+
+[Element Speaker Front]
+switch = off
+volume = off
+
+[Element Speaker Surround]
+switch = off
+volume = off
+
+[Element Speaker Side]
+switch = off
+volume = off
+
+[Element Speaker CLFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-mono.conf b/spa/plugins/alsa/mixer/paths/analog-output-mono.conf
new file mode 100644
index 0000000..5e49405
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-mono.conf
@@ -0,0 +1,99 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Intended for usage on boards that have a separate Mono output plug.
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 50
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = off
+volume = off
+
+[Element Master Mono]
+required = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+; This profile path is intended to control the speaker, not the
+; headphones. But it should not hurt if we leave the headphone jack
+; enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headphone+LO]
+switch = mute
+volume = zero
+
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+switch = off
+volume = off
+
+[Element Front]
+switch = off
+volume = off
+
+[Element Rear]
+switch = off
+volume = off
+
+[Element Surround]
+switch = off
+volume = off
+
+[Element Side]
+switch = off
+volume = off
+
+[Element Center]
+switch = off
+volume = off
+
+[Element LFE]
+switch = off
+volume = off
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf b/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf
new file mode 100644
index 0000000..756afa9
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-speaker-always.conf
@@ -0,0 +1,187 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Path for mixers that don't have a 'Speaker' control, but where we
+; force enable the speaker paths nonetheless.
+; Needed for some older Dell laptops.
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+description-key = analog-output-speaker
+
+[Properties]
+device.icon_name = audio-speakers
+
+[Jack Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out Front]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; This profile path is intended to control the speaker, not the
+; headphones. But it should not hurt if we leave the headphone jack
+; enabled nonetheless.
+[Element Headphone]
+switch = mute
+volume = zero
+
+[Element Headphone,1]
+switch = mute
+volume = zero
+
+[Element Headphone2]
+switch = mute
+volume = zero
+
+[Element Headphone+LO]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Front Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element Center Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element LFE Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element Bass Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf b/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf
new file mode 100644
index 0000000..72f928f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output-speaker.conf
@@ -0,0 +1,246 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Path for mixers that have a 'Speaker' control
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 100
+description-key = analog-output-speaker
+
+[Properties]
+device.icon_name = audio-speakers
+
+[Jack Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Headphone]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Line Out Front]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Front Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Rear Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Dock Line Out]
+state.plugged = no
+state.unplugged = unknown
+
+[Jack Speaker]
+required-any = any
+
+[Jack Speaker Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Speaker Front Phantom]
+required-any = any
+state.plugged = unknown
+state.unplugged = unknown
+
+[Jack Speaker - Output]
+required-any = any
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+; Make sure the internal speakers are not auto-muted once the system has speakers
+[Element Auto-Mute Mode]
+enumeration = select
+
+[Option Auto-Mute Mode:Disabled]
+name = analog-output-speaker
+
+; This profile path is intended to control the speaker, let's mute headphones
+; else there will be a spike when plugging in headphones
+[Element Headphone]
+switch = off
+volume = off
+
+[Element Headphone,1]
+switch = off
+volume = off
+
+[Element Headphone2]
+switch = off
+volume = off
+
+[Element Headphone+LO]
+switch = off
+volume = off
+
+[Element Speaker+LO]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Speaker]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Desktop Speaker]
+required-any = any
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Front Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+required-any = any
+
+[Element Speaker Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+required-any = any
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+required-any = any
+
+[Element Speaker Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+required-any = any
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Speaker Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element Center Speaker]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+required-any = any
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element LFE Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+required-any = any
+
+[Element Bass Speaker]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+required-any = any
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Speaker CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output.conf b/spa/plugins/alsa/mixer/paths/analog-output.conf
new file mode 100644
index 0000000..0f6b5f5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output.conf
@@ -0,0 +1,88 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Intended for the 'default' output. Note that a-o-speaker.conf has a
+; higher priority than this
+;
+; See analog-output.conf.common for an explanation on the directives
+
+[General]
+priority = 99
+
+[Element Hardware Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element Master Mono]
+switch = off
+volume = off
+
+[Element Front]
+switch = mute
+volume = merge
+override-map.1 = all-front
+override-map.2 = front-left,front-right
+
+[Element Rear]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Surround]
+switch = mute
+volume = merge
+override-map.1 = all-rear
+override-map.2 = rear-left,rear-right
+
+[Element Side]
+switch = mute
+volume = merge
+override-map.1 = all-side
+override-map.2 = side-left,side-right
+
+[Element Center]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,all-center
+
+[Element LFE]
+switch = mute
+volume = merge
+override-map.1 = lfe
+override-map.2 = lfe,lfe
+
+[Element CLFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+[Element Center/LFE]
+switch = mute
+volume = merge
+override-map.1 = all-center
+override-map.2 = all-center,lfe
+
+.include analog-output.conf.common
diff --git a/spa/plugins/alsa/mixer/paths/analog-output.conf.common b/spa/plugins/alsa/mixer/paths/analog-output.conf.common
new file mode 100644
index 0000000..028665d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/analog-output.conf.common
@@ -0,0 +1,199 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Common part of all paths
+
+; So here's generally how mixer paths are used by PA: PA goes through
+; a mixer path file from top to bottom and checks if a mixer element
+; described therein exists. If so it is added to the list of mixer
+; elements PA will control, keeping the order it read them in. If a
+; mixer element described here has set the required= or
+; required-absent= directives a path might not be accepted as valid
+; and is ignored in its entirety (see below). However usually if a
+; element listed here is missing this one element is ignored but not
+; the entire path.
+;
+; When a device shall be muted/unmuted *all* elements listed in a path
+; file with "switch = mute" will be toggled.
+;
+; When a device shall change its volume, PA will got through the list
+; of all elements with "volume = merge" and set the volume on the
+; first element. If that element does not support dB volumes, this is
+; where the story ends. If it does support dB volumes, PA divides the
+; requested volume by the volume that was set on this element, and
+; then go on to the next element with "volume = merge" and then set
+; that there, and so on. That way the first volume element in the
+; path will be the one that does the 'biggest' part of the overall
+; volume adjustment, with the remaining elements usually being set to
+; some value next to 0dB. This logic makes sure we get the full range
+; over all volume sliders and a very high granularity of volumes
+; already in hardware.
+;
+; All switches and enumerations set to "select" are exposed via the
+; "port" functionality of sinks/sources. Basically every possible
+; switch setting and every possible enumeration setting will be
+; combined and made into a "port". So make sure you don't list too
+; many switches/enums for exposing, because the number of ports might
+; rise exponentially.
+;
+; Only one path can be selected at a time. All paths that are valid
+; for an audio device will be exposed as "port" for the sink/source.
+
+
+; [General]
+; type = ... # The device type. It's highly recommended to set a type for every path.
+; # See parse_type() in alsa-mixer.c for supported values.
+; priority = ... # Priority for this path
+; description-key = ... # The path description is looked up from a table in path_verify() in
+; # src/modules/alsa/alsa-mixer.c. By default the path name (i.e. the file name
+; # minus the ".conf" suffix) is used as the lookup key, but if this option is
+; # set, then the given string is used as the key instead. In any case the
+; # "description" option can be used to override the path description.
+; description = ... # Description for this path. Overrides the normal description lookup logic, as
+; # described in the "description-key" documentation above.
+; mute-during-activation = yes | no # If this path supports hardware mute, should the hw mute be used while activating this
+; # path? In some cases this can reduce extra noises during port switching, while in other
+; # cases this can increase such noises. Default: no.
+; eld-device = ... # If this is an HDMI port, set to "auto" so that PulseAudio will try to read
+; # the monitor ELD information from the ALSA mixer. By default the ELD information
+; # is not read, because it's only applicable with HDMI. Earlier the "auto" option
+; # didn't exist, and the hw device index had to be manually configured. For
+; # backwards compatibility, it's still possible to manually configure the device
+; # index using this option.
+;
+; [Properties] # Property list for this path. The list is merged into the port property list.
+; <key> = <value> # Each property is defined on its own line.
+; ...
+;
+; [Option ...:...] # For each option of an enumeration or switch element
+; # that shall be exposed as a sink/source port. Needs to
+; # be named after the Element, followed by a colon, followed
+; # by the option name, resp. on/off if the element is a switch.
+; name = ... # Logical name to use in the path identifier
+; priority = ... # Priority if this is made into a device port
+; required = ignore | enumeration | any # In this element, this option must exist or the path will be invalid. ("any" is an alias for "enumeration".)
+; required-any = ignore | enumeration | any # In this element, either this or another option must exist (or an element)
+; required-absent = ignore | enumeration | any # In this element, this option must not exist or the path will be invalid
+;
+; [Element ...] # For each element that we shall control. The "..." here is the element name,
+; # or name and index separated by a comma.
+; required = ignore | switch | volume | enumeration | any # If set, require this element to be of this kind and available,
+; # otherwise don't consider this path valid for the card
+; required-any = ignore | switch | volume | enumeration | any # If set, at least one of the elements or jacks with required-any in this
+; # path must be present, otherwise this path is invalid for the card
+; required-absent = ignore | switch | volume # If set, require this element to not be of this kind and not
+; # available, otherwise don't consider this path valid for the card
+;
+; switch = ignore | mute | off | on | select # What to do with this switch: ignore it, make it follow mute status,
+; # always set it to off, always to on, or make it selectable as port.
+; # If set to 'select' you need to define an Option section for on
+; # and off
+; volume = ignore | merge | off | zero | <volume step> # What to do with this volume: ignore it, merge it into the device
+; # volume slider, always set it to the lowest value possible, or always
+; # set it to 0 dB (for whatever that means), or always set it to
+; # <volume step> (this only makes sense in path configurations where
+; # the exact hardware and driver are known beforehand).
+; volume-limit = <volume step> # Limit the maximum volume by disabling the volume steps above <volume step>.
+; enumeration = ignore | select # What to do with this enumeration, ignore it or make it selectable
+; # via device ports. If set to 'select' you need to define an Option section
+; # for each of the items you want to expose
+; direction = playback | capture # Is this relevant only for playback or capture? If not set this will implicitly be
+; # set the direction of the PCM device is opened as. Generally this doesn't need to be set
+; # unless you have a broken driver that has playback controls marked for capture or vice
+; # versa
+; direction-try-other = no | yes # If the element does not supported what is requested, try the other direction, too?
+;
+; override-map.1 = ... # Override the channel mask of the mixer control if the control only exposes a single channel
+; override-map.2 = ... # Override the channel masks of the mixer control if the control only exposes two channels
+; # Override maps should list for each element channel which high-level channels it controls via a
+; # channel mask. A channel mask may either be the name of a single channel, or the words "all-left",
+; # "all-right", "all-center", "all-front", "all-rear", and "all" to encode a specific subset of
+; # channels in a mask
+; [Jack ...] # For each jack that we will use for jack detection
+; # The name 'Jack Foo' must match ALSA's 'Foo Jack' control.
+; required = ignore | any # If not set to ignore, make the path invalid if this jack control is not present.
+; required-absent = ignore | any # If not set to ignore, make the path invalid if this jack control is present.
+; required-any = ignore | any # If not set to ignore, make the path invalid if no jack controls and no elements with
+; # the required-any are present.
+; state.plugged = yes | no | unknown # Normally a plugged jack would mean the port becomes available, and an unplugged means it's
+; state.unplugged = yes | no | unknown # unavailable, but the port status can be overridden by specifying state.plugged and/or state.unplugged.
+; append-pcm-to-name = no | yes # Add ",pcm=N" to the jack name? N is the hw PCM device index. HDMI jacks have
+; # the PCM device index in their name, but different drivers use different
+; # numbering schemes, so we can't hardcode the full jack name in our configuration
+; # files.
+
+[Element PCM]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
+
+[Element External Amplifier]
+switch = select
+
+[Option External Amplifier:on]
+name = output-amplifier-on
+priority = 10
+
+[Option External Amplifier:off]
+name = output-amplifier-off
+priority = 0
+
+[Element Bass Boost]
+switch = select
+
+[Option Bass Boost:on]
+name = output-bass-boost-on
+priority = 0
+
+[Option Bass Boost:off]
+name = output-bass-boost-off
+priority = 10
+
+[Element IEC958]
+switch = off
+
+[Element IEC958 Optical Raw]
+switch = off
+
+;;; 'Analog Output'
+
+[Element Analog Output]
+enumeration = select
+
+[Option Analog Output:Speakers]
+name = output-speaker
+priority = 10
+
+[Option Analog Output:Headphones]
+name = output-headphones
+priority = 9
+
+[Option Analog Output:FP Headphones]
+name = output-headphones
+priority = 8
+
+;;; 'Output Select'
+
+[Element Output Select]
+enumeration = select
+
+[Option Output Select:Speakers]
+name = output-speaker
+priority = 10
+
+[Option Output Select:Headphone]
+name = output-headphones
+priority = 9
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf
new file mode 100644
index 0000000..bb3cec1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-0.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort
+type = hdmi
+priority = 59
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf
new file mode 100644
index 0000000..3389a72
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-1.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 2
+type = hdmi
+priority = 58
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf
new file mode 100644
index 0000000..7607f8f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-10.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 11
+type = hdmi
+priority = 49
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf
new file mode 100644
index 0000000..316d810
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-2.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 3
+type = hdmi
+priority = 57
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf
new file mode 100644
index 0000000..0601ef7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-3.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 4
+type = hdmi
+priority = 56
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf
new file mode 100644
index 0000000..ded155b
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-4.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 5
+type = hdmi
+priority = 55
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf
new file mode 100644
index 0000000..de31791
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-5.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 6
+type = hdmi
+priority = 54
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf
new file mode 100644
index 0000000..6d72176
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-6.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 7
+type = hdmi
+priority = 53
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf
new file mode 100644
index 0000000..d5d0771
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-7.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 8
+type = hdmi
+priority = 52
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf
new file mode 100644
index 0000000..0b8f9cd
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-8.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 9
+type = hdmi
+priority = 51
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf
new file mode 100644
index 0000000..f15797c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/hdmi-output-9.conf
@@ -0,0 +1,12 @@
+[General]
+description = HDMI / DisplayPort 10
+type = hdmi
+priority = 50
+eld-device = auto
+
+[Properties]
+device.icon_name = video-display
+
+[Jack HDMI/DP]
+append-pcm-to-name = yes
+required = ignore
diff --git a/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf
new file mode 100644
index 0000000..babc839
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/iec958-stereo-input.conf
@@ -0,0 +1,20 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+[Element PCM Capture Source]
+enumeration = select
+
+[Option PCM Capture Source:IEC958 In]
+name = iec958-input
diff --git a/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf b/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf
new file mode 100644
index 0000000..d47e5eb
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/iec958-stereo-output.conf
@@ -0,0 +1,18 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+
+[Element IEC958]
+switch = mute
diff --git a/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf
new file mode 100644
index 0000000..5842bfe
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-chat-common.conf
@@ -0,0 +1,27 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Steelseries Arctis 5 USB headset stereo chat path. The headset has two
+; output devices. The first one is meant for voice audio, and the second
+; one meant for everything else. The purpose of this unusual design is to
+; provide separate volume controls for voice and other audio, which can be
+; useful in gaming.
+
+[General]
+priority = 50
+
+[Element Com Speaker]
+switch = mute
+volume = merge
diff --git a/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf
new file mode 100644
index 0000000..b758a6f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/steelseries-arctis-output-game-common.conf
@@ -0,0 +1,27 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Steelseries Arctis 5 USB headset stereo game path. The headset has two
+; output devices. The first one is meant for voice audio, and the second
+; one meant for everything else. The purpose of this unusual design is to
+; provide separate volume controls for voice and other audio, which can be
+; useful in gaming.
+
+[General]
+priority = 99
+
+[Element PCM]
+switch = mute
+volume = merge
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf
new file mode 100644
index 0000000..9fa7fe9
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-input.conf
@@ -0,0 +1,34 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; USB gaming headset microphone input path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-input-microphone-headset
+
+[Element Headset]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf
new file mode 100644
index 0000000..6df662f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-mono.conf
@@ -0,0 +1,34 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; USB gaming headset mono output path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-output-headphones-mono
+
+[Element PCM]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf
new file mode 100644
index 0000000..1a1e794
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/usb-gaming-headset-output-stereo.conf
@@ -0,0 +1,34 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; USB gaming headset mono output path. These headsets usually have two
+; output devices. The first one is mono, meant for voice audio, and the second
+; one is stereo, meant for everything else. The purpose of this unusual design
+; is to provide separate volume controls for voice and other audio, which can
+; be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+
+[General]
+description-key = analog-output-headphones
+
+[Element PCM,1]
+volume = merge
+switch = mute
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf b/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf
new file mode 100644
index 0000000..7f111f2
--- /dev/null
+++ b/spa/plugins/alsa/mixer/paths/virtual-surround-7.1.conf
@@ -0,0 +1,5 @@
+[Element PCM,1]
+switch = mute
+volume = merge
+override-map.1 = all
+override-map.2 = all-left,all-right
diff --git a/spa/plugins/alsa/mixer/profile-sets/analog-only.conf b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
new file mode 100644
index 0000000..badd5ec
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/analog-only.conf
@@ -0,0 +1,102 @@
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Some USB DACs appear to support IEC958, but don't physically have any
+; digital outputs.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# ...and if even that fails, try to use hw:0 as a mono device.
+[Mapping mono-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping multichannel-output]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = output
+
+[Mapping multichannel-input]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = input
diff --git a/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
new file mode 100644
index 0000000..3e42ea3
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/asus-xonar-se.conf
@@ -0,0 +1,93 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; ASUS Xonar SE card.
+; This card has two devices for each rear and front panel jacks.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo-front]
+description = Analog Stereo Front
+device-strings = hw:%f,1
+channel-map = left,right
+paths-output = analog-output analog-output-headphones
+paths-input = analog-input-mic analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+[Mapping analog-stereo-rear]
+description = Analog Stereo Rear
+device-strings = hw:%f,0
+channel-map = left,right
+paths-output = analog-output analog-output-speaker
+paths-input = analog-input analog-input-mic analog-input-linein
+priority = 14
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
diff --git a/spa/plugins/alsa/mixer/profile-sets/audigy.conf b/spa/plugins/alsa/mixer/profile-sets/audigy.conf
new file mode 100644
index 0000000..043596e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/audigy.conf
@@ -0,0 +1,94 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Creative Sound Blaster Audigy product line
+;
+; These are just copies of the mappings we find in default.conf, with the
+; small change of making analog-stereo and analog-mono non-fallback mappings.
+; This is needed because these cards only support duplex profiles with mono
+; inputs, and in the default configuration, with stereo being a fallback
+; mapping, the mono mapping is never tried.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+# Based on stereo-fallback
+[Mapping analog-stereo]
+device-strings = hw:%f
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# Based on mono-fallback
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+# The rest of these are identical to what's in default.conf
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
diff --git a/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf
new file mode 100644
index 0000000..1b6f61c
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/cmedia-high-speed-true-hdaudio.conf
@@ -0,0 +1,66 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+# Config for CMEDIA USB2.0 High-Speed True HD Audio 147a:e055
+# Added by Jean-Philippe Guillemin <h1p8r10n@gmail.com>
+
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 10
+
+# If everything else fails, try to use hw:0 as a stereo device.
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 8
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 7
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = hw:%f,2 hw:%f,0
+channel-map = left,right
+paths-output = iec958-stereo-output
+paths-input = iec958-stereo-input
+priority = 5
diff --git a/spa/plugins/alsa/mixer/profile-sets/default.conf b/spa/plugins/alsa/mixer/profile-sets/default.conf
new file mode 100644
index 0000000..f0c9d2a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/default.conf
@@ -0,0 +1,580 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Default profile definitions for the ALSA backend of PulseAudio. This
+; is used as fallback for all cards that have no special mapping
+; assigned (and should be good enough for the vast majority of
+; cards). If you want to assign a different profile set than this one
+; to a device, either set the udev property ACP_PROFILE_SET for the
+; card, or use the "profile_set" module argument when loading
+; module-alsa-card.
+;
+; So what is this about? Simply, what we do here is map ALSA devices
+; to how they are exposed in PA. We say which ALSA device string to
+; use to open a device, which channel mapping to use then, and which
+; mixer path to use. This is encoded in a 'mapping'. Multiple of these
+; mappings can be bound together in a 'profile' which is then directly
+; exposed in the UI as a card profile. Each mapping assigned to a
+; profile will result in one sink/source to be created if the profile
+; is selected for the card.
+;
+; Additionally, the path set configuration files can describe the
+; decibel values assigned to the steps of the volume elements. This
+; can be used to work around situations when the alsa driver doesn't
+; provide any decibel information, or when the information is
+; incorrect.
+
+
+; [General]
+; auto-profiles = no | yes # Instead of defining all profiles manually, autogenerate
+; # them by combining every input mapping with every output mapping.
+;
+; [Mapping id]
+; device-strings = ... # ALSA device string. %f will be replaced by the card identifier.
+; channel-map = ... # Channel mapping to use for this device
+; description = ... # Description for the mapping. Note that it's better to set the description
+; # in the well_known_descriptions table in alsa-mixer.c than with this
+; # option, because the descriptions in alsa-mixer.c are translatable.
+; description-key = ... # A custom key for the well_known_descriptions table (by default the mapping
+; # name is used).
+; paths-input = ... # A list of mixer paths to use. Every path in this list will be probed.
+; # If multiple are found to be working they will be available as device ports
+; paths-output = ...
+; element-input = ... # Instead of configuring a full mixer path simply configure a single
+; # mixer element for volume/mute handling. The value can be an element
+; # name, or name and index separated by a comma.
+; element-output = ...
+; priority = ...
+; direction = any | input | output # Only useful for?
+;
+; exact-channels = yes | no # If no, and the exact number of channels is not supported,
+; # allow device to be opened with another channel count
+; fallback = no | yes # This mapping will only be considered if all non-fallback mappings fail
+; intended-roles = ... # Set the device.intended_roles property for the sink/source.
+;
+; [Profile id]
+; input-mappings = ... # Lists mappings for sources on this profile, those mapping must be
+; # defined in this file too
+; output-mappings = ... # Lists mappings for sinks on this profile, those mappings must be
+; # defined in this file too
+; description = ...
+; priority = ... # Numeric value to deduce priority for this profile
+; skip-probe = no | yes # Skip probing for availability? If this is yes then this profile
+; # will be assumed as working without probing. Makes initialization
+; # a bit faster but only works if the card is really known well.
+;
+; fallback = no | yes # This profile will only be considered if all non-fallback profiles fail
+; [DecibelFix element] # Decibel fixes can be used to work around missing or incorrect dB
+; # information from alsa. A decibel fix is a table that maps volume steps
+; # to decibel values for one volume element. The "element" part in the
+; # section title is the name of the volume element (or name and index
+; # separated by a comma).
+; #
+; # NOTE: This feature is meant just as a help for figuring out the correct
+; # decibel values. PulseAudio is not the correct place to maintain the
+; # decibel mappings!
+; #
+; # If you need this feature, then you should make sure that when you have
+; # the correct values figured out, the alsa driver developers get informed
+; # too, so that they can fix the driver.
+;
+; db-values = ... # The option value consists of pairs of step numbers and decibel values.
+; # The pairs are separated with whitespace, and steps are separated from
+; # the corresponding decibel values with a colon. The values must be in an
+; # increasing order. Here's an example of a valid string:
+; #
+; # "0:-40.50 1:-38.70 3:-33.00 11:0"
+; #
+; # The lowest step imposes a lower limit for hardware volume and the
+; # highest step correspondingly imposes a higher limit. That means that
+; # that the mixer will never be set outside those values - the rest of the
+; # volume scale is done using software volume.
+; #
+; # As can be seen in the example, you don't need to specify a dB value for
+; # each step. The dB values for skipped steps will be linearly interpolated
+; # using the nearest steps that are given.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+# ...and if even that fails, try to use hw:0 as a mono device.
+[Mapping mono-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headset-mic
+priority = 1
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 13
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-input = analog-input analog-input-linein analog-input-mic
+paths-output = analog-output analog-output-lineout analog-output-speaker
+priority = 12
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 9
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 8
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 8
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra1]
+description = Digital Stereo (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra1]
+description = Digital Surround 5.1 (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra1]
+description = Digital Surround 7.1 (HDMI 2)
+device-strings = hdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra1]
+description = Digital Surround 5.1 (HDMI 2/DTS)
+device-strings = dcahdmi:%f,1
+paths-output = hdmi-output-1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra2]
+description = Digital Stereo (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra2]
+description = Digital Surround 5.1 (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra2]
+description = Digital Surround 7.1 (HDMI 3)
+device-strings = hdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra2]
+description = Digital Surround 5.1 (HDMI 3/DTS)
+device-strings = dcahdmi:%f,2
+paths-output = hdmi-output-2
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra3]
+description = Digital Stereo (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra3]
+description = Digital Surround 5.1 (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra3]
+description = Digital Surround 7.1 (HDMI 4)
+device-strings = hdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra3]
+description = Digital Surround 5.1 (HDMI 4/DTS)
+device-strings = dcahdmi:%f,3
+paths-output = hdmi-output-3
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra4]
+description = Digital Stereo (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra4]
+description = Digital Surround 5.1 (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra4]
+description = Digital Surround 7.1 (HDMI 5)
+device-strings = hdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra4]
+description = Digital Surround 5.1 (HDMI 5/DTS)
+device-strings = dcahdmi:%f,4
+paths-output = hdmi-output-4
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra5]
+description = Digital Stereo (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra5]
+description = Digital Surround 5.1 (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra5]
+description = Digital Surround 7.1 (HDMI 6)
+device-strings = hdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra5]
+description = Digital Surround 5.1 (HDMI 6/DTS)
+device-strings = dcahdmi:%f,5
+paths-output = hdmi-output-5
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra6]
+description = Digital Stereo (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra6]
+description = Digital Surround 5.1 (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra6]
+description = Digital Surround 7.1 (HDMI 7)
+device-strings = hdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra6]
+description = Digital Surround 5.1 (HDMI 7/DTS)
+device-strings = dcahdmi:%f,6
+paths-output = hdmi-output-6
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra7]
+description = Digital Stereo (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra7]
+description = Digital Surround 5.1 (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra7]
+description = Digital Surround 7.1 (HDMI 8)
+device-strings = hdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra7]
+description = Digital Surround 5.1 (HDMI 8/DTS)
+device-strings = dcahdmi:%f,7
+paths-output = hdmi-output-7
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra8]
+description = Digital Stereo (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra8]
+description = Digital Surround 5.1 (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra8]
+description = Digital Surround 7.1 (HDMI 9)
+device-strings = hdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra8]
+description = Digital Surround 5.1 (HDMI 9/DTS)
+device-strings = dcahdmi:%f,8
+paths-output = hdmi-output-8
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra9]
+description = Digital Stereo (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra9]
+description = Digital Surround 5.1 (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra9]
+description = Digital Surround 7.1 (HDMI 10)
+device-strings = hdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra9]
+description = Digital Surround 5.1 (HDMI 10/DTS)
+device-strings = dcahdmi:%f,9
+paths-output = hdmi-output-9
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-stereo-extra10]
+description = Digital Stereo (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = left,right
+priority = 7
+direction = output
+
+[Mapping hdmi-surround-extra10]
+description = Digital Surround 5.1 (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping hdmi-surround71-extra10]
+description = Digital Surround 7.1 (HDMI 11)
+device-strings = hdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 6
+direction = output
+
+[Mapping hdmi-dts-surround-extra10]
+description = Digital Surround 5.1 (HDMI 11/DTS)
+device-strings = dcahdmi:%f,10
+paths-output = hdmi-output-10
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 6
+direction = output
+
+[Mapping multichannel-output]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = output
+
+[Mapping multichannel-input]
+device-strings = hw:%f
+channel-map = left,right,rear-left,rear-right
+exact-channels = false
+fallback = yes
+priority = 1
+direction = input
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf
new file mode 100644
index 0000000..1186552
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/dell-dock-tb16-usb-audio.conf
@@ -0,0 +1,55 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Dell Dock TB16 USB audio
+;
+; This card has two stereo pairs of output, One Mono input.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-headphone]
+description = Headphone
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-speaker]
+description = Speaker
+device-strings = hw:%f,1,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-mic]
+description = Headset-Mic
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = input
+
+
+[Profile output:analog-stereo-speaker]
+description = Speaker
+output-mappings = analog-stereo-speaker
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone+input:analog-stereo-mic]
+description = Headset
+output-mappings = analog-stereo-headphone
+input-mappings = analog-stereo-mic
+priority = 80
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf b/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf
new file mode 100644
index 0000000..41924f4
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/force-speaker-and-int-mic.conf
@@ -0,0 +1,153 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; This profile forces speaker and internal mic ports even if we have no way
+; of identifying those.
+; See default.conf for explanations.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic-always analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 1
+
+[Mapping analog-stereo]
+device-strings = front:%f hw:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic-always analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 10
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-4-channel-input]
+# Alsa doesn't currently provide any better device name than "hw" for 4-channel
+# input. If this causes trouble at some point, then we will need to get a new
+# device name standardized in alsa.
+device-strings = hw:%f
+channel-map = aux0,aux1,aux2,aux3
+priority = 1
+direction = input
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 4
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 3
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 3
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf b/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf
new file mode 100644
index 0000000..dec57d5
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/force-speaker.conf
@@ -0,0 +1,152 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; This profile forces a speaker port even if we have no way of identifying it.
+; See default.conf for explanations.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 1
+
+[Mapping analog-stereo]
+device-strings = front:%f hw:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always analog-output-headphones analog-output-headphones-2 analog-output-mono
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line
+priority = 10
+
+[Mapping analog-surround-21]
+device-strings = surround21:%f
+channel-map = front-left,front-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround40:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround41:%f
+channel-map = front-left,front-right,rear-left,rear-right,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround50:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 8
+direction = output
+
+[Mapping analog-surround-71]
+device-strings = surround71:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+description = Analog Surround 7.1
+paths-output = analog-output analog-output-lineout analog-output-speaker-always
+priority = 7
+direction = output
+
+[Mapping analog-4-channel-input]
+# Alsa doesn't currently provide any better device name than "hw" for 4-channel
+# input. If this causes trouble at some point, then we will need to get a new
+# device name standardized in alsa.
+device-strings = hw:%f
+channel-map = aux0,aux1,aux2,aux3
+priority = 1
+direction = input
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-input = iec958-stereo-input
+paths-output = iec958-stereo-output
+priority = 5
+
+[Mapping iec958-ac3-surround-40]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = iec958-stereo-output
+priority = 2
+direction = output
+
+[Mapping iec958-ac3-surround-51]
+device-strings = a52:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping iec958-dts-surround-51]
+device-strings = dca:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = iec958-stereo-output
+priority = 3
+direction = output
+
+[Mapping hdmi-stereo]
+description = Digital Stereo (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = left,right
+priority = 4
+direction = output
+
+[Mapping hdmi-surround]
+description = Digital Surround 5.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 3
+direction = output
+
+[Mapping hdmi-surround71]
+description = Digital Surround 7.1 (HDMI)
+device-strings = hdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+priority = 3
+direction = output
+
+[Mapping hdmi-dts-surround]
+description = Digital Surround 5.1 (HDMI/DTS)
+device-strings = dcahdmi:%f
+paths-output = hdmi-output-0
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+priority = 1
+direction = output
+
+; An example for defining multiple-sink profiles
+#[Profile output:analog-stereo+output:iec958-stereo+input:analog-stereo]
+#description = Foobar
+#output-mappings = analog-stereo iec958-stereo
+#input-mappings = analog-stereo
diff --git a/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf
new file mode 100644
index 0000000..a683a4e
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-120w-g2.conf
@@ -0,0 +1,35 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; HP Thunderbolt Dock 120W G2
+;
+; This dock has a 3.5mm headset connector. Both input and output are stereo.
+;
+; There's a separate speakerphone module called "HP Thunderbolt Dock Audio
+; Module", which can be attached to this dock. The module will appear in ALSA
+; as a separate USB sound card, configuration for it is in
+; hp-tbt-dock-audio-module.conf.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-headset]
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Profile output:analog-stereo-headset+input:analog-stereo-headset]
+output-mappings = analog-stereo-headset
+input-mappings = analog-stereo-headset
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf
new file mode 100644
index 0000000..692ab8d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/hp-tbt-dock-audio-module.conf
@@ -0,0 +1,36 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; HP Thunderbolt Dock Audio Module
+;
+; This device attaches to the "HP Thunderbolt Dock 120W G2" dock. The audio
+; module provides a speakerphone with echo cancellation and appears in ALSA as
+; a USB sound card with stereo input and output.
+;
+; The dock itself has a 3.5mm headset connector and appears as a separate USB
+; sound card, configuration for it is in hp-tbt-dock-120w-g2.conf.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-speakerphone]
+device-strings = hw:%f,0,0
+channel-map = left,right
+intended-roles = phone
+
+[Profile output:analog-stereo-speakerphone+input:analog-stereo-speakerphone]
+output-mappings = analog-stereo-speakerphone
+input-mappings = analog-stereo-speakerphone
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf b/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf
new file mode 100644
index 0000000..d51fd17
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/kinect-audio.conf
@@ -0,0 +1,38 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Audio profile for the Microsoft Kinect Sensor device in UAC mode.
+;
+; Copyright (C) 2011 Antonio Ospite <ospite@studenti.unina.it>
+;
+; This device has an array of four microphones, and no playback capability.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping input-4-channels]
+device-strings = hw:%f
+channel-map = front-left,front-right,rear-left,rear-right
+description = 4 Channels Input
+direction = input
+priority = 5
+
+[Profile input:mic-array]
+description = Microphone Array
+input-mappings = input-4-channels
+priority = 2
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf b/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf
new file mode 100644
index 0000000..5122907
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/maudio-fasttrack-pro.conf
@@ -0,0 +1,86 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; M-Audio FastTrack Pro
+;
+; This card has one duplex stereo channel called A and an additional
+; stereo output channel called B.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a-output]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+; Try both device 0 and device 1 for input, see
+; http://mailman.alsa-project.org/pipermail/alsa-devel/2012-March/050701.html
+[Mapping analog-stereo-a-input]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0 hw:%f,1,0
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-b-output]
+description = Analog Stereo Channel B
+device-strings = hw:%f,1,0
+channel-map = left,right
+direction = output
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channel A, Analog Stereo output Channel B
+output-mappings = analog-stereo-a-output analog-stereo-b-output
+input-mappings = analog-stereo-a-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-a-output+input:analog-stereo-a-input]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a-output
+input-mappings = analog-stereo-a-input
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b-output
+input-mappings =
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a-output
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b-output
+priority = 6
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a-input
+priority = 2
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf
new file mode 100644
index 0000000..f7cbc15
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio4dj.conf
@@ -0,0 +1,90 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Audio 4 DJ
+;
+; This card has two stereo pairs of input and two stereo pairs of
+; output, named channels A and B. Channel B has an additional
+; Headphone connector.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-b-output]
+description = Analog Stereo Channel B (Headphones)
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-b-input]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B (Headphones)
+output-mappings = analog-stereo-a analog-stereo-b-output
+input-mappings = analog-stereo-a analog-stereo-b-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a
+input-mappings = analog-stereo-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B (Headphones)
+output-mappings = analog-stereo-b-output
+input-mappings = analog-stereo-b-input
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B (Headphones)
+output-mappings = analog-stereo-b-output
+priority = 6
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-b]
+description = Analog Stereo Input Channel B
+input-mappings = analog-stereo-b-input
+priority = 1
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf
new file mode 100644
index 0000000..dc1b780
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-audio8dj.conf
@@ -0,0 +1,161 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Audio 8 DJ
+;
+; This card has four stereo pairs of input and four stereo pairs of
+; output, named channels A to D. Channel C has an additional Mic/Line
+; connector, channel D an additional Headphone connector.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+
+# Since we want to set a different description for channel C's/D's input
+# and output we define two separate mappings for them
+[Mapping analog-stereo-c-output]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-c-input]
+description = Analog Stereo Channel C (Line/Mic)
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-d-output]
+description = Analog Stereo Channel D (Headphones)
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-d-input]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B, C (Line/Mic), D (Headphones)
+output-mappings = analog-stereo-a analog-stereo-b analog-stereo-c-output analog-stereo-d-output
+input-mappings = analog-stereo-a analog-stereo-b analog-stereo-c-input analog-stereo-d-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-d+input:analog-stereo-c]
+description = Analog Stereo Channel D (Headphones) Output, Channel C (Line/Mic) Input
+output-mappings = analog-stereo-d-output
+input-mappings = analog-stereo-c-input
+priority = 90
+skip-probe = yes
+
+[Profile output:analog-stereo-c-d+input:analog-stereo-c-d]
+description = Analog Stereo Duplex Channels C (Line/Mic), D (Line/Mic)
+output-mappings = analog-stereo-c-output analog-stereo-d-output
+input-mappings = analog-stereo-c-input analog-stereo-d-input
+priority = 80
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-a
+input-mappings = analog-stereo-a
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-b
+input-mappings = analog-stereo-b
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-c+input:analog-stereo-c]
+description = Analog Stereo Duplex Channel C (Line/Mic)
+output-mappings = analog-stereo-c-output
+input-mappings = analog-stereo-c-input
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-d+input:analog-stereo-d]
+description = Analog Stereo Duplex Channel D (Headphones)
+output-mappings = analog-stereo-d-output
+input-mappings = analog-stereo-d-input
+priority = 70
+skip-probe = yes
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 6
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b
+priority = 5
+skip-probe = yes
+
+[Profile output:analog-stereo-c]
+description = Analog Stereo Output Channel C
+output-mappings = analog-stereo-c-output
+priority = 7
+skip-probe = yes
+
+[Profile output:analog-stereo-d]
+description = Analog Stereo Output Channel D (Headphones)
+output-mappings = analog-stereo-d-output
+priority = 8
+skip-probe = yes
+
+[Profile input:analog-stereo-a]
+description = Analog Stereo Input Channel A
+input-mappings = analog-stereo-a
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-b]
+description = Analog Stereo Input Channel B
+input-mappings = analog-stereo-b
+priority = 1
+skip-probe = yes
+
+[Profile input:analog-stereo-c]
+description = Analog Stereo Input Channel C (Line/Mic)
+input-mappings = analog-stereo-c-input
+priority = 4
+skip-probe = yes
+
+[Profile input:analog-stereo-d]
+description = Analog Stereo Input Channel D
+input-mappings = analog-stereo-d-input
+priority = 3
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf
new file mode 100644
index 0000000..60db1dd
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-komplete-audio6.conf
@@ -0,0 +1,110 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Komplete Audio 6
+;
+; This card has three stereo pairs of input and three stereo pairs of
+; output.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-ab]
+description = Analog Stereo 1/2
+device-strings = hw:%f,0,0
+channel-map = left,right,aux0,aux1,aux2,aux3
+direction = output
+
+[Mapping analog-stereo-out-cd]
+description = Analog Stereo 3/4
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,left,right,aux2,aux3
+direction = output
+
+[Mapping stereo-out-ef]
+description = Stereo 5/6 (S/PDIF)
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,aux2,aux3,left,right
+direction = output
+
+[Mapping analog-mono-in-a]
+description = Analog Mono Input 1
+device-strings = hw:%f,0,0
+channel-map = mono,aux0,aux1,aux2,aux3,aux4
+direction = input
+
+[Mapping analog-mono-in-b]
+description = Analog Mono Input 2
+device-strings = hw:%f,0,0
+channel-map = aux0,mono,aux1,aux2,aux3,aux4
+direction = input
+
+[Mapping analog-stereo-in-ab]
+description = Analog Stereo Input 1/2
+device-strings = hw:%f,0,0
+channel-map = left,right,aux0,aux1,aux2,aux3
+direction = input
+
+[Mapping analog-stereo-in-cd]
+description = Analog Stereo Input 3/4
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,left,right,aux2,aux3
+direction = input
+
+[Mapping stereo-in-ef]
+description = Stereo Input 5/6 (S/PDIF)
+device-strings = hw:%f,0,0
+channel-map = aux0,aux1,aux2,aux3,left,right
+direction = input
+
+[Profile output:analog-stereo-out-ab+input:analog-stereo-in-ab]
+description = Analog Stereo Output 1/2, Analog Stereo Input 1/2
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-stereo-in-ab
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-out-ab+input:analog-mono-in-a]
+description = Analog Stereo Output 1/2, Analog Mono Input 1
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-mono-in-a
+priority = 95
+skip-probe = yes
+
+[Profile output:analog-stereo-out-ab+input:analog-mono-in-b]
+description = Analog Stereo Output 1/2, Analog Mono Input 2
+output-mappings = analog-stereo-out-ab
+input-mappings = analog-mono-in-b
+priority = 90
+skip-probe = yes
+
+[Profile output:analog-stereo-out-cd+input:analog-stereo-in-cd]
+description = Analog Stereo Output 3/4, Analog Stereo Input 3/4
+output-mappings = analog-stereo-out-cd
+input-mappings = analog-stereo-in-cd
+priority = 80
+skip-probe = yes
+
+[Profile output:stereo-out-ef+input:stereo-in-ef]
+description = Stereo Output 5/6 (S/PDIF), Stereo Input 5/6 (S/PDIF)
+output-mappings = stereo-out-ef
+input-mappings = stereo-in-ef
+priority = 70
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf
new file mode 100644
index 0000000..35b3d06
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-korecontroller.conf
@@ -0,0 +1,84 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Kore Controller
+;
+; This card has one stereo pairs of input and two stereo pairs of
+; output, named "Master" and "Headphone". The master channel has
+; an additional Coax S/PDIF connector which is always on.
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-master-out]
+description = Analog Stereo Master Channel
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-headphone-out]
+description = Analog Stereo Headphone Channel
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-input]
+description = Analog Stereo
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Master Output, Headphones Output
+output-mappings = analog-stereo-master-out analog-stereo-headphone-out
+input-mappings = analog-stereo-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-master+input:analog-stereo-input]
+description = Analog Stereo Duplex Master Output
+output-mappings = analog-stereo-master-out
+input-mappings = analog-stereo-input
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone-out+input:analog-stereo-input]
+description = Analog Stereo Headphones Output
+output-mappings = analog-stereo-headphone-out
+input-mappings = analog-stereo-input
+priority = 30
+skip-probe = yes
+
+[Profile output:analog-stereo-master]
+description = Analog Stereo Master Output
+output-mappings = analog-stereo-master-out
+priority = 3
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone]
+description = Analog Stereo Headphones Output
+output-mappings = analog-stereo-headphone-out
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-input]
+description = Analog Stereo Input
+input-mappings = analog-stereo-input
+priority = 1
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf
new file mode 100644
index 0000000..c210297
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio10.conf
@@ -0,0 +1,130 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Audio 10 DJ
+;
+; This card has five stereo pairs of input and five stereo pairs of
+; output
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-out-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-c]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-d]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-in-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-in-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-c]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,2
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-d]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,3
+channel-map = left,right
+direction = input
+
+
+
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels Main, A, B, C, D
+output-mappings = analog-stereo-out-main analog-stereo-out-a analog-stereo-out-b analog-stereo-out-c analog-stereo-out-d
+input-mappings = analog-stereo-in-main analog-stereo-in-a analog-stereo-in-b analog-stereo-in-c analog-stereo-in-d
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main+input:analog-stereo-main]
+description = Analog Stereo Duplex Main
+output-mappings = analog-stereo-out-main
+input-mappings = analog-stereo-in-main
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-out-a
+input-mappings = analog-stereo-in-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-out-b
+input-mappings = analog-stereo-in-b
+priority = 30
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-c]
+description = Analog Stereo Duplex Channel C
+output-mappings = analog-stereo-out-c
+input-mappings = analog-stereo-in-c
+priority = 20
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-d]
+description = Analog Stereo Duplex Channel D
+output-mappings = analog-stereo-out-d
+input-mappings = analog-stereo-in-d
+priority = 10
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf
new file mode 100644
index 0000000..145dace
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio2.conf
@@ -0,0 +1,53 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Traktor Audio 2
+;
+; This card has two stereo pairs of output.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,0
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Profile output:analog-stereo-a]
+description = Analog Stereo Output Channel A
+output-mappings = analog-stereo-a
+priority = 60
+skip-probe = yes
+
+[Profile output:analog-stereo-b]
+description = Analog Stereo Output Channel B
+output-mappings = analog-stereo-b
+priority = 50
+skip-probe = yes
+
+[Profile analog-stereo-all]
+description = Analog Stereo Output Channels A & B
+output-mappings = analog-stereo-a analog-stereo-b
+priority = 100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf
new file mode 100644
index 0000000..a08e9fc
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktor-audio6.conf
@@ -0,0 +1,91 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Audio 6 DJ
+;
+; This card has three stereo pairs of input and three stereo pairs of
+; output
+;
+; We knowingly only define a subset of the theoretically possible
+; mapping combinations as profiles here.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-out-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-out-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-out-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-in-main]
+description = Analog Stereo Main
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-in-a]
+description = Analog Stereo Channel A
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-in-b]
+description = Analog Stereo Channel B
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex Channels A, B (Headphones)
+output-mappings = analog-stereo-out-main analog-stereo-out-a analog-stereo-out-b
+input-mappings = analog-stereo-in-main analog-stereo-in-a analog-stereo-in-b
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main+input:analog-stereo-main]
+description = Analog Stereo Duplex Channel Main
+output-mappings = analog-stereo-out-main
+input-mappings = analog-stereo-in-main
+priority = 50
+skip-probe = yes
+
+[Profile output:analog-stereo-a+input:analog-stereo-a]
+description = Analog Stereo Duplex Channel A
+output-mappings = analog-stereo-out-a
+input-mappings = analog-stereo-in-a
+priority = 40
+skip-probe = yes
+
+[Profile output:analog-stereo-b+input:analog-stereo-b]
+description = Analog Stereo Duplex Channel B
+output-mappings = analog-stereo-out-b
+input-mappings = analog-stereo-in-b
+priority = 30
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf
new file mode 100644
index 0000000..934965f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/native-instruments-traktorkontrol-s4.conf
@@ -0,0 +1,80 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Native Instruments Traktor Kontrol S4
+;
+; This controller has two stereo pairs of input (named "Channel C" and
+; "Channel D") and two stereo pairs of output, one "Main Out" and
+; "Headphone Out".
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-output-main]
+description = Analog Stereo Main Out
+device-strings = hw:%f,0,0
+channel-map = left,right
+
+[Mapping analog-stereo-output-headphone]
+description = Analog Stereo Headphones Out
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = output
+
+[Mapping analog-stereo-c-input]
+description = Analog Stereo Channel C
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Mapping analog-stereo-d-input]
+description = Analog Stereo Channel D
+device-strings = hw:%f,0,1
+channel-map = left,right
+direction = input
+
+[Profile output:analog-stereo-all+input:analog-stereo-all]
+description = Analog Stereo Duplex
+output-mappings = analog-stereo-output-main analog-stereo-output-headphone
+input-mappings = analog-stereo-c-input analog-stereo-d-input
+priority = 100
+skip-probe = yes
+
+[Profile output:analog-stereo-main]
+description = Analog Stereo Main Output
+output-mappings = analog-stereo-output-main
+priority = 4
+skip-probe = yes
+
+[Profile output:analog-stereo-headphone]
+description = Analog Stereo Output Headphones Out
+output-mappings = analog-stereo-output-headphone
+priority = 3
+skip-probe = yes
+
+[Profile input:analog-stereo-c]
+description = Analog Stereo Input Channel C
+input-mappings = analog-stereo-c-input
+priority = 2
+skip-probe = yes
+
+[Profile input:analog-stereo-d]
+description = Analog Stereo Input Channel D
+input-mappings = analog-stereo-d-input
+priority = 1
+skip-probe = yes
+
diff --git a/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf b/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf
new file mode 100644
index 0000000..d5d1d65
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/sb-omni-surround-5.1.conf
@@ -0,0 +1,112 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Creative Sound Blaster Omni Surround 5.1
+;
+; This config supports Linux 4.3-rc1+.
+; By default there are some non-existing (physically) inputs and outputs that
+; are not present in this config.
+; Also in addition to natively supported modes (such as stereo, 5.1 and stereo
+; S/PDIF) following useful output modes are added: 2.1, 4.0, 4.1 and 5.0.
+;
+; NOTE: in 2.1 and 4.1 physical LFE output will be different than in 5.1 mode.
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-stereo-input]
+device-strings = hw:%f
+channel-map = left,right
+paths-input = analog-input-mic analog-input-linein
+direction = input
+
+[Mapping analog-stereo-output]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-21]
+device-strings = surround51:%f
+channel-map = front-left,front-right,aux1,aux2,aux3,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-40]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-41]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,aux1,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-50]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center
+paths-output = analog-output
+direction = output
+
+[Mapping analog-surround-51]
+device-strings = surround51:%f
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe
+paths-output = analog-output
+direction = output
+
+[Mapping iec958-stereo]
+device-strings = iec958:%f
+channel-map = left,right
+paths-output = iec958-stereo-output
+direction = output
+
+[Profile output:analog-stereo-output+input:analog-stereo-input]
+output-mappings = analog-stereo-output
+input-mappings = analog-stereo-input
+priority = 7
+
+[Profile output:analog-surround-21+input:analog-stereo-input]
+output-mappings = analog-surround-21
+input-mappings = analog-stereo-input
+priority = 6
+
+[Profile output:analog-surround-40+input:analog-stereo-input]
+output-mappings = analog-surround-40
+input-mappings = analog-stereo-input
+priority = 5
+
+[Profile output:analog-surround-41+input:analog-stereo-input]
+output-mappings = analog-surround-41
+input-mappings = analog-stereo-input
+priority = 4
+
+[Profile output:analog-surround-50+input:analog-stereo-input]
+output-mappings = analog-surround-50
+input-mappings = analog-stereo-input
+priority = 3
+
+[Profile output:analog-surround-51+input:analog-stereo-input]
+output-mappings = analog-surround-51
+input-mappings = analog-stereo-input
+priority = 2
+
+[Profile output:iec958-stereo+input:analog-stereo-input]
+output-mappings = iec958-stereo
+input-mappings = analog-stereo-input
+priority = 1
diff --git a/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf
new file mode 100644
index 0000000..0ac1576
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf
@@ -0,0 +1,58 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; USB Gaming DAC.
+; These devices have two output devices. The first one is mono, meant for
+; voice audio, and the second one is 7.1 surround, meant for everything
+; else. The 7.1 surround is mapped to headphones within the device.
+; The purpose of the mono/7.1 design is to provide separate volume
+; controls for voice and other audio, which can be useful in gaming.
+;
+; Works with:
+; Sennheiser GSX 1000
+; Sennheiser GSX 1200
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = no
+
+[Mapping analog-chat-output]
+device-strings = hw:%f,0
+channel-map = mono
+paths-output = analog-chat-output
+direction = output
+priority = 4000
+intended-roles = phone
+
+[Mapping analog-output-surround71]
+device-strings = hw:%f,1
+channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right
+paths-output = virtual-surround-7.1
+priority = 4100
+direction = output
+
+[Mapping analog-chat-input]
+device-strings = hw:%f,0
+channel-map = mono
+paths-input = analog-chat-input
+priority = 4100
+direction = input
+
+[Profile output:analog-output-surround71+output:analog-output-chat+input:analog-input]
+output-mappings = analog-output-surround71 analog-chat-output
+input-mappings = analog-chat-input
+priority = 5100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf b/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf
new file mode 100644
index 0000000..809d015
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/simple-headphones-mic.conf
@@ -0,0 +1,42 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; This is a profile meant for simple (stereo + mic) headphones.
+; default.conf also works but using this one will hide some profiles
+; that don't make sense like IEC958 and multichannel inputs.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 15
+
+# If everything else fails, try to use hw:0 as a stereo device...
+[Mapping stereo-fallback]
+device-strings = hw:%f
+fallback = yes
+channel-map = front-left,front-right
+paths-output = analog-output analog-output-lineout analog-output-speaker analog-output-headphones analog-output-headphones-2
+paths-input = analog-input-front-mic analog-input-rear-mic analog-input-internal-mic analog-input-dock-mic analog-input analog-input-mic analog-input-linein analog-input-aux analog-input-video analog-input-tvtuner analog-input-fm analog-input-mic-line analog-input-headphone-mic analog-input-headset-mic
+priority = 1
+
+[Mapping analog-mono]
+device-strings = hw:%f,0,0
+channel-map = mono
+direction = input
diff --git a/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf b/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf
new file mode 100644
index 0000000..0c58917
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/steelseries-arctis-common-usb-audio.conf
@@ -0,0 +1,23 @@
+[General]
+auto-profiles = yes
+
+[Mapping analog-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = left,right
+paths-input = analog-input-mic
+paths-output = steelseries-arctis-output-chat-common
+intended-roles = phone
+
+[Mapping analog-game]
+description-key = gaming-headset-game
+device-strings = hw:%f,1,0
+channel-map = left,right
+paths-output = steelseries-arctis-output-game-common
+direction = output
+
+[Profile output:analog-chat+output:analog-game+input:analog-chat]
+output-mappings = analog-chat analog-game
+input-mappings = analog-chat
+priority = 5100
+skip-probe = yes
diff --git a/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf b/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf
new file mode 100644
index 0000000..3c2b398
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/texas-instruments-pcm2902.conf
@@ -0,0 +1,75 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; Texas Instruments PCM2902
+;
+; This is a generic chip used in multiple products, including at least
+; Behringer U-Phoria UMC22, Behringer Xenyx 302USB, Intopic Jazz-UB700 and
+; some unbranded "usb mini microphone".
+;
+; Behringer UMC22 has stereo input (representing two physical mono inputs),
+; others have mono input.
+;
+; Some devices have a mic input path, but at least Behringer Xenyx 302USB
+; doesn't have any input mixer controls.
+;
+; Since the UMC22 card has only stereo input PCM device but is commonly used
+; with mono mics, we define special mono mappings using "mono,aux1" and
+; "aux1,mono" channel maps. If we had only had the standard stereo input
+; mapping, the user would have to record stereo tracks with one channel silent,
+; which would be inconvenient.
+;
+; This config also removes default digital input/output mappings that do
+; not physically exist on cards that we've seen so far.
+;
+; Originally added by Nazar Mokrynskyi <nazar@mokrynskyi.com> for Behringer
+; UMC22.
+
+[General]
+auto-profiles = yes
+
+[Mapping analog-stereo-input]
+device-strings = hw:%f
+channel-map = left,right
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 4
+
+[Mapping analog-mono]
+device-strings = hw:%f
+channel-map = mono
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 3
+
+[Mapping analog-mono-left]
+device-strings = hw:%f
+channel-map = mono,aux1
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 2
+
+[Mapping analog-mono-right]
+device-strings = hw:%f
+channel-map = aux1,mono
+paths-input = analog-input-mic analog-input
+direction = input
+priority = 1
+
+[Mapping analog-stereo-output]
+device-strings = front:%f
+channel-map = left,right
+paths-output = analog-output
+direction = output
diff --git a/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf
new file mode 100644
index 0000000..adda54d
--- /dev/null
+++ b/spa/plugins/alsa/mixer/profile-sets/usb-gaming-headset.conf
@@ -0,0 +1,64 @@
+# This file is part of PulseAudio.
+#
+# PulseAudio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# PulseAudio is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
+
+; USB gaming headset.
+; These headsets usually have two output devices. The first one is meant
+; for voice audio, and the second one is meant for everything else.
+; The purpose of this unusual design is to provide separate volume
+; controls for voice and other audio, which can be useful in gaming.
+;
+; Works with:
+; Steelseries Arctis 7
+; Steelseries Arctis Pro Wireless.
+; Lucidsound LS31
+; Astro A50
+;
+; See default.conf for an explanation on the directives used here.
+
+[General]
+auto-profiles = yes
+
+[Mapping mono-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = mono
+paths-output = usb-gaming-headset-output-mono
+paths-input = usb-gaming-headset-input
+intended-roles = phone
+
+[Mapping stereo-chat]
+description-key = gaming-headset-chat
+device-strings = hw:%f,0,0
+channel-map = left,right
+paths-output = usb-gaming-headset-output-stereo
+paths-input = usb-gaming-headset-input
+intended-roles = phone
+
+[Mapping stereo-game]
+description-key = gaming-headset-game
+device-strings = hw:%f,1,0
+channel-map = left,right
+paths-output = usb-gaming-headset-output-stereo
+direction = output
+
+[Profile output:mono-chat+output:stereo-game+input:mono-chat]
+output-mappings = mono-chat stereo-game
+input-mappings = mono-chat
+priority = 5100
+
+[Profile output:stereo-game+output:stereo-chat+input:mono-chat]
+output-mappings = stereo-game stereo-chat
+input-mappings = mono-chat
+priority = 5100
diff --git a/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0 b/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0
new file mode 100644
index 0000000..082c9a1
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/ATI IXP--Realtek ALC655 rev 0
@@ -0,0 +1,150 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 29 [94%] [-3.00dB] [on]
+ Front Right: Playback 29 [94%] [-3.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [12.00dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [off] Capture [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 0 [0%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'PCM' 'Analog In' 'IEC958 In'
+ Item0: 'PCM'
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [on]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [on] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [on] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 12 [80%] [18.00dB] [on]
+ Front Right: Capture 12 [80%] [18.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'Duplicate Front',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x b/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x
new file mode 100644
index 0000000..b8f61fa
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Brooktree Bt878--Bt87x
@@ -0,0 +1,24 @@
+Simple mixer control 'FM',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Mic/Line',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cvolume-joined
+ Capture channels: Mono
+ Limits: Capture 0 - 15
+ Mono: Capture 13 [87%]
+Simple mixer control 'Capture Boost',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'TV Tuner',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [on]
diff --git a/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3 b/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3
new file mode 100644
index 0000000..a500a81
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Ensoniq AudioPCI--Cirrus Logic CS4297A rev 3
@@ -0,0 +1,135 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 63 [100%] [0.00dB] [on]
+ Front Right: Playback 63 [100%] [0.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Headphone',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control '3D Control - Center',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Depth',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Switch',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 23 [74%] [0.00dB] [on]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [off]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mic'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 15 [100%] [22.50dB] [on]
+ Front Right: Capture 15 [100%] [22.50dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
diff --git a/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI b/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI
new file mode 100644
index 0000000..244f24a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA ATI HDMI--ATI R6xx HDMI
@@ -0,0 +1,4 @@
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981 b/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981
new file mode 100644
index 0000000..165522f
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA Intel--Analog Devices AD1981
@@ -0,0 +1,62 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 63 [100%] [3.00dB] [on]
+ Front Right: Playback 63 [100%] [3.00dB] [on]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Capture [off]
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Capture [on]
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Default PCM',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'PCM' 'ADC'
+ Item0: 'PCM'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
diff --git a/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A b/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A
new file mode 100644
index 0000000..28a2e73
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/HDA Intel--Realtek ALC889A
@@ -0,0 +1,113 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 64 [100%] [0.00dB] [on]
+Simple mixer control 'Headphone',0
+ Capabilities: pswitch
+ Playback channels: Front Left - Front Right
+ Mono:
+ Front Left: Playback [on]
+ Front Right: Playback [on]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 255
+ Mono:
+ Front Left: Playback 255 [100%] [0.00dB]
+ Front Right: Playback 255 [100%] [0.00dB]
+Simple mixer control 'Front',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 44 [69%] [-20.00dB] [on]
+ Front Right: Playback 44 [69%] [-20.00dB] [on]
+Simple mixer control 'Front Mic',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Front Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 0 [0%] [-64.00dB] [on]
+ Front Right: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 64
+ Mono: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Side',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 64
+ Mono:
+ Front Left: Playback 0 [0%] [-64.00dB] [on]
+ Front Right: Playback 0 [0%] [-64.00dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-34.50dB] [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off]
+Simple mixer control 'Mic Boost',0
+ Capabilities: volume
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: 0 - 3
+ Front Left: 0 [0%]
+ Front Right: 0 [0%]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [on] Capture [on]
+Simple mixer control 'IEC958 Default PCM',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 46
+ Front Left: Capture 23 [50%] [7.00dB] [on]
+ Front Right: Capture 23 [50%] [7.00dB] [on]
+Simple mixer control 'Capture',1
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 46
+ Front Left: Capture 0 [0%] [-16.00dB] [off]
+ Front Right: Capture 0 [0%] [-16.00dB] [off]
+Simple mixer control 'Input Source',0
+ Capabilities: cenum
+ Items: 'Mic' 'Front Mic' 'Line'
+ Item0: 'Mic'
+Simple mixer control 'Input Source',1
+ Capabilities: cenum
+ Items: 'Mic' 'Front Mic' 'Line'
+ Item0: 'Mic'
diff --git a/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A b/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A
new file mode 100644
index 0000000..3ddd8af
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Intel 82801CA-ICH3--Analog Devices AD1881A
@@ -0,0 +1,128 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 63
+ Mono:
+ Front Left: Playback 44 [70%] [-28.50dB] [on]
+ Front Right: Playback 60 [95%] [-4.50dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 17 [55%] [-21.00dB] [on]
+Simple mixer control '3D Control - Center',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Depth',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 15
+ Mono: 0 [0%]
+Simple mixer control '3D Control - Switch',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 9 [29%] [-21.00dB] [on]
+ Front Right: Playback 9 [29%] [-21.00dB] [on]
+Simple mixer control 'PCM Out Path & Mute',0
+ Capabilities: enum
+ Items: 'pre 3D' 'post 3D'
+ Item0: 'pre 3D'
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 9 [29%] [-21.00dB] [on] Capture [off]
+ Front Right: Playback 9 [29%] [-21.00dB] [on] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 8 [53%] [-21.00dB] [on]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 13 [87%] [19.50dB] [on]
+ Front Right: Capture 13 [87%] [19.50dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer b/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer
new file mode 100644
index 0000000..38cf677
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/Logitech USB Speaker--USB Mixer
@@ -0,0 +1,27 @@
+Simple mixer control 'Bass',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 48
+ Mono: 22 [46%]
+Simple mixer control 'Bass Boost',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Treble',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 48
+ Mono: 25 [52%]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 44
+ Mono:
+ Front Left: Playback 10 [23%] [-31.00dB] [on]
+ Front Right: Playback 10 [23%] [-31.00dB] [on]
+Simple mixer control 'Auto Gain Control',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
diff --git a/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer b/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer
new file mode 100644
index 0000000..9cb4fa7
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/USB Audio--USB Mixer
@@ -0,0 +1,37 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 255
+ Mono: Playback 105 [41%] [-28.97dB] [on]
+Simple mixer control 'Line',0
+ Capabilities: pvolume cvolume pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 255 Capture 0 - 128
+ Front Left: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [off]
+ Front Right: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined cvolume cvolume-joined pswitch pswitch-joined cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: Playback 0 - 255 Capture 0 - 128
+ Mono: Playback 191 [75%] [34.38dB] [off] Capture 0 [0%] [0.18dB] [on]
+Simple mixer control 'Mic Capture',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 In',0
+ Capabilities: cswitch cswitch-joined
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Input 1',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
+Simple mixer control 'Input 2',0
+ Capabilities: cswitch cswitch-joined cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Mono
+ Mono: Capture [off]
diff --git a/spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer b/spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer
new file mode 100644
index 0000000..783f826
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/USB Device 0x46d:0x9a4--USB Mixer
@@ -0,0 +1,5 @@
+Simple mixer control 'Mic',0
+ Capabilities: cvolume cvolume-joined cswitch cswitch-joined
+ Capture channels: Mono
+ Limits: Capture 0 - 3072
+ Mono: Capture 1536 [50%] [23.00dB] [on]
diff --git a/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888 b/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888
new file mode 100644
index 0000000..15e7b5a
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/VIA 8237--Analog Devices AD1888
@@ -0,0 +1,211 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [0.00dB] [on]
+ Front Right: Playback 31 [100%] [0.00dB] [on]
+Simple mixer control 'Master Mono',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Master Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Headphone Jack Sense',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 23 [74%] [0.00dB] [on]
+ Front Right: Playback 23 [74%] [0.00dB] [on]
+Simple mixer control 'Surround',0
+ Capabilities: pvolume pswitch
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [0.00dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Line Jack Sense',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [on]
+ Front Right: Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Mono
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-34.50dB] [off]
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Output',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 3 [100%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'A/D Converter'
+ Item0: 'AC-Link'
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'Downmix',0
+ Capabilities: enum
+ Items: 'Off' '6 -> 4' '6 -> 2'
+ Item0: 'Off'
+Simple mixer control 'Exchange Front/Surround',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'High Pass Filter Enable',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Input Source Select',0
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Input Source Select',1
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Spread Front to Surround and Center/LFE',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'VIA DXS',0
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',1
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',2
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'VIA DXS',3
+ Capabilities: pvolume
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB]
+ Front Right: Playback 31 [100%] [-48.00dB]
+Simple mixer control 'V_REFOUT Enable',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
diff --git a/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+ b/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+
new file mode 100644
index 0000000..d4f3db6
--- /dev/null
+++ b/spa/plugins/alsa/mixer/samples/VIA 8237--C-Media Electronics CMI9761A+
@@ -0,0 +1,160 @@
+Simple mixer control 'Master',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 0 [0%] [-46.50dB] [off]
+ Front Right: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'PCM',0
+ Capabilities: pvolume pswitch pswitch-joined
+ Playback channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Mono:
+ Front Left: Playback 31 [100%] [-48.00dB] [off]
+ Front Right: Playback 31 [100%] [-48.00dB] [off]
+Simple mixer control 'Surround',0
+ Capabilities: pswitch
+ Playback channels: Front Left - Front Right
+ Mono:
+ Front Left: Playback [off]
+ Front Right: Playback [off]
+Simple mixer control 'Surround Jack Mode',0
+ Capabilities: enum
+ Items: 'Shared' 'Independent'
+ Item0: 'Shared'
+Simple mixer control 'Center',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 31 [100%] [0.00dB] [off]
+Simple mixer control 'LFE',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 31
+ Mono: Playback 0 [0%] [-46.50dB] [off]
+Simple mixer control 'Line',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'CD',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mic',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [on]
+Simple mixer control 'Mic Boost (+20dB)',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'Mic Select',0
+ Capabilities: enum
+ Items: 'Mic1' 'Mic2'
+ Item0: 'Mic1'
+Simple mixer control 'Video',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Phone',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'IEC958',0
+ Capabilities: pswitch pswitch-joined cswitch cswitch-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Mono: Playback [off] Capture [off]
+Simple mixer control 'IEC958 Capture Monitor',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Capture Valid',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Output',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [off]
+Simple mixer control 'IEC958 Playback AC97-SPSA',0
+ Capabilities: volume volume-joined
+ Playback channels: Mono
+ Capture channels: Mono
+ Limits: 0 - 3
+ Mono: 3 [100%]
+Simple mixer control 'IEC958 Playback Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'ADC' 'SPDIF-In'
+ Item0: 'AC-Link'
+Simple mixer control 'PC Speaker',0
+ Capabilities: pvolume pvolume-joined pswitch pswitch-joined
+ Playback channels: Mono
+ Limits: Playback 0 - 15
+ Mono: Playback 0 [0%] [-45.00dB] [off]
+Simple mixer control 'Aux',0
+ Capabilities: pvolume pswitch pswitch-joined cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Playback channels: Front Left - Front Right
+ Capture channels: Front Left - Front Right
+ Limits: Playback 0 - 31
+ Front Left: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+ Front Right: Playback 0 [0%] [-34.50dB] [off] Capture [off]
+Simple mixer control 'Mono Output Select',0
+ Capabilities: enum
+ Items: 'Mix' 'Mic'
+ Item0: 'Mix'
+Simple mixer control 'Capture',0
+ Capabilities: cvolume cswitch cswitch-joined
+ Capture channels: Front Left - Front Right
+ Limits: Capture 0 - 15
+ Front Left: Capture 0 [0%] [0.00dB] [on]
+ Front Right: Capture 0 [0%] [0.00dB] [on]
+Simple mixer control 'Mix',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Mix Mono',0
+ Capabilities: cswitch cswitch-exclusive
+ Capture exclusive group: 0
+ Capture channels: Front Left - Front Right
+ Front Left: Capture [off]
+ Front Right: Capture [off]
+Simple mixer control 'Channel Mode',0
+ Capabilities: enum
+ Items: '2ch' '4ch' '6ch'
+ Item0: '2ch'
+Simple mixer control 'DAC Clock Source',0
+ Capabilities: enum
+ Items: 'AC-Link' 'SPDIF-In' 'Both'
+ Item0: 'AC-Link'
+Simple mixer control 'External Amplifier',0
+ Capabilities: pswitch pswitch-joined
+ Playback channels: Mono
+ Mono: Playback [on]
+Simple mixer control 'Input Source Select',0
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
+Simple mixer control 'Input Source Select',1
+ Capabilities: enum
+ Items: 'Input1' 'Input2'
+ Item0: 'Input1'
diff --git a/spa/plugins/alsa/test-hw-params.c b/spa/plugins/alsa/test-hw-params.c
new file mode 100644
index 0000000..7315061
--- /dev/null
+++ b/spa/plugins/alsa/test-hw-params.c
@@ -0,0 +1,173 @@
+/* Spa
+ *
+ * Copyright © 2022 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <getopt.h>
+#include <math.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/defs.h>
+
+#define DEFAULT_DEVICE "default"
+
+
+struct state {
+ const char *device;
+ snd_output_t *output;
+ snd_pcm_t *hndl;
+};
+
+#define CHECK(s,msg,...) { \
+ int __err; \
+ if ((__err = (s)) < 0) { \
+ fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err)); \
+ return __err; \
+ } \
+}
+
+static const char *get_class(snd_pcm_class_t c)
+{
+ switch (c) {
+ case SND_PCM_CLASS_GENERIC:
+ return "generic";
+ case SND_PCM_CLASS_MULTI:
+ return "multichannel";
+ case SND_PCM_CLASS_MODEM:
+ return "modem";
+ case SND_PCM_CLASS_DIGITIZER:
+ return "digitizer";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *get_subclass(snd_pcm_subclass_t c)
+{
+ switch (c) {
+ case SND_PCM_SUBCLASS_GENERIC_MIX:
+ return "generic-mix";
+ case SND_PCM_SUBCLASS_MULTI_MIX:
+ return "multichannel-mix";
+ default:
+ return "unknown";
+ }
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " -D, --device device name (default '%s')\n"
+ " -C, --capture capture mode (default playback)\n",
+ name, DEFAULT_DEVICE);
+}
+
+int main(int argc, char *argv[])
+{
+ struct state state = { 0, };
+ snd_pcm_hw_params_t *hparams;
+ snd_pcm_info_t *info;
+ snd_pcm_sync_id_t sync;
+ snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
+ snd_pcm_chmap_query_t **maps;
+ int c, i;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "device", required_argument, NULL, 'D' },
+ { "capture", no_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ state.device = DEFAULT_DEVICE;
+
+ while ((c = getopt_long(argc, argv, "hD:C", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'D':
+ state.device = optarg;
+ break;
+ case 'C':
+ stream = SND_PCM_STREAM_CAPTURE;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ CHECK(snd_output_stdio_attach(&state.output, stdout, 0), "attach failed");
+
+ fprintf(stdout, "opening device: '%s'\n", state.device);
+
+ CHECK(snd_pcm_open(&state.hndl, state.device, stream, 0),
+ "open %s failed", state.device);
+
+ snd_pcm_info_alloca(&info);
+ snd_pcm_info(state.hndl, info);
+
+ fprintf(stdout, "info:\n");
+ fprintf(stdout, " device: %u\n", snd_pcm_info_get_device(info));
+ fprintf(stdout, " subdevice: %u\n", snd_pcm_info_get_subdevice(info));
+ fprintf(stdout, " stream: %s\n", snd_pcm_stream_name(snd_pcm_info_get_stream(info)));
+ fprintf(stdout, " card: %d\n", snd_pcm_info_get_card(info));
+ fprintf(stdout, " id: '%s'\n", snd_pcm_info_get_id(info));
+ fprintf(stdout, " name: '%s'\n", snd_pcm_info_get_name(info));
+ fprintf(stdout, " subdevice name: '%s'\n", snd_pcm_info_get_subdevice_name(info));
+ fprintf(stdout, " class: %s\n", get_class(snd_pcm_info_get_class(info)));
+ fprintf(stdout, " subclass: %s\n", get_subclass(snd_pcm_info_get_subclass(info)));
+ fprintf(stdout, " subdevice count: %u\n", snd_pcm_info_get_subdevices_count(info));
+ fprintf(stdout, " subdevice avail: %u\n", snd_pcm_info_get_subdevices_avail(info));
+ sync = snd_pcm_info_get_sync(info);
+ fprintf(stdout, " sync: %08x:%08x:%08x:%08x\n",
+ sync.id32[0], sync.id32[1], sync.id32[2],sync.id32[3]);
+
+ /* channel maps */
+ if ((maps = snd_pcm_query_chmaps(state.hndl)) != NULL) {
+ fprintf(stdout, "channels:\n");
+
+ for (i = 0; maps[i]; i++) {
+ snd_pcm_chmap_t* map = &maps[i]->map;
+ char buf[2048];
+
+ snd_pcm_chmap_print(map, sizeof(buf), buf);
+
+ fprintf(stdout, " %d: %s\n", map->channels, buf);
+ }
+ snd_pcm_free_chmaps(maps);
+ }
+
+ /* hw params */
+ snd_pcm_hw_params_alloca(&hparams);
+ snd_pcm_hw_params_any(state.hndl, hparams);
+
+ snd_pcm_hw_params_dump(hparams, state.output);
+
+ snd_pcm_close(state.hndl);
+
+ return EXIT_SUCCESS;
+}
diff --git a/spa/plugins/alsa/test-timer.c b/spa/plugins/alsa/test-timer.c
new file mode 100644
index 0000000..cc5e5f1
--- /dev/null
+++ b/spa/plugins/alsa/test-timer.c
@@ -0,0 +1,310 @@
+/* Spa
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <getopt.h>
+#include <math.h>
+#include <sys/timerfd.h>
+
+#include <alsa/asoundlib.h>
+
+#include <spa/utils/dll.h>
+#include <spa/utils/defs.h>
+
+#define DEFAULT_DEVICE "hw:0"
+
+#define M_PI_M2 (M_PI + M_PI)
+
+#define BW_PERIOD (SPA_NSEC_PER_SEC * 3)
+
+struct state {
+ const char *device;
+ unsigned int format;
+ unsigned int rate;
+ unsigned int channels;
+ snd_pcm_uframes_t period;
+ snd_pcm_uframes_t buffer_frames;
+
+ snd_pcm_t *hndl;
+ int timerfd;
+
+ double max_error;
+ float accumulator;
+
+ uint64_t next_time;
+ uint64_t prev_time;
+
+ struct spa_dll dll;
+};
+
+static int set_timeout(struct state *state, uint64_t time)
+{
+ struct itimerspec ts;
+ ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
+ ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
+ ts.it_interval.tv_sec = 0;
+ ts.it_interval.tv_nsec = 0;
+ return timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL);
+}
+
+#define CHECK(s,msg,...) { \
+ int __err; \
+ if ((__err = (s)) < 0) { \
+ fprintf(stderr, msg ": %s\n", ##__VA_ARGS__, snd_strerror(__err)); \
+ return __err; \
+ } \
+}
+
+#define LOOP(type,areas,scale) { \
+ uint32_t i, j; \
+ type *samples, v; \
+ samples = (type*)((uint8_t*)areas[0].addr + (areas[0].first + offset*areas[0].step) / 8); \
+ for (i = 0; i < frames; i++) { \
+ state->accumulator += M_PI_M2 * 440 / state->rate; \
+ if (state->accumulator >= M_PI_M2) \
+ state->accumulator -= M_PI_M2; \
+ v = sin(state->accumulator) * scale; \
+ for (j = 0; j < state->channels; j++) \
+ *samples++ = v; \
+ } \
+}
+
+static int write_period(struct state *state)
+{
+ snd_pcm_uframes_t frames = state->period;
+ snd_pcm_uframes_t offset;
+ const snd_pcm_channel_area_t* areas;
+
+ snd_pcm_mmap_begin(state->hndl, &areas, &offset, &frames);
+
+ switch (state->format) {
+ case SND_PCM_FORMAT_S32_LE:
+ LOOP(int32_t, areas, 0x7fffffff);
+ break;
+ case SND_PCM_FORMAT_S16_LE:
+ LOOP(int16_t, areas, 0x7fff);
+ break;
+ default:
+ break;
+ }
+
+ snd_pcm_mmap_commit(state->hndl, offset, frames) ;
+
+ return 0;
+}
+
+static int on_timer_wakeup(struct state *state)
+{
+ snd_pcm_sframes_t delay;
+ double error, corr;
+#if 1
+ snd_pcm_sframes_t avail;
+ CHECK(snd_pcm_avail_delay(state->hndl, &avail, &delay), "delay");
+#else
+ snd_pcm_uframes_t avail;
+ snd_htimestamp_t tstamp;
+ uint64_t then;
+
+ CHECK(snd_pcm_htimestamp(state->hndl, &avail, &tstamp), "htimestamp");
+ delay = state->buffer_frames - avail;
+
+ then = SPA_TIMESPEC_TO_NSEC(&tstamp);
+ if (then != 0) {
+ if (then < state->next_time) {
+ delay -= (state->next_time - then) * state->rate / SPA_NSEC_PER_SEC;
+ } else {
+ delay += (then - state->next_time) * state->rate / SPA_NSEC_PER_SEC;
+ }
+ }
+#endif
+
+ /* calculate the error, we want to have exactly 1 period of
+ * samples remaining in the device when we wakeup. */
+ error = (double)delay - (double)state->period;
+ if (error > state->max_error)
+ error = state->max_error;
+ else if (error < -state->max_error)
+ error = -state->max_error;
+
+ /* update the dll with the error, this gives a rate correction */
+ corr = spa_dll_update(&state->dll, error);
+
+ /* set our new adjusted timeout. alternatively, this value can
+ * instead be used to drive a resampler if this device is
+ * slaved. */
+ state->next_time += state->period / corr * 1e9 / state->rate;
+ set_timeout(state, state->next_time);
+
+ if (state->next_time - state->prev_time > BW_PERIOD) {
+ state->prev_time = state->next_time;
+ fprintf(stdout, "corr:%f error:%f bw:%f\n",
+ corr, error, state->dll.bw);
+ }
+ /* pull in new samples write a new period */
+ write_period(state);
+
+ return 0;
+}
+
+static unsigned int format_from_string(const char *str)
+{
+ if (strcmp(str, "S32_LE") == 0)
+ return SND_PCM_FORMAT_S32_LE;
+ else if (strcmp(str, "S32_BE") == 0)
+ return SND_PCM_FORMAT_S32_BE;
+ else if (strcmp(str, "S24_LE") == 0)
+ return SND_PCM_FORMAT_S24_LE;
+ else if (strcmp(str, "S24_BE") == 0)
+ return SND_PCM_FORMAT_S24_BE;
+ else if (strcmp(str, "S24_3LE") == 0)
+ return SND_PCM_FORMAT_S24_3LE;
+ else if (strcmp(str, "S24_3_BE") == 0)
+ return SND_PCM_FORMAT_S24_3BE;
+ else if (strcmp(str, "S16_LE") == 0)
+ return SND_PCM_FORMAT_S16_LE;
+ else if (strcmp(str, "S16_BE") == 0)
+ return SND_PCM_FORMAT_S16_BE;
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " -D, --device device name (default %s)\n",
+ name, DEFAULT_DEVICE);
+}
+
+int main(int argc, char *argv[])
+{
+ struct state state = { 0, };
+ snd_pcm_hw_params_t *hparams;
+ snd_pcm_sw_params_t *sparams;
+ struct timespec now;
+ int c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "device", required_argument, NULL, 'D' },
+ { "format", required_argument, NULL, 'f' },
+ { "rate", required_argument, NULL, 'r' },
+ { "channels", required_argument, NULL, 'c' },
+ { NULL, 0, NULL, 0}
+ };
+ state.device = DEFAULT_DEVICE;
+ state.format = SND_PCM_FORMAT_S16_LE;
+ state.rate = 44100;
+ state.channels = 2;
+ state.period = 1024;
+
+ while ((c = getopt_long(argc, argv, "hD:f:r:c:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'D':
+ state.device = optarg;
+ break;
+ case 'f':
+ state.format = format_from_string(optarg);
+ break;
+ case 'r':
+ state.rate = atoi(optarg);
+ break;
+ case 'c':
+ state.channels = atoi(optarg);
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ CHECK(snd_pcm_open(&state.hndl, state.device, SND_PCM_STREAM_PLAYBACK, 0),
+ "open %s failed", state.device);
+
+ /* hw params */
+ snd_pcm_hw_params_alloca(&hparams);
+ snd_pcm_hw_params_any(state.hndl, hparams);
+ CHECK(snd_pcm_hw_params_set_access(state.hndl, hparams,
+ SND_PCM_ACCESS_MMAP_INTERLEAVED), "set interleaved");
+ CHECK(snd_pcm_hw_params_set_format(state.hndl, hparams,
+ state.format), "set format");
+ CHECK(snd_pcm_hw_params_set_channels_near(state.hndl, hparams,
+ &state.channels), "set channels");
+ CHECK(snd_pcm_hw_params_set_rate_near(state.hndl, hparams,
+ &state.rate, 0), "set rate");
+ CHECK(snd_pcm_hw_params(state.hndl, hparams), "hw_params");
+
+ CHECK(snd_pcm_hw_params_get_buffer_size(hparams, &state.buffer_frames), "get_buffer_size_max");
+
+ fprintf(stdout, "opened format:%s rate:%u channels:%u\n",
+ snd_pcm_format_name(state.format),
+ state.rate, state.channels);
+
+ snd_pcm_sw_params_alloca(&sparams);
+#if 0
+ CHECK(snd_pcm_sw_params_current(state.hndl, sparams), "sw_params_current");
+ CHECK(snd_pcm_sw_params_set_tstamp_mode(state.hndl, sparams, SND_PCM_TSTAMP_ENABLE),
+ "sw_params_set_tstamp_type");
+ CHECK(snd_pcm_sw_params_set_tstamp_type(state.hndl, sparams, SND_PCM_TSTAMP_TYPE_MONOTONIC),
+ "sw_params_set_tstamp_type");
+ CHECK(snd_pcm_sw_params(state.hndl, sparams), "sw_params");
+#endif
+
+ spa_dll_init(&state.dll);
+ spa_dll_set_bw(&state.dll, SPA_DLL_BW_MAX, state.period, state.rate);
+ state.max_error = SPA_MAX(256.0, state.period / 2.0f);
+
+ if ((state.timerfd = timerfd_create(CLOCK_MONOTONIC, 0)) < 0)
+ perror("timerfd");
+
+ CHECK(snd_pcm_prepare(state.hndl), "prepare");
+
+ /* before we start, write one period */
+ write_period(&state);
+
+ /* set our first timeout for now */
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ state.prev_time = state.next_time = SPA_TIMESPEC_TO_NSEC(&now);
+ set_timeout(&state, state.next_time);
+
+ /* and start playback */
+ CHECK(snd_pcm_start(state.hndl), "start");
+
+ /* wait for timer to expire and call the wakeup function,
+ * this can be done in a poll loop as well */
+ while (true) {
+ uint64_t expirations;
+ CHECK(read(state.timerfd, &expirations, sizeof(expirations)), "read");
+ on_timer_wakeup(&state);
+ }
+
+ snd_pcm_drain(state.hndl);
+ snd_pcm_close(state.hndl);
+ close(state.timerfd);
+
+ return EXIT_SUCCESS;
+}