Skip to content

Commit

Permalink
Add delay adjustment (Sync) control to ALSA CTL
Browse files Browse the repository at this point in the history
  • Loading branch information
borine committed Jul 22, 2023
1 parent f3badce commit 63747e6
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 16 deletions.
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
==============================
Expand Down
27 changes: 21 additions & 6 deletions doc/bluealsa-plugins.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

::

Expand All @@ -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
Expand Down
94 changes: 90 additions & 4 deletions src/asound/bluealsa-ctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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,
};

/**
Expand Down Expand Up @@ -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 */
Expand All @@ -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];
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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;
Expand All @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions test/test-alsa-ctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand Down

0 comments on commit 63747e6

Please sign in to comment.