diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
commit | 7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch) | |
tree | d483300dab478b994fe199a5d19d18d74153718a /spa/plugins/alsa | |
parent | Initial commit. (diff) | |
download | pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.tar.xz pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.zip |
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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, ¶m, &b)) <= 0) + return res; + break; + + case SPA_PARAM_Format: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_format_audio_build(&b, id, &port->current_format); + break; + + case SPA_PARAM_Buffers: + if (!port->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(0), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, + MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, + MAX_FRAGMENT_SIZE), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(0)); + break; + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + break; + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct impl *this, struct port *port) +{ + if (port->n_buffers > 0) { + spa_log_info(this->log, NAME " %p: clear buffers", this); + port->n_buffers = 0; + this->started_node = false; + } + return 0; +} + +static int +compress_setup(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate) +{ + struct compr_config *config; + struct snd_codec *codec; + uint32_t channels, rate; + + memset(&this->codec, 0, sizeof(this->codec)); + memset(&this->compr_conf, 0, sizeof(this->compr_conf)); + + config = &this->compr_conf; + codec = &this->codec; + + switch (info->media_subtype) { + case SPA_MEDIA_SUBTYPE_vorbis: + codec->id = SND_AUDIOCODEC_VORBIS; + rate = info->info.vorbis.rate; + channels = info->info.vorbis.channels; + break; + case SPA_MEDIA_SUBTYPE_mp3: + codec->id = SND_AUDIOCODEC_MP3; + rate = info->info.mp3.rate; + channels = info->info.mp3.channels; + break; + case SPA_MEDIA_SUBTYPE_aac: + codec->id = SND_AUDIOCODEC_AAC; + rate = info->info.aac.rate; + channels = info->info.aac.channels; + break; + case SPA_MEDIA_SUBTYPE_flac: + codec->id = SND_AUDIOCODEC_FLAC; + /* + * Taken from the fcplay utility in tinycompress. Required for + * FLAC to work. + */ + codec->options.flac_d.sample_size = 16; + codec->options.flac_d.min_blk_size = 16; + codec->options.flac_d.max_blk_size = 65535; + codec->options.flac_d.min_frame_size = 11; + codec->options.flac_d.max_frame_size = 8192 * 4; + rate = info->info.flac.rate; + channels = info->info.flac.channels; + break; + case SPA_MEDIA_SUBTYPE_wma: + codec->id = SND_AUDIOCODEC_WMA; + /* + * WMA does not work with Compress-Offload if codec profile + * is not set. + */ + switch (info->info.wma.profile) { + case SPA_AUDIO_WMA_PROFILE_WMA9: + codec->profile = SND_AUDIOPROFILE_WMA9; + break; + case SPA_AUDIO_WMA_PROFILE_WMA9_PRO: + codec->profile = SND_AUDIOPROFILE_WMA9_PRO; + break; + case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS; + break; + case SPA_AUDIO_WMA_PROFILE_WMA10: + codec->profile = SND_AUDIOPROFILE_WMA10; + break; + case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS: + codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS; + break; + default: + spa_log_error(this->log, NAME " %p: Invalid WMA codec profile", this); + return -EINVAL; + } + codec->bit_rate = info->info.wma.bitrate; + codec->align = info->info.wma.block_align; + rate = info->info.wma.rate; + channels = info->info.wma.channels; + break; + case SPA_MEDIA_SUBTYPE_alac: + codec->id = SND_AUDIOCODEC_ALAC; + rate = info->info.alac.rate; + channels = info->info.alac.channels; + break; + case SPA_MEDIA_SUBTYPE_ape: + codec->id = SND_AUDIOCODEC_APE; + rate = info->info.ape.rate; + channels = info->info.ape.channels; + break; + case SPA_MEDIA_SUBTYPE_ra: + codec->id = SND_AUDIOCODEC_REAL; + rate = info->info.ra.rate; + channels = info->info.ra.channels; + break; + case SPA_MEDIA_SUBTYPE_amr: + if (info->info.amr.band_mode == SPA_AUDIO_AMR_BAND_MODE_WB) + codec->id = SND_AUDIOCODEC_AMRWB; + else + codec->id = SND_AUDIOCODEC_AMR; + rate = info->info.amr.rate; + channels = info->info.amr.channels; + break; + break; + default: + return -ENOTSUP; + } + + codec->ch_in = channels; + codec->ch_out = channels; + codec->sample_rate = rate; + *out_rate = rate; + + codec->rate_control = 0; + codec->level = 0; + codec->ch_mode = 0; + codec->format = 0; + + spa_log_info(this->log, NAME " %p: Codec info, profile: %d align: %d rate: %d bitrate: %d", + this, codec->profile, codec->align, codec->sample_rate, codec->bit_rate); + + if (!is_codec_supported_by_name(this->props.device, 0, codec)) { + spa_log_error(this->log, NAME " %p: Requested codec is not supported by DSP", this); + return -EINVAL; + } + + config->codec = codec; + config->fragment_size = MIN_FRAGMENT_SIZE; + config->fragments = MIN_NUM_FRAGMENTS; + + return 0; +} + +static int +port_set_format(struct impl *this, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + int res; + struct port *port = &this->port; + + if (format == NULL) { + port->have_format = false; + clear_buffers(this, port); + } else { + struct spa_audio_info info = { 0 }; + uint32_t rate; + + if ((res = spa_format_audio_parse(format, &info)) < 0) { + spa_log_error(this->log, NAME " %p: format parse error: %s", this, + spa_strerror(res)); + return res; + } + + if ((res = compress_setup(this, &info, &rate)) < 0) { + spa_log_error(this->log, NAME " %p: can't setup compress: %s", + this, spa_strerror(res)); + return res; + } + + port->current_format = info; + port->have_format = true; + port->info.rate = SPA_FRACTION(1, rate); + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + emit_node_info(this, false); + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + + if (port->have_format) { + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + } else { + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + + emit_port_info(this, port, false); + + return 0; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + return port_set_format(this, direction, port_id, flags, param); + default: + return -ENOENT; + } + return 0; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + struct impl *this = object; + struct port *port; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + if (!port->have_format) + return -EIO; + + clear_buffers(this, port); + + for (i = 0; i < n_buffers; i++) { + struct buffer *b; + struct spa_data *d = buffers[i]->datas; + + b = &port->buffers[i]; + b->id = i; + b->flags = 0; + b->outbuf = buffers[i]; + + if (d[0].data == NULL) { + spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, + buffers[i]); + return -EINVAL; + } + } + port->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct impl *this = object; + struct port *port; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + port = &this->port; + + switch (id) { + case SPA_IO_Buffers: + port->io = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_process(void *object) +{ + struct impl *this = object; + struct port *port; + struct spa_io_buffers *io; + struct buffer *b; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + port = &this->port; + + io = port->io; + spa_return_val_if_fail(io != NULL, -EIO); + + if (io->status != SPA_STATUS_HAVE_DATA) + return io->status; + + if (io->buffer_id >= port->n_buffers) { + io->status = -EINVAL; + return io->status; + } + + b = &port->buffers[io->buffer_id]; + + for (i = 0; i < b->outbuf->n_datas; i++) { + int32_t offs, size; + int32_t wrote; + void *buf; + + struct spa_data *d = b->outbuf->datas; + d = b->outbuf->datas; + + offs = SPA_MIN(d->chunk->offset, d->maxsize); + size = SPA_MIN(d->maxsize - offs, d->chunk->size); + buf = SPA_PTROFF(d[0].data, offs, void); + + wrote = write_compress(this, buf, size); + if (wrote < 0) { + spa_log_error(this->log, NAME " %p: Error playing sample: %s", + this, compress_get_error(this->compress)); + io->status = wrote; + return SPA_STATUS_STOPPED; + } + } + + io->status = SPA_STATUS_OK; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct impl *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct impl *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct impl); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct impl *this; + struct port *port; + const char *str; + uint32_t i; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct impl *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + spa_hook_list_init(&this->hooks); + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = MAX_PORTS; + this->info.max_output_ports = 0; + this->info.flags = SPA_NODE_FLAG_RT | + SPA_NODE_FLAG_IN_PORT_CONFIG | + SPA_NODE_FLAG_NEED_CONFIGURE; + this->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); + this->info.params = this->params; + this->info.n_params = 1; + reset_props(&this->props); + + port = &this->port; + port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + port->info = SPA_PORT_INFO_INIT(); + port->info.flags = SPA_PORT_FLAG_NO_REF | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + port->info.params = port->params; + port->info.n_params = 4; + port->written = 0; + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "clock.quantum-limit")) { + spa_atou32(s, &this->quantum_limit, 0); + } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { + this->props.channels = atoi(s); + } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { + this->props.rate = atoi(s); + } + } + + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) { + if ((str[0] == 'h') || (str[1] == 'w') || (str[2] == ':')) { + snprintf(this->props.device, sizeof(this->props.device), "%s", str); + } else { + spa_log_error(this->log, NAME " %p: Invalid Compress-Offload hw %s", this, str); + return -EINVAL; + } + } else { + spa_log_error(this->log, NAME " %p: Invalid compress hw", this); + return -EINVAL; + } + + /* + * TODO: + * + * Move this to use new compress_get_supported_codecs_by_name API once + * merged upstream. + * + * Right now, we pretend all codecs are supported and then error out + * at runtime in port_set_format during compress_setup if not + * supported. + */ + this->num_codecs = SPA_N_ELEMENTS (codec_info); + for (i = 0; i < this->num_codecs; i++) { + this->codecs_supported[i] = codec_info[i].codec_id; + } + + spa_log_info(this->log, NAME " %p: Initialized Compress-Offload sink %s", + this, this->props.device); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity <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(¶ms)); + + if ((this->is_iec958 || this->is_hdmi) && iec958_codecs != NULL) { + uint32_t i, codecs[16], n_codecs; + n_codecs = spa_pod_copy_array(iec958_codecs, SPA_TYPE_Id, + codecs, SPA_N_ELEMENTS(codecs)); + this->iec958_codecs = 1ULL << SPA_AUDIO_IEC958_CODEC_PCM; + for (i = 0; i < n_codecs; i++) + this->iec958_codecs |= 1ULL << codecs[i]; + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[NODE_Props].user++; + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_EnumFormat].user++; + } + spa_alsa_parse_prop_params(this, params); + if (lat_ns != -1) { + struct spa_process_latency_info info; + info = this->process_latency; + info.ns = lat_ns; + handle_process_latency(this, &info); + } + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + case SPA_PARAM_ProcessLatency: + { + struct spa_process_latency_info info; + if (param == NULL) + spa_zero(info); + else if ((res = spa_process_latency_parse(param, &info)) < 0) + return res; + + handle_process_latency(this, &info); + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_alsa_open(this, NULL)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (this->have_format) + return 0; + if ((res = spa_alsa_close(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Suspend: + case SPA_NODE_COMMAND_Pause: + if ((res = spa_alsa_pause(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct state *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int +impl_node_sync(void *object, int seq) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + + struct state *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + return spa_alsa_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + switch (this->current_format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + param = spa_format_audio_raw_build(&b, id, + &this->current_format.info.raw); + break; + case SPA_MEDIA_SUBTYPE_iec958: + param = spa_format_audio_iec958_build(&b, id, + &this->current_format.info.iec958); + break; + case SPA_MEDIA_SUBTYPE_dsd: + param = spa_format_audio_dsd_build(&b, id, + &this->current_format.info.dsd); + break; + default: + return -EIO; + } + break; + + case SPA_PARAM_Buffers: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->frame_size * this->frame_scale, + 16 * this->frame_size * this->frame_scale, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + struct spa_latency_info latency = this->latency[result.index]; + if (latency.direction == SPA_DIRECTION_INPUT) + spa_process_latency_info_add(&this->process_latency, &latency); + param = spa_latency_build(&b, id, &latency); + break; + } + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct state *this) +{ + if (this->n_buffers > 0) { + spa_list_init(&this->ready); + this->n_buffers = 0; + } + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + const struct spa_pod *format) +{ + struct state *this = object; + int err = 0; + + if (format == NULL) { + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "clear format"); + spa_alsa_close(this); + clear_buffers(this); + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio) + return -EINVAL; + + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_iec958: + if (spa_format_audio_iec958_parse(format, &info.info.iec958) < 0) + return -EINVAL; + break; + case SPA_MEDIA_SUBTYPE_dsd: + if (spa_format_audio_dsd_parse(format, &info.info.dsd) < 0) + return -EINVAL; + break; + default: + return -EINVAL; + } + + if ((err = spa_alsa_set_format(this, &info, flags)) < 0) + return err; + + this->current_format = info; + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (this->have_format) { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->port_params[PORT_Latency].user++; + } else { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, false); + + return err; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, direction, port_id, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + emit_port_info(this, false); + res = 0; + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + uint32_t i; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (this->n_buffers > 0) { + spa_alsa_pause(this); + if ((res = clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !this->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = BUFFER_FLAG_OUT; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_log_debug(this->log, "%p: %d %p data:%p", this, i, b->buf, d[0].data); + } + this->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + this->io = data; + break; + case SPA_IO_RateMatch: + this->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + return -ENOTSUP; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct spa_io_buffers *io; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if ((io = this->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p: process %d %d/%d", this, io->status, + io->buffer_id, this->n_buffers); + + if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { + io->status = SPA_STATUS_NEED_DATA; + return SPA_STATUS_HAVE_DATA; + } + if (io->status == SPA_STATUS_HAVE_DATA && + io->buffer_id < this->n_buffers) { + struct buffer *b = &this->buffers[io->buffer_id]; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_warn(this->log, "%p: buffer %u in use", + this, io->buffer_id); + io->status = -EINVAL; + return -EINVAL; + } + spa_log_trace_fp(this->log, "%p: queue buffer %u", this, io->buffer_id); + spa_list_append(&this->ready, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + io->buffer_id = SPA_ID_INVALID; + + spa_alsa_write(this); + + io->status = SPA_STATUS_OK; + } + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_alsa_close(this); + spa_alsa_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) +{ + struct state *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "a data loop is needed"); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "a data system is needed"); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_Node, + SPA_VERSION_NODE, + &impl_node, this); + + spa_hook_list_init(&this->hooks); + + this->stream = SND_PCM_STREAM_PLAYBACK; + this->port_direction = SPA_DIRECTION_INPUT; + this->latency[this->port_direction] = SPA_LATENCY_INFO( + this->port_direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info = SPA_NODE_INFO_INIT(); + this->info.max_input_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + + reset_props(&this->props); + + this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + this->port_info = SPA_PORT_INFO_INIT(); + this->port_info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->port_info.params = this->port_params; + this->port_info.n_params = N_PORT_PARAMS; + + spa_list_init(&this->ready); + + return spa_alsa_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <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(¶ms)); + + spa_alsa_parse_prop_params(this, params); + if (lat_ns != -1) { + struct spa_process_latency_info info; + info = this->process_latency; + info.ns = lat_ns; + handle_process_latency(this, &info); + } + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + case SPA_PARAM_ProcessLatency: + { + struct spa_process_latency_info info; + if (param == NULL) + spa_zero(info); + else if ((res = spa_process_latency_parse(param, &info)) < 0) + return res; + + handle_process_latency(this, &info); + + emit_node_info(this, false); + emit_port_info(this, false); + break; + } + default: + return -ENOENT; + } + + return 0; +} + +static int impl_node_send_command(void *object, const struct spa_command *command) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(command != NULL, -EINVAL); + + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_ParamBegin: + if ((res = spa_alsa_open(this, NULL)) < 0) + return res; + break; + case SPA_NODE_COMMAND_ParamEnd: + if (this->have_format) + return 0; + if ((res = spa_alsa_close(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Start: + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + if ((res = spa_alsa_start(this)) < 0) + return res; + break; + case SPA_NODE_COMMAND_Pause: + case SPA_NODE_COMMAND_Suspend: + if ((res = spa_alsa_pause(this)) < 0) + return res; + break; + default: + return -ENOTSUP; + } + return 0; +} + +static int +impl_node_add_listener(void *object, + struct spa_hook *listener, + const struct spa_node_events *events, + void *data) +{ + struct state *this = object; + struct spa_hook_list save; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_hook_list_isolate(&this->hooks, &save, listener, events, data); + + emit_node_info(this, true); + emit_port_info(this, true); + + spa_hook_list_join(&this->hooks, &save); + + return 0; +} + +static int +impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); + + return 0; +} + +static int impl_node_sync(void *object, int seq) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); + + return 0; +} + + +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) +{ + return -ENOTSUP; +} + +static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) +{ + return -ENOTSUP; +} + +static int +impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct state *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[1024]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_EnumFormat: + return spa_alsa_enum_format(this, seq, start, num, filter); + + case SPA_PARAM_Format: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_format_audio_raw_build(&b, id, &this->current_format.info.raw); + break; + + case SPA_PARAM_Buffers: + if (!this->have_format) + return -EIO; + if (result.index > 0) + return 0; + + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(this->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + this->quantum_limit * this->frame_size, + 16 * this->frame_size, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(this->frame_size)); + break; + + case SPA_PARAM_Meta: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_IO: + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); + break; + default: + return 0; + } + break; + + case SPA_PARAM_Latency: + switch (result.index) { + case 0: case 1: + { + struct spa_latency_info latency = this->latency[result.index]; + if (latency.direction == SPA_DIRECTION_OUTPUT) + spa_process_latency_info_add(&this->process_latency, &latency); + param = spa_latency_build(&b, id, &latency); + break; + } + default: + return 0; + } + break; + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int clear_buffers(struct state *this) +{ + if (this->n_buffers > 0) { + spa_list_init(&this->free); + spa_list_init(&this->ready); + this->n_buffers = 0; + } + return 0; +} + +static int port_set_format(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, const struct spa_pod *format) +{ + struct state *this = object; + int err = 0; + + if (format == NULL) { + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "clear format"); + spa_alsa_close(this); + clear_buffers(this); + } else { + struct spa_audio_info info = { 0 }; + + if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) + return err; + + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if ((err = spa_alsa_set_format(this, &info, flags)) < 0) + return err; + + this->current_format = info; + } + + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PROPS; + emit_node_info(this, false); + + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.rate = SPA_FRACTION(1, this->rate); + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + if (this->have_format) { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + this->port_params[PORT_Latency].user++; + } else { + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + } + emit_port_info(this, false); + + return err; +} + +static int +impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct state *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + switch (id) { + case SPA_PARAM_Format: + res = port_set_format(this, direction, port_id, flags, param); + break; + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if (param == NULL) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (direction == info.direction) + return -EINVAL; + + this->latency[info.direction] = info; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_params[PORT_Latency].user++; + emit_port_info(this, false); + break; + } + default: + res = -ENOENT; + break; + } + return res; +} + +static int +impl_node_port_use_buffers(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + struct state *this = object; + int res; + uint32_t i; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: use %d buffers", this, n_buffers); + + if (this->n_buffers > 0) { + spa_alsa_pause(this); + if ((res = clear_buffers(this)) < 0) + return res; + } + if (n_buffers > 0 && !this->have_format) + return -EIO; + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; + + for (i = 0; i < n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + struct spa_data *d = buffers[i]->datas; + + b->buf = buffers[i]; + b->id = i; + b->flags = 0; + + b->h = spa_buffer_find_meta_data(b->buf, SPA_META_Header, sizeof(*b->h)); + + if (d[0].data == NULL) { + spa_log_error(this->log, "%p: need mapped memory", this); + return -EINVAL; + } + spa_list_append(&this->free, &b->link); + } + this->n_buffers = n_buffers; + + return 0; +} + +static int +impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); + + spa_log_debug(this->log, "%p: io %d %p %zd", this, id, data, size); + + switch (id) { + case SPA_IO_Buffers: + this->io = data; + break; + case SPA_IO_RateMatch: + this->rate_match = data; + break; + default: + return -ENOENT; + } + return 0; +} + +static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) +{ + struct state *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + spa_return_val_if_fail(port_id == 0, -EINVAL); + + if (this->n_buffers == 0) + return -EIO; + + if (buffer_id >= this->n_buffers) + return -EINVAL; + + spa_alsa_recycle_buffer(this, buffer_id); + + return 0; +} + +static int impl_node_process(void *object) +{ + struct state *this = object; + struct spa_io_buffers *io; + struct buffer *b; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + if ((io = this->io) == NULL) + return -EIO; + + spa_log_trace_fp(this->log, "%p; status %d", this, io->status); + + if (io->status == SPA_STATUS_HAVE_DATA) + return SPA_STATUS_HAVE_DATA; + + if (io->buffer_id < this->n_buffers) { + spa_alsa_recycle_buffer(this, io->buffer_id); + io->buffer_id = SPA_ID_INVALID; + } + + if (spa_list_is_empty(&this->ready) && this->following) { + if (this->freewheel) + spa_alsa_skip(this); + else + spa_alsa_read(this); + } + if (spa_list_is_empty(&this->ready) || !this->following) + return SPA_STATUS_OK; + + b = spa_list_first(&this->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + spa_log_trace_fp(this->log, "%p: dequeue buffer %d", this, b->id); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + + return SPA_STATUS_HAVE_DATA; +} + +static const struct spa_node_methods impl_node = { + SPA_VERSION_NODE_METHODS, + .add_listener = impl_node_add_listener, + .set_callbacks = impl_node_set_callbacks, + .sync = impl_node_sync, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, + .set_io = impl_node_set_io, + .send_command = impl_node_send_command, + .add_port = impl_node_add_port, + .remove_port = impl_node_remove_port, + .port_enum_params = impl_node_port_enum_params, + .port_set_param = impl_node_port_set_param, + .port_use_buffers = impl_node_port_use_buffers, + .port_set_io = impl_node_port_set_io, + .port_reuse_buffer = impl_node_port_reuse_buffer, + .process = impl_node_process, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct state *this; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + this = (struct state *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) + *interface = &this->node; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + struct state *this; + spa_return_val_if_fail(handle != NULL, -EINVAL); + this = (struct state *) handle; + spa_alsa_close(this); + spa_alsa_clear(this); + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct state); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct state *this; + + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(handle != NULL, -EINVAL); + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + this = (struct state *) handle; + + this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + alsa_log_topic_init(this->log); + + this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); + this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + + if (this->data_loop == NULL) { + spa_log_error(this->log, "%p: a data loop is needed", this); + return -EINVAL; + } + if (this->data_system == NULL) { + spa_log_error(this->log, "%p: a data system is needed", this); + return -EINVAL; + } + + this->node.iface = SPA_INTERFACE_INIT(SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, &impl_node, this); + + spa_hook_list_init(&this->hooks); + this->stream = SND_PCM_STREAM_CAPTURE; + this->port_direction = SPA_DIRECTION_OUTPUT; + this->latency[this->port_direction] = SPA_LATENCY_INFO( + this->port_direction, + .min_quantum = 1.0f, + .max_quantum = 1.0f); + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + + this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | + SPA_NODE_CHANGE_MASK_PROPS | + SPA_NODE_CHANGE_MASK_PARAMS; + this->info.max_output_ports = 1; + this->info.flags = SPA_NODE_FLAG_RT; + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->params[NODE_ProcessLatency] = SPA_PARAM_INFO(SPA_PARAM_ProcessLatency, SPA_PARAM_INFO_READWRITE); + this->info.params = this->params; + this->info.n_params = N_NODE_PARAMS; + reset_props(&this->props); + + this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS | + SPA_PORT_CHANGE_MASK_PARAMS; + this->port_info = SPA_PORT_INFO_INIT(); + this->port_info.flags = SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); + this->port_params[PORT_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); + this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); + this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); + this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + this->port_params[PORT_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); + this->port_info.params = this->port_params; + this->port_info.n_params = N_PORT_PARAMS; + + spa_list_init(&this->free); + spa_list_init(&this->ready); + + return spa_alsa_init(this, info); +} + +static const struct spa_interface_info impl_interfaces[] = { + {SPA_TYPE_INTERFACE_Node,}, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + if (*index >= SPA_N_ELEMENTS(impl_interfaces)) + return 0; + + *info = &impl_interfaces[(*index)++]; + + return 1; +} + +static const struct spa_dict_item info_items[] = { + { SPA_KEY_FACTORY_AUTHOR, "Wim Taymans <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(¶ms); + CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available"); + + debug_hw_params(state, __func__, params); + + CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample"); + + if (state->default_channels != 0) { + rchannels = state->default_channels; + CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &rchannels), "set_channels"); + if (state->default_channels != rchannels) { + spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)", + state->props.device, state->default_channels, rchannels); + } + } + if (state->default_rate != 0) { + rrate = state->default_rate; + CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &rrate, 0), "set_rate_near"); + if (state->default_rate != rrate) { + spa_log_warn(state->log, "%s: Rate doesn't match (requested %u, got %u)", + state->props.device, state->default_rate, rrate); + } + } + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + 0); + + snd_pcm_format_mask_alloca(&fmask); + snd_pcm_hw_params_get_format_mask(params, fmask); + + snd_pcm_access_mask_alloca(&amask); + snd_pcm_hw_params_get_access_mask(params, amask); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_format, 0); + + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); + choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); + + j = 0; + SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) { + if (fi->format == SND_PCM_FORMAT_UNKNOWN) + continue; + + if (snd_pcm_format_mask_test(fmask, fi->format)) { + if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED) || + snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_NONINTERLEAVED)) && + fi->spa_pformat != SPA_AUDIO_FORMAT_UNKNOWN && + (state->default_format == 0 || state->default_format == fi->spa_pformat)) { + if (j++ == 0) + spa_pod_builder_id(b, fi->spa_pformat); + spa_pod_builder_id(b, fi->spa_pformat); + } + if ((snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_MMAP_INTERLEAVED) || + snd_pcm_access_mask_test(amask, SND_PCM_ACCESS_RW_INTERLEAVED)) && + (state->default_format == 0 || state->default_format == fi->spa_format)) { + if (j++ == 0) + spa_pod_builder_id(b, fi->spa_format); + spa_pod_builder_id(b, fi->spa_format); + } + } + } + if (j == 0) { + char buf[1024]; + int i, r, offs; + + for (i = 0, offs = 0; i <= SND_PCM_FORMAT_LAST; i++) { + if (snd_pcm_format_mask_test(fmask, (snd_pcm_format_t)i)) { + r = snprintf(&buf[offs], sizeof(buf) - offs, + "%s ", snd_pcm_format_name((snd_pcm_format_t)i)); + if (r < 0 || r + offs >= (int)sizeof(buf)) + return -ENOSPC; + offs += r; + } + } + spa_log_warn(state->log, "%s: no format found (def:%d) formats:%s", + state->props.device, state->default_format, buf); + + for (i = 0, offs = 0; i <= SND_PCM_ACCESS_LAST; i++) { + if (snd_pcm_access_mask_test(amask, (snd_pcm_access_t)i)) { + r = snprintf(&buf[offs], sizeof(buf) - offs, + "%s ", snd_pcm_access_name((snd_pcm_access_t)i)); + if (r < 0 || r + offs >= (int)sizeof(buf)) + return -ENOSPC; + offs += r; + } + } + spa_log_warn(state->log, "%s: access:%s", state->props.device, buf); + return -ENOTSUP; + } + if (j > 1) + choice->body.type = SPA_CHOICE_Enum; + spa_pod_builder_pop(b, &f[1]); + + if ((res = add_rate(state, 1, 1, false, index & 0xffff, next, 0, params, b)) != 1) + return res; + + if ((res = add_channels(state, false, index & 0xffff, next, params, b)) != 1) + return res; + + *result = spa_pod_builder_pop(b, &f[0]); + return 1; +} + +static bool codec_supported(uint32_t codec, unsigned int chmax, unsigned int rmax) +{ + switch (codec) { + case SPA_AUDIO_IEC958_CODEC_PCM: + case SPA_AUDIO_IEC958_CODEC_DTS: + case SPA_AUDIO_IEC958_CODEC_AC3: + case SPA_AUDIO_IEC958_CODEC_MPEG: + case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC: + if (chmax >= 2) + return true; + break; + case SPA_AUDIO_IEC958_CODEC_EAC3: + if (rmax >= 48000 * 4 && chmax >= 2) + return true; + break; + case SPA_AUDIO_IEC958_CODEC_TRUEHD: + case SPA_AUDIO_IEC958_CODEC_DTSHD: + if (chmax >= 8) + return true; + break; + } + return false; +} + +static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *next, + struct spa_pod **result, struct spa_pod_builder *b) +{ + int res, err, dir; + snd_pcm_t *hndl; + snd_pcm_hw_params_t *params; + struct spa_pod_frame f[2]; + unsigned int rmin, rmax; + unsigned int chmin, chmax; + uint32_t i, c, codecs[16], n_codecs; + + if ((index & 0xffff) > 0) + return 0; + + if (!(state->is_iec958 || state->is_hdmi)) + return 0; + if (state->iec958_codecs == 0) + return 0; + + hndl = state->hndl; + snd_pcm_hw_params_alloca(¶ms); + CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available"); + + debug_hw_params(state, __func__, params); + + CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample"); + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_iec958), + 0); + + CHECK(snd_pcm_hw_params_get_channels_min(params, &chmin), "get_channels_min"); + CHECK(snd_pcm_hw_params_get_channels_max(params, &chmax), "get_channels_max"); + spa_log_debug(state->log, "channels (%d %d)", chmin, chmax); + + CHECK(snd_pcm_hw_params_get_rate_min(params, &rmin, &dir), "get_rate_min"); + CHECK(snd_pcm_hw_params_get_rate_max(params, &rmax, &dir), "get_rate_max"); + spa_log_debug(state->log, "rate (%d %d)", rmin, rmax); + + if (state->default_rate != 0) { + if (rmin < state->default_rate) + rmin = state->default_rate; + if (rmax > state->default_rate) + rmax = state->default_rate; + } + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_iec958Codec, 0); + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + + n_codecs = spa_alsa_get_iec958_codecs(state, codecs, SPA_N_ELEMENTS(codecs)); + for (i = 0, c = 0; i < n_codecs; i++) { + if (!codec_supported(codecs[i], chmax, rmax)) + continue; + if (c++ == 0) + spa_pod_builder_id(b, codecs[i]); + spa_pod_builder_id(b, codecs[i]); + } + spa_pod_builder_pop(b, &f[1]); + + if ((res = add_rate(state, 1, 1, true, index & 0xffff, next, 0, params, b)) != 1) + return res; + + (*next)++; + *result = spa_pod_builder_pop(b, &f[0]); + return 1; +} + +static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next, + struct spa_pod **result, struct spa_pod_builder *b) +{ + int res, err; + snd_pcm_t *hndl; + snd_pcm_hw_params_t *params; + snd_pcm_format_mask_t *fmask; + struct spa_pod_frame f[2]; + int32_t interleave; + + if ((index & 0xffff) > 0) + return 0; + + hndl = state->hndl; + snd_pcm_hw_params_alloca(¶ms); + CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration: no configurations available"); + + debug_hw_params(state, __func__, params); + + snd_pcm_format_mask_alloca(&fmask); + snd_pcm_hw_params_get_format_mask(params, fmask); + + if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_BE)) + interleave = 4; + else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U32_LE)) + interleave = -4; + else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_BE)) + interleave = 2; + else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U16_LE)) + interleave = -2; + else if (snd_pcm_format_mask_test(fmask, SND_PCM_FORMAT_DSD_U8)) + interleave = 1; + else + return 0; + + CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample"); + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsd), + 0); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_bitorder, 0); + spa_pod_builder_id(b, SPA_PARAM_BITORDER_msb); + + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_interleave, 0); + spa_pod_builder_int(b, interleave); + + /* Use a lower rate limit of 352800 (= 44100 * 64 / 8). This is because in + * PipeWire, DSD rates are given in bytes, not bits, so 352800 corresponds + * to the bit rate of DSD64. (The "64" in DSD64 means "64 times the rate + * of 44.1 kHz".) Some hardware may report rates lower than that, for example + * 176400. This would correspond to "DSD32" (which does not exist). Trying + * to use such a rate with DSD hardware does not work and may cause undefined + * behavior in said hardware. */ + if ((res = add_rate(state, 8, SPA_ABS(interleave), true, index & 0xffff, + next, 44100, params, b)) != 1) + return res; + + if ((res = add_channels(state, true, index & 0xffff, next, params, b)) != 1) + return res; + + *result = spa_pod_builder_pop(b, &f[0]); + return 1; +} + +int +spa_alsa_enum_format(struct state *state, int seq, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + uint8_t buffer[4096]; + struct spa_pod_builder b = { 0 }; + struct spa_pod *fmt; + int err, res; + bool opened; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened, + state->have_format, state->started); + + opened = state->opened; + if (!state->started && state->have_format) + spa_alsa_close(state); + if ((err = spa_alsa_open(state, NULL)) < 0) + return err; + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + if (result.index < 0x10000) { + if ((res = enum_pcm_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x10000; + goto next; + } + } + else if (result.index < 0x20000) { + if ((res = enum_iec958_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x20000; + goto next; + } + } + else if (result.index < 0x30000) { + if ((res = enum_dsd_formats(state, result.index, &result.next, &fmt, &b)) != 1) { + result.next = 0x30000; + goto next; + } + } + else + goto enum_end; + + if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) + goto next; + + spa_node_emit_result(&state->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + enum_end: + res = 0; + if (!opened) + spa_alsa_close(state); + return res; +} + +int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags) +{ + unsigned int rrate, rchannels, val, rscale = 1; + snd_pcm_uframes_t period_size; + int err, dir; + snd_pcm_hw_params_t *params; + snd_pcm_format_t rformat; + snd_pcm_access_mask_t *amask; + snd_pcm_t *hndl; + unsigned int periods; + bool match = true, planar = false, is_batch; + char spdif_params[128] = ""; + + spa_log_debug(state->log, "opened:%d format:%d started:%d", state->opened, + state->have_format, state->started); + + state->use_mmap = !state->disable_mmap; + + switch (fmt->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + { + struct spa_audio_info_raw *f = &fmt->info.raw; + rrate = f->rate; + rchannels = f->channels; + rformat = spa_format_to_alsa(f->format, &planar); + break; + } + case SPA_MEDIA_SUBTYPE_iec958: + { + struct spa_audio_info_iec958 *f = &fmt->info.iec958; + unsigned aes3; + + spa_log_info(state->log, "using IEC958 Codec:%s rate:%d", + spa_debug_type_find_short_name(spa_type_audio_iec958_codec, f->codec), + f->rate); + + rformat = SND_PCM_FORMAT_S16_LE; + rchannels = 2; + rrate = f->rate; + + switch (f->codec) { + case SPA_AUDIO_IEC958_CODEC_PCM: + case SPA_AUDIO_IEC958_CODEC_DTS: + case SPA_AUDIO_IEC958_CODEC_AC3: + case SPA_AUDIO_IEC958_CODEC_MPEG: + case SPA_AUDIO_IEC958_CODEC_MPEG2_AAC: + break; + case SPA_AUDIO_IEC958_CODEC_EAC3: + /* EAC3 has 3 rates, 32, 44.1 and 48KHz. We need to + * open the device in 4x that rate. Some clients + * already multiply (mpv,..) others don't (vlc). */ + if (rrate <= 48000) + rrate *= 4; + break; + case SPA_AUDIO_IEC958_CODEC_TRUEHD: + case SPA_AUDIO_IEC958_CODEC_DTSHD: + rchannels = 8; + break; + default: + return -ENOTSUP; + } + switch (rrate) { + case 22050: aes3 = IEC958_AES3_CON_FS_22050; break; + case 24000: aes3 = IEC958_AES3_CON_FS_24000; break; + case 32000: aes3 = IEC958_AES3_CON_FS_32000; break; + case 44100: aes3 = IEC958_AES3_CON_FS_44100; break; + case 48000: aes3 = IEC958_AES3_CON_FS_48000; break; + case 88200: aes3 = IEC958_AES3_CON_FS_88200; break; + case 96000: aes3 = IEC958_AES3_CON_FS_96000; break; + case 176400: aes3 = IEC958_AES3_CON_FS_176400; break; + case 192000: aes3 = IEC958_AES3_CON_FS_192000; break; + case 768000: aes3 = IEC958_AES3_CON_FS_768000; break; + default: aes3 = IEC958_AES3_CON_FS_NOTID; break; + } + spa_scnprintf(spdif_params, sizeof(spdif_params), + ",AES0=0x%x,AES1=0x%x,AES2=0x%x,AES3=0x%x", + IEC958_AES0_CON_EMPHASIS_NONE | IEC958_AES0_NONAUDIO, + IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER, + 0, aes3); + break; + } + case SPA_MEDIA_SUBTYPE_dsd: + { + struct spa_audio_info_dsd *f = &fmt->info.dsd; + + rrate = f->rate; + rchannels = f->channels; + + switch (f->interleave) { + case 4: + rformat = SND_PCM_FORMAT_DSD_U32_BE; + rrate /= 4; + rscale = 4; + break; + case -4: + rformat = SND_PCM_FORMAT_DSD_U32_LE; + rrate /= 4; + rscale = 4; + break; + case 2: + rformat = SND_PCM_FORMAT_DSD_U16_BE; + rrate /= 2; + rscale = 2; + break; + case -2: + rformat = SND_PCM_FORMAT_DSD_U16_LE; + rrate /= 2; + rscale = 2; + break; + case 1: + rformat = SND_PCM_FORMAT_DSD_U8; + rscale = 1; + break; + default: + return -ENOTSUP; + } + break; + } + default: + return -ENOTSUP; + } + + if (rformat == SND_PCM_FORMAT_UNKNOWN) { + spa_log_warn(state->log, "%s: unknown format", + state->props.device); + return -EINVAL; + } + + if (!state->started && state->have_format) + spa_alsa_close(state); + if ((err = spa_alsa_open(state, spdif_params)) < 0) + return err; + + hndl = state->hndl; + + snd_pcm_hw_params_alloca(¶ms); + /* choose all parameters */ + CHECK(snd_pcm_hw_params_any(hndl, params), "Broken configuration for playback: no configurations available"); + + debug_hw_params(state, __func__, params); + + /* set hardware resampling, no resample */ + CHECK(snd_pcm_hw_params_set_rate_resample(hndl, params, 0), "set_rate_resample"); + + /* set the interleaved/planar read/write format */ + snd_pcm_access_mask_alloca(&amask); + snd_pcm_hw_params_get_access_mask(params, amask); + + if (state->use_mmap) { + if ((err = snd_pcm_hw_params_set_access(hndl, params, + planar ? SND_PCM_ACCESS_MMAP_NONINTERLEAVED + : SND_PCM_ACCESS_MMAP_INTERLEAVED)) < 0) { + spa_log_debug(state->log, "%p: MMAP not possible: %s", state, + snd_strerror(err)); + state->use_mmap = false; + } + } + if (!state->use_mmap) { + if ((err = snd_pcm_hw_params_set_access(hndl, params, + planar ? SND_PCM_ACCESS_RW_NONINTERLEAVED + : SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + spa_log_error(state->log, "%s: RW not possible: %s", + state->props.device, snd_strerror(err)); + return err; + } + } + + /* set the sample format */ + spa_log_debug(state->log, "%p: Stream parameters are %iHz fmt:%s access:%s-%s channels:%i", + state, rrate, snd_pcm_format_name(rformat), + state->use_mmap ? "mmap" : "rw", + planar ? "planar" : "interleaved", rchannels); + CHECK(snd_pcm_hw_params_set_format(hndl, params, rformat), "set_format"); + + /* set the count of channels */ + val = rchannels; + CHECK(snd_pcm_hw_params_set_channels_near(hndl, params, &val), "set_channels"); + if (rchannels != val) { + spa_log_warn(state->log, "%s: Channels doesn't match (requested %u, got %u)", + state->props.device, rchannels, val); + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) + return -EINVAL; + if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + rchannels = val; + fmt->info.raw.channels = rchannels; + match = false; + } + + if (!state->multi_rate && + state->card->format_ref > 0 && + state->card->rate != rrate) { + spa_log_error(state->log, "%p: card already opened at rate:%i", + state, state->card->rate); + return -EINVAL; + } + + /* set the stream rate */ + val = rrate; + CHECK(snd_pcm_hw_params_set_rate_near(hndl, params, &val, 0), "set_rate_near"); + if (rrate != val) { + spa_log_warn(state->log, "%s: Rate doesn't match (requested %iHz, got %iHz)", + state->props.device, rrate, val); + if (!SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_NEAREST)) + return -EINVAL; + if (fmt->media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + rrate = val; + fmt->info.raw.rate = rrate; + match = false; + } + if (rchannels == 0 || rrate == 0) { + spa_log_error(state->log, "%s: invalid channels:%d or rate:%d", + state->props.device, rchannels, rrate); + return -EIO; + } + + state->format = rformat; + state->channels = rchannels; + state->rate = rrate; + state->frame_size = snd_pcm_format_physical_width(rformat) / 8; + state->frame_scale = rscale; + state->planar = planar; + state->blocks = 1; + if (planar) + state->blocks *= rchannels; + else + state->frame_size *= rchannels; + + state->have_format = true; + if (state->card->format_ref++ == 0) + state->card->rate = rrate; + + dir = 0; + period_size = state->default_period_size; + is_batch = snd_pcm_hw_params_is_batch(params) && + !state->disable_batch; + + if (is_batch) { + if (period_size == 0) + period_size = state->position ? state->position->clock.duration : DEFAULT_PERIOD; + if (period_size == 0) + period_size = DEFAULT_PERIOD; + /* batch devices get their hw pointers updated every period. Make + * the period smaller and add one period of headroom. Limit the + * period size to our default so that we don't create too much + * headroom. */ + period_size = SPA_MIN(period_size, DEFAULT_PERIOD) / 2; + spa_log_info(state->log, "%s: batch mode, period_size:%ld", + state->props.device, period_size); + } else { + if (period_size == 0) + period_size = DEFAULT_PERIOD; + /* disable ALSA wakeups, we use a timer */ + if (snd_pcm_hw_params_can_disable_period_wakeup(params)) + CHECK(snd_pcm_hw_params_set_period_wakeup(hndl, params, 0), "set_period_wakeup"); + } + + CHECK(snd_pcm_hw_params_set_period_size_near(hndl, params, &period_size, &dir), "set_period_size_near"); + + if (period_size == 0) { + spa_log_error(state->log, "%s: invalid period_size 0 (driver error?)", state->props.device); + return -EIO; + } + + state->period_frames = period_size; + + if (state->default_period_num != 0) { + periods = state->default_period_num; + CHECK(snd_pcm_hw_params_set_periods_near(hndl, params, &periods, &dir), "set_periods"); + state->buffer_frames = period_size * periods; + } else { + CHECK(snd_pcm_hw_params_get_buffer_size_max(params, &state->buffer_frames), "get_buffer_size_max"); + + state->buffer_frames = SPA_MIN(state->buffer_frames, state->quantum_limit * 4)* state->frame_scale; + + CHECK(snd_pcm_hw_params_set_buffer_size_min(hndl, params, &state->buffer_frames), "set_buffer_size_min"); + CHECK(snd_pcm_hw_params_set_buffer_size_near(hndl, params, &state->buffer_frames), "set_buffer_size_near"); + periods = state->buffer_frames / period_size; + } + if (state->buffer_frames == 0) { + spa_log_error(state->log, "%s: invalid buffer_frames 0 (driver error?)", state->props.device); + return -EIO; + } + + state->headroom = state->default_headroom; + if (is_batch) + state->headroom += period_size; + + if (spa_strstartswith(state->props.device, "a52") || + spa_strstartswith(state->props.device, "dca")) + state->min_delay = SPA_MIN(2048u, state->buffer_frames); + else + state->min_delay = 0; + + state->headroom = SPA_MIN(state->headroom, state->buffer_frames); + state->start_delay = state->default_start_delay; + + state->latency[state->port_direction].min_rate = + state->latency[state->port_direction].max_rate = + SPA_MAX(state->min_delay, state->headroom); + + spa_log_info(state->log, "%s (%s): format:%s access:%s-%s rate:%d channels:%d " + "buffer frames %lu, period frames %lu, periods %u, frame_size %zd " + "headroom %u start-delay:%u", + state->props.device, + state->stream == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", + snd_pcm_format_name(state->format), + state->use_mmap ? "mmap" : "rw", + planar ? "planar" : "interleaved", + state->rate, state->channels, state->buffer_frames, state->period_frames, + periods, state->frame_size, state->headroom, state->start_delay); + + /* write the parameters to device */ + CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params"); + + return match ? 0 : 1; +} + +static int set_swparams(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + int err = 0; + snd_pcm_sw_params_t *params; + + snd_pcm_sw_params_alloca(¶ms); + + /* get the current params */ + CHECK(snd_pcm_sw_params_current(hndl, params), "sw_params_current"); + + CHECK(snd_pcm_sw_params_set_tstamp_mode(hndl, params, SND_PCM_TSTAMP_ENABLE), + "sw_params_set_tstamp_mode"); + CHECK(snd_pcm_sw_params_set_tstamp_type(hndl, params, SND_PCM_TSTAMP_TYPE_MONOTONIC), + "sw_params_set_tstamp_type"); +#if 0 + snd_pcm_uframes_t boundary; + CHECK(snd_pcm_sw_params_get_boundary(params, &boundary), "get_boundary"); + + CHECK(snd_pcm_sw_params_set_stop_threshold(hndl, params, boundary), "set_stop_threshold"); +#endif + + /* start the transfer */ + CHECK(snd_pcm_sw_params_set_start_threshold(hndl, params, LONG_MAX), "set_start_threshold"); + + CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event"); + + /* write the parameters to the playback device */ + CHECK(snd_pcm_sw_params(hndl, params), "sw_params"); + + if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_DEBUG))) { + spa_log_debug(state->log, "state after sw_params:"); + snd_pcm_dump(hndl, state->output); + fflush(state->log_file); + } + + return 0; +} + +static int set_timeout(struct state *state, uint64_t time) +{ + struct itimerspec ts; + + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, + state->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); + return 0; +} + +int spa_alsa_silence(struct state *state, snd_pcm_uframes_t silence) +{ + snd_pcm_t *hndl = state->hndl; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t frames, offset; + int i, res; + + if (state->use_mmap) { + frames = state->buffer_frames; + + if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + silence = SPA_MIN(silence, frames); + + spa_log_trace_fp(state->log, "%p: frames:%ld offset:%ld silence %ld", + state, frames, offset, silence); + snd_pcm_areas_silence(my_areas, offset, state->channels, silence, state->format); + + if (SPA_UNLIKELY((res = snd_pcm_mmap_commit(hndl, offset, silence)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->props.device, snd_strerror(res)); + return res; + } + } else { + uint8_t buffer[silence * state->frame_size]; + memset(buffer, 0, silence * state->frame_size); + + if (state->planar) { + void *bufs[state->channels]; + for (i = 0; i < state->channels; i++) + bufs[i] = buffer; + snd_pcm_writen(hndl, bufs, silence); + } else { + snd_pcm_writei(hndl, buffer, silence); + } + } + return 0; +} + +static inline int do_start(struct state *state) +{ + int res; + if (SPA_UNLIKELY(!state->alsa_started)) { + spa_log_trace(state->log, "%p: snd_pcm_start", state); + if ((res = snd_pcm_start(state->hndl)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_start: %s", + state->props.device, snd_strerror(res)); + return res; + } + state->alsa_started = true; + } + return 0; +} + +static int alsa_recover(struct state *state, int err) +{ + int res, st; + snd_pcm_status_t *status; + + snd_pcm_status_alloca(&status); + if (SPA_UNLIKELY((res = snd_pcm_status(state->hndl, status)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_status error: %s", + state->props.device, snd_strerror(res)); + goto recover; + } + + st = snd_pcm_status_get_state(status); + switch (st) { + case SND_PCM_STATE_XRUN: + { + struct timeval now, trigger, diff; + uint64_t delay, missing; + + snd_pcm_status_get_tstamp (status, &now); + snd_pcm_status_get_trigger_tstamp (status, &trigger); + timersub(&now, &trigger, &diff); + + delay = SPA_TIMEVAL_TO_USEC(&diff); + missing = delay * state->rate / SPA_USEC_PER_SEC; + + spa_log_trace(state->log, "%p: xrun of %"PRIu64" usec %"PRIu64, + state, delay, missing); + + spa_node_call_xrun(&state->callbacks, + SPA_TIMEVAL_TO_USEC(&trigger), delay, NULL); + + state->sample_count += missing ? missing : state->threshold; + break; + } + case SND_PCM_STATE_SUSPENDED: + spa_log_info(state->log, "%s: recover from state %s", + state->props.device, snd_pcm_state_name(st)); + res = snd_pcm_resume(state->hndl); + if (res >= 0) + return res; + err = -ESTRPIPE; + break; + default: + spa_log_error(state->log, "%s: recover from error state %s", + state->props.device, snd_pcm_state_name(st)); + break; + } + +recover: + if (SPA_UNLIKELY((res = snd_pcm_recover(state->hndl, err, true)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_recover error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_dll_init(&state->dll); + state->alsa_recovering = true; + state->alsa_started = false; + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom); + + return do_start(state); +} + +static int get_avail(struct state *state, uint64_t current_time) +{ + int res, missed; + snd_pcm_sframes_t avail; + + if (SPA_UNLIKELY((avail = snd_pcm_avail(state->hndl)) < 0)) { + if ((res = alsa_recover(state, avail)) < 0) + return res; + if ((avail = snd_pcm_avail(state->hndl)) < 0) { + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d missed) snd_pcm_avail after recover: %s", + state->props.device, missed, snd_strerror(avail)); + } + avail = state->threshold * 2; + } + } else { + state->alsa_recovering = false; + } + return avail; +} + +#if 0 +static int get_avail_htimestamp(struct state *state, uint64_t current_time) +{ + int res, missed; + snd_pcm_uframes_t avail; + snd_htimestamp_t tstamp; + uint64_t then; + + if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) { + if ((res = alsa_recover(state, avail)) < 0) + return res; + if ((res = snd_pcm_htimestamp(state->hndl, &avail, &tstamp)) < 0) { + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_warn(state->log, "%s: (%d missed) snd_pcm_htimestamp error: %s", + state->props.device, missed, snd_strerror(res)); + } + avail = state->threshold * 2; + } + } else { + state->alsa_recovering = false; + } + + if ((then = SPA_TIMESPEC_TO_NSEC(&tstamp)) != 0) { + if (then < current_time) + avail += (current_time - then) * state->rate / SPA_NSEC_PER_SEC; + else + avail -= (then - current_time) * state->rate / SPA_NSEC_PER_SEC; + } + return SPA_MIN(avail, state->buffer_frames); +} +#endif + +static int get_status(struct state *state, uint64_t current_time, + snd_pcm_uframes_t *delay, snd_pcm_uframes_t *target) +{ + int avail; + + if ((avail = get_avail(state, current_time)) < 0) + return avail; + + avail = SPA_MIN(avail, (int)state->buffer_frames); + + *target = state->threshold + state->headroom; + + if (state->resample && state->rate_match) { + state->delay = state->rate_match->delay; + state->read_size = state->rate_match->size; + } else { + state->delay = 0; + state->read_size = state->threshold; + } + + if (state->stream == SND_PCM_STREAM_PLAYBACK) { + *delay = state->buffer_frames - avail; + } else { + *delay = avail; + *target = SPA_MAX(*target, state->read_size); + } + *target = SPA_CLAMP(*target, state->min_delay, state->buffer_frames); + return 0; +} + +static int update_time(struct state *state, uint64_t current_time, snd_pcm_sframes_t delay, + snd_pcm_sframes_t target, bool follower) +{ + double err, corr; + int32_t diff; + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + err = delay - target; + else + err = target - delay; + + if (SPA_UNLIKELY(state->dll.bw == 0.0)) { + spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, state->rate); + state->next_time = current_time; + state->base_time = current_time; + } + diff = (int32_t) (state->last_threshold - state->threshold); + + if (SPA_UNLIKELY(diff != 0)) { + err -= diff; + spa_log_trace(state->log, "%p: follower:%d quantum change %d -> %d (%d) %f", + state, follower, state->last_threshold, state->threshold, diff, err); + state->last_threshold = state->threshold; + state->alsa_sync = true; + state->alsa_sync_warning = false; + } + if (err > state->max_error) { + err = state->max_error; + state->alsa_sync = true; + } else if (err < -state->max_error) { + err = -state->max_error; + state->alsa_sync = true; + } + + if (!follower || state->matching) + corr = spa_dll_update(&state->dll, err); + else + corr = 1.0; + + if (diff < 0) + state->next_time += diff / corr * 1e9 / state->rate; + + if (SPA_UNLIKELY((state->next_time - state->base_time) > BW_PERIOD)) { + state->base_time = state->next_time; + + spa_log_debug(state->log, "%s: follower:%d match:%d rate:%f " + "bw:%f thr:%u del:%ld target:%ld err:%f max:%f", + state->props.device, follower, state->matching, + corr, state->dll.bw, state->threshold, delay, target, + err, state->max_error); + } + + if (state->rate_match) { + if (state->stream == SND_PCM_STREAM_PLAYBACK) + state->rate_match->rate = corr; + else + state->rate_match->rate = 1.0/corr; + + SPA_FLAG_UPDATE(state->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, state->matching); + } + + state->next_time += state->threshold / corr * 1e9 / state->rate; + + if (SPA_LIKELY(!follower && state->clock)) { + state->clock->nsec = current_time; + state->clock->position += state->duration; + state->clock->duration = state->duration; + state->clock->delay = delay + state->delay; + state->clock->rate_diff = corr; + state->clock->next_nsec = state->next_time; + } + + spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %f %ld %f %f %u", + state, follower, current_time, corr, delay, err, state->threshold * corr, + state->threshold); + + return 0; +} + +static inline bool is_following(struct state *state) +{ + return state->position && state->clock && state->position->clock.id != state->clock->id; +} + +static int setup_matching(struct state *state) +{ + state->matching = state->following; + + if (state->position == NULL) + return -ENOTSUP; + + spa_log_debug(state->log, "driver clock:'%s' our clock:'%s'", + state->position->clock.name, state->clock_name); + + if (spa_streq(state->position->clock.name, state->clock_name)) + state->matching = false; + + state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching; + + spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d", + state->position->clock.name, state->rate_denom, + state->clock_name, state->rate, + state->matching, state->resample); + return 0; +} + +static inline void check_position_config(struct state *state) +{ + if (SPA_UNLIKELY(state->position == NULL)) + return; + + if (SPA_UNLIKELY((state->duration != state->position->clock.duration) || + (state->rate_denom != state->position->clock.rate.denom))) { + state->duration = state->position->clock.duration; + state->rate_denom = state->position->clock.rate.denom; + state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom); + state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f); + state->resample = ((uint32_t)state->rate != state->rate_denom) || state->matching; + state->alsa_sync = true; + } +} + +int spa_alsa_write(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t written, frames, offset, off, to_write, total_written, max_write; + snd_pcm_sframes_t commitres; + int res = 0, missed; + size_t frame_size = state->frame_size; + + check_position_config(state); + + max_write = state->buffer_frames; + + if (state->following && state->alsa_started) { + uint64_t current_time; + snd_pcm_uframes_t delay, target; + + current_time = state->position->clock.nsec; + + if (SPA_UNLIKELY((res = get_status(state, current_time, &delay, &target)) < 0)) + return res; + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0)) + return res; + + if (SPA_UNLIKELY(state->alsa_sync)) { + enum spa_log_level lev; + + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; + + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, " + "resync (%d missed)", state->props.device, delay, + target, state->threshold, missed); + } + + if (delay > target) + snd_pcm_rewind(state->hndl, delay - target); + else if (delay < target) + spa_alsa_silence(state, target - delay); + delay = target; + state->alsa_sync = false; + } else + state->alsa_sync_warning = true; + } + + total_written = 0; +again: + + frames = max_write; + if (state->use_mmap && frames > 0) { + if (SPA_UNLIKELY((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_log_trace_fp(state->log, "%p: begin %ld %ld %d", + state, offset, frames, state->threshold); + off = offset; + } else { + off = 0; + } + + to_write = frames; + written = 0; + + while (!spa_list_is_empty(&state->ready) && to_write > 0) { + size_t n_bytes, n_frames; + struct buffer *b; + struct spa_data *d; + uint32_t i, offs, size, last_offset; + + b = spa_list_first(&state->ready, struct buffer, link); + d = b->buf->datas; + + offs = d[0].chunk->offset + state->ready_offset; + last_offset = d[0].chunk->size; + size = last_offset - state->ready_offset; + + offs = SPA_MIN(offs, d[0].maxsize); + size = SPA_MIN(d[0].maxsize - offs, size); + + n_frames = SPA_MIN(size / frame_size, to_write); + n_bytes = n_frames * frame_size; + + if (SPA_LIKELY(state->use_mmap)) { + for (i = 0; i < b->buf->n_datas; i++) { + spa_memcpy(SPA_PTROFF(my_areas[i].addr, off * frame_size, void), + SPA_PTROFF(d[i].data, offs, void), n_bytes); + } + } else { + void *bufs[b->buf->n_datas]; + for (i = 0; i < b->buf->n_datas; i++) + bufs[i] = SPA_PTROFF(d[i].data, offs, void); + + if (state->planar) + snd_pcm_writen(hndl, bufs, n_frames); + else + snd_pcm_writei(hndl, bufs[0], n_frames); + } + + state->ready_offset += n_bytes; + + if (state->ready_offset >= last_offset) { + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + state->io->buffer_id = b->id; + spa_log_trace_fp(state->log, "%p: reuse buffer %u", state, b->id); + + spa_node_call_reuse_buffer(&state->callbacks, 0, b->id); + + state->ready_offset = 0; + } + written += n_frames; + off += n_frames; + to_write -= n_frames; + } + + spa_log_trace_fp(state->log, "%p: commit %ld %ld %"PRIi64, + state, offset, written, state->sample_count); + total_written += written; + + if (state->use_mmap && written > 0) { + if (SPA_UNLIKELY((commitres = snd_pcm_mmap_commit(hndl, offset, written)) < 0)) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error: %s", + state->props.device, snd_strerror(commitres)); + if (commitres != -EPIPE && commitres != -ESTRPIPE) + return res; + } + if (commitres > 0 && written != (snd_pcm_uframes_t) commitres) { + spa_log_warn(state->log, "%s: mmap_commit wrote %ld instead of %ld", + state->props.device, commitres, written); + } + } + + if (!spa_list_is_empty(&state->ready) && written > 0) + goto again; + + state->sample_count += total_written; + + if (SPA_UNLIKELY(!state->alsa_started && (total_written > 0 || frames == 0))) + do_start(state); + + return 0; +} + +void spa_alsa_recycle_buffer(struct state *this, uint32_t buffer_id) +{ + struct buffer *b = &this->buffers[buffer_id]; + + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUT)) { + spa_log_trace_fp(this->log, "%p: recycle buffer %u", this, buffer_id); + spa_list_append(&this->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } +} + +static snd_pcm_uframes_t +push_frames(struct state *state, + const snd_pcm_channel_area_t *my_areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t frames) +{ + snd_pcm_uframes_t total_frames = 0; + + if (spa_list_is_empty(&state->free)) { + spa_log_warn(state->log, "%s: no more buffers", state->props.device); + total_frames = frames; + } else { + size_t n_bytes, left, frame_size = state->frame_size; + struct buffer *b; + struct spa_data *d; + uint32_t i, avail, l0, l1; + + b = spa_list_first(&state->free, struct buffer, link); + spa_list_remove(&b->link); + + if (b->h) { + b->h->seq = state->sample_count; + b->h->pts = state->next_time; + b->h->dts_offset = 0; + } + + d = b->buf->datas; + + avail = d[0].maxsize / frame_size; + total_frames = SPA_MIN(avail, frames); + n_bytes = total_frames * frame_size; + + if (my_areas) { + left = state->buffer_frames - offset; + l0 = SPA_MIN(n_bytes, left * frame_size); + l1 = n_bytes - l0; + + for (i = 0; i < b->buf->n_datas; i++) { + spa_memcpy(d[i].data, + SPA_PTROFF(my_areas[i].addr, offset * frame_size, void), + l0); + if (SPA_UNLIKELY(l1 > 0)) + spa_memcpy(SPA_PTROFF(d[i].data, l0, void), + my_areas[i].addr, + l1); + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = frame_size; + } + } else { + void *bufs[b->buf->n_datas]; + for (i = 0; i < b->buf->n_datas; i++) { + bufs[i] = d[i].data; + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = frame_size; + } + if (state->planar) { + snd_pcm_readn(state->hndl, bufs, total_frames); + } else { + snd_pcm_readi(state->hndl, bufs[0], total_frames); + } + } + spa_log_trace_fp(state->log, "%p: wrote %ld frames into buffer %d", + state, total_frames, b->id); + + spa_list_append(&state->ready, &b->link); + } + return total_frames; +} + + +int spa_alsa_read(struct state *state) +{ + snd_pcm_t *hndl = state->hndl; + snd_pcm_uframes_t total_read = 0, to_read, max_read; + const snd_pcm_channel_area_t *my_areas; + snd_pcm_uframes_t read, frames, offset; + snd_pcm_sframes_t commitres; + int res = 0, missed; + + check_position_config(state); + + max_read = state->buffer_frames; + + if (state->following && state->alsa_started) { + uint64_t current_time; + snd_pcm_uframes_t avail, delay, target; + + current_time = state->position->clock.nsec; + + if ((res = get_status(state, current_time, &delay, &target)) < 0) + return res; + + avail = delay; + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, true)) < 0)) + return res; + + if (state->alsa_sync) { + enum spa_log_level lev; + + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; + + if ((missed = ratelimit_test(&state->rate_limit, current_time)) >= 0) { + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u, " + "resync (%d missed)", state->props.device, delay, + target, state->threshold, missed); + } + + if (delay < target) + max_read = target - delay; + else if (delay > target) + snd_pcm_forward(state->hndl, delay - target); + delay = target; + state->alsa_sync = false; + } else + state->alsa_sync_warning = true; + + if (avail < state->read_size) + max_read = 0; + } + + frames = SPA_MIN(max_read, state->read_size); + + if (state->use_mmap) { + to_read = state->buffer_frames; + if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &to_read)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_mmap_begin error: %s", + state->props.device, snd_strerror(res)); + return res; + } + spa_log_trace_fp(state->log, "%p: begin offs:%ld frames:%ld to_read:%ld thres:%d", state, + offset, frames, to_read, state->threshold); + } else { + my_areas = NULL; + offset = 0; + } + + if (frames > 0) { + read = push_frames(state, my_areas, offset, frames); + total_read += read; + } else { + spa_alsa_skip(state); + total_read += state->read_size; + read = 0; + } + + if (state->use_mmap && read > 0) { + spa_log_trace_fp(state->log, "%p: commit offs:%ld read:%ld count:%"PRIi64, state, + offset, read, state->sample_count); + if ((commitres = snd_pcm_mmap_commit(hndl, offset, read)) < 0) { + spa_log_error(state->log, "%s: snd_pcm_mmap_commit error %lu %lu: %s", + state->props.device, frames, read, snd_strerror(commitres)); + if (commitres != -EPIPE && commitres != -ESTRPIPE) + return res; + } + if (commitres > 0 && read != (snd_pcm_uframes_t) commitres) { + spa_log_warn(state->log, "%s: mmap_commit read %ld instead of %ld", + state->props.device, commitres, read); + } + } + + state->sample_count += total_read; + + return 0; +} + +int spa_alsa_skip(struct state *state) +{ + struct buffer *b; + struct spa_data *d; + uint32_t i, avail, total_frames, n_bytes, frames; + + if (spa_list_is_empty(&state->free)) { + spa_log_warn(state->log, "%s: no more buffers", state->props.device); + return -EPIPE; + } + + frames = state->read_size; + + b = spa_list_first(&state->free, struct buffer, link); + spa_list_remove(&b->link); + + d = b->buf->datas; + + avail = d[0].maxsize / state->frame_size; + total_frames = SPA_MIN(avail, frames); + n_bytes = total_frames * state->frame_size; + + for (i = 0; i < b->buf->n_datas; i++) { + memset(d[i].data, 0, n_bytes); + d[i].chunk->offset = 0; + d[i].chunk->size = n_bytes; + d[i].chunk->stride = state->frame_size; + } + spa_list_append(&state->ready, &b->link); + + return 0; +} + + +static int handle_play(struct state *state, uint64_t current_time, + snd_pcm_uframes_t delay, snd_pcm_uframes_t target) +{ + int res; + + if (state->alsa_started && SPA_UNLIKELY(delay > target + state->max_error)) { + spa_log_trace(state->log, "%p: early wakeup %lu %lu", state, delay, target); + if (delay > target * 3) + delay = target * 3; + state->next_time = current_time + (delay - target) * SPA_NSEC_PER_SEC / state->rate; + return -EAGAIN; + } + + if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, false)) < 0)) + return res; + + if (spa_list_is_empty(&state->ready)) { + struct spa_io_buffers *io = state->io; + + spa_log_trace_fp(state->log, "%p: %d", state, io->status); + + io->status = SPA_STATUS_NEED_DATA; + + res = spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA); + } + else { + res = spa_alsa_write(state); + } + return res; +} + +static int handle_capture(struct state *state, uint64_t current_time, + snd_pcm_uframes_t delay, snd_pcm_uframes_t target) +{ + int res; + struct spa_io_buffers *io; + + if (SPA_UNLIKELY(delay < target)) { + spa_log_trace(state->log, "%p: early wakeup %ld %ld", state, delay, target); + state->next_time = current_time + (target - delay) * SPA_NSEC_PER_SEC / + state->rate; + return -EAGAIN; + } + + if (SPA_UNLIKELY(res = update_time(state, current_time, delay, target, false)) < 0) + return res; + + if ((res = spa_alsa_read(state)) < 0) + return res; + + if (spa_list_is_empty(&state->ready)) + return 0; + + io = state->io; + if (io != NULL && + (io->status != SPA_STATUS_HAVE_DATA || state->rate_match != NULL)) { + struct buffer *b; + + if (io->buffer_id < state->n_buffers) + spa_alsa_recycle_buffer(state, io->buffer_id); + + b = spa_list_first(&state->ready, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace_fp(state->log, "%p: output buffer:%d", state, b->id); + } + spa_node_call_ready(&state->callbacks, SPA_STATUS_HAVE_DATA); + return 0; +} + +static void alsa_on_timeout_event(struct spa_source *source) +{ + struct state *state = source->data; + snd_pcm_uframes_t delay, target; + uint64_t expire, current_time; + int res; + + if (SPA_LIKELY(state->started)) { + if (SPA_UNLIKELY((res = spa_system_timerfd_read(state->data_system, + state->timerfd, &expire)) < 0)) { + /* we can get here when the timer is changed since the last + * timerfd wakeup, for example by do_reassign_follower() executed + * in the same epoll wakeup cycle */ + if (res != -EAGAIN) + spa_log_warn(state->log, "%p: error reading timerfd: %s", + state, spa_strerror(res)); + return; + } + } + + check_position_config(state); + + current_time = state->next_time; + + if (SPA_UNLIKELY(get_status(state, current_time, &delay, &target) < 0)) { + spa_log_error(state->log, "get_status error"); + state->next_time += state->threshold * 1e9 / state->rate; + goto done; + } + +#ifndef FASTPATH + if (SPA_UNLIKELY(spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { + struct timespec now; + uint64_t nsec; + if (spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now) < 0) + return; + nsec = SPA_TIMESPEC_TO_NSEC(&now); + spa_log_trace_fp(state->log, "%p: timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64 + " %d %"PRIi64, state, delay, target, nsec, nsec, + nsec - current_time, state->threshold, state->sample_count); + } +#endif + + if (state->stream == SND_PCM_STREAM_PLAYBACK) + handle_play(state, current_time, delay, target); + else + handle_capture(state, current_time, delay, target); + +done: + if (state->next_time > current_time + SPA_NSEC_PER_SEC || + current_time > state->next_time + SPA_NSEC_PER_SEC) { + spa_log_error(state->log, "%s: impossible timeout %lu %lu %"PRIu64" %"PRIu64" %"PRIi64 + " %d %"PRIi64, state->props.device, delay, target, current_time, state->next_time, + state->next_time - current_time, state->threshold, state->sample_count); + state->next_time = current_time + state->threshold * 1e9 / state->rate; + } + set_timeout(state, state->next_time); +} + +static void reset_buffers(struct state *this) +{ + uint32_t i; + + spa_list_init(&this->free); + spa_list_init(&this->ready); + + for (i = 0; i < this->n_buffers; i++) { + struct buffer *b = &this->buffers[i]; + if (this->stream == SND_PCM_STREAM_PLAYBACK) { + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUT); + spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); + } else { + spa_list_append(&this->free, &b->link); + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); + } + } +} + +static int set_timers(struct state *state) +{ + struct timespec now; + int res; + + if ((res = spa_system_clock_gettime(state->data_system, CLOCK_MONOTONIC, &now)) < 0) + return res; + state->next_time = SPA_TIMESPEC_TO_NSEC(&now); + + if (state->following) { + set_timeout(state, 0); + } else { + set_timeout(state, state->next_time); + } + return 0; +} + +int spa_alsa_start(struct state *state) +{ + int err; + + if (state->started) + return 0; + + if (state->position) { + state->duration = state->position->clock.duration; + state->rate_denom = state->position->clock.rate.denom; + } + else { + spa_log_warn(state->log, "%s: no position set, using defaults", + state->props.device); + state->duration = 1024; + state->rate_denom = state->rate; + } + if (state->rate_denom == 0) { + spa_log_error(state->log, "%s: unset rate_denom", state->props.device); + return -EIO; + } + if (state->duration == 0) { + spa_log_error(state->log, "%s: unset duration", state->props.device); + return -EIO; + } + + state->following = is_following(state); + setup_matching(state); + + spa_dll_init(&state->dll); + state->threshold = SPA_SCALE32_UP(state->duration, state->rate, state->rate_denom); + state->last_threshold = state->threshold; + state->max_error = SPA_MAX(256.0f, state->threshold / 2.0f); + + spa_log_debug(state->log, "%p: start %d duration:%d rate:%d follower:%d match:%d resample:%d", + state, state->threshold, state->duration, state->rate_denom, + state->following, state->matching, state->resample); + + CHECK(set_swparams(state), "swparams"); + + if ((err = snd_pcm_prepare(state->hndl)) < 0 && err != -EBUSY) { + spa_log_error(state->log, "%s: snd_pcm_prepare error: %s", + state->props.device, snd_strerror(err)); + return err; + } + + state->source.func = alsa_on_timeout_event; + state->source.data = state; + state->source.fd = state->timerfd; + state->source.mask = SPA_IO_IN; + state->source.rmask = 0; + spa_loop_add_source(state->data_loop, &state->source); + + reset_buffers(state); + state->alsa_sync = true; + state->alsa_sync_warning = false; + state->alsa_recovering = false; + state->alsa_started = false; + + /* start capture now, playback will start after first write */ + if (state->stream == SND_PCM_STREAM_PLAYBACK) + spa_alsa_silence(state, state->start_delay + state->threshold + state->headroom); + else if ((err = do_start(state)) < 0) + return err; + + set_timers(state); + + state->started = true; + + return 0; +} + +static int do_reassign_follower(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + set_timers(state); + spa_dll_init(&state->dll); + return 0; +} + +int spa_alsa_reassign_follower(struct state *state) +{ + bool following, freewheel; + + if (!state->started) + return 0; + + following = is_following(state); + if (following != state->following) { + spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following); + state->following = following; + spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + } + setup_matching(state); + + freewheel = state->position && + SPA_FLAG_IS_SET(state->position->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL); + + if (state->freewheel != freewheel) { + spa_log_debug(state->log, "%p: freewheel %d->%d", state, state->freewheel, freewheel); + state->freewheel = freewheel; + if (freewheel) + snd_pcm_pause(state->hndl, 1); + else + snd_pcm_pause(state->hndl, 0); + } + + state->alsa_sync_warning = false; + return 0; +} + +static int do_remove_source(struct spa_loop *loop, + bool async, + uint32_t seq, + const void *data, + size_t size, + void *user_data) +{ + struct state *state = user_data; + struct itimerspec ts; + + spa_loop_remove_source(state->data_loop, &state->source); + ts.it_value.tv_sec = 0; + ts.it_value.tv_nsec = 0; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + spa_system_timerfd_settime(state->data_system, state->timerfd, 0, &ts, NULL); + + return 0; +} + +int spa_alsa_pause(struct state *state) +{ + int err; + + if (!state->started) + return 0; + + spa_log_debug(state->log, "%p: pause", state); + + spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + + if ((err = snd_pcm_drop(state->hndl)) < 0) + spa_log_error(state->log, "%s: snd_pcm_drop %s", state->props.device, + snd_strerror(err)); + + state->started = false; + + return 0; +} diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h new file mode 100644 index 0000000..9c4a868 --- /dev/null +++ b/spa/plugins/alsa/alsa-pcm.h @@ -0,0 +1,374 @@ +/* Spa ALSA Sink + * + * Copyright © 2018 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SPA_ALSA_UTILS_H +#define SPA_ALSA_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <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; +} |