From 7cf3bd535e4c4bfe11e085ae7e685f931b296db1 Mon Sep 17 00:00:00 2001 From: Alexander Stein Date: Thu, 10 Apr 2025 13:01:19 +0200 Subject: [PATCH 01/27] bat: Fix buffer time configuration By swapping time configuration some hardware constraints can be supported. Fixes the error: Set parameter to device error: buffer time: 500000 hw:tqmtlv320aic32: Invalid argument(-22) Signed-off-by: Alexander Stein Signed-off-by: Takashi Iwai --- bat/alsa.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bat/alsa.c b/bat/alsa.c index 67489c4..457e79a 100644 --- a/bat/alsa.c +++ b/bat/alsa.c @@ -221,7 +221,17 @@ static int set_snd_pcm_params(struct bat *bat, struct pcm_container *sndpcm) period_time = buffer_time / DIV_BUFFERTIME; - /* Set buffer time and period time */ + /* Set period time and buffer time */ + err = snd_pcm_hw_params_set_period_time_near(sndpcm->handle, + params, &period_time, 0); + if (err < 0) { + fprintf(bat->err, _("Set parameter to device error: ")); + fprintf(bat->err, _("period time: %d %s: %s(%d)\n"), + period_time, + device_name, snd_strerror(err), err); + return err; + } + err = snd_pcm_hw_params_set_buffer_time_near(sndpcm->handle, params, &buffer_time, 0); if (err < 0) { @@ -231,16 +241,6 @@ static int set_snd_pcm_params(struct bat *bat, struct pcm_container *sndpcm) device_name, snd_strerror(err), err); return err; } - - err = snd_pcm_hw_params_set_period_time_near(sndpcm->handle, - params, &period_time, 0); - if (err < 0) { - fprintf(bat->err, _("Set parameter to device error: ")); - fprintf(bat->err, _("period time: %d %s: %s(%d)\n"), - period_time, - device_name, snd_strerror(err), err); - return err; - } } /* Write the parameters to the driver */ From 753081c9e53e4614b6e309173485864255c5224a Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Wed, 24 Sep 2025 13:51:47 +0200 Subject: [PATCH 02/27] alsactl: restore udev rules - fix HDA analog device check Signed-off-by: Jaroslav Kysela --- alsactl/90-alsa-restore.rules.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alsactl/90-alsa-restore.rules.in b/alsactl/90-alsa-restore.rules.in index 5db6080..b3a80d5 100644 --- a/alsactl/90-alsa-restore.rules.in +++ b/alsactl/90-alsa-restore.rules.in @@ -8,7 +8,7 @@ LABEL="alsa_restore_go" ENV{ALSA_CARD_NUMBER}="$attr{device/number}" # mark HDA analog card; HDMI/DP card does not have capture devices -DRIVERS=="snd_hda_intel", TEST=="device/pcmC$env{ALSA_CARD_NUMBER}D0p", RUN+="/bin/sh -c 'echo ALSA_CARD_HDA_ANALOG=$env{ALSA_CARD_NUMBER} >> /run/udev/alsa-hda-analog-card'" +DRIVERS=="snd_hda_intel", TEST=="device/pcmC$env{ALSA_CARD_NUMBER}D0c", RUN+="/bin/sh -c 'echo ALSA_CARD_HDA_ANALOG=$env{ALSA_CARD_NUMBER} >> /run/udev/alsa-hda-analog-card'" # check for ACP hardware TEST=="device/device/acp3x-dmic-capture", GOTO="alsa_hda_analog" From 8957eb224bf70133670b3543e4605de4293d223c Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Wed, 24 Sep 2025 14:05:53 +0200 Subject: [PATCH 03/27] github: CI: add awk package for Fedora to build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71af32f..0cd46a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: - name: Prepare environment run: | dnf -y upgrade - dnf -y install @development-tools libtool bzip2 gettext-devel ncurses-devel + dnf -y install @development-tools libtool bzip2 gettext-devel ncurses-devel awk - name: Checkout alsa-lib uses: actions/checkout@v4 From 862ba9c1f8dfcb5d0600a6562b53ae09bc8e02c1 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 7 Nov 2025 17:00:42 +0100 Subject: [PATCH 04/27] alsactl: add support for new log handler (alsa-lib 1.2.15) Signed-off-by: Jaroslav Kysela --- alsactl/alsactl.c | 4 ++++ alsactl/alsactl.h | 3 ++- alsactl/utils.c | 30 +++++++++++++++++++++++++++--- aplay/aplay.c | 7 +++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/alsactl/alsactl.c b/alsactl/alsactl.c index 8ad991a..5614ea7 100644 --- a/alsactl/alsactl.c +++ b/alsactl/alsactl.c @@ -441,7 +441,11 @@ int main(int argc, char *argv[]) syslog(LOG_INFO, "alsactl " SND_UTIL_VERSION_STR " daemon started"); } +#if SND_LIB_VER(1, 2, 15) < SND_LIB_VERSION snd_lib_error_set_handler(error_handler); +#else + snd_lib_log_set_handler(log_handler); +#endif if (!strcmp(cmd, "init")) { res = init(cfgdir, initfile, initflags | FLAG_UCM_FBOOT | FLAG_UCM_BOOT, cardname); diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h index 6ae45e3..f86f8d9 100644 --- a/alsactl/alsactl.h +++ b/alsactl/alsactl.h @@ -24,7 +24,8 @@ void info_(const char *fcn, long line, const char *fmt, ...); void error_(const char *fcn, long line, const char *fmt, ...); void cerror_(const char *fcn, long line, int cond, const char *fmt, ...); void dbg_(const char *fcn, long line, const char *fmt, ...); -void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...); +void error_handler(const char *file, int line, const char *function, int errcode, const char *fmt, ...); +void log_handler(int prio, int interface, const char *file, int line, const char *function, int errcode, const char *fmt, va_list arg); #if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95) #define info(...) do { info_(__func__, __LINE__, __VA_ARGS__); } while (0) diff --git a/alsactl/utils.c b/alsactl/utils.c index d8cbf53..42e6eba 100644 --- a/alsactl/utils.c +++ b/alsactl/utils.c @@ -177,7 +177,7 @@ void dbg_(const char *fcn, long line, const char *fmt, ...) va_end(ap); } -void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) +void error_handler(const char *file, int line, const char *function, int errcode, const char *fmt, ...) { char buf[2048]; va_list arg; @@ -187,12 +187,36 @@ void error_handler(const char *file, int line, const char *function, int err, co va_end(arg); if (use_syslog) syslog(LOG_ERR, "alsa-lib %s:%i:(%s) %s%s%s\n", file, line, function, - buf, err ? ": " : "", err ? snd_strerror(err) : ""); + buf, errcode ? ": " : "", errcode ? snd_strerror(errcode) : ""); else fprintf(stderr, "alsa-lib %s:%i:(%s) %s%s%s\n", file, line, function, - buf, err ? ": " : "", err ? snd_strerror(err) : ""); + buf, errcode ? ": " : "", errcode ? snd_strerror(errcode) : ""); } +#if SND_LIB_VER(1, 2, 15) >= SND_LIB_VERSION +void log_handler(int prio, int interface, const char *file, int line, const char *function, int errcode, const char *fmt, va_list arg) +{ + char buf[2048], level[50] = ""; + const char *text1, *text2; + + if (!snd_lib_log_filter(prio, interface, NULL)) + return; + + text1 = snd_lib_log_priority(prio); + text2 = snd_lib_log_interface(interface); + if (text1 || text2) + snprintf(level, sizeof(level), "[%s.%s] ", text1 ? text1 : "", text2 ? text2 : ""); + + vsnprintf(buf, sizeof(buf), fmt, arg); + if (use_syslog) + syslog(LOG_ERR, "alsa-lib %s:%i:(%s) %s%s%s%s\n", file, line, function, level, + buf, errcode ? ": " : "", errcode ? snd_strerror(errcode) : ""); + else + fprintf(stderr, "alsa-lib %s:%i:(%s) %s%s%s%s\n", file, line, function, level, + buf, errcode ? ": " : "", errcode ? snd_strerror(errcode) : ""); +} +#endif + int load_configuration(const char *file, snd_config_t **top, int *open_failed) { snd_config_t *config; diff --git a/aplay/aplay.c b/aplay/aplay.c index c58c2e9..bdc88c2 100644 --- a/aplay/aplay.c +++ b/aplay/aplay.c @@ -1464,6 +1464,13 @@ static void set_params(void) chunk_size, buffer_size); prg_exit(EXIT_FAILURE); } + if (dump_hw_params) { + fprintf(stderr, _("HW Params of device \"%s\":\n"), + snd_pcm_name(handle)); + fprintf(stderr, "--------------------\n"); + snd_pcm_hw_params_dump(params, log); + fprintf(stderr, "--------------------\n"); + } err = snd_pcm_sw_params_current(handle, swparams); if (err < 0) { error(_("Unable to get current sw params.")); From 55017b0222eb27cb05faa69ac3e3584f5183d1c0 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 7 Nov 2025 17:04:53 +0100 Subject: [PATCH 05/27] alsamixer: add support for new log handler (alsa-lib 1.2.15) Signed-off-by: Jaroslav Kysela --- alsamixer/mainloop.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/alsamixer/mainloop.c b/alsamixer/mainloop.c index 7ca785b..3cb51b4 100644 --- a/alsamixer/mainloop.c +++ b/alsamixer/mainloop.c @@ -34,6 +34,7 @@ static WINDOW *curses_initialized; +#if SND_LIB_VER(1, 2, 15) < SND_LIB_VERSION static void black_hole_error_handler(const char *file ATTRIBUTE_UNUSED, int line ATTRIBUTE_UNUSED, const char *function ATTRIBUTE_UNUSED, @@ -41,6 +42,18 @@ static void black_hole_error_handler(const char *file ATTRIBUTE_UNUSED, const char *fmt ATTRIBUTE_UNUSED, ...) { } +#else +static void black_hole_log_handler(int prio ATTRIBUTE_UNUSED, + int interface ATTRIBUTE_UNUSED, + const char *file ATTRIBUTE_UNUSED, + int line ATTRIBUTE_UNUSED, + const char *function ATTRIBUTE_UNUSED, + int errcode ATTRIBUTE_UNUSED, + const char *fmt ATTRIBUTE_UNUSED, + va_list arg ATTRIBUTE_UNUSED) +{ +} +#endif void initialize_curses(bool use_color, bool use_mouse) { @@ -55,7 +68,11 @@ void initialize_curses(bool use_color, bool use_mouse) if (use_mouse) mousemask(ALL_MOUSE_EVENTS, NULL); +#if SND_LIB_VER(1, 2, 15) < SND_LIB_VERSION snd_lib_error_set_handler(black_hole_error_handler); +#else + snd_lib_log_set_handler(black_hole_log_handler); +#endif } void app_shutdown(void) From 4b87eb4feb2eb1a9ed4ad2b99c2a41adfc82228b Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 7 Nov 2025 17:11:16 +0100 Subject: [PATCH 06/27] aconnect: add support for new log handler (alsa-lib 1.2.15) Signed-off-by: Jaroslav Kysela --- seq/aconnect/aconnect.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/seq/aconnect/aconnect.c b/seq/aconnect/aconnect.c index edd028f..d782ef0 100644 --- a/seq/aconnect/aconnect.c +++ b/seq/aconnect/aconnect.c @@ -31,11 +31,13 @@ static int show_all; -static void error_handler(const char *file, int line, const char *function, int err, const char *fmt, ...) + +#if SND_LIB_VER(1, 2, 15) < SND_LIB_VERSION +static void error_handler(const char *file, int line, const char *function, int errcode, const char *fmt, ...) { va_list arg; - if (err == ENOENT) /* Ignore those misleading "warnings" */ + if (errcode == ENOENT) /* Ignore those misleading "warnings" */ return; va_start(arg, fmt); fprintf(stderr, "ALSA lib %s:%i:(%s) ", file, line, function); @@ -45,6 +47,16 @@ static void error_handler(const char *file, int line, const char *function, int putc('\n', stderr); va_end(arg); } +#else +static snd_lib_log_handler_t original_log_handler; +static void log_handler(int prio, int interface, const char *file, int line, const char *function, int errcode, const char *fmt, va_list arg) +{ + if (prio == SND_LOG_ERROR && errcode == ENOENT) /* Ignore those misleading "warnings" */ + return; + if (original_log_handler) + original_log_handler(prio, interface, file, line, function, errcode, fmt, arg); +} +#endif static void usage(void) { @@ -356,7 +368,11 @@ int main(int argc, char **argv) return 1; } +#if SND_LIB_VER(1, 2, 15) < SND_LIB_VERSION snd_lib_error_set_handler(error_handler); +#else + original_log_handler = snd_lib_log_set_handler(log_handler); +#endif switch (command) { case LIST: From 04ffa6a091e22732288a4b25c207bf76bff83e27 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Wed, 12 Nov 2025 15:55:10 +0100 Subject: [PATCH 07/27] github: add coverity.yml (coverity.com) workflow Signed-off-by: Jaroslav Kysela --- .github/workflows/coverity.yml | 97 ++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/coverity.yml diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml new file mode 100644 index 0000000..d50edc2 --- /dev/null +++ b/.github/workflows/coverity.yml @@ -0,0 +1,97 @@ +name: Coverity Scan + +on: + workflow_dispatch: +# push: +# branches: +# - master +# schedule: +# # Run once a week on Sunday at midnight UTC +# - cron: '0 4 * * 0' + +jobs: + coverity: + runs-on: ubuntu-latest + container: + image: fedora:latest + steps: + - name: Prepare environment + run: | + dnf -y upgrade + dnf -y install @development-tools libtool bzip2 gettext-devel ncurses-devel awk curl wget tar file + + - name: Checkout alsa-lib + uses: actions/checkout@v4 + with: + repository: alsa-project/alsa-lib + ref: master + path: alsa-lib + + - name: Configure alsa-lib + run: | + cd alsa-lib + head -5 configure.ac + libtoolize --force --copy --automake + aclocal + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + echo "Version: $(cat version)" + + - name: Build alsa-lib + run: | + cd alsa-lib + make + + - name: Install alsa-lib + run: | + cd alsa-lib + make install + + - name: Checkout alsa-utils + uses: actions/checkout@v4 + with: + path: alsa-utils + + - name: Configure alsa-utils + run: | + cd alsa-utils + mv configure.ac configure.ac.old + sed -e 's;po/Makefile.in;;' < configure.ac.old > configure.ac + aclocal + gettextize -c -f --no-changelog + libtoolize --force --copy --automake + aclocal -I m4 + autoheader + automake --foreign --copy --add-missing + autoconf + export CFLAGS="-O2 -Wall -W -Wunused-const-variable=0 -pipe -g" + ./configure + + - name: Download Coverity Build Tool + run: | + cd alsa-utils + wget -q https://scan.coverity.com/download/linux64 \ + --post-data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=${{ secrets.COVERITY_SCAN_PROJECT }}" \ + -O coverity_tool.tar.gz + mkdir coverity-tool + tar xzf coverity_tool.tar.gz --strip-components=1 -C coverity-tool + + - name: Build with Coverity Build Tool + run: | + cd alsa-utils + export PATH="$PWD/coverity-tool/bin:$PATH" + cov-build --dir cov-int make + + - name: Submit results to Coverity Scan + run: | + cd alsa-utils + tar czvf alsa-utils.tgz cov-int + curl --form token=${{ secrets.COVERITY_SCAN_TOKEN }} \ + --form email=${{ secrets.COVERITY_SCAN_EMAIL }} \ + --form file=@alsa-utils.tgz \ + --form version="$(git describe --tags --always)" \ + --form description="ALSA Utils Coverity Scan" \ + https://scan.coverity.com/builds?project=alsa-project%2Falsa-utils From 685a8667ad1cda47ae31d5a77cb27be22694dd24 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 13 Nov 2025 10:13:44 +0100 Subject: [PATCH 08/27] alsactl: free scandir list in snd_card_clean_cfgdir Signed-off-by: Jaroslav Kysela --- alsactl/utils.c | 1 + 1 file changed, 1 insertion(+) diff --git a/alsactl/utils.c b/alsactl/utils.c index 42e6eba..d0f8940 100644 --- a/alsactl/utils.c +++ b/alsactl/utils.c @@ -353,6 +353,7 @@ int snd_card_clean_cfgdir(const char *cfgdir, int cardno) lasterr = -errno; } } + free(list); return lasterr; } From 007a74a867ab131cf8835783d78effda27239612 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 13 Nov 2025 15:09:44 +0100 Subject: [PATCH 09/27] github: coverity.yml - run at 4am on Sunday Signed-off-by: Jaroslav Kysela --- .github/workflows/coverity.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index d50edc2..449151d 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -5,9 +5,9 @@ on: # push: # branches: # - master -# schedule: -# # Run once a week on Sunday at midnight UTC -# - cron: '0 4 * * 0' + schedule: + # Run once a week on Sunday at 4am UTC + - cron: '0 4 * * 0' jobs: coverity: From 241fd2aecc702360682734c31055a18bc4ad63da Mon Sep 17 00:00:00 2001 From: Takashi Iwai Date: Tue, 28 Oct 2025 13:29:59 +0100 Subject: [PATCH 10/27] amidi: Ignore inactive MIDI ports as default at listing When listing the devices, currently we show all MIDI ports including inactive ones. But those inactive ports are rarely useful, and it'd be more convenient to filter them out. This patch introduces the filtering of inactive ports at listing devices via amidi -l option. When user needs to scan all MIDI ports including inactive ports, pass the new option -x in addition. Signed-off-by: Takashi Iwai --- amidi/amidi.1 | 5 +++++ amidi/amidi.c | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/amidi/amidi.1 b/amidi/amidi.1 index 5bc24ba..d3d321f 100644 --- a/amidi/amidi.1 +++ b/amidi/amidi.1 @@ -52,6 +52,11 @@ Prints the current version. .I \-l, \-\-list\-devices Prints a list of all hardware MIDI ports. +.TP +.I \-x, \-\-list\-inactive +Use together with \fI\-l\fP option. +Print all MIDI ports including inactive ports. + .TP .I \-L, \-\-list\-rawmidis Prints all RawMIDI definitions. diff --git a/amidi/amidi.c b/amidi/amidi.c index 75fb8c0..9a2aaf7 100644 --- a/amidi/amidi.c +++ b/amidi/amidi.c @@ -57,6 +57,7 @@ static int stop; static int sysex_interval; static snd_rawmidi_t *input, **inputp; static snd_rawmidi_t *output, **outputp; +static int list_all; static void error(const char *format, ...) { @@ -76,6 +77,7 @@ static void usage(void) "-h, --help this help\n" "-V, --version print current version\n" "-l, --list-devices list all hardware ports\n" + "-x, --list-inactive list inactive ports, too\n" "-L, --list-rawmidis list all RawMIDI definitions\n" "-p, --port=name select port by name\n" "-s, --send=file send the contents of a (.syx) file\n" @@ -151,6 +153,9 @@ static void list_device(snd_ctl_t *ctl, int card, int device) card, device, sub, snd_strerror(err)); return; } + if (!list_all && + (snd_rawmidi_info_get_flags(info) & SNDRV_RAWMIDI_INFO_STREAM_INACTIVE)) + continue; name = snd_rawmidi_info_get_name(info); sub_name = snd_rawmidi_info_get_subdevice_name(info); if (sub == 0 && sub_name[0] == '\0') { @@ -471,11 +476,12 @@ static void add_send_hex_data(const char *str) int main(int argc, char *argv[]) { - static const char short_options[] = "hVlLp:s:r:S::dt:aci:T:"; + static const char short_options[] = "hVlxLp:s:r:S::dt:aci:T:"; static const struct option long_options[] = { {"help", 0, NULL, 'h'}, {"version", 0, NULL, 'V'}, {"list-devices", 0, NULL, 'l'}, + {"list-inactive", 0, NULL, 'x'}, {"list-rawmidis", 0, NULL, 'L'}, {"port", 1, NULL, 'p'}, {"send", 1, NULL, 's'}, @@ -508,6 +514,9 @@ int main(int argc, char *argv[]) case 'l': do_device_list = 1; break; + case 'x': + list_all = 1; + break; case 'L': do_rawmidi_list = 1; break; From bdf5c137786319f92ba85409a9899b676b817674 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 21 Nov 2025 12:25:42 +0100 Subject: [PATCH 11/27] alsactl: ucm: implement boot parameters and card group sync infrastructure Implement boot parameter management for multi-card synchronization. Add boot_params.c with read/write functions for Boot control element, card group configuration file support, and validity checking. Extend init_ucm.c with boot/restore logic and card state handling. Add -G (group-file) and -m (force-ucm-restore) command line options. Signed-off-by: Jaroslav Kysela --- alsactl/Makefile.am | 3 +- alsactl/alsactl.c | 12 + alsactl/alsactl.h | 26 + alsactl/boot_params.c | 1083 +++++++++++++++++++++++++++++++++++++++++ alsactl/init_parse.c | 2 +- alsactl/init_ucm.c | 266 +++++++++- alsactl/lock.c | 34 ++ alsactl/state.c | 6 +- 8 files changed, 1410 insertions(+), 22 deletions(-) create mode 100644 alsactl/boot_params.c diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index e771717..1f6b712 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -11,7 +11,8 @@ AM_CFLAGS = -D_GNU_SOURCE AM_CPPFLAGS = -I$(top_srcdir)/include -alsactl_SOURCES=alsactl.c state.c lock.c utils.c init_parse.c init_ucm.c \ +alsactl_SOURCES=alsactl.c state.c lock.c utils.c \ + init_parse.c init_ucm.c boot_params.c \ daemon.c monitor.c clean.c info.c alsactl_CFLAGS=$(AM_CFLAGS) -D__USE_GNU \ diff --git a/alsactl/alsactl.c b/alsactl/alsactl.c index 5614ea7..adde783 100644 --- a/alsactl/alsactl.c +++ b/alsactl/alsactl.c @@ -44,6 +44,9 @@ #ifndef SYS_LOCKPATH #define SYS_LOCKPATH "/var/lock" #endif +#ifndef SYS_CARD_GROUP +#define SYS_CARD_GROUP SYS_ASOUND_DIR "/card-group.state" +#endif int debugflag = 0; int force_restore = 1; @@ -52,6 +55,7 @@ int do_lock = 0; int use_syslog = 0; char *command; char *statefile = NULL; +char *groupfile = SYS_CARD_GROUP; char *lockpath = SYS_LOCKPATH; char *lockfile = SYS_LOCKFILE; @@ -78,6 +82,7 @@ static struct arg args[] = { { 'v', "version", "print version of this program" }, { HEADER, NULL, "Available state options:" }, { FILEARG | 'f', "file", "configuration file (default " SYS_ASOUNDRC ")" }, +{ FILEARG | 'G', "group-file", "card group configuration file (default " SYS_CARD_GROUP ")" }, { FILEARG | 'a', "config-dir", "boot / hotplug configuration directory (default " SYS_ASOUND_DIR ")" }, { 'l', "lock", "use file locking to serialize concurrent access" }, { 'L', "no-lock", "do not use file locking to serialize concurrent access" }, @@ -105,6 +110,7 @@ static struct arg args[] = { #ifdef HAVE_ALSA_USE_CASE_H { 'D', "ucm-defaults", "execute also the UCM 'defaults' section" }, { 'U', "no-ucm", "don't init with UCM" }, +{ 'm', "force-ucm-restore", "force UCM restore for boot card groups" }, #if SND_LIB_VER(1, 2, 5) < SND_LIB_VERSION { 'X', "ucm-nodev", "show UCM no device errors" }, #endif @@ -301,6 +307,9 @@ int main(int argc, char *argv[]) case 'f': cfgfile = optarg; break; + case 'G': + groupfile = optarg; + break; case 'a': cfgdir = optarg; break; @@ -341,6 +350,9 @@ int main(int argc, char *argv[]) case 'U': initflags |= FLAG_UCM_DISABLED; break; + case 'm': + initflags |= FLAG_UCM_RESTORE; + break; case 'X': initflags |= FLAG_UCM_NODEV; break; diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h index f86f8d9..6f7b3a2 100644 --- a/alsactl/alsactl.h +++ b/alsactl/alsactl.h @@ -10,6 +10,7 @@ extern int do_lock; extern int use_syslog; extern char *command; extern char *statefile; +extern char *groupfile; extern char *lockpath; extern char *lockfile; @@ -44,6 +45,18 @@ void log_handler(int prio, int interface, const char *file, int line, const char #define FLAG_UCM_BOOT (1<<2) #define FLAG_UCM_DEFAULTS (1<<3) #define FLAG_UCM_NODEV (1<<4) +#define FLAG_UCM_RESTORE (1<<5) + +enum { + CARD_STATE_WAIT = 1, /* skip configuration (wait for sync) */ + CARD_STATE_SKIP = 2, /* skip card */ + CARD_STATE_RESTORED = 3, /* card was restored */ +}; + +static inline bool card_state_is_okay(int state) +{ + return state >= CARD_STATE_WAIT && state <= CARD_STATE_RESTORED; +} void snd_card_iterator_init(struct snd_card_iterator *iter, int cardno); int snd_card_iterator_sinit(struct snd_card_iterator *iter, const char *cardname); @@ -53,14 +66,27 @@ int snd_card_iterator_error(struct snd_card_iterator *iter); int load_configuration(const char *file, snd_config_t **top, int *open_failed); int init(const char *cfgdir, const char *file, int flags, const char *cardname); int init_ucm(int flags, int cardno); +bool validate_boot_time(long long boot_time, long long current_time, long long synctime); +int read_boot_params(snd_ctl_t *handle, long long *boot_time, long long *sync_time, long long *restore_time, long long *primary_card); +int write_boot_params(snd_ctl_t *handle, long long boot_time, long long sync_time, long long restore_time, long long primary_card); +int card_group_load(snd_config_t **config); +int card_group_save(snd_config_t *config); +int card_group_get_int64(snd_config_t *config_group, const char *id, long long *val); +int card_group_set_int64(snd_config_t *config_group, const char *id, long long val); +int check_boot_params_validity(snd_ctl_t *handle, int cardno, char **boot_card_group, bool *valid, bool *in_sync, bool *restored, int *primary_card, long long *synctime); +int update_boot_params(snd_ctl_t *handle, int cardno, const char *boot_card_group, bool valid, bool restored, long long synctime); +int boot_params_remove_card(int cardno); int state_lock(const char *file, int timeout); int state_unlock(int lock_fd, const char *file); int card_lock(int card_number, int timeout); int card_unlock(int lock_fd, int card_number); +int group_state_lock(const char *file, int timeout); +int group_state_unlock(int lock_fd, const char *file); int save_state(const char *file, const char *cardname); int load_state(const char *cfgdir, const char *file, const char *initfile, int initflags, const char *cardname, int do_init); +int wait_for_card(long long timeout, int cardno); int power(const char *argv[], int argc); int monitor(const char *name); int general_info(const char *name); diff --git a/alsactl/boot_params.c b/alsactl/boot_params.c new file mode 100644 index 0000000..736597e --- /dev/null +++ b/alsactl/boot_params.c @@ -0,0 +1,1083 @@ +/* + * Advanced Linux Sound Architecture Control Program - Boot Parameters + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include +#include +#include +#include +#include +#include "alsactl.h" + +/* + * Validate boot time + * Returns: true if boot_time is valid and within synchronization time, false otherwise + */ +bool validate_boot_time(long long boot_time, long long current_time, long long synctime) +{ + long long diff; + + if (boot_time <= 0) + return false; + + diff = current_time - boot_time; + if (diff < 0) { + /* boot_time is in the future - invalid */ + return false; + } + + if (synctime > 0 && diff >= synctime) { + /* boot_time has exceeded timeout - invalid */ + return false; + } + + return true; +} + +/* + * Read boot parameters from the '.Boot' control element + * Returns: 0 on success, negative error code on failure + */ +int read_boot_params(snd_ctl_t *handle, long long *boot_time, long long *sync_time, + long long *restore_time, long long *primary_card) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_value_t *value; + snd_ctl_elem_info_t *info; + int err; + + if (boot_time) + *boot_time = -1; + if (sync_time) + *sync_time = -1; + if (restore_time) + *restore_time = -1; + if (primary_card) + *primary_card = -1; + + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&value); + snd_ctl_elem_info_alloca(&info); + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name(id, ".Boot"); + snd_ctl_elem_id_set_index(id, 0); + + snd_ctl_elem_info_set_id(info, id); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + if (err == -ENOENT) + return 0; + error("Cannot read '.Boot' control info: %s", snd_strerror(err)); + return err; + } + + if (snd_ctl_elem_info_get_type(info) != SND_CTL_ELEM_TYPE_INTEGER64) { + error("'.Boot' control element is not of type INTEGER64"); + return -EINVAL; + } + + if (snd_ctl_elem_info_get_count(info) != 4) { + error("'.Boot' control element does not have 3 values"); + return -EINVAL; + } + + snd_ctl_elem_value_set_id(value, id); + err = snd_ctl_elem_read(handle, value); + if (err < 0) { + error("Cannot read '.Boot' control: %s", snd_strerror(err)); + return err; + } + + dbg("Read boot params: boot_time=%lld sync_time=%lld restore_time=%lld primary_card=%lld", + snd_ctl_elem_value_get_integer64(value, 0), + snd_ctl_elem_value_get_integer64(value, 1), + snd_ctl_elem_value_get_integer64(value, 2), + snd_ctl_elem_value_get_integer64(value, 3)); + + if (boot_time) + *boot_time = snd_ctl_elem_value_get_integer64(value, 0); + if (sync_time) + *sync_time = snd_ctl_elem_value_get_integer64(value, 1); + if (restore_time) + *restore_time = snd_ctl_elem_value_get_integer64(value, 2); + if (primary_card) + *primary_card = snd_ctl_elem_value_get_integer64(value, 3); + + return 0; +} + +/* + * Write boot parameters to the '.Boot' control element + * Returns: 0 on success, negative error code on failure + */ +int write_boot_params(snd_ctl_t *handle, long long boot_time, long long sync_time, + long long restore_time, long long primary_card) +{ + snd_ctl_elem_id_t *id; + snd_ctl_elem_value_t *value; + snd_ctl_elem_info_t *info; + int err; + + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&value); + snd_ctl_elem_info_alloca(&info); + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name(id, ".Boot"); + snd_ctl_elem_id_set_index(id, 0); + + snd_ctl_elem_info_set_id(info, id); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + if (err == -ENOENT) { + /* Element not found, create a new user element with 3 integer64 values */ + dbg("'.Boot' control not found, creating new user element"); + /* Do not save this element to the state file */ + snd_ctl_elem_info_set_inactive(info, 1); + snd_ctl_elem_info_set_read_write(info, 1, 1); + err = snd_ctl_add_integer64_elem_set(handle, info, 1, 4, -1, LLONG_MAX, 0); + if (err < 0) { + error("Cannot create '.Boot' user element: %s", snd_strerror(err)); + return err; + } + /* Re-read the element info after creation */ + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + error("Cannot read '.Boot' control info after creation: %s", snd_strerror(err)); + return err; + } + } else { + error("Cannot read '.Boot' control info: %s", snd_strerror(err)); + return err; + } + } + + dbg("Write boot params: boot_time=%lld sync_time=%lld restore_time=%lld primary_card=%lld", + boot_time, sync_time, restore_time, primary_card); + + if (snd_ctl_elem_info_get_type(info) != SND_CTL_ELEM_TYPE_INTEGER64) { + error("'.Boot' control element is not of type INTEGER64"); + return -EINVAL; + } + + if (snd_ctl_elem_info_get_count(info) != 4) { + error("'.Boot' control element does not have 3 values"); + return -EINVAL; + } + + snd_ctl_elem_value_set_id(value, id); + snd_ctl_elem_value_set_integer64(value, 0, boot_time); + snd_ctl_elem_value_set_integer64(value, 1, sync_time); + snd_ctl_elem_value_set_integer64(value, 2, restore_time); + snd_ctl_elem_value_set_integer64(value, 3, primary_card); + + err = snd_ctl_elem_write(handle, value); + if (err < 0) { + error("Cannot write '.Boot' control: %s", snd_strerror(err)); + return err; + } + + return 0; +} + +/* + * Structure for the group configuration file: + * + * { + * card.0 # primary card in group + * card.1 # optional - next card in group + * card.2 # optional - next card in group + * boot_realtime # boot time (CLOCK_REALTIME) in seconds + * boot_last_update # timestamp of last configuration update (CLOCK_REALTIME) in seconds + * boot_monotonic # boot time (CLOCK_MONOTONIC_RAW) in seconds + * boot_synctime # synchronization time window in seconds + * } + */ + +/* + * Read card group configuration from file + * Returns: 0 on success, negative error code on failure + */ +int card_group_load(snd_config_t **config) +{ + snd_input_t *in; + int err; + + if (!config) + return -EINVAL; + + *config = NULL; + + err = snd_config_top(config); + if (err < 0) { + error("Cannot create top config: %s", snd_strerror(err)); + return err; + } + + err = snd_input_stdio_open(&in, groupfile, "r"); + if (err < 0) { + if (err == -ENOENT) { + dbg("Card group file '%s' not found", groupfile); + return 0; + } + error("Cannot open card group file '%s' for reading: %s", groupfile, snd_strerror(err)); + goto _err; + } + + err = snd_config_load(*config, in); + snd_input_close(in); + if (err < 0) { + error("Cannot load card group file '%s': %s", groupfile, snd_strerror(err)); + goto _err; + } + + return 0; + +_err: + snd_config_delete(*config); + *config = NULL; + return err; +} + +/* + * Write card group configuration to file + * Returns: 0 on success, negative error code on failure + */ +int card_group_save(snd_config_t *config) +{ + snd_output_t *out; + char temp_file[PATH_MAX]; + int err; + + if (!config) + return -EINVAL; + + snprintf(temp_file, sizeof(temp_file), "%s.new", groupfile); + + err = snd_output_stdio_open(&out, temp_file, "w"); + if (err < 0) { + error("Cannot open temporary card group file '%s' for writing: %s", temp_file, snd_strerror(err)); + return err; + } + + err = snd_config_save(config, out); + snd_output_close(out); + if (err < 0) { + error("Cannot save temporary card group file '%s': %s", temp_file, snd_strerror(err)); + return err; + } + + err = rename(temp_file, groupfile); + if (err < 0) { + err = -errno; + error("Cannot rename temporary card group file '%s' to '%s': %s", temp_file, groupfile, strerror(errno)); + return err; + } + + return 0; +} + +/* + * Get int64 value from card group configuration + * Returns: 0 on success, negative error code on failure + */ +int card_group_get_int64(snd_config_t *config_group, const char *id, long long *val) +{ + snd_config_t *node; + int err; + + if (!config_group || !id || !val) + return -EINVAL; + + err = snd_config_search(config_group, id, &node); + if (err < 0) + return err; + + err = snd_config_get_integer64(node, val); + if (err < 0) { + long ival; + err = snd_config_get_integer(node, &ival); + if (err < 0) + return err; + *val = ival; + } + + return 0; +} + +/* + * Set int64 value in card group configuration + * Returns: 0 on success, negative error code on failure + */ +int card_group_set_int64(snd_config_t *config_group, const char *id, long long val) +{ + snd_config_t *node; + int err; + + if (!config_group || !id) + return -EINVAL; + + err = snd_config_search(config_group, id, &node); + if (err < 0) { +_create: + err = snd_config_make_integer64(&node, id); + if (err < 0) { + error("Cannot create int64 node for id '%s': %s", id, snd_strerror(err)); + return err; + } + err = snd_config_add(config_group, node); + if (err < 0) { + error("Cannot add int64 node for id '%s': %s", id, snd_strerror(err)); + snd_config_delete(node); + return err; + } + } else { + /* alsa-lib should implement automatic type conversion */ + if (snd_config_get_type(node) == SND_CONFIG_TYPE_INTEGER) { + snd_config_delete(node); + goto _create; + } + } + + err = snd_config_set_integer64(node, val); + if (err < 0) { + error("Cannot set int64 value for id '%s': %s", id, snd_strerror(err)); + return err; + } + + return 0; +} + +/* + * Helper: Find or create card compound within a group + * Returns: 0 on success, negative error code on failure + */ +static int card_group_get_or_create_card_compound(snd_config_t *config_group, snd_config_t **card_compound) +{ + int err; + + if (!config_group || !card_compound) + return -EINVAL; + + err = snd_config_search(config_group, "card", card_compound); + if (err < 0) { + /* Create card compound */ + err = snd_config_make_compound(card_compound, "card", 0); + if (err < 0) { + error("Cannot create card compound: %s", snd_strerror(err)); + return err; + } + err = snd_config_add(config_group, *card_compound); + if (err < 0) { + error("Cannot add card compound: %s", snd_strerror(err)); + snd_config_delete(*card_compound); + return err; + } + } + + return 0; +} + +/* + * Helper: Determine the primary card in the card compound + * Returns: card number, otherwise error code + */ +static long card_group_find_primary(snd_config_t *card_compound) +{ + snd_config_iterator_t i, next; + + if (!card_compound) + return -EINVAL; + + if (snd_config_get_type(card_compound) != SND_CONFIG_TYPE_COMPOUND) + return -EINVAL; + + snd_config_for_each(i, next, card_compound) { + snd_config_t *card_node = snd_config_iterator_entry(i); + long card_val; + int err; + + err = snd_config_get_integer(card_node, &card_val); + if (err < 0) + return -EINVAL; + + return card_val; + } + + return -ENOENT; +} + +/* + * Helper: Find card node in card compound + * Returns: card node if found, NULL otherwise + */ +static snd_config_t *card_group_find_card_node(snd_config_t *card_compound, int cardno) +{ + snd_config_iterator_t i, next; + + if (!card_compound) + return NULL; + + if (snd_config_get_type(card_compound) != SND_CONFIG_TYPE_COMPOUND) + return NULL; + + snd_config_for_each(i, next, card_compound) { + snd_config_t *card_node = snd_config_iterator_entry(i); + long card_val; + int err; + + err = snd_config_get_integer(card_node, &card_val); + if (err < 0) + continue; + + if ((int)card_val == cardno) + return card_node; + } + + return NULL; +} + +/* + * Helper: Add card to card compound + * Returns: 0 on success, negative error code on failure + */ +static int card_group_add_card(snd_config_t *card_compound, int cardno) +{ + snd_config_t *new_card_node; + char card_id[16]; + int card_index = 0; + int err; + + if (!card_compound) + return -EINVAL; + + /* Find next available card index */ + while (card_index < 100) { + snprintf(card_id, sizeof(card_id), "%d", card_index); + if (snd_config_search(card_compound, card_id, &new_card_node) < 0) + break; + card_index++; + } + + err = snd_config_make_integer(&new_card_node, card_id); + if (err < 0) { + error("Cannot create card node: %s", snd_strerror(err)); + return err; + } + + err = snd_config_set_integer(new_card_node, cardno); + if (err < 0) { + error("Cannot set card number: %s", snd_strerror(err)); + snd_config_delete(new_card_node); + return err; + } + + err = snd_config_add(card_compound, new_card_node); + if (err < 0) { + error("Cannot add card node: %s", snd_strerror(err)); + snd_config_delete(new_card_node); + return err; + } + + return 0; +} + +/* + * Check boot parameters validity + * Returns: 0 on success, negative error code on failure + */ +int check_boot_params_validity(snd_ctl_t *handle, int cardno, char **boot_card_group, bool *valid, bool *in_sync, bool *restored, int *primary_card, long long *synctime) +{ + long long boot_time = -1; + long long restore_time = -1; + long long primary_card_val = -1; + long long boot_synctime = -1; + snd_config_t *config = NULL; + snd_config_t *config_group = NULL; + const char *card_group_name = NULL; + long long group_boot_realtime = -1; + long long group_boot_monotonic = -1; + long long group_boot_synctime = -1; + struct timespec ts_realtime, ts_monotonic; + long long diff_realtime, diff_monotonic, diff; + snd_config_iterator_t i, next; + int err = 0; + bool is_valid = false; + + if (valid) + *valid = false; + if (in_sync) + *in_sync = false; + if (restored) + *restored = false; + if (primary_card) + *primary_card = -1; + if (boot_card_group) + *boot_card_group = NULL; + + err = read_boot_params(handle, &boot_time, &boot_synctime, &restore_time, &primary_card_val); + if (err < 0) { + dbg("Boot element not present or error reading: %s", snd_strerror(err)); + err = 0; + goto out; + } + + if (boot_time <= 0) { + dbg("boot_time is not greater than zero: %lld", boot_time); + goto out; + } + + err = card_group_load(&config); + if (err < 0) { + dbg("Error loading card group configuration: %s", snd_strerror(err)); + err = 0; + goto out; + } + + if (!config) { + dbg("No group configuration found"); + goto out; + } + + /* Find the card number in card groups - prefer group with newest boot_realtime */ + snd_config_for_each(i, next, config) { + snd_config_t *n = snd_config_iterator_entry(i); + snd_config_t *card_compound; + const char *group_id; + long long current_boot_realtime; + + if (snd_config_get_id(n, &group_id) < 0) + continue; + + if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) + continue; + + err = snd_config_search(n, "card", &card_compound); + if (err < 0) + continue; + + if (!card_group_find_card_node(card_compound, cardno)) + continue; + + err = card_group_get_int64(n, "boot_realtime", ¤t_boot_realtime); + if (err < 0) { + dbg("boot_realtime not found in group '%s', skipping", group_id); + continue; + } + + if (current_boot_realtime > group_boot_realtime) { + group_boot_realtime = current_boot_realtime; + config_group = n; + card_group_name = group_id; + } + } + + if (!card_group_name) { + dbg("Card %d not found in any group configuration", cardno); + err = 0; + goto out; + } + + err = card_group_get_int64(config_group, "boot_monotonic", &group_boot_monotonic); + if (err < 0) { + dbg("boot_monotonic not found in group '%s'", card_group_name); + err = 0; + goto out; + } + + err = card_group_get_int64(config_group, "boot_synctime", &group_boot_synctime); + if (err < 0) { + dbg("boot_synctime not found in group '%s'", card_group_name); + err = 0; + goto out; + } + + if (*synctime > 0 && group_boot_synctime != *synctime) { + err = -EINVAL; + error("Synchronization time window does not match (%lld != %lld)", *synctime, group_boot_synctime); + goto out; + } + + if (boot_synctime > 0 && group_boot_synctime != boot_synctime) { + err = -EINVAL; + error("Element synchronization time window does not match (%lld != %lld)", boot_synctime, group_boot_synctime); + goto out; + } + + if (clock_gettime(CLOCK_REALTIME, &ts_realtime) < 0) { + err = -errno; + error("Failed to get CLOCK_REALTIME: %s", strerror(errno)); + goto out; + } + + if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts_monotonic) < 0) { + err = -errno; + error("Failed to get CLOCK_MONOTONIC_RAW: %s", strerror(errno)); + goto out; + } + + diff_monotonic = ts_monotonic.tv_sec - group_boot_monotonic; + diff_realtime = ts_realtime.tv_sec - group_boot_realtime; + diff = diff_realtime - diff_monotonic; + dbg("Card group '%s' sync diffs - %lld, %lld, %lld", + card_group_name, (long long)diff_monotonic, (long long)diff_realtime, (long long)diff); + /* if the time difference is too big (30 seconds) - obsolete configuration */ + is_valid = diff < 30 || diff > -30; + + if (valid) + *valid = is_valid; + + if (is_valid) { + if (boot_card_group) { + *boot_card_group = strdup(card_group_name); + if (!*boot_card_group) { + err = -ENOMEM; + goto out; + } + } + if (primary_card) { + *primary_card = (int)primary_card_val; + dbg("Card group '%s' primary_card %d", card_group_name, *primary_card); + } + if (restored) { + *restored = restore_time > 0; + dbg("Card group '%s' restored %d", card_group_name, *restored); + } + if (in_sync) { + *in_sync = ts_realtime.tv_sec - group_boot_realtime < group_boot_synctime; + dbg("Card group '%s' in_sync %d - %lld, %lld, %lld", + card_group_name, *in_sync, (long long)ts_realtime.tv_sec, + (long long)group_boot_realtime, (long long)group_boot_synctime); + } + + if (synctime) + *synctime = group_boot_synctime; + } + +out: + if (config) + snd_config_delete(config); + return err; +} + +/* + * Remove card from boot parameters - scans all groups and removes all invalid + * cards in group containing the card. + * Returns: 0 = no change, 1 = change (card(s) removed), negative error code on failure + */ +static int boot_params_remove_card_config(snd_config_t *group_config, int cardno) +{ + snd_config_t *card_compound, *card_node; + snd_config_iterator_t i, next; + const char *group_id; + struct timespec ts_monotonic = {0}; + long primary_card, card_val; + int err, changes; + bool valid; + + if (snd_config_get_id(group_config, &group_id) < 0) + return -EINVAL; + + err = snd_config_search(group_config, "card", &card_compound); + if (err < 0) + return 0; + + primary_card = card_group_find_primary(card_compound); + if (primary_card == cardno) { +_primary: + dbg("Removing group '%s' (primary card %d)", group_id, cardno); + snd_config_delete(group_config); + return 1; + } + + card_node = card_group_find_card_node(card_compound, cardno); + if (card_node == NULL) + return 0; + + dbg("Removing card %d in group '%s'", cardno, group_id); + + changes = 1; + snd_config_delete(card_node); + +_retry: + snd_config_for_each(i, next, card_compound) { + snd_config_t *card_node = snd_config_iterator_entry(i); + snd_ctl_t *handle = NULL; + char name[32]; + long long boot_time = -1; + + err = snd_config_get_integer(card_node, &card_val); + if (err < 0) + continue; + + valid = false; + + sprintf(name, "hw:%ld", card_val); + err = snd_ctl_open(&handle, name, SND_CTL_READONLY); + if (err >= 0) { + err = read_boot_params(handle, &boot_time, NULL, NULL, NULL); + snd_ctl_close(handle); + if (err < 0) { + dbg("Unable to read boot params for card %ld: %s", card_val, snd_strerror(err)); + continue; + } + + if (ts_monotonic.tv_sec == 0) { + if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts_monotonic) < 0) { + dbg("Failed to get CLOCK_MONOTONIC_RAW: %s", strerror(errno)); + return changes; + } + } + valid = validate_boot_time(boot_time, ts_monotonic.tv_sec, 0); + } else { + dbg("Unable to open ctl handle for card %ld: %s", card_val, snd_strerror(err)); + } + + if (!valid) { + if (card_val == primary_card) { + dbg("Primary card %ld is invalid in group '%s'", card_val, group_id); + goto _primary; + } + changes++; + dbg("Removing another card %ld in group '%s'", card_val, group_id); + snd_config_delete(card_node); + goto _retry; + } + } + if (snd_config_is_empty(card_compound)) { + dbg("No other cards in group '%s', removing", group_id); + snd_config_delete(group_config); + } + + return changes > 0; +} + +/* + * Remove card from boot parameters - scans all groups and removes all invalid + * cards in group containing the card. + * Returns: 0 on success, negative error code on failure + */ +int boot_params_remove_card(int cardno) +{ + snd_config_t *config = NULL; + snd_config_iterator_t i, next; + const char *group_id = NULL; + int groups_changed = 0; + int err = 0; + + /* Load the group configuration */ + err = card_group_load(&config); + if (err < 0) { + error("Error loading card group configuration: %s", snd_strerror(err)); + goto out; + } + + if (!config) { + dbg("No group configuration found"); + err = 0; + goto out; + } + + /* Scan all groups and remove any that contain this card */ +restart_scan: + snd_config_for_each(i, next, config) { + snd_config_t *group = snd_config_iterator_entry(i); + + if (snd_config_get_id(group, &group_id) < 0) + continue; + + if (snd_config_get_type(group) != SND_CONFIG_TYPE_COMPOUND) + continue; + + err = boot_params_remove_card_config(group, cardno); + if (err < 0) { + error("Unable to remove card %d from group '%s': %s", group_id, cardno, snd_strerror(err)); + continue; + } + if (err > 0) + groups_changed++; + goto restart_scan; + } + + if (groups_changed == 0) { + dbg("Card %d not found in any group", cardno); + err = 0; + goto out; + } + + dbg("Update %d group(s) containing card %d", groups_changed, cardno); + + /* Save the updated configuration */ + err = card_group_save(config); + if (err < 0) { + error("Cannot save card group configuration: %s", snd_strerror(err)); + goto out; + } + +out: + if (config) + snd_config_delete(config); + return err; +} + +/* + * Update restored time for all cards in boot group + * cards in group containing the card. + */ +static void boot_params_update_restored(snd_config_t *card_compound, int skip_cardno, + long long boot_time, long long restored, long long primary_cardno) +{ + snd_config_iterator_t i, next; + int err; + + /* Scan all groups and remove any that contain this card */ + snd_config_for_each(i, next, card_compound) { + snd_config_t *card_node = snd_config_iterator_entry(i); + long long boot_time_val, boot_synctime, boot_primary; + snd_ctl_t *handle; + char name[32]; + long card_val; + + err = snd_config_get_integer(card_node, &card_val); + if (err < 0) + continue; + + if (skip_cardno == (long)card_val) + continue; + + sprintf(name, "hw:%ld", card_val); + err = snd_ctl_open(&handle, name, SND_CTL_READONLY); + if (err < 0) { + dbg("Unable to open ctl handle for card %ld: %s", card_val, snd_strerror(err)); + continue; + } + + err = read_boot_params(handle, &boot_time_val, &boot_synctime, NULL, &boot_primary); + if (err < 0) { + dbg("Unable to read boot params for card %ld: %s", card_val, snd_strerror(err)); + goto _next; + } + + if (boot_time_val != boot_time) { + dbg("Boot time mismatch (%lld != %lld)", boot_time, boot_time_val); + goto _next; + } + + if (boot_primary != primary_cardno) { + dbg("Primary card mismatch (%lld != %lld)", boot_primary, primary_cardno); + goto _next; + } + + err = write_boot_params(handle, boot_time_val, boot_synctime, restored, boot_primary); + if (err < 0) { + dbg("Unable to save boot params: %s", snd_strerror(err)); + goto _next; + } + +_next: + snd_ctl_close(handle); + } +} + +/* + * Update boot parameters + * Returns: 0 on success, negative error code on failure + */ +int update_boot_params(snd_ctl_t *handle, int cardno, const char *boot_card_group, + bool valid, bool restored, long long synctime) +{ + snd_config_t *config = NULL; + snd_config_t *config_group = NULL; + snd_config_t *card_compound = NULL; + struct timespec ts_realtime, ts_monotonic; + long long value; + long long boot_time = 0; + long long restore_time; + long long primary_card; + int err = 0; + + if (!boot_card_group) { + error("boot_card_group parameter is required"); + return -EINVAL; + } + + if (synctime <= 0) { + error("synchronization time window is required"); + return -EINVAL; + } + + err = card_group_load(&config); + if (err < 0) { + error("Error loading card group configuration: %s", snd_strerror(err)); + goto out; + } + + if (!config) { + err = snd_config_top(&config); + if (err < 0) { + error("Cannot create top config: %s", snd_strerror(err)); + goto out; + } + } + + /* If valid is false, remove the boot_card_group from configuration */ + if (!valid) { + err = snd_config_search(config, boot_card_group, &config_group); + if (err == 0 && config_group) { + err = boot_params_remove_card_config(config_group, cardno); + if (err < 0) { + error("Cannot manage group '%s': %s", boot_card_group, snd_strerror(err)); + goto out; + } + dbg("Updated group '%s' in configuration", boot_card_group); + } + } + + if (clock_gettime(CLOCK_REALTIME, &ts_realtime) < 0) { + err = -errno; + error("Failed to get CLOCK_REALTIME: %s", strerror(errno)); + goto out; + } + ts_realtime.tv_nsec = 0; + + if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts_monotonic) < 0) { + err = -errno; + error("Failed to get CLOCK_MONOTONIC_RAW: %s", strerror(errno)); + goto out; + } + ts_monotonic.tv_nsec = 0; + restore_time = ts_monotonic.tv_sec; + + err = snd_config_search(config, boot_card_group, &config_group); + if (err < 0) { + err = snd_config_make_compound(&config_group, boot_card_group, 0); + if (err < 0) { + error("Cannot create group '%s': %s", boot_card_group, snd_strerror(err)); + goto out; + } + err = snd_config_add(config, config_group); + if (err < 0) { + error("Cannot add group '%s': %s", boot_card_group, snd_strerror(err)); + snd_config_delete(config_group); + goto out; + } + } + + err = card_group_get_or_create_card_compound(config_group, &card_compound); + if (err < 0) + goto out; + + primary_card = card_group_find_primary(card_compound); + if (primary_card < 0) { + dbg("Primary card not found, using %d", cardno); + primary_card = cardno; + } + + if (!card_group_find_card_node(card_compound, cardno)) { + err = card_group_add_card(card_compound, cardno); + if (err < 0) + goto out; + } + + if (primary_card != cardno || valid) { + err = card_group_get_int64(config_group, "boot_realtime", &value); + if (err < 0) { + err = card_group_set_int64(config_group, "boot_realtime", ts_realtime.tv_sec); + if (err < 0) { + error("Cannot set boot_realtime: %s", snd_strerror(err)); + goto out; + } + dbg("Set boot_realtime to %lld", (long long)ts_realtime.tv_sec); + } else { + dbg("Preserving existing boot_realtime: %lld", value); + ts_realtime.tv_sec = value; + } + + err = card_group_get_int64(config_group, "boot_monotonic", &value); + if (err < 0) { + err = card_group_set_int64(config_group, "boot_monotonic", ts_monotonic.tv_sec); + if (err < 0) { + error("Cannot set boot_monotonic: %s", snd_strerror(err)); + goto out; + } + dbg("Set boot_monotonic to %lld", (long long)ts_monotonic.tv_sec); + } else { + dbg("Preserving existing boot_monotonic: %lld", value); + ts_monotonic.tv_sec = value; + } + } else { + err = card_group_set_int64(config_group, "boot_realtime", ts_realtime.tv_sec); + if (err < 0) { + error("Cannot set boot_realtime: %s", snd_strerror(err)); + goto out; + } + dbg("Set boot_realtime to %lld", (long long)ts_realtime.tv_sec); + err = card_group_set_int64(config_group, "boot_monotonic", ts_monotonic.tv_sec); + if (err < 0) { + error("Cannot set boot_monotonic: %s", snd_strerror(err)); + goto out; + } + dbg("Set boot_monotonic to %lld", (long long)ts_monotonic.tv_sec); + } + + if (synctime > 0) { + err = card_group_set_int64(config_group, "boot_synctime", synctime); + if (err < 0) { + error("Cannot set boot_synctime: %s", snd_strerror(err)); + goto out; + } + } + + err = card_group_set_int64(config_group, "boot_last_update", ts_realtime.tv_sec); + if (err < 0) { + error("Cannot set boot_last_update: %s", snd_strerror(err)); + goto out; + } + + err = card_group_save(config); + if (err < 0) { + error("Cannot save card group configuration: %s", snd_strerror(err)); + goto out; + } + + /* Update '.Boot' control element on the card */ + boot_time = ts_monotonic.tv_sec; + if (!restored) + restore_time = -1; + + err = write_boot_params(handle, boot_time, synctime, restore_time, primary_card); + if (err < 0) { + error("Cannot write boot parameters: %s", snd_strerror(err)); + goto out; + } + + if (primary_card == cardno) + boot_params_update_restored(card_compound, primary_card, boot_time, restore_time, primary_card); + + dbg("Updated boot parameters for card %d in group '%s'", cardno, boot_card_group); + +out: + if (config) + snd_config_delete(config); + return err; +} + diff --git a/alsactl/init_parse.c b/alsactl/init_parse.c index 267db4b..a34cb0e 100644 --- a/alsactl/init_parse.c +++ b/alsactl/init_parse.c @@ -1762,7 +1762,7 @@ int init(const char *cfgdir, const char *filename, int flags, const char *cardna continue; } err = init_ucm(flags, iter.card); - if (err == 0) + if (err == 0 || card_state_is_okay(err)) continue; err = init_space(&space, iter.card); if (err != 0) diff --git a/alsactl/init_ucm.c b/alsactl/init_ucm.c index 31e8daf..386f415 100644 --- a/alsactl/init_ucm.c +++ b/alsactl/init_ucm.c @@ -21,52 +21,280 @@ #include "aconfig.h" #include +#include +#include #include "alsactl.h" #ifdef HAVE_ALSA_USE_CASE_H #include +#define DEFAULT_SYNC_TIME 20 + /* - * Keep it as simple as possible. Execute commands from the - * FixedBootSequence and BootSequence only. + * Helper: Check if card should skip initialization based on boot parameters + * Returns: 1 if should skip, 2 if should skip other card, 0 if should continue, negative on error + * If check_restored is true, also checks if card state is already restored + */ +static int should_skip_initialization(snd_ctl_t *ctl, int cardno, int flags, + char **boot_card_group, bool *valid, + bool *in_sync, bool *restored, int *primary_card, + long long *synctime) +{ + int err; + + err = check_boot_params_validity(ctl, cardno, boot_card_group, valid, in_sync, restored, primary_card, synctime); + if (err < 0) { + dbg("boot parameters validity failed: %s", snd_strerror(err)); + return err; + } + + /* do nothing for other cards in group */ + if (*valid && *primary_card != cardno) { + dbg("Skipping card %d - not primary (primary is %d)", cardno, *primary_card); + return CARD_STATE_SKIP; + } + + /* for immediate initialization, caller must set UCM force-restore flag */ + if (*valid && *in_sync && (flags & FLAG_UCM_RESTORE) == 0) { + dbg("Skipping card %d - in sync and no force-restore flag", cardno); + return CARD_STATE_WAIT; + } + return 0; +} + +/* + * Helper: Get boot card group configuration from UCM + * Returns: 0 on success, negative on error + */ +static int get_boot_card_group_config(snd_use_case_mgr_t *uc_mgr, char **boot_card_group, long long *synctime) +{ + char *sync_time = NULL; + int err; + + err = snd_use_case_get(uc_mgr, "=BootCardGroup", (const char **)boot_card_group); + if (err != 0 || *boot_card_group == NULL) { + return -ENOENT; + } + + dbg("BootCardGroup found: %s", *boot_card_group); + + /* Get optional sync time */ + err = snd_use_case_get(uc_mgr, "=BootCardSyncTime", (const char **)&sync_time); + if (err == 0 && sync_time != NULL) { + char *endptr; + errno = 0; + *synctime = strtoll(sync_time, &endptr, 10); + if (errno != 0 || *endptr != '\0' || endptr == sync_time) { + error("Invalid BootCardSyncTime value '%s'", sync_time); + *synctime = DEFAULT_SYNC_TIME; + } + free(sync_time); + } + + return 0; +} + +/* + * Helper: Open UCM manager with appropriate flags + * Returns: 0 on success, negative on error + */ +static int open_ucm_manager(snd_use_case_mgr_t **uc_mgr, int cardno, int flags, + bool valid, bool fixed_boot) +{ + char id[64], *nodev, *in_boot; + int err; + + nodev = (flags & FLAG_UCM_NODEV) ? "" : "-"; + in_boot = (valid || !fixed_boot) ? "" : "<<>>"; + snprintf(id, sizeof(id), "%s%shw:%d", nodev, in_boot, cardno); + + err = snd_use_case_mgr_open(uc_mgr, id); + dbg("ucm open '%s': %d", id, err); + + return err; +} + +/* + * Helper: Reopen UCM manager without InBoot flag + * Returns: 0 on success, negative on error + */ +static int reopen_ucm_manager(snd_use_case_mgr_t **uc_mgr, int cardno, int flags) +{ + char id[64], *nodev; + int err; + + snd_use_case_mgr_close(*uc_mgr); + + nodev = (flags & FLAG_UCM_NODEV) ? "" : "-"; + snprintf(id, sizeof(id), "%shw:%d", nodev, cardno); + + err = snd_use_case_mgr_open(uc_mgr, id); + dbg("ucm reopen '%s': %d", id, err); + + return err; +} + +/* + * Helper: Execute boot sequences + * Returns: 0 on success, negative on error + */ +static int execute_boot_sequences(snd_use_case_mgr_t *uc_mgr, int flags, bool fixed_boot) +{ + int err = 0; + + if (fixed_boot) { + err = snd_use_case_set(uc_mgr, "_fboot", NULL); + dbg("ucm _fboot: %d", err); + if (err == -ENOENT && (flags & FLAG_UCM_BOOT) != 0) { + /* _fboot not found but _boot requested - continue */ + err = 0; + } else if (err < 0) { + return err; + } + } + + if (flags & FLAG_UCM_BOOT) { + err = snd_use_case_set(uc_mgr, "_boot", NULL); + dbg("ucm _boot: %d", err); + if (err < 0) + return err; + + if ((flags & FLAG_UCM_DEFAULTS) != 0) + err = snd_use_case_set(uc_mgr, "_defaults", NULL); + } + + return err; +} + +/* + * Execute commands from the FixedBootSequence and BootSequence. + * Handle also card groups. + * Returns: 0 = success, 1 = skip this card (e.g. linked or in-sync), negative on error */ int init_ucm(int flags, int cardno) { snd_use_case_mgr_t *uc_mgr; - char id[32], *nodev; - int err; + char id[64]; + char *boot_card_group = NULL, *boot_card_group_verify = NULL; + bool fixed_boot, valid = false, in_sync = false, restored = false; + snd_ctl_t *ctl = NULL; + int err, primary_card = -1, lock_fd = -1; + long long synctime = -1; if (flags & FLAG_UCM_DISABLED) { dbg("ucm disabled"); return -ENXIO; } - nodev = (flags & FLAG_UCM_NODEV) ? "" : "-"; - snprintf(id, sizeof(id), "%shw:%d", nodev, cardno); - err = snd_use_case_mgr_open(&uc_mgr, id); - dbg("ucm open '%s': %d", id, err); - if (err < 0) + fixed_boot = (flags & FLAG_UCM_FBOOT) != 0; + + snprintf(id, sizeof(id), "hw:%d", cardno); + err = snd_ctl_open(&ctl, id, 0); + if (err < 0) { + dbg("UCM: unable to open control device '%s': %s", id, snd_strerror(err)); return err; - if (flags & FLAG_UCM_FBOOT) { - err = snd_use_case_set(uc_mgr, "_fboot", NULL); - dbg("ucm _fboot: %d", err); - if (err == -ENOENT && (flags & FLAG_UCM_BOOT) != 0) { - /* nothing */ + } + + err = should_skip_initialization(ctl, cardno, flags, &boot_card_group, &valid, + &in_sync, &restored, &primary_card, &synctime); + if (err != 0) + goto _fin; + + if (valid) { + if (restored) { + err = CARD_STATE_RESTORED; + goto _fin; + } + lock_fd = group_state_lock(groupfile, LOCK_TIMEOUT); + if (lock_fd < 0) { + err = lock_fd; + goto _fin; + } + } + + err = open_ucm_manager(&uc_mgr, cardno, flags, valid, fixed_boot); + if (err < 0) + goto _fin; + + if (!fixed_boot) + goto _execute_boot; + + if (!valid) { + err = get_boot_card_group_config(uc_mgr, &boot_card_group, &synctime); + if (err == -ENOENT) { + /* No BootCardGroup - remove any existing boot params */ + err = boot_params_remove_card(cardno); + if (err < 0) + goto _error; + goto _execute_boot; } else if (err < 0) { goto _error; } + + if (lock_fd < 0) { + lock_fd = group_state_lock(groupfile, LOCK_TIMEOUT); + if (lock_fd < 0) { + err = lock_fd; + goto _error; + } + } + + err = should_skip_initialization(ctl, cardno, flags, &boot_card_group_verify, + &valid, &in_sync, &restored, &primary_card, &synctime); + if (err != 0) + goto _error; + + if (valid && (boot_card_group_verify == NULL || strcmp(boot_card_group_verify, boot_card_group) != 0)) { + dbg("expected different boot card group (got '%s', expected '%s')", boot_card_group_verify, boot_card_group); + err = -EINVAL; + goto _error; + } + + if ((flags & FLAG_UCM_RESTORE) == 0 && (!valid || restored)) { + dbg("Skipping card %d (group '%s') - %s and no force-restore flag", cardno, boot_card_group, + !valid ? "validity not passed" : "already restored"); + if (!valid) { + /* create initial 'Boot' element */ + err = update_boot_params(ctl, cardno, boot_card_group, 0, restored, synctime); + if (err < 0) + goto _error; + } + err = restored ? CARD_STATE_RESTORED : CARD_STATE_WAIT; + goto _error; + } + + err = reopen_ucm_manager(&uc_mgr, cardno, flags); + if (err < 0) + goto _fin; } - if (flags & FLAG_UCM_BOOT) { - err = snd_use_case_set(uc_mgr, "_boot", NULL); - dbg("ucm _boot: %d", err); + +_execute_boot: + if (flags & FLAG_UCM_FBOOT) + restored = true; + + if (boot_card_group) { + err = update_boot_params(ctl, cardno, boot_card_group, valid, restored, synctime); if (err < 0) goto _error; - if ((flags & FLAG_UCM_DEFAULTS) != 0) - err = snd_use_case_set(uc_mgr, "_defaults", NULL); } + + err = execute_boot_sequences(uc_mgr, flags, fixed_boot); + if (err < 0) + goto _error; + + err = 0; + _error: snd_use_case_mgr_close(uc_mgr); +_fin: + if (lock_fd >= 0) + group_state_unlock(lock_fd, groupfile); + if (ctl) + snd_ctl_close(ctl); + free(boot_card_group); + free(boot_card_group_verify); + dbg("ucm init complete %d", err); return err; } diff --git a/alsactl/lock.c b/alsactl/lock.c index 4927b70..e3df982 100644 --- a/alsactl/lock.c +++ b/alsactl/lock.c @@ -181,6 +181,40 @@ int state_unlock(int _fd, const char *file) return err; } +static void group_state_lock_file(char *buf, size_t buflen) +{ + const char *name = strrchr(groupfile, '/'); + if (name && name[0]) + name++; + else + name = "card-group.state"; + snprintf(buf, buflen, "%s/%s.lock", lockpath, name); +} + +int group_state_lock(const char *file, int timeout) +{ + char fn[PATH_SIZE]; + int err; + + group_state_lock_file(fn, sizeof(fn)); + err = state_lock_(fn, 1, timeout, -1); + if (err < 0) + error("file %s lock error: %s", file, strerror(-err)); + return err; +} + +int group_state_unlock(int _fd, const char *file) +{ + char fn[PATH_SIZE]; + int err; + + group_state_lock_file(fn, sizeof(fn)); + err = state_lock_(fn, 0, 10, _fd); + if (err < 0) + error("file %s unlock error: %s", file, strerror(-err)); + return err; +} + static void card_lock_file(char *buf, size_t buflen, int card_number) { snprintf(buf, buflen, "%s/card%i.lock", lockpath, card_number); diff --git a/alsactl/state.c b/alsactl/state.c index 3ccaab6..f9b9260 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -1752,7 +1752,10 @@ int load_state(const char *cfgdir, const char *file, continue; } /* error is ignored */ - init_ucm(initflags | FLAG_UCM_FBOOT, iter.card); + err = init_ucm(initflags | FLAG_UCM_FBOOT, iter.card); + /* return code 1 and 2 -> postpone initialization */ + if (card_state_is_okay(err)) + goto unlock_card; /* do a check if controls matches state file */ if (do_init && set_controls(iter.card, config, 0)) { err = init(cfgdir, initfile, initflags | FLAG_UCM_BOOT, cardname1); @@ -1766,6 +1769,7 @@ int load_state(const char *cfgdir, const char *file, finalerr = err; initfailed(iter.card, "restore", err); } +unlock_card: card_unlock(lock_fd, iter.card); } err = finalerr ? finalerr : snd_card_iterator_error(&iter); From 82caf6e7f35f85d64ce0c2049ea47db1133e61e7 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 27 Nov 2025 17:04:46 +0100 Subject: [PATCH 12/27] alsactl: add -Y option to export card states as key=value pairs Add export.c with support for exporting card states as key=value pairs. Exports ALSA_CARD_NUMBER/ALSA_CARD_STATE for single cards, or ALSA_CARD#_STATE for multiple cards. States: active, skip, waiting. Add -Y (--export) option to restore command for state export. This feature is designed for udev IMPORT{program} use. Signed-off-by: Jaroslav Kysela --- alsactl/Makefile.am | 2 +- alsactl/alsactl.c | 7 +++ alsactl/alsactl.h | 6 ++ alsactl/export.c | 133 ++++++++++++++++++++++++++++++++++++++++++++ alsactl/state.c | 6 +- 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 alsactl/export.c diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index 1f6b712..6725c9c 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -13,7 +13,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/include alsactl_SOURCES=alsactl.c state.c lock.c utils.c \ init_parse.c init_ucm.c boot_params.c \ - daemon.c monitor.c clean.c info.c + daemon.c monitor.c clean.c info.c export.c alsactl_CFLAGS=$(AM_CFLAGS) -D__USE_GNU \ -DSYS_ASOUNDRC=\"$(ASOUND_STATE_DIR)/asound.state\" \ diff --git a/alsactl/alsactl.c b/alsactl/alsactl.c index adde783..8ea4a84 100644 --- a/alsactl/alsactl.c +++ b/alsactl/alsactl.c @@ -53,6 +53,7 @@ int force_restore = 1; int ignore_nocards = 0; int do_lock = 0; int use_syslog = 0; +int do_export = 0; char *command; char *statefile = NULL; char *groupfile = SYS_CARD_GROUP; @@ -97,6 +98,7 @@ static struct arg args[] = { { FILEARG | 'r', "runstate", "save restore and init state to this file (only errors)" }, { 0, NULL, " default settings is 'no file set'" }, { 'R', "remove", "remove runstate file at first, otherwise append errors" }, +{ 'Y', "export", "export card state as key=value pairs (restore command only)" }, { INTARG | 'p', "period", "store period in seconds for the daemon command" }, { FILEARG | 'e', "pid-file", "pathname for the process id (daemon mode)" }, { HEADER, NULL, "Available init options:" }, @@ -362,6 +364,9 @@ int main(int argc, char *argv[]) case 'R': removestate = 1; break; + case 'Y': + do_export = 1; + break; case 'P': force_restore = 0; break; @@ -470,6 +475,8 @@ int main(int argc, char *argv[]) if (removestate) remove(statefile); res = load_state(cfgdir, cfgfile, initfile, initflags, cardname, init_fallback); + if (do_export && res >= 0) + res = export_cards(cardname); if (!strcmp(cmd, "rdaemon")) { do_nice(use_nice, sched_idle); res = state_daemon(cfgfile, cardname, period, pidfile); diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h index 6f7b3a2..aeee114 100644 --- a/alsactl/alsactl.h +++ b/alsactl/alsactl.h @@ -8,6 +8,7 @@ extern int force_restore; extern int ignore_nocards; extern int do_lock; extern int use_syslog; +extern int do_export; extern char *command; extern char *statefile; extern char *groupfile; @@ -96,6 +97,11 @@ int state_daemon_kill(const char *pidfile, const char *cmd); int clean(const char *cardname, char *const *extra_args); int snd_card_clean_cfgdir(const char *cfgdir, int cardno); +/* export */ + +int export_card_state_set(int card, int state); +int export_cards(const char *cardname); + /* utils */ int file_map(const char *filename, char **buf, size_t *bufsize); diff --git a/alsactl/export.c b/alsactl/export.c new file mode 100644 index 0000000..422a92c --- /dev/null +++ b/alsactl/export.c @@ -0,0 +1,133 @@ +/* + * Advanced Linux Sound Architecture Control Program - Export + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/** + * Export variable syntax: + * + * For single card: + * + * ALSA_CARD_NUMBER= + * ALSA_CARD_STATE= + * + * For multiple cards: + * + * ALSA_CARD#_STATE= # where # is replaced with the card number + * + * State list: + * + * active # card was initialized and active + * skip # card was skipped (other card in group) + * waiting # card is waiting for the initial configuration + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include "alsactl.h" + +/* Global array to store card states (0 = active, 1 = waiting) */ +static int export_card_state[32]; + +/** + * export_card_state_set - Set card state + * @card: Card number + * @state: Card state (0 = active, 1 = waiting) + * + * Returns 0 on success, negative error code on failure + */ +int export_card_state_set(int card, int state) +{ + /* Check bounds */ + if (card < 0 || (unsigned long)card >= ARRAY_SIZE(export_card_state)) + return -EINVAL; + + export_card_state[card] = state; + return 0; +} + +/** + * export_card_state_print - Export and print card state information + * @iter: Card iterator containing current card information + * + * Prints key=value pairs based on iterator's single flag and export_card_state array. + * Returns 0 on success, negative error code on failure + */ +static int export_card_state_print(struct snd_card_iterator *iter) +{ + const char *state; + int istate; + + if (!iter) + return -EINVAL; + + /* Check bounds */ + if (iter->card < 0 || (unsigned long)iter->card >= ARRAY_SIZE(export_card_state)) + return -EINVAL; + + /* Determine state from export_card_state array */ + istate = export_card_state[iter->card]; + switch (istate) { + case CARD_STATE_WAIT: state = "waiting"; break; + case CARD_STATE_SKIP: state = "skip"; break; + default: state = "active"; break; + } + + if (iter->single) { + /* Single card export format */ + printf("ALSA_CARD_NUMBER=%d\n", iter->card); + printf("ALSA_CARD_STATE=%s\n", state); + } else { + /* Multiple cards export format */ + printf("ALSA_CARD%d_STATE=%s\n", iter->card, state); + } + + return 0; +} + +/** + * export_cards - Export state for all cards + * @cardname: Card name or NULL for all cards + * + * Returns 0 on success, negative error code on failure + */ +int export_cards(const char *cardname) +{ + struct snd_card_iterator iter; + const char *name; + int ret; + + ret = snd_card_iterator_sinit(&iter, cardname); + if (ret < 0) + return ret; + + while ((name = snd_card_iterator_next(&iter)) != NULL) { + ret = export_card_state_print(&iter); + if (ret < 0) + break; + } + + if (ret == 0) + ret = snd_card_iterator_error(&iter); + + return ret; +} diff --git a/alsactl/state.c b/alsactl/state.c index f9b9260..4d326a1 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -1729,6 +1729,8 @@ int load_state(const char *cfgdir, const char *file, } err = init(cfgdir, initfile, initflags | FLAG_UCM_FBOOT | FLAG_UCM_BOOT, cardname1); card_unlock(lock_fd, iter.card); + if (card_state_is_okay(err)) + export_card_state_set(iter.card, err); if (err < 0) { finalerr = err; initfailed(iter.card, "init", err); @@ -1754,8 +1756,10 @@ int load_state(const char *cfgdir, const char *file, /* error is ignored */ err = init_ucm(initflags | FLAG_UCM_FBOOT, iter.card); /* return code 1 and 2 -> postpone initialization */ - if (card_state_is_okay(err)) + if (card_state_is_okay(err)) { + export_card_state_set(iter.card, err); goto unlock_card; + } /* do a check if controls matches state file */ if (do_init && set_controls(iter.card, config, 0)) { err = init(cfgdir, initfile, initflags | FLAG_UCM_BOOT, cardname1); From 8d46253ac3716bd6f7e6bac78aed6955dba0b21b Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 27 Nov 2025 18:39:17 +0100 Subject: [PATCH 13/27] alsactl: ucm: add wrestore command and wait_for_card() for boot synchronization Implement wait.c with wait_for_card() function to monitor Boot control element and wait for card readiness using event-based polling. Add "wrestore" command to wait for card ready state before restore. Add FLAG_UCM_WAIT flag and move DEFAULT_SYNC_TIME to alsactl.h. Signed-off-by: Jaroslav Kysela --- alsactl/Makefile.am | 2 +- alsactl/alsactl.c | 6 +- alsactl/alsactl.h | 2 + alsactl/init_ucm.c | 2 - alsactl/state.c | 4 + alsactl/wait.c | 200 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 alsactl/wait.c diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index 6725c9c..26d80ba 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -11,7 +11,7 @@ AM_CFLAGS = -D_GNU_SOURCE AM_CPPFLAGS = -I$(top_srcdir)/include -alsactl_SOURCES=alsactl.c state.c lock.c utils.c \ +alsactl_SOURCES=alsactl.c state.c lock.c utils.c wait.c \ init_parse.c init_ucm.c boot_params.c \ daemon.c monitor.c clean.c info.c export.c diff --git a/alsactl/alsactl.c b/alsactl/alsactl.c index 8ea4a84..a91462e 100644 --- a/alsactl/alsactl.c +++ b/alsactl/alsactl.c @@ -123,6 +123,7 @@ static struct arg args[] = { { CARDCMD, "restore", "load current driver setup for one or each soundcards" }, { EMPCMD, NULL, " from configuration file" }, { CARDCMD, "nrestore", "like restore, but notify the daemon to rescan soundcards" }, +{ CARDCMD, "wrestore", "wait for card ready, then restore" }, { CARDCMD, "init", "initialize driver to a default state" }, { CARDCMD, "daemon", "store state periodically for one or each soundcards" }, { CARDCMD, "rdaemon", "like daemon but do the state restore at first" }, @@ -471,9 +472,12 @@ int main(int argc, char *argv[]) res = save_state(cfgfile, cardname); } else if (!strcmp(cmd, "restore") || !strcmp(cmd, "rdaemon") || - !strcmp(cmd, "nrestore")) { + !strcmp(cmd, "nrestore") || + !strcmp(cmd, "wrestore")) { if (removestate) remove(statefile); + if (!strcmp(cmd, "wrestore")) + initflags |= FLAG_UCM_WAIT; res = load_state(cfgdir, cfgfile, initfile, initflags, cardname, init_fallback); if (do_export && res >= 0) res = export_cards(cardname); diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h index aeee114..994485a 100644 --- a/alsactl/alsactl.h +++ b/alsactl/alsactl.h @@ -2,6 +2,7 @@ #include #define LOCK_TIMEOUT 10 +#define DEFAULT_SYNC_TIME 20 extern int debugflag; extern int force_restore; @@ -47,6 +48,7 @@ void log_handler(int prio, int interface, const char *file, int line, const char #define FLAG_UCM_DEFAULTS (1<<3) #define FLAG_UCM_NODEV (1<<4) #define FLAG_UCM_RESTORE (1<<5) +#define FLAG_UCM_WAIT (1<<6) enum { CARD_STATE_WAIT = 1, /* skip configuration (wait for sync) */ diff --git a/alsactl/init_ucm.c b/alsactl/init_ucm.c index 386f415..60967c4 100644 --- a/alsactl/init_ucm.c +++ b/alsactl/init_ucm.c @@ -29,8 +29,6 @@ #include -#define DEFAULT_SYNC_TIME 20 - /* * Helper: Check if card should skip initialization based on boot parameters * Returns: 1 if should skip, 2 if should skip other card, 0 if should continue, negative on error diff --git a/alsactl/state.c b/alsactl/state.c index 4d326a1..2ce9b62 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -1721,6 +1721,8 @@ int load_state(const char *cfgdir, const char *file, while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { if (!do_init) break; + if (initflags & FLAG_UCM_WAIT) + wait_for_card(-1, iter.card); lock_fd = card_lock(iter.card, LOCK_TIMEOUT); if (lock_fd < 0) { finalerr = lock_fd; @@ -1747,6 +1749,8 @@ int load_state(const char *cfgdir, const char *file, if (err < 0) goto out; while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { + if (initflags & FLAG_UCM_WAIT) + wait_for_card(-1, iter.card); lock_fd = card_lock(iter.card, LOCK_TIMEOUT); if (lock_fd < 0) { initfailed(iter.card, "lock", lock_fd); diff --git a/alsactl/wait.c b/alsactl/wait.c new file mode 100644 index 0000000..bebcc3c --- /dev/null +++ b/alsactl/wait.c @@ -0,0 +1,200 @@ +/* + * Advanced Linux Sound Architecture Control Program - Wait for Boot + * Copyright (c) by Jaroslav Kysela + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include "alsactl.h" + +/** + * \brief Wait for card boot synchronization using Boot control element + * \param timeout Maximum wait time in seconds + * \param cardno Card number + * \return 0 on success, negative error code on failure + * + * This function waits until the card releases the 'waiting' state (UCM). + * It monitors the '.Boot' control element and uses snd_ctl_wait() and + * snd_ctl_read() for event-based waiting, similar to boot_wait() in + * ../alsa-lib/alsa-lib/src/ucm/main.c + */ +int wait_for_card(long long timeout, int cardno) +{ + snd_ctl_t *handle; + snd_ctl_event_t *event; + snd_ctl_elem_id_t *id; + snd_ctl_elem_info_t *info; + snd_ctl_elem_value_t *value; + long long boot_time_val, restore_time_val; + long long synctime = -1; + struct timespec start_time, now; + char name[32]; + int err; + + sprintf(name, "hw:%d", cardno); + err = snd_ctl_open(&handle, name, SND_CTL_READONLY); + if (err < 0) { + error("snd_ctl_open error for %s: %s", name, snd_strerror(err)); + return err; + } + + /* Try to get synctime from boot_synctime in group configuration */ + if (timeout <= 0) { + bool valid = false, restored = false; + err = check_boot_params_validity(handle, cardno, NULL, &valid, NULL, &restored, NULL, &synctime); + if (err == 0 && synctime > 0) { + timeout = synctime; + dbg("Using boot_synctime value: %lld seconds", timeout); + } else { + timeout = DEFAULT_SYNC_TIME; + } + /* Break early if boot params are invalid or already restored */ + if (!valid || restored) { + dbg("Boot params check: valid=%d, restored=%d - skipping wait", valid, restored); + snd_ctl_close(handle); + return 0; + } + } + + snd_ctl_event_alloca(&event); + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_value_alloca(&value); + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name(id, ".Boot"); + + snd_ctl_elem_info_set_id(info, id); + err = snd_ctl_elem_info(handle, info); + if (err < 0) { + dbg("Boot control element not present on card %d, skipping wait", cardno); + snd_ctl_close(handle); + return 0; /* No Boot element, no wait needed */ + } + + if (snd_ctl_elem_info_get_type(info) != SND_CTL_ELEM_TYPE_INTEGER64) { + error("Boot control element is not INTEGER64 type on card %d", cardno); + snd_ctl_close(handle); + return -EINVAL; + } + + if (snd_ctl_elem_info_get_count(info) < 3) { + error("Boot control element does not have count >= 3 on card %d", cardno); + snd_ctl_close(handle); + return -EINVAL; + } + + err = snd_ctl_subscribe_events(handle, 1); + if (err < 0) { + error("Cannot subscribe to control events: %s", snd_strerror(err)); + snd_ctl_close(handle); + return err; + } + + clock_gettime(CLOCK_MONOTONIC_RAW, &start_time); + + dbg("Waiting for card %d to become ready (timeout=%lld seconds)", cardno, timeout); + + while (1) { + long long diff, remaining = 0; + long long sync_time_val = -1; + + clock_gettime(CLOCK_MONOTONIC_RAW, &now); + + /* Read current Boot control values */ + err = read_boot_params(handle, &boot_time_val, &sync_time_val, &restore_time_val, NULL); + if (err < 0) { + error("Failed to read Boot control element: %s", snd_strerror(err)); + goto _fin; + } + + dbg("Boot info: boot_time=%lld, sync_time=%lld, restore_time=%lld", boot_time_val, sync_time_val, restore_time_val); + + if (restore_time_val > 0) { + diff = now.tv_sec - restore_time_val; + dbg("Controls already restored (diff=%lld seconds), card is ready", diff); + err = 0; + goto _fin; + } + + /* note that realtime may differ from monotonic time, add one second for safety */ + diff = now.tv_sec - start_time.tv_sec; + if (diff > timeout + 1) { + dbg("Maximum wait time exceeded (%lld >= %lld seconds), proceeding", diff, timeout); + break; + } + + remaining = timeout - diff; + + /* Use synctime from element to limit timeout if available */ + if (sync_time_val > 0 && sync_time_val < timeout) { + dbg("Limiting timeout from %lld to sync_time %lld seconds", timeout, sync_time_val); + timeout = sync_time_val; + } + + if (!validate_boot_time(boot_time_val, now.tv_sec, timeout)) { + if (boot_time_val > 0) { + diff = now.tv_sec - boot_time_val; + dbg("Boot timeout reached (%lld >= %lld seconds), proceeding", diff, timeout); + } + break; + } + + diff = now.tv_sec - boot_time_val; + if (timeout - diff < remaining) + remaining = timeout - diff; + if (remaining <= 0) + remaining = 1; + + dbg("Waiting %lld seconds", remaining); + err = snd_ctl_wait(handle, remaining * 1000); + if (err < 0) { + error("snd_ctl_wait failed: %s", snd_strerror(err)); + goto _fin; + } + + if (err == 0) + continue; /* Timeout, no events */ + + /* Read and check events */ + while (snd_ctl_read(handle, event) > 0) { + if (!(snd_ctl_event_elem_get_mask(event) & SND_CTL_EVENT_MASK_VALUE)) + continue; /* Not a value change event */ + + if (snd_ctl_event_elem_get_interface(event) != SND_CTL_ELEM_IFACE_CARD || + snd_ctl_event_elem_get_index(event) != 0 || + strcmp(snd_ctl_event_elem_get_name(event), ".Boot") != 0) + continue; + + dbg("Boot control element value changed"); + break; + } + } + + err = 0; +_fin: + snd_ctl_subscribe_events(handle, 0); + snd_ctl_close(handle); + return err; +} From 8a71ec4b737eb4d54b4b007d0009bdbe12278474 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 27 Nov 2025 18:17:14 +0100 Subject: [PATCH 14/27] alsactl: add systemd service to handle deferred card initialization Add alsa-card-wait@.service to handle cards in waiting state. Update udev rules to use --export and trigger the wait service when ALSA_CARD_STATE=waiting. Service runs "alsactl wrestore" for deferred card initialization. Signed-off-by: Jaroslav Kysela --- alsactl/90-alsa-restore.rules.in | 5 +++-- alsactl/Makefile.am | 7 ++++++- alsactl/alsa-card-wait@.service.in | 12 ++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 alsactl/alsa-card-wait@.service.in diff --git a/alsactl/90-alsa-restore.rules.in b/alsactl/90-alsa-restore.rules.in index b3a80d5..6edf97a 100644 --- a/alsactl/90-alsa-restore.rules.in +++ b/alsactl/90-alsa-restore.rules.in @@ -24,7 +24,8 @@ IMPORT{program}="/usr/bin/cat /run/udev/alsa-hda-analog-card" ENV{ALSA_CARD_HDA_ANALOG}!="", ENV{ALSA_CARD_NUMBER}="$env{ALSA_CARD_HDA_ANALOG}" LABEL="alsa_restore_std" -TEST!="@daemonswitch@", RUN+="@sbindir@/alsactl@args@ restore $env{ALSA_CARD_NUMBER}" -TEST=="@daemonswitch@", RUN+="@sbindir@/alsactl@args@ nrestore $env{ALSA_CARD_NUMBER}" +TEST!="@daemonswitch@", IMPORT{program}="@sbindir@/alsactl@args@ --export restore $env{ALSA_CARD_NUMBER}" +TEST=="@daemonswitch@", IMPORT{program}="@sbindir@/alsactl@args@ --export nrestore $env{ALSA_CARD_NUMBER}" +ENV{ALSA_CARD_STATE}=="waiting", ENV{SYSTEMD_WANTS}="alsa-card-wait@$env{ALSA_CARD_NUMBER}.service" LABEL="alsa_restore_end" diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index 26d80ba..e3ba05b 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -31,7 +31,8 @@ if HAVE_SYSTEMD systemdsystemunit_DATA = \ alsa-state.service \ - alsa-restore.service + alsa-restore.service \ + alsa-card-wait@.service.in install-data-hook: $(MKDIR_P) -m 0755 \ @@ -60,6 +61,9 @@ alsa-state.service: alsa-state.service.in alsa-restore.service: alsa-restore.service.in $(edit) +alsa-card-wait@.service: alsa-card-wait@.service.in + $(edit) + 90-alsa-restore.rules: 90-alsa-restore.rules.in $(edit) @@ -71,6 +75,7 @@ EXTRA_DIST += \ CLEANFILES = \ alsa-state.service \ alsa-restore.service \ + alsa-card-wait@.service \ 90-alsa-restore.rules %.7: %.xml diff --git a/alsactl/alsa-card-wait@.service.in b/alsactl/alsa-card-wait@.service.in new file mode 100644 index 0000000..816074f --- /dev/null +++ b/alsactl/alsa-card-wait@.service.in @@ -0,0 +1,12 @@ +# +# ALSA card initialization handler for cards in waiting state +# This service is triggered by udev when ALSA_CARD_STATE=waiting +# + +[Unit] +Description=ALSA Card Initialization for card %I + +[Service] +Type=oneshot +RemainAfterExit=no +ExecStart=@sbindir@/alsactl@args@ wrestore %i From 3f66770aa97d5ee86def5bf1b64f31b67c8b5dd9 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Wed, 3 Dec 2025 19:37:01 +0100 Subject: [PATCH 15/27] alsactl: fix state restore to handle dynamic user control elements Track user control elements separately and adjust control count difference checking to account for dynamic user elements. Prevents false positives in re-init detection when only user controls have changed. Signed-off-by: Jaroslav Kysela --- alsactl/state.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/alsactl/state.c b/alsactl/state.c index 2ce9b62..ee88d35 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -1511,7 +1511,7 @@ static int set_controls(int card, snd_config_t *top, int doit) snd_ctl_elem_id_t *elem_id; snd_config_t *control; snd_config_iterator_t i, next; - int err, controls1 = -1, controls2 = -1; + int err, controls1 = -1, controls2 = -1, ucontrols = -1, diff; unsigned int idx, count = 0; char name[32], tmpid[16]; const char *id; @@ -1571,7 +1571,7 @@ static int set_controls(int card, snd_config_t *top, int doit) count = snd_ctl_elem_list_get_count(list); dbg("list count: %u", count); if (count == 0) - goto _check; + goto _free; snd_ctl_elem_list_set_offset(list, 0); if (snd_ctl_elem_list_alloc_space(list, count) < 0) { error("No enough memory..."); @@ -1581,13 +1581,15 @@ static int set_controls(int card, snd_config_t *top, int doit) error("Cannot determine controls (2): %s", snd_strerror(err)); goto _free; } - controls2 = 0; - /* skip non-readable elements */ + controls2 = ucontrols = 0; + /* skip non-readable and count user elements */ for (idx = 0; idx < count; ++idx) { snd_ctl_elem_info_clear(elem_info); snd_ctl_elem_list_get_id(list, idx, elem_id); snd_ctl_elem_info_set_id(elem_info, elem_id); if (snd_ctl_elem_info(handle, elem_info) == 0) { + if (snd_ctl_elem_info_is_user(elem_info)) + ucontrols++; if (!snd_ctl_elem_info_is_readable(elem_info)) continue; controls2++; @@ -1596,9 +1598,9 @@ static int set_controls(int card, snd_config_t *top, int doit) /* check if we have additional controls in driver */ /* in this case we should go through init procedure */ - _check: - dbg("controls1=%i controls2=%i", controls1, controls2); - if (controls1 >= 0 && controls1 != controls2) { + diff = controls2 - controls1; + dbg("controls1=%i controls2=%i ucontrols=%i diff=%i", controls1, controls2, ucontrols, diff); + if (controls1 >= 0 && (-diff > ucontrols || diff > ucontrols)) { /* not very informative */ /* but value is used for check only */ err = -EAGAIN; From bd1643809ed50f1242c6c89e9bb885de062fe5ec Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Wed, 3 Dec 2025 19:46:27 +0100 Subject: [PATCH 16/27] alsactl: update state file correctly when initialization failed There was missing continue statement to save the correct state reporting. Signed-off-by: Jaroslav Kysela --- alsactl/state.c | 1 + 1 file changed, 1 insertion(+) diff --git a/alsactl/state.c b/alsactl/state.c index ee88d35..c6a1630 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -1738,6 +1738,7 @@ int load_state(const char *cfgdir, const char *file, if (err < 0) { finalerr = err; initfailed(iter.card, "init", err); + continue; } initfailed(iter.card, "restore", -ENOENT); } From f047a8a66917960ef5a9ff0e41de507f0e58f381 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 4 Dec 2025 14:11:48 +0100 Subject: [PATCH 17/27] alsactl: move udev/systemd files to conf subdirectory It is a cleanup to not mix C source code with system configuration files. Signed-off-by: Jaroslav Kysela --- alsactl/.gitignore | 8 +++-- alsactl/Makefile.am | 31 ++++++++++--------- alsactl/{ => conf}/90-alsa-restore.rules.in | 0 alsactl/{ => conf}/alsa-card-wait@.service.in | 0 alsactl/{ => conf}/alsa-restore.service.in | 0 alsactl/{ => conf}/alsa-state.service.in | 0 6 files changed, 21 insertions(+), 18 deletions(-) rename alsactl/{ => conf}/90-alsa-restore.rules.in (100%) rename alsactl/{ => conf}/alsa-card-wait@.service.in (100%) rename alsactl/{ => conf}/alsa-restore.service.in (100%) rename alsactl/{ => conf}/alsa-state.service.in (100%) diff --git a/alsactl/.gitignore b/alsactl/.gitignore index 56ab3a2..e8fe186 100644 --- a/alsactl/.gitignore +++ b/alsactl/.gitignore @@ -1,3 +1,5 @@ -alsa-store.service -alsa-restore.service -90-alsa-restore.rules +conf/90-alsa-restore.rules +conf/alsa-state.service +conf/alsa-store.service +conf/alsa-restore.service +conf/alsa-card-wait@.service diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index e3ba05b..48b7c7a 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -25,14 +25,14 @@ noinst_HEADERS=alsactl.h list.h init_sysdeps.c init_utils_string.c \ init_utils_run.c init_sysfs.c udevrules_DATA = \ - 90-alsa-restore.rules + conf/90-alsa-restore.rules if HAVE_SYSTEMD systemdsystemunit_DATA = \ - alsa-state.service \ - alsa-restore.service \ - alsa-card-wait@.service.in + conf/alsa-state.service \ + conf/alsa-restore.service \ + conf/alsa-card-wait@.service install-data-hook: $(MKDIR_P) -m 0755 \ @@ -55,28 +55,29 @@ edit = \ -e "s;@args\@;$${args};g" \ < $< > $@ || rm $@ -alsa-state.service: alsa-state.service.in +conf/alsa-state.service: conf/alsa-state.service.in $(edit) -alsa-restore.service: alsa-restore.service.in +conf/alsa-restore.service: conf/alsa-restore.service.in $(edit) -alsa-card-wait@.service: alsa-card-wait@.service.in +conf/alsa-card-wait@.service: conf/alsa-card-wait@.service.in $(edit) -90-alsa-restore.rules: 90-alsa-restore.rules.in +conf/90-alsa-restore.rules: conf/90-alsa-restore.rules.in $(edit) EXTRA_DIST += \ - alsa-state.service.in \ - alsa-restore.service.in \ - 90-alsa-restore.rules.in + conf/alsa-state.service.in \ + conf/alsa-restore.service.in \ + conf/alsa-card-wait@.service.in \ + conf/90-alsa-restore.rules.in CLEANFILES = \ - alsa-state.service \ - alsa-restore.service \ - alsa-card-wait@.service \ - 90-alsa-restore.rules + conf/alsa-state.service \ + conf/alsa-restore.service \ + conf/alsa-card-wait@.service \ + conf/90-alsa-restore.rules %.7: %.xml xmlto man $? diff --git a/alsactl/90-alsa-restore.rules.in b/alsactl/conf/90-alsa-restore.rules.in similarity index 100% rename from alsactl/90-alsa-restore.rules.in rename to alsactl/conf/90-alsa-restore.rules.in diff --git a/alsactl/alsa-card-wait@.service.in b/alsactl/conf/alsa-card-wait@.service.in similarity index 100% rename from alsactl/alsa-card-wait@.service.in rename to alsactl/conf/alsa-card-wait@.service.in diff --git a/alsactl/alsa-restore.service.in b/alsactl/conf/alsa-restore.service.in similarity index 100% rename from alsactl/alsa-restore.service.in rename to alsactl/conf/alsa-restore.service.in diff --git a/alsactl/alsa-state.service.in b/alsactl/conf/alsa-state.service.in similarity index 100% rename from alsactl/alsa-state.service.in rename to alsactl/conf/alsa-state.service.in From 1ad182507669008875860ecda8b00ac564bcbe6e Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Thu, 4 Dec 2025 15:06:39 +0100 Subject: [PATCH 18/27] alsactl: ucm: restore controls for other cards in group When the primary card restores controls, all other cards in the card group should do this operation, too. Signed-off-by: Jaroslav Kysela --- alsactl/alsactl.h | 1 + alsactl/boot_params.c | 2 ++ alsactl/state.c | 43 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/alsactl/alsactl.h b/alsactl/alsactl.h index 994485a..2aefb89 100644 --- a/alsactl/alsactl.h +++ b/alsactl/alsactl.h @@ -98,6 +98,7 @@ int state_daemon(const char *file, const char *cardname, int period, int state_daemon_kill(const char *pidfile, const char *cmd); int clean(const char *cardname, char *const *extra_args); int snd_card_clean_cfgdir(const char *cfgdir, int cardno); +void add_linked_card(int cardno); /* export */ diff --git a/alsactl/boot_params.c b/alsactl/boot_params.c index 736597e..f44526a 100644 --- a/alsactl/boot_params.c +++ b/alsactl/boot_params.c @@ -893,6 +893,8 @@ static void boot_params_update_restored(snd_config_t *card_compound, int skip_ca goto _next; } + add_linked_card(card_val); + _next: snd_ctl_close(handle); } diff --git a/alsactl/state.c b/alsactl/state.c index c6a1630..2942f3f 100644 --- a/alsactl/state.c +++ b/alsactl/state.c @@ -29,6 +29,33 @@ #include #include "alsactl.h" +static int linked_cards[16]; + +static void init_linked_cards(void) +{ + size_t index; + + for (index = 0; index < ARRAY_SIZE(linked_cards); index++) + linked_cards[index] = -1; +} + +void add_linked_card(int cardno) +{ + size_t index; + + for (index = 0; index < ARRAY_SIZE(linked_cards); index++) { + if (linked_cards[index] == cardno) + return; + } + + for (index = 0; index < ARRAY_SIZE(linked_cards); index++) { + if (linked_cards[index] < 0) { + linked_cards[index] = cardno; + return; + } + } + error("Too many linked cards!"); +} static char *id_str(snd_ctl_elem_id_t *id) { @@ -1703,10 +1730,11 @@ int load_state(const char *cfgdir, const char *file, const char *initfile, int initflags, const char *cardname, int do_init) { - int err, finalerr = 0, open_failed, lock_fd; + int err, finalerr = 0, open_failed, lock_fd, cardno; struct snd_card_iterator iter; snd_config_t *config; const char *cardname1; + size_t index; config = NULL; err = load_configuration(file, &config, &open_failed); @@ -1723,6 +1751,7 @@ int load_state(const char *cfgdir, const char *file, while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { if (!do_init) break; + init_linked_cards(); if (initflags & FLAG_UCM_WAIT) wait_for_card(-1, iter.card); lock_fd = card_lock(iter.card, LOCK_TIMEOUT); @@ -1752,6 +1781,7 @@ int load_state(const char *cfgdir, const char *file, if (err < 0) goto out; while ((cardname1 = snd_card_iterator_next(&iter)) != NULL) { + init_linked_cards(); if (initflags & FLAG_UCM_WAIT) wait_for_card(-1, iter.card); lock_fd = card_lock(iter.card, LOCK_TIMEOUT); @@ -1780,6 +1810,17 @@ int load_state(const char *cfgdir, const char *file, finalerr = err; initfailed(iter.card, "restore", err); } + /* for linked cards, restore controls, too */ + for (index = 0; index < ARRAY_SIZE(linked_cards); index++) { + if ((cardno = linked_cards[index]) < 0) + break; + dbg("Restore for linked card %d", cardno); + if ((err = set_controls(cardno, config, 1))) { + if (!force_restore) + finalerr = err; + initfailed(cardno, "restore", err); + } + } unlock_card: card_unlock(lock_fd, iter.card); } From e5316c1aa6446c3b9156d1bbe5507bd76ef85971 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 5 Dec 2025 11:23:46 +0100 Subject: [PATCH 19/27] Revert "aplay: fix S24_LE wav header" This reverts commit 9cdcbe0e932b97fac88a6195f87647159a6c7194. It causes regression (see Link). The physical sample width is stored in byte_p_spl. Closes: https://github.com/alsa-project/alsa-utils/issues/308 Signed-off-by: Jaroslav Kysela --- aplay/aplay.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aplay/aplay.c b/aplay/aplay.c index bdc88c2..12d5c0b 100644 --- a/aplay/aplay.c +++ b/aplay/aplay.c @@ -2726,11 +2726,11 @@ static void begin_wave(int fd, size_t cnt) case SND_PCM_FORMAT_S16_LE: bits = 16; break; - case SND_PCM_FORMAT_S24_LE: /* S24_LE is 24 bits stored in 32 bit width with 8 bit padding */ case SND_PCM_FORMAT_S32_LE: - case SND_PCM_FORMAT_FLOAT_LE: + case SND_PCM_FORMAT_FLOAT_LE: bits = 32; break; + case SND_PCM_FORMAT_S24_LE: case SND_PCM_FORMAT_S24_3LE: bits = 24; break; From e78583ab7cde7a2ccbb56339dd51608fb78c5395 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 5 Dec 2025 10:47:30 +0100 Subject: [PATCH 20/27] aplay: reorganize format handling in begin_wave() Use strictly snd_pcm_format_physical_width/snd_pcm_format_width functions to determine the sample bit width. Signed-off-by: Jaroslav Kysela --- aplay/aplay.c | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/aplay/aplay.c b/aplay/aplay.c index 12d5c0b..63a5214 100644 --- a/aplay/aplay.c +++ b/aplay/aplay.c @@ -2710,7 +2710,7 @@ static void begin_wave(int fd, size_t cnt) WaveHeader h; WaveFmtBody f; WaveChunkHeader cf, cd; - int bits; + int width, physical_width; uint32_t tmp; uint16_t tmp2; @@ -2718,23 +2718,21 @@ static void begin_wave(int fd, size_t cnt) if (cnt == (size_t)-2) cnt = 0x7fffff00; - bits = 8; + width = snd_pcm_format_physical_width(hwparams.format); + physical_width = snd_pcm_format_width(hwparams.format); + + if (width < 0 || physical_width < 0) + goto _format; + switch ((unsigned long) hwparams.format) { case SND_PCM_FORMAT_U8: - bits = 8; - break; case SND_PCM_FORMAT_S16_LE: - bits = 16; - break; + case SND_PCM_FORMAT_S24_LE: /* S24_LE is 24 bits stored in 32 bit width with 8 bit padding */ case SND_PCM_FORMAT_S32_LE: - case SND_PCM_FORMAT_FLOAT_LE: - bits = 32; - break; - case SND_PCM_FORMAT_S24_LE: + case SND_PCM_FORMAT_FLOAT_LE: case SND_PCM_FORMAT_S24_3LE: - bits = 24; - break; default: +_format: error(_("Wave doesn't support %s format..."), snd_pcm_format_name(hwparams.format)); prg_exit(EXIT_FAILURE); } @@ -2752,17 +2750,11 @@ static void begin_wave(int fd, size_t cnt) f.format = LE_SHORT(WAV_FMT_PCM); f.channels = LE_SHORT(hwparams.channels); f.sample_fq = LE_INT(hwparams.rate); -#if 0 - tmp2 = (samplesize == 8) ? 1 : 2; - f.byte_p_spl = LE_SHORT(tmp2); - tmp = dsp_speed * hwparams.channels * (uint32_t) tmp2; -#else - tmp2 = hwparams.channels * snd_pcm_format_physical_width(hwparams.format) / 8; + tmp2 = hwparams.channels * physical_width / 8; f.byte_p_spl = LE_SHORT(tmp2); tmp = (uint32_t) tmp2 * hwparams.rate; -#endif f.byte_p_sec = LE_INT(tmp); - f.bit_p_spl = LE_SHORT(bits); + f.bit_p_spl = LE_SHORT(width); cd.type = WAV_DATA; cd.length = LE_INT(cnt); From 5910fb39b5470a6f352e0ed39b69571d1afecc5b Mon Sep 17 00:00:00 2001 From: qaqland Date: Thu, 27 Nov 2025 11:13:27 +0800 Subject: [PATCH 21/27] alsactl: fix error handling in check_control_cdev() Closes: https://github.com/alsa-project/alsa-utils/pull/310 Signed-off-by: qaqland Signed-off-by: Jaroslav Kysela --- alsactl/monitor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alsactl/monitor.c b/alsactl/monitor.c index 36e9398..e830ba5 100644 --- a/alsactl/monitor.c +++ b/alsactl/monitor.c @@ -176,7 +176,7 @@ static int check_control_cdev(int infd, bool *retry) ssize_t len = read(infd, buf, sizeof(*ev) + NAME_MAX); if (len < 0) { if (errno != EAGAIN) - err = errno; + err = -errno; break; } else if (len == 0) { break; From 6ffd9ede1315ec1ee60409b0d43b139836064931 Mon Sep 17 00:00:00 2001 From: Achill Gilgenast Date: Mon, 24 Nov 2025 15:53:04 +0100 Subject: [PATCH 22/27] configure: Allow systemd service installation without systemd.pc If the systemdsystemunitdir got passed anyway as a variable, we don't need the systemd.pc dependency anymore and allow building without a systemd build-dependency. Relevant in Alpine Linux, where we allowed systemd services to be (sub-)packaged (e.g. for downstreams like postmarketOS), but don't have systemd pacakged in Alpine itself, yet. Closes: https://github.com/alsa-project/alsa-utils/pull/309 Signed-off-by: Achill Gilgenast Signed-off-by: Jaroslav Kysela --- configure.ac | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 4532778..00a613c 100644 --- a/configure.ac +++ b/configure.ac @@ -419,8 +419,8 @@ AC_ARG_WITH([systemdsystemunitdir], if test "x$with_systemdsystemunitdir" != xno; then AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir]) fi -AM_CONDITIONAL(HAVE_SYSTEMD, [test "$have_min_systemd" = "yes" \ - -a -n "$with_systemdsystemunitdir" -a "x$with_systemdsystemunitdir" != xno ]) +AM_CONDITIONAL(HAVE_SYSTEMD, [test -n "$with_systemdsystemunitdir" \ + -a "x$with_systemdsystemunitdir" != xno ]) AC_ARG_WITH([asound-state-dir], AS_HELP_STRING([--with-asound-state-dir=DIR], [Directory to place asound.state file in]), From 2440724b5da25253bf8d3d8c6075ae125437cba3 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Sun, 7 Dec 2025 10:31:09 +0100 Subject: [PATCH 23/27] alsactl: fix error message arguments (remove card) Signed-off-by: Jaroslav Kysela --- alsactl/boot_params.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alsactl/boot_params.c b/alsactl/boot_params.c index f44526a..af0f4fd 100644 --- a/alsactl/boot_params.c +++ b/alsactl/boot_params.c @@ -810,7 +810,7 @@ restart_scan: err = boot_params_remove_card_config(group, cardno); if (err < 0) { - error("Unable to remove card %d from group '%s': %s", group_id, cardno, snd_strerror(err)); + error("Unable to remove card %d from group '%s': %s", cardno, group_id, snd_strerror(err)); continue; } if (err > 0) From e4de8633828c1719aad621723d995ee56803c387 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Sun, 7 Dec 2025 20:42:00 +0100 Subject: [PATCH 24/27] Release v1.2.15 Signed-off-by: Jaroslav Kysela --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 00a613c..0b6d82a 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ dnl Process this file with autoconf to produce a configure script. AC_PREREQ(2.59) -AC_INIT(alsa-utils, 1.2.14) +AC_INIT(alsa-utils, 1.2.15) AC_CONFIG_SRCDIR([aplay/aplay.c]) AC_PREFIX_DEFAULT(/usr) AM_INIT_AUTOMAKE([subdir-objects]) From 10587067fcf5b05e7fa5a84c38238dbffd079c4f Mon Sep 17 00:00:00 2001 From: Rudi Heitbaum Date: Mon, 8 Dec 2025 11:12:34 +0000 Subject: [PATCH 25/27] alsactl: fix build when in subdirectory create the conf directory as part of the edit recipe Closes: https://github.com/alsa-project/alsa-utils/pull/311 Signed-off-by: Rudi Heitbaum Signed-off-by: Jaroslav Kysela --- alsactl/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/alsactl/Makefile.am b/alsactl/Makefile.am index 48b7c7a..c8729e0 100644 --- a/alsactl/Makefile.am +++ b/alsactl/Makefile.am @@ -47,6 +47,7 @@ endif edit = \ extratest=$$(echo ' $(ALSACTL_UDEV_EXTRATEST)' | sed -e 's/__/ /g' -e 's/^ $$//'); \ args=$$(echo ' $(ALSACTL_UDEV_ARGS)' | sed -e 's/__/ /g' -e 's/^ $$//'); \ + mkdir -p conf; \ $(SED) -r -e 's,@sbindir\@,$(sbindir),g' \ -e 's,@mydatadir\@,$(mydatadir),g' \ -e 's,@daemonswitch\@,$(ALSACTL_DAEMONSWITCH),g' \ From a12ec83fa93e16a76f5c57f128f819dfe332f96c Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 16 Dec 2025 21:55:54 +0800 Subject: [PATCH 26/27] aplay: add missing break before the default case Add the break before the default case back. Otherwise, all cases will fall into the default/error case. Closes: https://github.com/alsa-project/alsa-utils/pull/315 Fixes: e78583ab7cde ("aplay: reorganize format handling in begin_wave()") Signed-off-by: Bard Liao Signed-off-by: Jaroslav Kysela --- aplay/aplay.c | 1 + 1 file changed, 1 insertion(+) diff --git a/aplay/aplay.c b/aplay/aplay.c index 63a5214..737d087 100644 --- a/aplay/aplay.c +++ b/aplay/aplay.c @@ -2731,6 +2731,7 @@ static void begin_wave(int fd, size_t cnt) case SND_PCM_FORMAT_S32_LE: case SND_PCM_FORMAT_FLOAT_LE: case SND_PCM_FORMAT_S24_3LE: + break; default: _format: error(_("Wave doesn't support %s format..."), snd_pcm_format_name(hwparams.format)); From 7b97437d78d3d2620b25ea37349ad291c53affa0 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Fri, 19 Dec 2025 11:32:02 +0100 Subject: [PATCH 27/27] Release v1.2.15.1 Signed-off-by: Jaroslav Kysela --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 0b6d82a..6f2705c 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ dnl Process this file with autoconf to produce a configure script. AC_PREREQ(2.59) -AC_INIT(alsa-utils, 1.2.15) +AC_INIT(alsa-utils, 1.2.15.1) AC_CONFIG_SRCDIR([aplay/aplay.c]) AC_PREFIX_DEFAULT(/usr) AM_INIT_AUTOMAKE([subdir-objects])