From d74f4a6a59f6919be5e96094aba1530d7bb032b0 Mon Sep 17 00:00:00 2001 From: borine <32966433+borine@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:08:01 +0000 Subject: [PATCH] aplay: Refactor code related to ALSA mixer --- test/test-utils-aplay.c | 4 +- utils/aplay/alsa-mixer.c | 149 +++++++++++++++++++++++++--- utils/aplay/alsa-mixer.h | 70 ++++++++++++- utils/aplay/aplay.c | 207 ++++++++------------------------------- 4 files changed, 245 insertions(+), 185 deletions(-) diff --git a/test/test-utils-aplay.c b/test/test-utils-aplay.c index 5deeeab98..acc30b74c 100644 --- a/test/test-utils-aplay.c +++ b/test/test-utils-aplay.c @@ -245,14 +245,12 @@ CK_START_TEST(test_play_mixer_setup) { NULL), -1); spawn_terminate(&sp_ba_aplay, 500); - char output[8192] = ""; + char output[16384] = ""; ck_assert_int_gt(spawn_read(&sp_ba_aplay, NULL, 0, output, sizeof(output)), 0); #if DEBUG ck_assert_ptr_ne(strstr(output, "Opening ALSA mixer: name=bluealsa:DEV=23:45:67:89:AB:CD elem=SCO index=0"), NULL); - ck_assert_ptr_ne(strstr(output, - "Setting up ALSA mixer volume synchronization"), NULL); #endif spawn_close(&sp_ba_aplay, NULL); diff --git a/utils/aplay/alsa-mixer.c b/utils/aplay/alsa-mixer.c index 416d72929..07280dac6 100644 --- a/utils/aplay/alsa-mixer.c +++ b/utils/aplay/alsa-mixer.c @@ -1,6 +1,6 @@ /* * BlueALSA - alsa-mixer.c - * Copyright (c) 2016-2021 Arkadiusz Bokowy + * Copyright (c) 2016-2025 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -10,16 +10,42 @@ #include "alsa-mixer.h" +#include +#include #include #include -int alsa_mixer_open(snd_mixer_t **mixer, snd_mixer_elem_t **elem, - const char *dev_name, const char *elem_name, unsigned int elem_idx, +#include "shared/log.h" + +static int alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned int mask) { + struct alsa_mixer *mixer = snd_mixer_elem_get_callback_private(elem); + if (mask & SND_CTL_EVENT_MASK_REMOVE) + /* The element has been removed and cannot + * now be used - we must close the mixer. */ + return -1; + if (mask & SND_CTL_EVENT_MASK_VALUE) + mixer->event_handler(mixer->event_handler_userdata); + return 0; +} + +void alsa_mixer_init( + struct alsa_mixer *mixer, + alsa_mixer_event_handler handler, + void *userdata) { + memset(mixer, 0, sizeof(*mixer)); + mixer->event_handler = handler; + mixer->event_handler_userdata = userdata; +} + +int alsa_mixer_open( + struct alsa_mixer *mixer, + const char *dev_name, + const char *elem_name, + unsigned int elem_idx, char **msg) { - snd_mixer_t *_mixer = NULL; - snd_mixer_elem_t *_elem; char buf[256]; + long vmin_db; int err; snd_mixer_selem_id_t *id; @@ -27,36 +53,131 @@ int alsa_mixer_open(snd_mixer_t **mixer, snd_mixer_elem_t **elem, snd_mixer_selem_id_set_name(id, elem_name); snd_mixer_selem_id_set_index(id, elem_idx); - if ((err = snd_mixer_open(&_mixer, 0)) != 0) { + if ((err = snd_mixer_open(&mixer->mixer, 0)) != 0) { snprintf(buf, sizeof(buf), "Open mixer: %s", snd_strerror(err)); goto fail; } - if ((err = snd_mixer_attach(_mixer, dev_name)) != 0) { + if ((err = snd_mixer_attach(mixer->mixer, dev_name)) != 0) { snprintf(buf, sizeof(buf), "Attach mixer: %s", snd_strerror(err)); goto fail; } - if ((err = snd_mixer_selem_register(_mixer, NULL, NULL)) != 0) { + if ((err = snd_mixer_selem_register(mixer->mixer, NULL, NULL)) != 0) { snprintf(buf, sizeof(buf), "Register mixer class: %s", snd_strerror(err)); goto fail; } - if ((err = snd_mixer_load(_mixer)) != 0) { + if ((err = snd_mixer_load(mixer->mixer)) != 0) { snprintf(buf, sizeof(buf), "Load mixer elements: %s", snd_strerror(err)); goto fail; } - if ((_elem = snd_mixer_find_selem(_mixer, id)) == NULL) { + if ((mixer->elem = snd_mixer_find_selem(mixer->mixer, id)) == NULL) { snprintf(buf, sizeof(buf), "Mixer element not found"); err = -1; goto fail; } - *mixer = _mixer; - *elem = _elem; + mixer->has_mute_switch = snd_mixer_selem_has_playback_switch(mixer->elem); + + if ((err = snd_mixer_selem_get_playback_dB_range(mixer->elem, + &vmin_db, &mixer->volume_db_max_value)) != 0) + warn("Couldn't get ALSA mixer playback dB range: %s", snd_strerror(err)); + + snd_mixer_elem_set_callback(mixer->elem, alsa_mixer_elem_callback); + snd_mixer_elem_set_callback_private(mixer->elem, mixer); + return 0; fail: - if (_mixer != NULL) - snd_mixer_close(_mixer); + alsa_mixer_close(mixer); if (msg != NULL) *msg = strdup(buf); return err; } + +void alsa_mixer_close( + struct alsa_mixer *mixer) { + if (mixer->mixer != NULL) + snd_mixer_close(mixer->mixer); + mixer->mixer = NULL; + mixer->elem = NULL; +} + +int alsa_mixer_get_volume( + const struct alsa_mixer *mixer, + unsigned int vmax, + unsigned int *volume, + bool *muted) { + + snd_mixer_elem_t *elem = mixer->elem; + long long volume_db_sum = 0; + bool alsa_muted = true; + + snd_mixer_selem_channel_id_t ch; + for (ch = 0; snd_mixer_selem_has_playback_channel(elem, ch) == 1; ch++) { + + long ch_volume_db; + int ch_switch = 1; + int err; + + if ((err = snd_mixer_selem_get_playback_dB(elem, ch, &ch_volume_db)) != 0) { + error("Couldn't get ALSA mixer playback dB level: %s", snd_strerror(err)); + return -1; + } + + /* Mute switch is an optional feature for a mixer element. */ + if (mixer->has_mute_switch && + (err = snd_mixer_selem_get_playback_switch(elem, ch, &ch_switch)) != 0) { + error("Couldn't get ALSA mixer playback switch: %s", snd_strerror(err)); + return -1; + } + + volume_db_sum += ch_volume_db; + /* Normalize volume level so it will not exceed 0.00 dB. */ + volume_db_sum -= mixer->volume_db_max_value; + + if (ch_switch == 1) + alsa_muted = false; + + } + + /* Safety check for undefined behavior from + * out-of-bounds dB conversion. */ + assert(volume_db_sum <= 0LL); + + /* Convert dB to loudness using decibel formula and + * round to the nearest integer. */ + *volume = lround(pow(2, (0.01 * volume_db_sum / ch) / 10) * vmax); + + /* If mixer element supports playback switch, + * return the actual mute state to the caller. */ + if (mixer->has_mute_switch) + *muted = alsa_muted; + + return 0; +} + +int alsa_mixer_set_volume( + struct alsa_mixer *mixer, + unsigned int vmax, + unsigned int volume, + bool muted) { + + /* Convert loudness to dB using decibel formula. */ + long db = 10 * log2(1.0 * volume / vmax) * 100; + /* Shift dB level so it will match hardware range. */ + db += mixer->volume_db_max_value; + + int err; + if ((err = snd_mixer_selem_set_playback_dB_all(mixer->elem, db, 0)) != 0) { + error("Couldn't set ALSA mixer playback dB level: %s", snd_strerror(err)); + return -1; + } + + /* Mute switch is an optional feature for a mixer element. */ + if (mixer->has_mute_switch && + (err = snd_mixer_selem_set_playback_switch_all(mixer->elem, !muted)) != 0) { + error("Couldn't set ALSA mixer playback mute switch: %s", snd_strerror(err)); + return -1; + } + + return 0; +} diff --git a/utils/aplay/alsa-mixer.h b/utils/aplay/alsa-mixer.h index d3556c7a4..25e8bc319 100644 --- a/utils/aplay/alsa-mixer.h +++ b/utils/aplay/alsa-mixer.h @@ -1,6 +1,6 @@ /* * BlueALSA - alsa-mixer.h - * Copyright (c) 2016-2021 Arkadiusz Bokowy + * Copyright (c) 2016-2025 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -8,13 +8,77 @@ * */ +#pragma once #ifndef BLUEALSA_APLAY_ALSAMIXER_H_ #define BLUEALSA_APLAY_ALSAMIXER_H_ +#include + #include -int alsa_mixer_open(snd_mixer_t **mixer, snd_mixer_elem_t **elem, - const char *dev_name, const char *elem_name, unsigned int elem_idx, +typedef void (*alsa_mixer_event_handler)(void *userdata); + +struct alsa_mixer { + + /* The ALSA mixer handle. */ + snd_mixer_t *mixer; + snd_mixer_elem_t *elem; + + long volume_db_max_value; + bool has_mute_switch; + + alsa_mixer_event_handler event_handler; + void *event_handler_userdata; + +}; + +void alsa_mixer_init( + struct alsa_mixer *mixer, + alsa_mixer_event_handler handler, + void *userdata); + +int alsa_mixer_open( + struct alsa_mixer *mixer, + const char *dev_name, + const char *elem_name, + unsigned int elem_idx, char **msg); +void alsa_mixer_close( + struct alsa_mixer *mixer); + +inline static bool alsa_mixer_is_open( + const struct alsa_mixer *mixer) { + return mixer->mixer != NULL && mixer->elem != NULL; +} + +inline static int alsa_mixer_poll_descriptors_count( + struct alsa_mixer *mixer) { + return snd_mixer_poll_descriptors_count(mixer->mixer); +} + +inline static int alsa_mixer_poll_descriptors( + struct alsa_mixer *mixer, + struct pollfd* pfds, + unsigned int space) { + return snd_mixer_poll_descriptors(mixer->mixer, pfds, space); +} + +inline static int alsa_mixer_handle_events( + struct alsa_mixer *mixer) { + return snd_mixer_handle_events(mixer->mixer); +} + +int alsa_mixer_get_volume( + const struct alsa_mixer *mixer, + unsigned int vmax, + unsigned int *volume, + bool *muted); + +int alsa_mixer_set_volume( + struct alsa_mixer *mixer, + unsigned int vmax, + unsigned int volume, + bool muted); + #endif diff --git a/utils/aplay/aplay.c b/utils/aplay/aplay.c index 2bfd0cda7..45d1caa10 100644 --- a/utils/aplay/aplay.c +++ b/utils/aplay/aplay.c @@ -1,6 +1,6 @@ /* * BlueALSA - aplay.c - * Copyright (c) 2016-2024 Arkadiusz Bokowy + * Copyright (c) 2016-2025 Arkadiusz Bokowy * * This file is a part of bluez-alsa. * @@ -65,10 +65,7 @@ struct io_worker { /* opened playback PCM device */ struct alsa_pcm alsa_pcm; /* mixer for volume control */ - snd_mixer_t *snd_mixer; - snd_mixer_elem_t *snd_mixer_elem; - long mixer_volume_db_max_value; - bool mixer_has_mute_switch; + struct alsa_mixer alsa_mixer; /* if true, playback is active */ atomic_bool active; /* human-readable BT address */ @@ -331,96 +328,20 @@ static int pause_device_player(const struct ba_pcm *ba_pcm) { return ret; } -/** - * Open ALSA mixer element for volume control. */ -static int io_worker_mixer_open( - struct io_worker *worker, - const char *dev_name, - const char *elem_name, - unsigned int elem_idx) { - - if (dev_name == NULL) - return 0; - - debug("Opening ALSA mixer: name=%s elem=%s index=%u", - dev_name, elem_name, elem_idx); - - snd_mixer_elem_t *elem; - long vmin_db, vmax_db = 0; - bool has_mute_switch; - char *tmp; - int err; - - if (alsa_mixer_open(&worker->snd_mixer, &elem, - dev_name, elem_name, elem_idx, &tmp) != 0) { - warn("Couldn't open ALSA mixer: %s", tmp); - free(tmp); - return -1; - } - - has_mute_switch = snd_mixer_selem_has_playback_switch(elem); - - if ((err = snd_mixer_selem_get_playback_dB_range(elem, &vmin_db, &vmax_db)) != 0) - warn("Couldn't get ALSA mixer playback dB range: %s", snd_strerror(err)); - - worker->snd_mixer_elem = elem; - worker->mixer_has_mute_switch = has_mute_switch; - worker->mixer_volume_db_max_value = vmax_db; - - return 0; -} - /** * Update BlueALSA PCM volume according to ALSA mixer element. */ static int io_worker_mixer_volume_sync_ba_pcm( struct io_worker *worker, struct ba_pcm *ba_pcm) { - snd_mixer_elem_t *elem = worker->snd_mixer_elem; - const int vmax = BA_PCM_VOLUME_MAX(ba_pcm); - long long volume_db_sum = 0; - bool muted = true; - - snd_mixer_selem_channel_id_t ch; - for (ch = 0; snd_mixer_selem_has_playback_channel(elem, ch) == 1; ch++) { - - long ch_volume_db; - int ch_switch = 1; - int err; - - if ((err = snd_mixer_selem_get_playback_dB(elem, ch, &ch_volume_db)) != 0) { - error("Couldn't get ALSA mixer playback dB level: %s", snd_strerror(err)); - return -1; - } - - /* Mute switch is an optional feature for a mixer element. */ - if (worker->mixer_has_mute_switch && - (err = snd_mixer_selem_get_playback_switch(elem, ch, &ch_switch)) != 0) { - error("Couldn't get ALSA mixer playback switch: %s", snd_strerror(err)); - return -1; - } - - volume_db_sum += ch_volume_db; - /* Normalize volume level so it will not exceed 0.00 dB. */ - volume_db_sum -= worker->mixer_volume_db_max_value; - - if (ch_switch == 1) - muted = false; - - } - - /* Safety check for undefined behavior from - * out-of-bounds dB conversion. */ - assert(volume_db_sum <= 0LL); - - /* Convert dB to loudness using decibel formula and - * round to the nearest integer. */ - int volume = lround(pow(2, (0.01 * volume_db_sum / ch) / 10) * vmax); - + unsigned int volume; /* If mixer element does not support playback switch, - * use our global muted state. */ - if (!worker->mixer_has_mute_switch) - muted = pcm_muted; + * use our global muted state as a default value. */ + bool muted = pcm_muted; + + const int vmax = BA_PCM_VOLUME_MAX(ba_pcm); + if (alsa_mixer_get_volume(&worker->alsa_mixer, vmax, &volume, &muted) != 0) + return -1; for (size_t i = 0; i < ba_pcm->channels; i++) { ba_pcm->volume[i].muted = muted; @@ -439,7 +360,7 @@ static int io_worker_mixer_volume_sync_ba_pcm( /** * Update ALSA mixer element according to BlueALSA PCM volume. */ -static int io_worker_mixer_volume_sync_snd_mixer_elem( +static int io_worker_mixer_volume_sync_alsa_mixer( struct io_worker *worker, struct ba_pcm *ba_pcm) { @@ -447,8 +368,7 @@ static int io_worker_mixer_volume_sync_snd_mixer_elem( if (ba_pcm->soft_volume) return 0; - snd_mixer_elem_t *elem = worker->snd_mixer_elem; - if (elem == NULL) + if (!alsa_mixer_is_open(&worker->alsa_mixer)) return 0; /* User can connect BlueALSA PCM to mono, stereo or multi-channel output. @@ -468,55 +388,13 @@ static int io_worker_mixer_volume_sync_snd_mixer_elem( pcm_muted = muted; const unsigned int vmax = BA_PCM_VOLUME_MAX(ba_pcm); - /* convert loudness to dB using decibel formula */ - long db = 10 * log2(1.0 * volume_sum / ba_pcm->channels / vmax) * 100; - db += worker->mixer_volume_db_max_value; - - int err; - if ((err = snd_mixer_selem_set_playback_dB_all(elem, db, 0)) != 0) { - error("Couldn't set ALSA mixer playback dB level: %s", snd_strerror(err)); - return -1; - } - - /* mute switch is an optional feature for a mixer element */ - if (worker->mixer_has_mute_switch && - (err = snd_mixer_selem_set_playback_switch_all(elem, !muted)) != 0) { - error("Couldn't set ALSA mixer playback mute switch: %s", snd_strerror(err)); - return -1; - } - - return 0; -} - -static int io_worker_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned int mask) { - struct io_worker *worker = snd_mixer_elem_get_callback_private(elem); - if (mask & SND_CTL_EVENT_MASK_VALUE) - io_worker_mixer_volume_sync_ba_pcm(worker, &worker->ba_pcm); - return 0; + const unsigned int volume = volume_sum / ba_pcm->channels; + return alsa_mixer_set_volume(&worker->alsa_mixer, vmax, volume, muted); } -/** - * Setup volume synchronization between ALSA mixer and BlueALSA PCM. */ -static int io_worker_mixer_volume_sync_setup( - struct io_worker *worker) { - - /* skip setup in case of software volume */ - if (worker->ba_pcm.soft_volume) - return 0; - - snd_mixer_elem_t *elem = worker->snd_mixer_elem; - if (elem == NULL) - return 0; - - debug("Setting up ALSA mixer volume synchronization"); - - snd_mixer_elem_set_callback(elem, io_worker_mixer_elem_callback); - snd_mixer_elem_set_callback_private(elem, worker); - - /* initial synchronization */ +static void io_worker_mixer_event_callback(void *data) { + struct io_worker *worker = data; io_worker_mixer_volume_sync_ba_pcm(worker, &worker->ba_pcm); - - return 0; } static void io_worker_routine_exit(struct io_worker *worker) { @@ -529,11 +407,7 @@ static void io_worker_routine_exit(struct io_worker *worker) { worker->ba_pcm_ctrl_fd = -1; } alsa_pcm_close(&worker->alsa_pcm); - if (worker->snd_mixer != NULL) { - snd_mixer_close(worker->snd_mixer); - worker->snd_mixer_elem = NULL; - worker->snd_mixer = NULL; - } + alsa_mixer_close(&worker->alsa_mixer); debug("Exiting IO worker %s", worker->addr); } @@ -619,17 +493,16 @@ static void *io_worker_routine(struct io_worker *w) { { w->ba_pcm_fd, POLLIN, 0 }}; nfds_t nfds = 2; - if (w->snd_mixer != NULL) - nfds += snd_mixer_poll_descriptors_count(w->snd_mixer); - - if (nfds > ARRAYSIZE(fds)) { - error("Poll FD array size exceeded: %zu > %zu", nfds, ARRAYSIZE(fds)); - goto fail; + if (alsa_mixer_is_open(&w->alsa_mixer)) { + nfds += alsa_mixer_poll_descriptors_count(&w->alsa_mixer); + if (nfds <= ARRAYSIZE(fds)) + alsa_mixer_poll_descriptors(&w->alsa_mixer, fds + 2, nfds - 2); + else { + error("Poll FD array size exceeded: %zu > %zu", nfds, ARRAYSIZE(fds)); + goto fail; + } } - if (w->snd_mixer != NULL) - snd_mixer_poll_descriptors(w->snd_mixer, fds + 2, nfds - 2); - /* Reading from the FIFO won't block unless there is an open connection * on the writing side. However, the server does not open PCM FIFO until * a transport is created. With the A2DP, the transport is created when @@ -657,8 +530,8 @@ static void *io_worker_routine(struct io_worker *w) { if (fds[0].revents & POLLIN) break; - if (w->snd_mixer != NULL) - snd_mixer_handle_events(w->snd_mixer); + if (alsa_mixer_is_open(&w->alsa_mixer)) + alsa_mixer_handle_events(&w->alsa_mixer); size_t read_samples = 0; if (fds[1].revents & POLLIN) { @@ -745,8 +618,18 @@ static void *io_worker_routine(struct io_worker *w) { pcm_max_read_len = w->alsa_pcm.period_frames * w->alsa_pcm.frame_size; - io_worker_mixer_open(w, mixer_device, mixer_elem_name, mixer_elem_index); - io_worker_mixer_volume_sync_setup(w); + /* Skip mixer setup in case of software volume. */ + if (mixer_device != NULL && !w->ba_pcm.soft_volume) { + debug("Opening ALSA mixer: name=%s elem=%s index=%u", + mixer_device, mixer_elem_name, mixer_elem_index); + if (alsa_mixer_open(&w->alsa_mixer, mixer_device, + mixer_elem_name, mixer_elem_index, &tmp) == 0) + io_worker_mixer_volume_sync_ba_pcm(w, &w->ba_pcm); + else { + warn("Couldn't open ALSA mixer: %s", tmp); + free(tmp); + } + } /* Reset retry counters. */ pcm_open_retry_pcm_samples = 0; @@ -789,7 +672,7 @@ static void *io_worker_routine(struct io_worker *w) { ffb_seek(&buffer, read_samples); size_t samples = ffb_len_out(&buffer); - if (!w->mixer_has_mute_switch && pcm_muted) + if (!w->alsa_mixer.has_mute_switch && pcm_muted) snd_pcm_format_set_silence(pcm_format, buffer.data, samples); if (alsa_pcm_write(&w->alsa_pcm, &buffer) < 0) @@ -844,11 +727,7 @@ static void *io_worker_routine(struct io_worker *w) { ffb_rewind(&buffer); pcm_max_read_len = pcm_max_read_len_init; alsa_pcm_close(&w->alsa_pcm); - if (w->snd_mixer != NULL) { - snd_mixer_close(w->snd_mixer); - w->snd_mixer_elem = NULL; - w->snd_mixer = NULL; - } + alsa_mixer_close(&w->alsa_mixer); } fail: @@ -923,9 +802,7 @@ static struct io_worker *supervise_io_worker_start(const struct ba_pcm *ba_pcm) worker->ba_pcm_fd = -1; worker->ba_pcm_ctrl_fd = -1; alsa_pcm_init(&worker->alsa_pcm); - worker->snd_mixer = NULL; - worker->snd_mixer_elem = NULL; - worker->mixer_has_mute_switch = false; + alsa_mixer_init(&worker->alsa_mixer, io_worker_mixer_event_callback, worker); worker->active = false; debug("Creating IO worker %s", worker->addr); @@ -1047,7 +924,7 @@ static DBusHandlerResult dbus_signal_handler(DBusConnection *conn, DBusMessage * if (!dbus_message_iter_get_ba_pcm_props(&iter, NULL, pcm)) goto fail; if ((worker = supervise_io_worker(pcm)) != NULL) - io_worker_mixer_volume_sync_snd_mixer_elem(worker, pcm); + io_worker_mixer_volume_sync_alsa_mixer(worker, pcm); return DBUS_HANDLER_RESULT_HANDLED; }