From ab24c1c77180041033b0872e0498ec12241336b8 Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 15:03:22 -0500 Subject: [PATCH 1/7] fix: set the volume of the right control on the right USB audio card With a USB audio device as the output, the volume keys could do nothing: - SetRawVolume() set the first control whose name had PCM or Playback and Volume, and stopped. On a USB headset adapter that is "Sidetone Playback Volume" (the microphone fed back into the headphones), which comes before "Headset Playback Volume" in the order of the controls. - It took the first card that is not audiocodec. A device that registers two USB cards (a Spark 40 guitar amp: one for the amp, one for the playback) got the volume on the wrong card. Now SetRawVolume() takes the card that audiomon wrote into .asoundrc, and the first USB card only where the file names none. It ranks the volume controls: Master, then the outputs (Headphone, Headset, Speaker, Line Out, Digital Out, IEC958), then PCM, then other Playback controls. Capture, the microphone and the inputs played into the output (Sidetone, Monitor, Loopback, Bypass, line in) are not the output. It sets each volume and switch of the best rank; the switches mute at 0. Other controls keep their level. Related: #485 --- workspace/tg5040/libmsettings/msettings.c | 119 ++++++++++++++++++---- workspace/tg5050/libmsettings/msettings.c | 119 ++++++++++++++++++---- 2 files changed, 200 insertions(+), 38 deletions(-) diff --git a/workspace/tg5040/libmsettings/msettings.c b/workspace/tg5040/libmsettings/msettings.c index 9b1c11edd..53b62f27b 100644 --- a/workspace/tg5040/libmsettings/msettings.c +++ b/workspace/tg5040/libmsettings/msettings.c @@ -1287,6 +1287,31 @@ static int get_usbc_card_num() { return -1; } +// The card that audiomon routes the audio to: the "card N" line that it writes into +// .asoundrc for a USB audio device. A device can register more than one USB card +// (a guitar amp: one card for itself and one for the playback), thus the first USB +// card is not always the output. Returns -1 where the file names no card. +static int get_asoundrc_card_num() { + const char *userdata = getenv("USERDATA_PATH"); + if (!userdata) return -1; + + char path[512]; + snprintf(path, sizeof(path), "%s/.asoundrc", userdata); + FILE *fp = fopen(path, "r"); + if (!fp) return -1; + + char line[256]; + int card_num = -1; + while (fgets(line, sizeof(line), fp)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; + card_num = -1; + } + fclose(fp); + return card_num; +} + static int get_audiocodec_card_num() { FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1306,6 +1331,38 @@ static int get_audiocodec_card_num() { return -1; } +// The rank of a mixer control of a USB audio device as its output volume, or -1 for a +// control that is not the output: capture, the microphone, and an input played into +// the output (sidetone, monitor, loopback, bypass, line in) +static int get_dac_control_playback_priority(const char* control_name) { + int capture_control = strstr(control_name, "Capture") + || strstr(control_name, "Mic"); + + int monitor_control = strstr(control_name, "Sidetone") + || strstr(control_name, "Monitor") + || strstr(control_name, "Loopback") + || strstr(control_name, "Line Playback") + || strstr(control_name, " In ") + || strstr(control_name, "Bypass"); + + if (capture_control || monitor_control) return -1; + + if (strstr(control_name, "Master")) return 3; + + if (strstr(control_name, "Headphone") + || strstr(control_name, "Headset") + || strstr(control_name, "Speaker") + || strstr(control_name, "Line Out") + || strstr(control_name, "Digital Out") + || strstr(control_name, "IEC958")) return 2; + + if (strstr(control_name, "PCM")) return 1; + + if (strstr(control_name, "Playback")) return 0; + + return -1; +} + void SetRawVolume(int val) { // in: 0-100 if (settings->mute && GetMutedVolume() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) val = scaleVolume(GetMutedVolume()); @@ -1323,7 +1380,8 @@ void SetRawVolume(int val) { // in: 0-100 } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { // USB DAC path: grab the first card that is not called "audiocodec" - int card_num = get_usbc_card_num(); + int card_num = get_asoundrc_card_num(); + if (card_num < 0) card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); return; @@ -1335,24 +1393,47 @@ void SetRawVolume(int val) { // in: 0-100 return; } - const unsigned int num_controls = mixer_get_num_ctls(mixer); - for (unsigned int i = 0; i < num_controls; i++) { - struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); - const char *name = mixer_ctl_get_name(ctl); - if (!name) continue; - - if ((strstr(name, "PCM") || strstr(name, "Playback")) && (strstr(name, "Volume") || strstr(name, "volume"))) { - if (mixer_ctl_get_type(ctl) == MIXER_CTL_TYPE_INT) { - int min = mixer_ctl_get_range_min(ctl); - int max = mixer_ctl_get_range_max(ctl); - int volume = min + (val * (max - min)) / 100; - unsigned int num_values = mixer_ctl_get_num_values(ctl); - for (unsigned int i = 0; i < num_values; i++) - mixer_ctl_set_value(ctl, i, volume); - } - break; - } - } + const unsigned int num_controls = mixer_get_num_ctls(mixer); + + // find the best rank among the volume controls + int best_rank = -1; + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + if (!strstr(name, "Volume") && !strstr(name, "volume")) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank > best_rank) best_rank = rank; + } + + // drive each volume and switch of that rank; the other controls keep their level + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank < 0 || rank != best_rank) continue; + + enum mixer_ctl_type type = mixer_ctl_get_type(ctl); + unsigned int num_values = mixer_ctl_get_num_values(ctl); + int volume_control = strstr(name, "Volume") || strstr(name, "volume"); + int switch_control = strstr(name, "Switch") || strstr(name, "switch"); + + if (volume_control && type == MIXER_CTL_TYPE_INT) { + int min = mixer_ctl_get_range_min(ctl); + int max = mixer_ctl_get_range_max(ctl); + int volume = min + (val * (max - min)) / 100; + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, volume); + } + else if (switch_control && type == MIXER_CTL_TYPE_BOOL) { + // the lowest step could still be audible, mute for real at 0 + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, val > 0); + } + } mixer_close(mixer); } else { diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index a8ddea691..dc099d1c2 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -943,6 +943,31 @@ static int get_usbc_card_num() { return -1; } +// The card that audiomon routes the audio to: the "card N" line that it writes into +// .asoundrc for a USB audio device. A device can register more than one USB card +// (a guitar amp: one card for itself and one for the playback), thus the first USB +// card is not always the output. Returns -1 where the file names no card. +static int get_asoundrc_card_num() { + const char *userdata = getenv("USERDATA_PATH"); + if (!userdata) return -1; + + char path[512]; + snprintf(path, sizeof(path), "%s/.asoundrc", userdata); + FILE *fp = fopen(path, "r"); + if (!fp) return -1; + + char line[256]; + int card_num = -1; + while (fgets(line, sizeof(line), fp)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; + card_num = -1; + } + fclose(fp); + return card_num; +} + static int get_audiocodec_card_num() { FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -962,6 +987,38 @@ static int get_audiocodec_card_num() { return -1; } +// The rank of a mixer control of a USB audio device as its output volume, or -1 for a +// control that is not the output: capture, the microphone, and an input played into +// the output (sidetone, monitor, loopback, bypass, line in) +static int get_dac_control_playback_priority(const char* control_name) { + int capture_control = strstr(control_name, "Capture") + || strstr(control_name, "Mic"); + + int monitor_control = strstr(control_name, "Sidetone") + || strstr(control_name, "Monitor") + || strstr(control_name, "Loopback") + || strstr(control_name, "Line Playback") + || strstr(control_name, " In ") + || strstr(control_name, "Bypass"); + + if (capture_control || monitor_control) return -1; + + if (strstr(control_name, "Master")) return 3; + + if (strstr(control_name, "Headphone") + || strstr(control_name, "Headset") + || strstr(control_name, "Speaker") + || strstr(control_name, "Line Out") + || strstr(control_name, "Digital Out") + || strstr(control_name, "IEC958")) return 2; + + if (strstr(control_name, "PCM")) return 1; + + if (strstr(control_name, "Playback")) return 0; + + return -1; +} + void SetRawVolume(int val) { // in: 0-100 if (settings->mute && GetMutedVolume() != SETTINGS_DEFAULT_MUTE_NO_CHANGE) val = scaleVolume(GetMutedVolume()); @@ -979,7 +1036,8 @@ void SetRawVolume(int val) { // in: 0-100 } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { // USB DAC path: grab the first card that is not called "audiocodec" - int card_num = get_usbc_card_num(); + int card_num = get_asoundrc_card_num(); + if (card_num < 0) card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); return; @@ -991,24 +1049,47 @@ void SetRawVolume(int val) { // in: 0-100 return; } - const unsigned int num_controls = mixer_get_num_ctls(mixer); - for (unsigned int i = 0; i < num_controls; i++) { - struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); - const char *name = mixer_ctl_get_name(ctl); - if (!name) continue; - - if ((strstr(name, "PCM") || strstr(name, "Playback")) && (strstr(name, "Volume") || strstr(name, "volume"))) { - if (mixer_ctl_get_type(ctl) == MIXER_CTL_TYPE_INT) { - int min = mixer_ctl_get_range_min(ctl); - int max = mixer_ctl_get_range_max(ctl); - int volume = min + (val * (max - min)) / 100; - unsigned int num_values = mixer_ctl_get_num_values(ctl); - for (unsigned int i = 0; i < num_values; i++) - mixer_ctl_set_value(ctl, i, volume); - } - break; - } - } + const unsigned int num_controls = mixer_get_num_ctls(mixer); + + // find the best rank among the volume controls + int best_rank = -1; + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + if (!strstr(name, "Volume") && !strstr(name, "volume")) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank > best_rank) best_rank = rank; + } + + // drive each volume and switch of that rank; the other controls keep their level + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank < 0 || rank != best_rank) continue; + + enum mixer_ctl_type type = mixer_ctl_get_type(ctl); + unsigned int num_values = mixer_ctl_get_num_values(ctl); + int volume_control = strstr(name, "Volume") || strstr(name, "volume"); + int switch_control = strstr(name, "Switch") || strstr(name, "switch"); + + if (volume_control && type == MIXER_CTL_TYPE_INT) { + int min = mixer_ctl_get_range_min(ctl); + int max = mixer_ctl_get_range_max(ctl); + int volume = min + (val * (max - min)) / 100; + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, volume); + } + else if (switch_control && type == MIXER_CTL_TYPE_BOOL) { + // the lowest step could still be audible, mute for real at 0 + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, val > 0); + } + } mixer_close(mixer); } else { From e5c8af43af52b298c078317fb629e45df885ac1a Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 15:44:59 -0500 Subject: [PATCH 2/7] fix: say where the USB audio card comes from The USB DAC path takes the card from .asoundrc, which audiomon writes, and the first card that is not audiocodec only as a fallback. The comment of AUDIO_SINK_USBDAC still said that the DAC is card 1. --- workspace/desktop/libmsettings/msettings.h | 2 +- workspace/tg5040/libmsettings/msettings.c | 3 ++- workspace/tg5040/libmsettings/msettings.h | 2 +- workspace/tg5050/libmsettings/msettings.c | 3 ++- workspace/tg5050/libmsettings/msettings.h | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/workspace/desktop/libmsettings/msettings.h b/workspace/desktop/libmsettings/msettings.h index 0b302d08f..7b60e1c32 100644 --- a/workspace/desktop/libmsettings/msettings.h +++ b/workspace/desktop/libmsettings/msettings.h @@ -47,7 +47,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // assumes being exposed as card 1 to alsa +#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc int GetAudioSink(void); void SetAudioSink(int value); diff --git a/workspace/tg5040/libmsettings/msettings.c b/workspace/tg5040/libmsettings/msettings.c index 53b62f27b..ed293088b 100644 --- a/workspace/tg5040/libmsettings/msettings.c +++ b/workspace/tg5040/libmsettings/msettings.c @@ -1379,7 +1379,8 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: grab the first card that is not called "audiocodec" + // USB DAC path: the card that audiomon routes the audio to (.asoundrc), else + // the first card that is not called "audiocodec" int card_num = get_asoundrc_card_num(); if (card_num < 0) card_num = get_usbc_card_num(); if(card_num < 0) { diff --git a/workspace/tg5040/libmsettings/msettings.h b/workspace/tg5040/libmsettings/msettings.h index db46458bf..6e7370524 100644 --- a/workspace/tg5040/libmsettings/msettings.h +++ b/workspace/tg5040/libmsettings/msettings.h @@ -51,7 +51,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // assumes being exposed as card 1 to alsa +#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc int GetAudioSink(void); void SetAudioSink(int value); diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index dc099d1c2..764b2b20b 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -1035,7 +1035,8 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: grab the first card that is not called "audiocodec" + // USB DAC path: the card that audiomon routes the audio to (.asoundrc), else + // the first card that is not called "audiocodec" int card_num = get_asoundrc_card_num(); if (card_num < 0) card_num = get_usbc_card_num(); if(card_num < 0) { diff --git a/workspace/tg5050/libmsettings/msettings.h b/workspace/tg5050/libmsettings/msettings.h index ae756b649..3574ec2b9 100644 --- a/workspace/tg5050/libmsettings/msettings.h +++ b/workspace/tg5050/libmsettings/msettings.h @@ -54,7 +54,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // assumes being exposed as card 1 to alsa +#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc int GetAudioSink(void); void SetAudioSink(int value); From 99643f30176fd4f8eb91fc58268f47f8fba0f8a9 Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 15:47:59 -0500 Subject: [PATCH 3/7] refactor: find the USB audio card in one function get_asoundrc_card_num() and get_usbc_card_num() had one caller, which called the second only where the first gave no card. get_usbc_card_num() now reads .asoundrc first and falls back to the first card that is not audiocodec. --- workspace/tg5040/libmsettings/msettings.c | 55 +++++++++++------------ workspace/tg5050/libmsettings/msettings.c | 55 +++++++++++------------ 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/workspace/tg5040/libmsettings/msettings.c b/workspace/tg5040/libmsettings/msettings.c index ed293088b..debc1b28f 100644 --- a/workspace/tg5040/libmsettings/msettings.c +++ b/workspace/tg5040/libmsettings/msettings.c @@ -1268,7 +1268,31 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { return 0; } +// The USB audio card: the one that audiomon routes the audio to, from the "card N" +// line that it writes into .asoundrc, else the first card that is not the built-in +// codec. A device can register more than one USB card (the Spark 40 guitar amp: one +// for the amp and one for the playback), thus the first USB card is not always the +// output. static int get_usbc_card_num() { + const char *userdata = getenv("USERDATA_PATH"); + if (userdata) { + char path[512]; + snprintf(path, sizeof(path), "%s/.asoundrc", userdata); + FILE *rc = fopen(path, "r"); + if (rc) { + char line[256]; + int card_num = -1; + while (fgets(line, sizeof(line), rc)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; + card_num = -1; + } + fclose(rc); + if (card_num >= 0) return card_num; + } + } + FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1287,31 +1311,6 @@ static int get_usbc_card_num() { return -1; } -// The card that audiomon routes the audio to: the "card N" line that it writes into -// .asoundrc for a USB audio device. A device can register more than one USB card -// (a guitar amp: one card for itself and one for the playback), thus the first USB -// card is not always the output. Returns -1 where the file names no card. -static int get_asoundrc_card_num() { - const char *userdata = getenv("USERDATA_PATH"); - if (!userdata) return -1; - - char path[512]; - snprintf(path, sizeof(path), "%s/.asoundrc", userdata); - FILE *fp = fopen(path, "r"); - if (!fp) return -1; - - char line[256]; - int card_num = -1; - while (fgets(line, sizeof(line), fp)) { - const char *p = line; - while (*p == ' ' || *p == '\t') p++; - if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; - card_num = -1; - } - fclose(fp); - return card_num; -} - static int get_audiocodec_card_num() { FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1379,10 +1378,8 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: the card that audiomon routes the audio to (.asoundrc), else - // the first card that is not called "audiocodec" - int card_num = get_asoundrc_card_num(); - if (card_num < 0) card_num = get_usbc_card_num(); + // USB DAC path: the card that audiomon routes the audio to (see get_usbc_card_num()) + int card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); return; diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index 764b2b20b..83cc85aea 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -924,7 +924,31 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { return 0; } +// The USB audio card: the one that audiomon routes the audio to, from the "card N" +// line that it writes into .asoundrc, else the first card that is not the built-in +// codec. A device can register more than one USB card (the Spark 40 guitar amp: one +// for the amp and one for the playback), thus the first USB card is not always the +// output. static int get_usbc_card_num() { + const char *userdata = getenv("USERDATA_PATH"); + if (userdata) { + char path[512]; + snprintf(path, sizeof(path), "%s/.asoundrc", userdata); + FILE *rc = fopen(path, "r"); + if (rc) { + char line[256]; + int card_num = -1; + while (fgets(line, sizeof(line), rc)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; + card_num = -1; + } + fclose(rc); + if (card_num >= 0) return card_num; + } + } + FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -943,31 +967,6 @@ static int get_usbc_card_num() { return -1; } -// The card that audiomon routes the audio to: the "card N" line that it writes into -// .asoundrc for a USB audio device. A device can register more than one USB card -// (a guitar amp: one card for itself and one for the playback), thus the first USB -// card is not always the output. Returns -1 where the file names no card. -static int get_asoundrc_card_num() { - const char *userdata = getenv("USERDATA_PATH"); - if (!userdata) return -1; - - char path[512]; - snprintf(path, sizeof(path), "%s/.asoundrc", userdata); - FILE *fp = fopen(path, "r"); - if (!fp) return -1; - - char line[256]; - int card_num = -1; - while (fgets(line, sizeof(line), fp)) { - const char *p = line; - while (*p == ' ' || *p == '\t') p++; - if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; - card_num = -1; - } - fclose(fp); - return card_num; -} - static int get_audiocodec_card_num() { FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1035,10 +1034,8 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: the card that audiomon routes the audio to (.asoundrc), else - // the first card that is not called "audiocodec" - int card_num = get_asoundrc_card_num(); - if (card_num < 0) card_num = get_usbc_card_num(); + // USB DAC path: the card that audiomon routes the audio to (see get_usbc_card_num()) + int card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); return; From c1d7a1710a61f59e6ed6487bde7d6e8fec5780e9 Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 15:56:05 -0500 Subject: [PATCH 4/7] fix: take the mixer card from ctl.!default of .asoundrc The volume is a mixer control, thus the card is the one of the control interface, ctl.!default. The lookup took the first card line, which is pcm.!default. audiomon writes the same card in both today, but a file can name another one for each, as a Bluetooth .asoundrc does (pcm BlueALSA, ctl hw card 0). Any card line stays the fallback. --- workspace/tg5040/libmsettings/msettings.c | 24 ++++++++++++----------- workspace/tg5050/libmsettings/msettings.c | 24 ++++++++++++----------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/workspace/tg5040/libmsettings/msettings.c b/workspace/tg5040/libmsettings/msettings.c index debc1b28f..89e89c644 100644 --- a/workspace/tg5040/libmsettings/msettings.c +++ b/workspace/tg5040/libmsettings/msettings.c @@ -1268,31 +1268,34 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { return 0; } -// The USB audio card: the one that audiomon routes the audio to, from the "card N" -// line that it writes into .asoundrc, else the first card that is not the built-in -// codec. A device can register more than one USB card (the Spark 40 guitar amp: one -// for the amp and one for the playback), thus the first USB card is not always the -// output. static int get_usbc_card_num() { + // first choice - .asoundrc maintained by audiomon const char *userdata = getenv("USERDATA_PATH"); if (userdata) { char path[512]; snprintf(path, sizeof(path), "%s/.asoundrc", userdata); FILE *rc = fopen(path, "r"); if (rc) { + // the mixer is the control interface: the card of ctl.!default, else any card char line[256]; - int card_num = -1; + int in_ctl = 0, ctl_card = -1, any_card = -1, card_num; while (fgets(line, sizeof(line), rc)) { const char *p = line; while (*p == ' ' || *p == '\t') p++; - if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; - card_num = -1; + if (strncmp(p, "ctl.", 4) == 0) in_ctl = 1; + else if (strncmp(p, "pcm.", 4) == 0) in_ctl = 0; + else if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) { + if (in_ctl && ctl_card < 0) ctl_card = card_num; + if (any_card < 0) any_card = card_num; + } } fclose(rc); - if (card_num >= 0) return card_num; + if (ctl_card >= 0) return ctl_card; + if (any_card >= 0) return any_card; } } + // second choice - first non-built-in card FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1378,7 +1381,6 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: the card that audiomon routes the audio to (see get_usbc_card_num()) int card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); @@ -1393,7 +1395,7 @@ void SetRawVolume(int val) { // in: 0-100 const unsigned int num_controls = mixer_get_num_ctls(mixer); - // find the best rank among the volume controls + // find the best rank among the volume controls (-1 means not found) int best_rank = -1; for (unsigned int i = 0; i < num_controls; i++) { struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index 83cc85aea..3c1abffe4 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -924,31 +924,34 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { return 0; } -// The USB audio card: the one that audiomon routes the audio to, from the "card N" -// line that it writes into .asoundrc, else the first card that is not the built-in -// codec. A device can register more than one USB card (the Spark 40 guitar amp: one -// for the amp and one for the playback), thus the first USB card is not always the -// output. static int get_usbc_card_num() { + // first choice - .asoundrc maintained by audiomon const char *userdata = getenv("USERDATA_PATH"); if (userdata) { char path[512]; snprintf(path, sizeof(path), "%s/.asoundrc", userdata); FILE *rc = fopen(path, "r"); if (rc) { + // the mixer is the control interface: the card of ctl.!default, else any card char line[256]; - int card_num = -1; + int in_ctl = 0, ctl_card = -1, any_card = -1, card_num; while (fgets(line, sizeof(line), rc)) { const char *p = line; while (*p == ' ' || *p == '\t') p++; - if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) break; - card_num = -1; + if (strncmp(p, "ctl.", 4) == 0) in_ctl = 1; + else if (strncmp(p, "pcm.", 4) == 0) in_ctl = 0; + else if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) { + if (in_ctl && ctl_card < 0) ctl_card = card_num; + if (any_card < 0) any_card = card_num; + } } fclose(rc); - if (card_num >= 0) return card_num; + if (ctl_card >= 0) return ctl_card; + if (any_card >= 0) return any_card; } } + // second choice - first non-built-in card FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -1034,7 +1037,6 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: the card that audiomon routes the audio to (see get_usbc_card_num()) int card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); @@ -1049,7 +1051,7 @@ void SetRawVolume(int val) { // in: 0-100 const unsigned int num_controls = mixer_get_num_ctls(mixer); - // find the best rank among the volume controls + // find the best rank among the volume controls (-1 means not found) int best_rank = -1; for (unsigned int i = 0; i < num_controls; i++) { struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); From fd28731849f6bff272e94f8a2fe8c586f032071a Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 16:01:15 -0500 Subject: [PATCH 5/7] fix: list the three choices of the USB audio card in one comment --- workspace/tg5040/libmsettings/msettings.c | 6 +++--- workspace/tg5050/libmsettings/msettings.c | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/workspace/tg5040/libmsettings/msettings.c b/workspace/tg5040/libmsettings/msettings.c index 89e89c644..39d786189 100644 --- a/workspace/tg5040/libmsettings/msettings.c +++ b/workspace/tg5040/libmsettings/msettings.c @@ -1269,14 +1269,14 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { } static int get_usbc_card_num() { - // first choice - .asoundrc maintained by audiomon + // first choice - .asoundrc maintained by audiomon (card from ctl.!default section) + // second choice - .asoundrc maintained by audiomon (any card) const char *userdata = getenv("USERDATA_PATH"); if (userdata) { char path[512]; snprintf(path, sizeof(path), "%s/.asoundrc", userdata); FILE *rc = fopen(path, "r"); if (rc) { - // the mixer is the control interface: the card of ctl.!default, else any card char line[256]; int in_ctl = 0, ctl_card = -1, any_card = -1, card_num; while (fgets(line, sizeof(line), rc)) { @@ -1295,7 +1295,7 @@ static int get_usbc_card_num() { } } - // second choice - first non-built-in card + // fallback - first non-built-in card FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; diff --git a/workspace/tg5050/libmsettings/msettings.c b/workspace/tg5050/libmsettings/msettings.c index 3c1abffe4..12a343b12 100644 --- a/workspace/tg5050/libmsettings/msettings.c +++ b/workspace/tg5050/libmsettings/msettings.c @@ -925,14 +925,14 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { } static int get_usbc_card_num() { - // first choice - .asoundrc maintained by audiomon + // first choice - .asoundrc maintained by audiomon (card from ctl.!default section) + // second choice - .asoundrc maintained by audiomon (any card) const char *userdata = getenv("USERDATA_PATH"); if (userdata) { char path[512]; snprintf(path, sizeof(path), "%s/.asoundrc", userdata); FILE *rc = fopen(path, "r"); if (rc) { - // the mixer is the control interface: the card of ctl.!default, else any card char line[256]; int in_ctl = 0, ctl_card = -1, any_card = -1, card_num; while (fgets(line, sizeof(line), rc)) { @@ -951,7 +951,7 @@ static int get_usbc_card_num() { } } - // second choice - first non-built-in card + // fallback - first non-built-in card FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; From 408f46550e00cdfecdba084ff8fcb9a5cded08ec Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Thu, 1 Oct 2026 16:14:49 -0500 Subject: [PATCH 6/7] fix: describe AUDIO_SINK_USBDAC as the card that audiomon monitors --- workspace/desktop/libmsettings/msettings.h | 2 +- workspace/tg5040/libmsettings/msettings.h | 2 +- workspace/tg5050/libmsettings/msettings.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace/desktop/libmsettings/msettings.h b/workspace/desktop/libmsettings/msettings.h index 7b60e1c32..54c65888f 100644 --- a/workspace/desktop/libmsettings/msettings.h +++ b/workspace/desktop/libmsettings/msettings.h @@ -47,7 +47,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc +#define AUDIO_SINK_USBDAC 2 // USB DAC card, monitored by audiomon int GetAudioSink(void); void SetAudioSink(int value); diff --git a/workspace/tg5040/libmsettings/msettings.h b/workspace/tg5040/libmsettings/msettings.h index 6e7370524..47ae76ce4 100644 --- a/workspace/tg5040/libmsettings/msettings.h +++ b/workspace/tg5040/libmsettings/msettings.h @@ -51,7 +51,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc +#define AUDIO_SINK_USBDAC 2 // USB DAC card, monitored by audiomon int GetAudioSink(void); void SetAudioSink(int value); diff --git a/workspace/tg5050/libmsettings/msettings.h b/workspace/tg5050/libmsettings/msettings.h index 3574ec2b9..f16a85b2e 100644 --- a/workspace/tg5050/libmsettings/msettings.h +++ b/workspace/tg5050/libmsettings/msettings.h @@ -54,7 +54,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // the card is the one that audiomon writes into .asoundrc +#define AUDIO_SINK_USBDAC 2 // USB DAC card, monitored by audiomon int GetAudioSink(void); void SetAudioSink(int value); From e11a7142964258b744569733fa356b31ce31c5c9 Mon Sep 17 00:00:00 2001 From: Nikita Borodikhin Date: Sat, 3 Oct 2026 12:11:37 -0500 Subject: [PATCH 7/7] Apply the fix to h700 Apply the same USB DAC fix h700: take the card from the .asoundrc of audiomon, then drive the output volume control skipping sidetone and monitor controls. --- workspace/h700/libmsettings/msettings.c | 119 ++++++++++++++++++++---- workspace/h700/libmsettings/msettings.h | 2 +- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/workspace/h700/libmsettings/msettings.c b/workspace/h700/libmsettings/msettings.c index eea421100..d3069a7df 100644 --- a/workspace/h700/libmsettings/msettings.c +++ b/workspace/h700/libmsettings/msettings.c @@ -787,6 +787,33 @@ static int get_a2dp_simple_control_name(char *buf, size_t buflen) { } static int get_usbc_card_num() { + // first choice - .asoundrc maintained by audiomon (card from ctl.!default section) + // second choice - .asoundrc maintained by audiomon (any card) + const char *userdata = getenv("USERDATA_PATH"); + if (userdata) { + char path[512]; + snprintf(path, sizeof(path), "%s/.asoundrc", userdata); + FILE *rc = fopen(path, "r"); + if (rc) { + char line[256]; + int in_ctl = 0, ctl_card = -1, any_card = -1, card_num; + while (fgets(line, sizeof(line), rc)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "ctl.", 4) == 0) in_ctl = 1; + else if (strncmp(p, "pcm.", 4) == 0) in_ctl = 0; + else if (strncmp(p, "card ", 5) == 0 && sscanf(p + 5, "%d", &card_num) == 1) { + if (in_ctl && ctl_card < 0) ctl_card = card_num; + if (any_card < 0) any_card = card_num; + } + } + fclose(rc); + if (ctl_card >= 0) return ctl_card; + if (any_card >= 0) return any_card; + } + } + + // fallback - first non-built-in card FILE *fp = popen("cat /proc/asound/cards", "r"); if (!fp) return -1; @@ -824,6 +851,38 @@ static int get_audiocodec_card_num() { return -1; } +// The rank of a mixer control of a USB audio device as its output volume, or -1 for a +// control that is not the output: capture, the microphone, and an input played into +// the output (sidetone, monitor, loopback, bypass, line in) +static int get_dac_control_playback_priority(const char* control_name) { + int capture_control = strstr(control_name, "Capture") + || strstr(control_name, "Mic"); + + int monitor_control = strstr(control_name, "Sidetone") + || strstr(control_name, "Monitor") + || strstr(control_name, "Loopback") + || strstr(control_name, "Line Playback") + || strstr(control_name, " In ") + || strstr(control_name, "Bypass"); + + if (capture_control || monitor_control) return -1; + + if (strstr(control_name, "Master")) return 3; + + if (strstr(control_name, "Headphone") + || strstr(control_name, "Headset") + || strstr(control_name, "Speaker") + || strstr(control_name, "Line Out") + || strstr(control_name, "Digital Out") + || strstr(control_name, "IEC958")) return 2; + + if (strstr(control_name, "PCM")) return 1; + + if (strstr(control_name, "Playback")) return 0; + + return -1; +} + void SetRawVolume(int val) { // in: 0-100 if (GetAudioSink() == AUDIO_SINK_BLUETOOTH) { // bluealsa is a mixer plugin, not exposed as a separate card @@ -843,7 +902,6 @@ void SetRawVolume(int val) { // in: 0-100 } } else if (GetAudioSink() == AUDIO_SINK_USBDAC) { - // USB DAC path: grab the first card that is not called "audiocodec" int card_num = get_usbc_card_num(); if(card_num < 0) { printf("Failed to find USB audio card\n"); fflush(stdout); @@ -856,24 +914,47 @@ void SetRawVolume(int val) { // in: 0-100 return; } - const unsigned int num_controls = mixer_get_num_ctls(mixer); - for (unsigned int i = 0; i < num_controls; i++) { - struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); - const char *name = mixer_ctl_get_name(ctl); - if (!name) continue; - - if ((strstr(name, "PCM") || strstr(name, "Playback")) && (strstr(name, "Volume") || strstr(name, "volume"))) { - if (mixer_ctl_get_type(ctl) == MIXER_CTL_TYPE_INT) { - int min = mixer_ctl_get_range_min(ctl); - int max = mixer_ctl_get_range_max(ctl); - int volume = min + (val * (max - min)) / 100; - unsigned int num_values = mixer_ctl_get_num_values(ctl); - for (unsigned int i = 0; i < num_values; i++) - mixer_ctl_set_value(ctl, i, volume); - } - break; - } - } + const unsigned int num_controls = mixer_get_num_ctls(mixer); + + // find the best rank among the volume controls (-1 means not found) + int best_rank = -1; + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + if (!strstr(name, "Volume") && !strstr(name, "volume")) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank > best_rank) best_rank = rank; + } + + // drive each volume and switch of that rank; the other controls keep their level + for (unsigned int i = 0; i < num_controls; i++) { + struct mixer_ctl *ctl = mixer_get_ctl(mixer, i); + const char *name = mixer_ctl_get_name(ctl); + if (!name) continue; + + int rank = get_dac_control_playback_priority(name); + if (rank < 0 || rank != best_rank) continue; + + enum mixer_ctl_type type = mixer_ctl_get_type(ctl); + unsigned int num_values = mixer_ctl_get_num_values(ctl); + int volume_control = strstr(name, "Volume") || strstr(name, "volume"); + int switch_control = strstr(name, "Switch") || strstr(name, "switch"); + + if (volume_control && type == MIXER_CTL_TYPE_INT) { + int min = mixer_ctl_get_range_min(ctl); + int max = mixer_ctl_get_range_max(ctl); + int volume = min + (val * (max - min)) / 100; + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, volume); + } + else if (switch_control && type == MIXER_CTL_TYPE_BOOL) { + // the lowest step could still be audible, mute for real at 0 + for (unsigned int j = 0; j < num_values; j++) + mixer_ctl_set_value(ctl, j, val > 0); + } + } mixer_close(mixer); } else { diff --git a/workspace/h700/libmsettings/msettings.h b/workspace/h700/libmsettings/msettings.h index e75bc6852..5f2900ff6 100644 --- a/workspace/h700/libmsettings/msettings.h +++ b/workspace/h700/libmsettings/msettings.h @@ -51,7 +51,7 @@ void SetJack(int value); // 0-1 #define AUDIO_SINK_DEFAULT 0 // use system default, usually speaker (or jack if plugged in) #define AUDIO_SINK_BLUETOOTH 1 // software control via bluealsa, not a separate card -#define AUDIO_SINK_USBDAC 2 // assumes being exposed as card 1 to alsa +#define AUDIO_SINK_USBDAC 2 // USB DAC card, monitored by audiomon int GetAudioSink(void); void SetAudioSink(int value);