|
1 | 1 | /*
|
2 | 2 | * BlueALSA - alsa-mixer.c
|
3 |
| - * Copyright (c) 2016-2021 Arkadiusz Bokowy |
| 3 | + * Copyright (c) 2016-2025 Arkadiusz Bokowy |
4 | 4 | *
|
5 | 5 | * This file is a part of bluez-alsa.
|
6 | 6 | *
|
|
10 | 10 |
|
11 | 11 | #include "alsa-mixer.h"
|
12 | 12 |
|
| 13 | +#include <assert.h> |
| 14 | +#include <math.h> |
13 | 15 | #include <stdio.h>
|
14 | 16 | #include <string.h>
|
15 | 17 |
|
16 |
| -int alsa_mixer_open(snd_mixer_t **mixer, snd_mixer_elem_t **elem, |
17 |
| - const char *dev_name, const char *elem_name, unsigned int elem_idx, |
| 18 | +#include "shared/log.h" |
| 19 | + |
| 20 | +static int alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned int mask) { |
| 21 | + struct alsa_mixer *mixer = snd_mixer_elem_get_callback_private(elem); |
| 22 | + if (mask & SND_CTL_EVENT_MASK_REMOVE) |
| 23 | + /* The element has been removed and cannot |
| 24 | + * now be used - we must close the mixer. */ |
| 25 | + return -1; |
| 26 | + if (mask & SND_CTL_EVENT_MASK_VALUE) |
| 27 | + mixer->event_handler(mixer->event_handler_userdata); |
| 28 | + return 0; |
| 29 | +} |
| 30 | + |
| 31 | +void alsa_mixer_init( |
| 32 | + struct alsa_mixer *mixer, |
| 33 | + alsa_mixer_event_handler handler, |
| 34 | + void *userdata) { |
| 35 | + memset(mixer, 0, sizeof(*mixer)); |
| 36 | + mixer->event_handler = handler; |
| 37 | + mixer->event_handler_userdata = userdata; |
| 38 | +} |
| 39 | + |
| 40 | +int alsa_mixer_open( |
| 41 | + struct alsa_mixer *mixer, |
| 42 | + const char *dev_name, |
| 43 | + const char *elem_name, |
| 44 | + unsigned int elem_idx, |
18 | 45 | char **msg) {
|
19 | 46 |
|
20 |
| - snd_mixer_t *_mixer = NULL; |
21 |
| - snd_mixer_elem_t *_elem; |
22 | 47 | char buf[256];
|
| 48 | + long vmin_db; |
23 | 49 | int err;
|
24 | 50 |
|
25 | 51 | snd_mixer_selem_id_t *id;
|
26 | 52 | snd_mixer_selem_id_alloca(&id);
|
27 | 53 | snd_mixer_selem_id_set_name(id, elem_name);
|
28 | 54 | snd_mixer_selem_id_set_index(id, elem_idx);
|
29 | 55 |
|
30 |
| - if ((err = snd_mixer_open(&_mixer, 0)) != 0) { |
| 56 | + if ((err = snd_mixer_open(&mixer->mixer, 0)) != 0) { |
31 | 57 | snprintf(buf, sizeof(buf), "Open mixer: %s", snd_strerror(err));
|
32 | 58 | goto fail;
|
33 | 59 | }
|
34 |
| - if ((err = snd_mixer_attach(_mixer, dev_name)) != 0) { |
| 60 | + if ((err = snd_mixer_attach(mixer->mixer, dev_name)) != 0) { |
35 | 61 | snprintf(buf, sizeof(buf), "Attach mixer: %s", snd_strerror(err));
|
36 | 62 | goto fail;
|
37 | 63 | }
|
38 |
| - if ((err = snd_mixer_selem_register(_mixer, NULL, NULL)) != 0) { |
| 64 | + if ((err = snd_mixer_selem_register(mixer->mixer, NULL, NULL)) != 0) { |
39 | 65 | snprintf(buf, sizeof(buf), "Register mixer class: %s", snd_strerror(err));
|
40 | 66 | goto fail;
|
41 | 67 | }
|
42 |
| - if ((err = snd_mixer_load(_mixer)) != 0) { |
| 68 | + if ((err = snd_mixer_load(mixer->mixer)) != 0) { |
43 | 69 | snprintf(buf, sizeof(buf), "Load mixer elements: %s", snd_strerror(err));
|
44 | 70 | goto fail;
|
45 | 71 | }
|
46 |
| - if ((_elem = snd_mixer_find_selem(_mixer, id)) == NULL) { |
| 72 | + if ((mixer->elem = snd_mixer_find_selem(mixer->mixer, id)) == NULL) { |
47 | 73 | snprintf(buf, sizeof(buf), "Mixer element not found");
|
48 | 74 | err = -1;
|
49 | 75 | goto fail;
|
50 | 76 | }
|
51 | 77 |
|
52 |
| - *mixer = _mixer; |
53 |
| - *elem = _elem; |
| 78 | + mixer->has_mute_switch = snd_mixer_selem_has_playback_switch(mixer->elem); |
| 79 | + |
| 80 | + if ((err = snd_mixer_selem_get_playback_dB_range(mixer->elem, |
| 81 | + &vmin_db, &mixer->volume_db_max_value)) != 0) |
| 82 | + warn("Couldn't get ALSA mixer playback dB range: %s", snd_strerror(err)); |
| 83 | + |
| 84 | + snd_mixer_elem_set_callback(mixer->elem, alsa_mixer_elem_callback); |
| 85 | + snd_mixer_elem_set_callback_private(mixer->elem, mixer); |
| 86 | + |
54 | 87 | return 0;
|
55 | 88 |
|
56 | 89 | fail:
|
57 |
| - if (_mixer != NULL) |
58 |
| - snd_mixer_close(_mixer); |
| 90 | + alsa_mixer_close(mixer); |
59 | 91 | if (msg != NULL)
|
60 | 92 | *msg = strdup(buf);
|
61 | 93 | return err;
|
62 | 94 | }
|
| 95 | + |
| 96 | +void alsa_mixer_close( |
| 97 | + struct alsa_mixer *mixer) { |
| 98 | + if (mixer->mixer != NULL) |
| 99 | + snd_mixer_close(mixer->mixer); |
| 100 | + mixer->mixer = NULL; |
| 101 | + mixer->elem = NULL; |
| 102 | +} |
| 103 | + |
| 104 | +int alsa_mixer_get_volume( |
| 105 | + const struct alsa_mixer *mixer, |
| 106 | + unsigned int vmax, |
| 107 | + unsigned int *volume, |
| 108 | + bool *muted) { |
| 109 | + |
| 110 | + snd_mixer_elem_t *elem = mixer->elem; |
| 111 | + long long volume_db_sum = 0; |
| 112 | + bool alsa_muted = true; |
| 113 | + |
| 114 | + snd_mixer_selem_channel_id_t ch; |
| 115 | + for (ch = 0; snd_mixer_selem_has_playback_channel(elem, ch) == 1; ch++) { |
| 116 | + |
| 117 | + long ch_volume_db; |
| 118 | + int ch_switch = 1; |
| 119 | + int err; |
| 120 | + |
| 121 | + if ((err = snd_mixer_selem_get_playback_dB(elem, ch, &ch_volume_db)) != 0) { |
| 122 | + error("Couldn't get ALSA mixer playback dB level: %s", snd_strerror(err)); |
| 123 | + return -1; |
| 124 | + } |
| 125 | + |
| 126 | + /* Mute switch is an optional feature for a mixer element. */ |
| 127 | + if (mixer->has_mute_switch && |
| 128 | + (err = snd_mixer_selem_get_playback_switch(elem, ch, &ch_switch)) != 0) { |
| 129 | + error("Couldn't get ALSA mixer playback switch: %s", snd_strerror(err)); |
| 130 | + return -1; |
| 131 | + } |
| 132 | + |
| 133 | + volume_db_sum += ch_volume_db; |
| 134 | + /* Normalize volume level so it will not exceed 0.00 dB. */ |
| 135 | + volume_db_sum -= mixer->volume_db_max_value; |
| 136 | + |
| 137 | + if (ch_switch == 1) |
| 138 | + alsa_muted = false; |
| 139 | + |
| 140 | + } |
| 141 | + |
| 142 | + /* Safety check for undefined behavior from |
| 143 | + * out-of-bounds dB conversion. */ |
| 144 | + assert(volume_db_sum <= 0LL); |
| 145 | + |
| 146 | + /* Convert dB to loudness using decibel formula and |
| 147 | + * round to the nearest integer. */ |
| 148 | + *volume = lround(pow(2, (0.01 * volume_db_sum / ch) / 10) * vmax); |
| 149 | + |
| 150 | + /* If mixer element supports playback switch, |
| 151 | + * return the actual mute state to the caller. */ |
| 152 | + if (mixer->has_mute_switch) |
| 153 | + *muted = alsa_muted; |
| 154 | + |
| 155 | + return 0; |
| 156 | +} |
| 157 | + |
| 158 | +int alsa_mixer_set_volume( |
| 159 | + struct alsa_mixer *mixer, |
| 160 | + unsigned int vmax, |
| 161 | + unsigned int volume, |
| 162 | + bool muted) { |
| 163 | + |
| 164 | + /* Convert loudness to dB using decibel formula. */ |
| 165 | + long db = 10 * log2(1.0 * volume / vmax) * 100; |
| 166 | + /* Shift dB level so it will match hardware range. */ |
| 167 | + db += mixer->volume_db_max_value; |
| 168 | + |
| 169 | + int err; |
| 170 | + if ((err = snd_mixer_selem_set_playback_dB_all(mixer->elem, db, 0)) != 0) { |
| 171 | + error("Couldn't set ALSA mixer playback dB level: %s", snd_strerror(err)); |
| 172 | + return -1; |
| 173 | + } |
| 174 | + |
| 175 | + /* Mute switch is an optional feature for a mixer element. */ |
| 176 | + if (mixer->has_mute_switch && |
| 177 | + (err = snd_mixer_selem_set_playback_switch_all(mixer->elem, !muted)) != 0) { |
| 178 | + error("Couldn't set ALSA mixer playback mute switch: %s", snd_strerror(err)); |
| 179 | + return -1; |
| 180 | + } |
| 181 | + |
| 182 | + return 0; |
| 183 | +} |
0 commit comments