diff --git a/NEWS b/NEWS index 9633c1f41..9f2ab126f 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ unreleased - command line option to set real-time priority for IO threads - allow to select individual extended controls in ALSA plug-in +- codec-specific delay adjustment with ALSA control and persistency bluez-alsa v4.1.1 (2023-06-24) ============================== diff --git a/doc/bluealsa-plugins.7.rst b/doc/bluealsa-plugins.7.rst index 08006f1c9..82df8b992 100644 --- a/doc/bluealsa-plugins.7.rst +++ b/doc/bluealsa-plugins.7.rst @@ -98,9 +98,11 @@ PCM Parameters softvol value. The default value is **unchanged**. DELAY - An integer number which is added to the reported latency value in order to - manually adjust the audio synchronization. It is not normally required and - defaults to **0**. + An integer number which is added to the reported delay (latency) value in + order to manually adjust the audio synchronization. It is not normally + required and defaults to **0**. See the **EXT** parameter of the CTL plugin + in the `CTL Parameters`_ section below for a more flexible and convenient + method of manually adjusting the reported delay by using a mixer control. SRV The D-Bus service name of the BlueALSA daemon. Defaults to @@ -420,15 +422,15 @@ CTL Parameters EXT Causes the plugin to include extra controls. These are the controls for - Bluetooth codec selection, volume mode selection and/or battery level - indicator. + Bluetooth codec selection, volume mode selection, battery level + indicator and/or delay adjustment (sync). If the value is **yes** then all of these additional controls are included; if the value is **no** then none of them are included. The default is **no**. This parameter can also select individual controls by using a colon (':') separated list of control names. The control names are **codec**, **mode**, - and **battery**. For example: + **battery** and **sync**. For example: :: @@ -446,6 +448,19 @@ CTL Parameters The read-only battery level indicator will be shown only if the device supports battery level reporting. + The delay adjustment controls are called "Sync". They can be used to apply + a fixed adjustment to the delay reported by the associated PCM to the + application, and may be useful with applications that need to synchronize + the bluetooth audio stream with some some other stream, such as a video. + The values are in milliseconds from ``-3275 ms`` to ``+3275 ms`` in steps + of ``25 ms``. The playback control has index 0 and the capture control has + index 1. Each codec supported by a PCM has its own delay adjustment value. + Note that this control changes only the delay value reported to the + application by ALSA, it does not affect the actual delay (latency) of the + PCM stream. Values set by this control type are saved in the BlueALSA + persistent state files, and so are remembered and automatically applied + each time the PCM is used. + BTT Appends Bluetooth transport type (e.g. "-SNK" or "-HFP-AG") to the control element names. When using with the `Default Mode`_ this will reduce the diff --git a/src/asound/bluealsa-ctl.c b/src/asound/bluealsa-ctl.c index 4e9b3b370..da5846a5b 100644 --- a/src/asound/bluealsa-ctl.c +++ b/src/asound/bluealsa-ctl.c @@ -33,6 +33,11 @@ #include "shared/dbus-client.h" #include "shared/defs.h" +#define DELAY_SYNC_STEP 250 +#define DELAY_SYNC_MIN_VALUE ((int32_t)INT16_MIN / DELAY_SYNC_STEP * DELAY_SYNC_STEP) +#define DELAY_SYNC_MAX_VALUE ((int32_t)INT16_MAX / DELAY_SYNC_STEP * DELAY_SYNC_STEP) +#define DELAY_SYNC_NUM_VALUES (1 + (DELAY_SYNC_MAX_VALUE - DELAY_SYNC_MIN_VALUE) / DELAY_SYNC_STEP) + /** * Control element type. * @@ -44,6 +49,7 @@ enum ctl_elem_type { CTL_ELEM_TYPE_VOLUME_MODE, CTL_ELEM_TYPE_CODEC, CTL_ELEM_TYPE_BATTERY, + CTL_ELEM_TYPE_DELAY_SYNC, }; /** @@ -126,6 +132,8 @@ struct bluealsa_ctl { bool show_vol_mode; /* if true, show battery level indicator */ bool show_battery; + /* if true, show delay adjustment sync control */ + bool show_delay_sync; /* if true, append BT transport type to element names */ bool show_bt_transport; /* if true, this mixer is for a single Bluetooth device */ @@ -140,6 +148,8 @@ static const char *soft_volume_names[] = { "software", }; +static int bluealsa_read_event(snd_ctl_ext_t *ext, snd_ctl_elem_id_t *id, unsigned int *event_mask); + static int str2bdaddr(const char *str, bdaddr_t *ba) { unsigned int x[6]; @@ -472,9 +482,9 @@ static const char *transport2str(unsigned int transport) { } static int parse_extended(const char *extended, - bool *show_codec, bool *show_vol_mode, bool *show_battery) { + bool *show_codec, bool *show_vol_mode, bool *show_battery, bool *show_delay_sync) { - bool codec = false, vol_mode = false, battery = false; + bool codec = false, vol_mode = false, battery = false, sync = false; int ret = 0; switch (snd_config_get_bool_ascii(extended)) { @@ -484,6 +494,7 @@ static int parse_extended(const char *extended, codec = true; vol_mode = true; battery = true; + sync = true; break; default: { char *next, *ptr = NULL; @@ -497,6 +508,8 @@ static int parse_extended(const char *extended, vol_mode = true; else if (strcasecmp(next, "battery") == 0) battery = true; + else if (strcasecmp(next, "sync") == 0) + sync = true; else { ret = -1; break; @@ -508,6 +521,7 @@ static int parse_extended(const char *extended, *show_codec = codec; *show_vol_mode = vol_mode; *show_battery = battery; + *show_delay_sync = sync; } return ret; @@ -548,6 +562,8 @@ static void bluealsa_elem_set_name(struct bluealsa_ctl *ctl, struct ctl_elem *el label_max_len = sizeof(" SCO-HFP-AG") - 1; if (ctl->show_vol_mode) label_max_len += sizeof(" Mode") - 1; + else if (ctl->show_delay_sync) + label_max_len += sizeof(" Sync") - 1; if (ctl->show_battery) label_max_len = MAX(label_max_len, sizeof(" | Battery") - 1); @@ -601,6 +617,10 @@ static void bluealsa_elem_set_name(struct bluealsa_ctl *ctl, struct ctl_elem *el if (elem->type == CTL_ELEM_TYPE_VOLUME_MODE) strcat(elem->name, " Mode"); + if (elem->type == CTL_ELEM_TYPE_DELAY_SYNC) + strcat(elem->name, " Sync"); + + /* ALSA library determines the element type by checking it's * name suffix. This feature is not well documented, though. * A codec control is 'Global' (i.e. neither 'Playback' nor @@ -618,6 +638,7 @@ static void bluealsa_elem_set_name(struct bluealsa_ctl *ctl, struct ctl_elem *el break; case CTL_ELEM_TYPE_CODEC: case CTL_ELEM_TYPE_VOLUME_MODE: + case CTL_ELEM_TYPE_DELAY_SYNC: strcat(elem->name, " Enum"); break; } @@ -697,6 +718,23 @@ static size_t bluealsa_elem_list_add_pcm_elems(struct bluealsa_ctl *ctl, n++; } + /* add special delay adjustment "sync" element */ + if (ctl->show_delay_sync) { + elem_list[n].type = CTL_ELEM_TYPE_DELAY_SYNC; + elem_list[n].dev = dev; + elem_list[n].pcm = pcm; + elem_list[n].playback = playback; + elem_list[n].active = true; + bluealsa_elem_set_name(ctl, &elem_list[n], name, false); + + /* ALSA library permits only one enumeration type control for + * each simple control id. So we use different index numbers + * for capture and playback to get different ids. */ + elem_list[n].index = playback ? 0 : 1; + + n++; + } + /* add special battery level indicator element */ if (add_battery_elem && dev->battery_level != -1 && @@ -751,6 +789,8 @@ static int bluealsa_create_elem_list(struct bluealsa_ctl *ctl) { count += 1; if (ctl->show_vol_mode) count += 1; + if (ctl->show_delay_sync) + count += 1; } if ((elem_list = realloc(elem_list, count * sizeof(*elem_list))) == NULL) @@ -926,6 +966,11 @@ static int bluealsa_get_attribute(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, *type = SND_CTL_ELEM_TYPE_ENUMERATED; *count = 1; break; + case CTL_ELEM_TYPE_DELAY_SYNC: + *acc = SND_CTL_EXT_ACCESS_READWRITE; + *type = SND_CTL_ELEM_TYPE_ENUMERATED; + *count = 1; + break; case CTL_ELEM_TYPE_SWITCH: *acc = SND_CTL_EXT_ACCESS_READWRITE; *type = SND_CTL_ELEM_TYPE_BOOLEAN; @@ -979,6 +1024,7 @@ static int bluealsa_get_integer_info(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, case CTL_ELEM_TYPE_CODEC: case CTL_ELEM_TYPE_VOLUME_MODE: case CTL_ELEM_TYPE_SWITCH: + case CTL_ELEM_TYPE_DELAY_SYNC: return -EINVAL; } @@ -1011,6 +1057,7 @@ static int bluealsa_read_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, long break; case CTL_ELEM_TYPE_CODEC: case CTL_ELEM_TYPE_VOLUME_MODE: + case CTL_ELEM_TYPE_DELAY_SYNC: return -EINVAL; } @@ -1052,6 +1099,7 @@ static int bluealsa_write_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, lon break; case CTL_ELEM_TYPE_CODEC: case CTL_ELEM_TYPE_VOLUME_MODE: + case CTL_ELEM_TYPE_DELAY_SYNC: return -EINVAL; } @@ -1080,6 +1128,9 @@ int bluealsa_get_enumerated_info(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, unsi case CTL_ELEM_TYPE_VOLUME_MODE: *items = ARRAYSIZE(soft_volume_names); break; + case CTL_ELEM_TYPE_DELAY_SYNC: + *items = DELAY_SYNC_NUM_VALUES; + break; case CTL_ELEM_TYPE_BATTERY: case CTL_ELEM_TYPE_SWITCH: case CTL_ELEM_TYPE_VOLUME: @@ -1111,6 +1162,11 @@ int bluealsa_get_enumerated_name(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, strncpy(name, soft_volume_names[item], name_max_len - 1); name[name_max_len - 1] = '\0'; break; + case CTL_ELEM_TYPE_DELAY_SYNC: + if (item >= DELAY_SYNC_NUM_VALUES) + return -EINVAL; + snprintf(name, name_max_len, "%+d ms", (int16_t)((item * DELAY_SYNC_STEP) + DELAY_SYNC_MIN_VALUE) / 10); + break; case CTL_ELEM_TYPE_BATTERY: case CTL_ELEM_TYPE_SWITCH: case CTL_ELEM_TYPE_VOLUME: @@ -1160,6 +1216,15 @@ static int bluealsa_read_enumerated(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, case CTL_ELEM_TYPE_VOLUME_MODE: items[0] = pcm->soft_volume ? 1 : 0; break; + case CTL_ELEM_TYPE_DELAY_SYNC: { + int32_t val = ((DELAY_SYNC_STEP / 2) + (int32_t)pcm->delay_adjustment - INT16_MIN) / DELAY_SYNC_STEP; + if (val < 0) + val = 0; + if (val >= DELAY_SYNC_NUM_VALUES) + val = DELAY_SYNC_NUM_VALUES - 1; + items[0] = val; + break; + } case CTL_ELEM_TYPE_BATTERY: case CTL_ELEM_TYPE_SWITCH: case CTL_ELEM_TYPE_VOLUME: @@ -1171,6 +1236,13 @@ static int bluealsa_read_enumerated(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, return ret; } +static void process_events(snd_ctl_ext_t *ext) { + snd_ctl_elem_id_t *elem_id; + snd_ctl_elem_id_alloca(&elem_id); + unsigned int event_mask; + while (bluealsa_read_event(ext, elem_id, &event_mask) > 0) {} +} + static int bluealsa_write_enumerated(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, unsigned int *items) { struct bluealsa_ctl *ctl = (struct bluealsa_ctl *)ext->private_data; @@ -1190,7 +1262,7 @@ static int bluealsa_write_enumerated(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, if (!bluealsa_dbus_pcm_select_codec(&ctl->dbus_ctx, pcm->pcm_path, elem->codecs.codecs[items[0]].name, NULL, 0, NULL)) return -EIO; - memcpy(&pcm->codec, &elem->codecs.codecs[items[0]], sizeof(pcm->codec)); + process_events(&ctl->ext); break; case CTL_ELEM_TYPE_VOLUME_MODE: if (items[0] >= ARRAYSIZE(soft_volume_names)) @@ -1202,6 +1274,18 @@ static int bluealsa_write_enumerated(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, if (!bluealsa_dbus_pcm_update(&ctl->dbus_ctx, pcm, BLUEALSA_PCM_SOFT_VOLUME, NULL)) return -ENOMEM; break; + case CTL_ELEM_TYPE_DELAY_SYNC: { + if (items[0] >= DELAY_SYNC_NUM_VALUES) + return -EINVAL; + const int16_t delay_adjustment = (int32_t)(items[0] * DELAY_SYNC_STEP) + DELAY_SYNC_MIN_VALUE; + if (pcm->delay_adjustment == delay_adjustment) + return 0; + if (!bluealsa_dbus_pcm_set_delay_adjustment(&ctl->dbus_ctx, pcm->pcm_path, + pcm->codec.name, delay_adjustment, NULL)) + return -EIO; + process_events(&ctl->ext); + break; + } case CTL_ELEM_TYPE_BATTERY: case CTL_ELEM_TYPE_SWITCH: case CTL_ELEM_TYPE_VOLUME: @@ -1655,6 +1739,7 @@ SND_CTL_PLUGIN_DEFINE_FUNC(bluealsa) { bool show_bt_transport = false; bool show_codec = false; bool show_vol_mode = false; + bool show_delay_sync = false; bool dynamic = true; struct bluealsa_ctl *ctl; int ret; @@ -1691,7 +1776,7 @@ SND_CTL_PLUGIN_DEFINE_FUNC(bluealsa) { SNDERR("Invalid type for %s", id); return -EINVAL; } - if (parse_extended(extended, &show_codec, &show_vol_mode, &show_battery) < 0) { + if (parse_extended(extended, &show_codec, &show_vol_mode, &show_battery, &show_delay_sync) < 0) { SNDERR("Invalid extended options: %s", extended); return -EINVAL; } @@ -1756,6 +1841,7 @@ SND_CTL_PLUGIN_DEFINE_FUNC(bluealsa) { ctl->show_codec = show_codec; ctl->show_vol_mode = show_vol_mode; ctl->show_battery = show_battery; + ctl->show_delay_sync = show_delay_sync; ctl->show_bt_transport = show_bt_transport; ctl->single_device = single_device_mode; ctl->dynamic = dynamic; diff --git a/test/test-alsa-ctl.c b/test/test-alsa-ctl.c index c714e22aa..fe482c5a4 100644 --- a/test/test-alsa-ctl.c +++ b/test/test-alsa-ctl.c @@ -142,14 +142,14 @@ CK_START_TEST(test_controls_extended) { snd_ctl_elem_list_alloca(&elems); ck_assert_int_eq(snd_ctl_elem_list(ctl, elems), 0); - ck_assert_int_eq(snd_ctl_elem_list_get_count(elems), 16); - ck_assert_int_eq(snd_ctl_elem_list_alloc_space(elems, 16), 0); + ck_assert_int_eq(snd_ctl_elem_list_get_count(elems), 20); + ck_assert_int_eq(snd_ctl_elem_list_alloc_space(elems, 20), 0); ck_assert_int_eq(snd_ctl_elem_list(ctl, elems), 0); /* codec control element shall be after playback/capture elements */ ck_assert_str_eq(snd_ctl_elem_list_get_name(elems, 3), "12:34:56:78:9A:BC A2DP Codec Enum"); - ck_assert_str_eq(snd_ctl_elem_list_get_name(elems, 10), "12:34:56:78:9A:BC SCO Codec Enum"); - ck_assert_str_eq(snd_ctl_elem_list_get_name(elems, 15), "23:45:67:89:AB:CD A2DP Codec Enum"); + ck_assert_str_eq(snd_ctl_elem_list_get_name(elems, 11), "12:34:56:78:9A:BC SCO Codec Enum"); + ck_assert_str_eq(snd_ctl_elem_list_get_name(elems, 18), "23:45:67:89:AB:CD A2DP Codec Enum"); bool has_msbc = false; #if ENABLE_MSBC @@ -160,7 +160,7 @@ CK_START_TEST(test_controls_extended) { snd_ctl_elem_info_alloca(&info); /* 12:34:56:78:9A:BC SCO Codec Enum */ - snd_ctl_elem_info_set_numid(info, snd_ctl_elem_list_get_numid(elems, 10)); + snd_ctl_elem_info_set_numid(info, snd_ctl_elem_list_get_numid(elems, 11)); ck_assert_int_eq(snd_ctl_elem_info(ctl, info), 0); ck_assert_int_eq(snd_ctl_elem_info_get_items(info), has_msbc ? 2 : 1); snd_ctl_elem_info_set_item(info, 0); @@ -186,7 +186,7 @@ CK_START_TEST(test_controls_extended) { ck_assert_int_eq(snd_ctl_elem_write(ctl, elem), 0); /* 12:34:56:78:9A:BC SCO Codec Enum */ - snd_ctl_elem_value_set_numid(elem, snd_ctl_elem_list_get_numid(elems, 10)); + snd_ctl_elem_value_set_numid(elem, snd_ctl_elem_list_get_numid(elems, 11)); /* get currently selected SCO codec */ ck_assert_int_eq(snd_ctl_elem_read(ctl, elem), 0); ck_assert_int_eq(snd_ctl_elem_value_get_enumerated(elem, 0), has_msbc ? 1 : 0);