Skip to content

Commit

Permalink
Multi channels and sampling rates mode in ALSA PCM
Browse files Browse the repository at this point in the history
Fixes #710
  • Loading branch information
arkq committed Aug 7, 2024
1 parent 11d2313 commit 13b0bf8
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 83 deletions.
1 change: 1 addition & 0 deletions .github/spellcheck-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ EPMR
EQMID
errno
FB
FC
fdX
ffb
ffff
Expand Down
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ unreleased
==========

- optional support for Android 13 A2DP Opus codec
- multi channels and sampling rates mode for ALSA PCM plug-in
- bluealsa-aplay: fix volume synchronization on Raspberry Pi

bluez-alsa v4.2.0 (2024-05-11)
Expand Down
11 changes: 9 additions & 2 deletions doc/bluealsa-plugins.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,18 @@ PCM Parameters

For the A2DP profile it is possible to also specify a "configuration" for
the codec by appending the configuration as a hex string separated from the
codec name by a colon. For example:
codec name by a colon. The bits responsible for the number of channels and
the sampling frequency are set by the plugin with the respect to options
provided by the user (channel mode and sampling frequency bits act as a
mask). For example:

::

CODEC=aptx:4f0000000100ff
CODEC=SBC:FC450240

This SBC configuration limits the channel mode options to mono and dual
channel. So, in case of 2 channel audio stream, the plugin will negotiate
the dual channel mode instead of default (if supported) joint stereo mode.

VOL
Specifies the initial volume for the PCM when opened. The default value is
Expand Down
173 changes: 121 additions & 52 deletions src/asound/bluealsa-pcm.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
#include <strings.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/time.h>
#include <unistd.h>

Expand Down Expand Up @@ -73,6 +72,11 @@ struct bluealsa_pcm {

/* requested BlueALSA PCM */
struct ba_pcm ba_pcm;
/* user-provided codec configuration */
uint8_t ba_pcm_codec_config[64];
size_t ba_pcm_codec_config_len;
/* additional supported codecs */
struct ba_pcm_codecs ba_pcm_codecs;

/* PCM FIFO */
int ba_pcm_fd;
Expand Down Expand Up @@ -469,6 +473,7 @@ static snd_pcm_sframes_t bluealsa_pointer(snd_pcm_ioplug_t *io) {
static int bluealsa_close(snd_pcm_ioplug_t *io) {
struct bluealsa_pcm *pcm = io->private_data;
debug2("Closing");
ba_dbus_pcm_codecs_free(&pcm->ba_pcm_codecs);
ba_dbus_connection_ctx_free(&pcm->dbus_ctx);
if (pcm->event_fd != -1)
close(pcm->event_fd);
Expand Down Expand Up @@ -560,22 +565,48 @@ static int bluealsa_hw_params(snd_pcm_ioplug_t *io, snd_pcm_hw_params_t *params)

debug2("Initializing HW");

DBusError err = DBUS_ERROR_INIT;
int ret;

unsigned int channels;
if ((ret = snd_pcm_hw_params_get_channels(params, &channels)) < 0)
return ret;

unsigned int sampling;
if ((ret = snd_pcm_hw_params_get_rate(params, &sampling, NULL)) < 0)
return ret;

if (pcm->ba_pcm.channels != channels || pcm->ba_pcm.sampling != sampling) {
debug2("Changing BlueALSA PCM configuration: %u ch, %u Hz -> %u ch, %u Hz",
pcm->ba_pcm.channels, pcm->ba_pcm.sampling, channels, sampling);

if (ba_dbus_pcm_select_codec(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
pcm->ba_pcm.codec.name, pcm->ba_pcm_codec_config, pcm->ba_pcm_codec_config_len,
channels, sampling, BA_PCM_SELECT_CODEC_FLAG_NONE, &err)) {
pcm->ba_pcm.channels = channels;
pcm->ba_pcm.sampling = sampling;
}
else {
SNDERR("Couldn't change BlueALSA PCM configuration: %s", err.message);
return -dbus_error_to_errno(&err);
}

}

#if BLUEALSA_HW_PARAMS_FIX
if (bluealsa_fix_hw_params(io, params) < 0)
debug2("Warning - unable to fix incorrect buffer size in hw parameters");
#endif

snd_pcm_uframes_t period_size;
int ret;
if ((ret = snd_pcm_hw_params_get_period_size(params, &period_size, 0)) < 0)
if ((ret = snd_pcm_hw_params_get_period_size(params, &period_size, NULL)) < 0)
return ret;
snd_pcm_uframes_t buffer_size;
if ((ret = snd_pcm_hw_params_get_buffer_size(params, &buffer_size)) < 0)
return ret;

pcm->frame_size = (snd_pcm_format_physical_width(io->format) * io->channels) / 8;

DBusError err = DBUS_ERROR_INIT;
if (!ba_dbus_pcm_open(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
&pcm->ba_pcm_fd, &pcm->ba_pcm_ctrl_fd, &err)) {
debug2("Couldn't open PCM: %s", err.message);
Expand Down Expand Up @@ -1142,6 +1173,41 @@ static int str2profile(const char *str) {
return 0;
}

/**
* Extract codec name and configuration from the codec string. */
static int str2codec(const char *codec, char *name, size_t name_size,
uint8_t *config, size_t config_size, size_t *config_len) {

size_t name_len = strlen(codec);

char *delim;
/* Check for the delimiter which separates codec name and configuration. */
if ((delim = strchr(codec, ':')) != NULL) {

name_len = delim - codec;
delim++;

size_t config_hex_len;
if ((config_hex_len = strlen(delim)) > config_size * 2)
return -1;

ssize_t len;
if ((len = hex2bin(delim, config, config_hex_len)) == -1)
return -1;

*config_len = len;

}

if (name_len >= name_size)
return -1;

strncpy(name, codec, name_len);
name[name_len] = '\0';

return 0;
}

/**
* Convert volume string to volume level and mute state.
*
Expand Down Expand Up @@ -1229,6 +1295,13 @@ static int bluealsa_set_hw_constraint(struct bluealsa_pcm *pcm) {

debug2("Setting constraints");

const struct ba_pcm_codec *codec = &pcm->ba_pcm.codec;
for (size_t i = 0; i < pcm->ba_pcm_codecs.codecs_len; i++)
if (strcmp(pcm->ba_pcm_codecs.codecs[i].name, codec->name) == 0) {
codec = &pcm->ba_pcm_codecs.codecs[i];
break;
}

if ((err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS,
ARRAYSIZE(accesses), accesses)) < 0)
return err;
Expand All @@ -1254,55 +1327,28 @@ static int bluealsa_set_hw_constraint(struct bluealsa_pcm *pcm) {
min_p, 1024 * 1024)) < 0)
return err;

if ((err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_BUFFER_BYTES,
2 * min_p, 2 * 1024 * 1024)) < 0)
return err;
unsigned int list[ARRAYSIZE(codec->sampling)];
unsigned int n;

/* Populate the list of supported channels and sampling rates. For codecs
* with fixed configuration, the list will contain only one element. For
* other codecs, the list might contain all supported configurations. */

if ((err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_CHANNELS,
pcm->ba_pcm.channels, pcm->ba_pcm.channels)) < 0)
n = 0;
for (size_t i = 0; i < ARRAYSIZE(codec->channels) && codec->channels[i] != 0; i++)
list[n++] = codec->channels[i];
if ((err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_CHANNELS, n, list)) < 0)
return err;

if ((err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_RATE,
pcm->ba_pcm.sampling, pcm->ba_pcm.sampling)) < 0)
n = 0;
for (size_t i = 0; i < ARRAYSIZE(codec->sampling) && codec->sampling[i] != 0; i++)
list[n++] = codec->sampling[i];
if ((err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_RATE, n, list)) < 0)
return err;

return 0;
}

static bool bluealsa_select_pcm_codec(struct bluealsa_pcm *pcm, const char *codec, DBusError *err) {

char name[32] = { 0 };
size_t name_len = sizeof(name) - 1;
uint8_t config[64] = { 0 };
ssize_t config_len = 0;

const char *config_hex;
/* split the given string into name and configuration components */
if ((config_hex = strchr(codec, ':')) != NULL) {
name_len = MIN(name_len, (size_t)(config_hex - codec));
config_hex++;

size_t config_hex_len;
if ((config_hex_len = strlen(config_hex)) > sizeof(config) * 2) {
dbus_set_error(err, DBUS_ERROR_FAILED, "Invalid codec configuration: %s", config_hex);
return false;
}

if ((config_len = hex2bin(config_hex, config, config_hex_len)) == -1) {
dbus_set_error(err, DBUS_ERROR_FAILED, "%s", strerror(errno));
return false;
}

}

strncpy(name, codec, name_len);
if (!ba_dbus_pcm_select_codec(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
ba_dbus_pcm_codec_get_canonical_name(name), config, config_len, 0, 0, 0, err))
return false;

return true;
}

static bool bluealsa_update_pcm_volume(struct bluealsa_pcm *pcm,
int volume, int mute, DBusError *err) {
uint16_t old = pcm->ba_pcm.volume.raw;
Expand Down Expand Up @@ -1429,6 +1475,15 @@ SND_PCM_PLUGIN_DEFINE_FUNC(bluealsa) {
return -EINVAL;
}

char codec_name[32] = "";
uint8_t codec_config[sizeof(pcm->ba_pcm_codec_config)];
size_t codec_config_len = 0;
if (codec != NULL && str2codec(codec, codec_name, sizeof(codec_name),
codec_config, sizeof(codec_config), &codec_config_len) == -1) {
SNDERR("Invalid codec: %s", codec);
return -EINVAL;
}

int pcm_mute = -1;
int pcm_volume = -1;
if (volume != NULL && str2volume(volume, &pcm_volume, &pcm_mute) != 0) {
Expand Down Expand Up @@ -1497,21 +1552,31 @@ SND_PCM_PLUGIN_DEFINE_FUNC(bluealsa) {
goto fail;
}

if (codec != NULL && codec[0] != '\0') {
if (bluealsa_select_pcm_codec(pcm, codec, &err)) {
if (codec_name[0] != '\0') {
/* If the codec was given, change it now, so we can get the correct
* sampling rate and channels for HW constraints. */
const char *canonical = ba_dbus_pcm_codec_get_canonical_name(codec_name);
const bool name_changed = strcmp(canonical, pcm->ba_pcm.codec.name) != 0;
if (name_changed && !ba_dbus_pcm_select_codec(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
canonical, NULL, 0, 0, 0, BA_PCM_SELECT_CODEC_FLAG_NONE, &err)) {
SNDERR("Couldn't select BlueALSA PCM codec: %s", err.message);
dbus_error_free(&err);
}
else {

memcpy(pcm->ba_pcm_codec_config, codec_config, codec_config_len);
pcm->ba_pcm_codec_config_len = codec_config_len;

/* Changing the codec may change the audio format, sampling rate and/or
* channels. We need to refresh our cache of PCM properties. */
if (!ba_dbus_pcm_get(&pcm->dbus_ctx, &ba_addr, ba_profile,
if (name_changed && !ba_dbus_pcm_get(&pcm->dbus_ctx, &ba_addr, ba_profile,
stream == SND_PCM_STREAM_PLAYBACK ? BA_PCM_MODE_SINK : BA_PCM_MODE_SOURCE,
&pcm->ba_pcm, &err)) {
SNDERR("Couldn't get BlueALSA PCM: %s", err.message);
ret = -dbus_error_to_errno(&err);
goto fail;
}
}
else {
SNDERR("Couldn't select BlueALSA PCM codec: %s", err.message);
dbus_error_free(&err);

}
}

Expand All @@ -1533,6 +1598,10 @@ SND_PCM_PLUGIN_DEFINE_FUNC(bluealsa) {
if ((ret = snd_pcm_ioplug_create(&pcm->io, name, stream, mode)) < 0)
goto fail;

if (!ba_dbus_pcm_codecs_get(&pcm->dbus_ctx, pcm->ba_pcm.pcm_path,
&pcm->ba_pcm_codecs, &err))
SNDERR("Couldn't get BlueALSA PCM codecs: %s", err.message);

if ((ret = bluealsa_set_hw_constraint(pcm)) < 0) {
snd_pcm_ioplug_delete(&pcm->io);
goto fail;
Expand Down
4 changes: 3 additions & 1 deletion test/mock/dbus-ifaces.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
<property name="UUID" type="s" access="read" />
<property name="Codec" type="y" access="read" />
<property name="Vendor" type="u" access="read" />
<property name="Capabilities" type="ay" access="read" />
<property name="Capabilities" type="ay" access="read">
<annotation name="org.gtk.GDBus.C.ForceGVariant" value="true" />
</property>
<property name="Device" type="o" access="read" />
<property name="DelayReporting" type="b" access="read" />
</interface>
Expand Down
9 changes: 7 additions & 2 deletions test/mock/mock-bluealsa.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ static struct ba_transport *mock_transport_new_a2dp(struct ba_device *d,

char transport_path[128];
const int index = (strcmp(uuid, BT_UUID_A2DP_SINK) == 0) ? 1 : 2;
sprintf(transport_path, "%s/fd%u", d->bluez_dbus_path, index);
sprintf(transport_path, "%s/sep/fd%u", d->bluez_dbus_path, index);

g_autoptr(GAsyncQueue) sem = g_async_queue_new();
assert(mock_bluez_device_media_set_configuration(d->bluez_dbus_path, transport_path,
Expand Down Expand Up @@ -310,6 +310,11 @@ void mock_bluealsa_run(void) {
while (events--)
mock_sem_wait(mock_sem_ready);

/* Create remote SEP on device 1, so we could test SEP configuration. */
mock_bluez_device_media_endpoint_add(MOCK_BLUEZ_DEVICE_1_SEP_PATH,
MOCK_BLUEZ_DEVICE_1_PATH, BT_UUID_A2DP_SINK, a2dp_sbc_sink.config.codec_id,
&a2dp_sbc_sink.config.capabilities, a2dp_sbc_sink.config.caps_size);

GPtrArray *tt = g_ptr_array_new();

if (config.profile.a2dp_source) {
Expand Down Expand Up @@ -404,7 +409,7 @@ void mock_bluealsa_run(void) {

#if ENABLE_MIDI
if (config.profile.midi)
g_ptr_array_add(tt, mock_transport_new_midi(MOCK_BLUEZ_MIDI_PATH_1));
g_ptr_array_add(tt, mock_transport_new_midi(MOCK_BLUEZ_MIDI_PATH));
#endif

mock_sem_wait(mock_sem_timeout);
Expand Down
32 changes: 30 additions & 2 deletions test/mock/mock-bluez.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,34 @@ static void mock_bluez_device_add(const char *device_path, const char *adapter_p

}

static gboolean mock_bluez_media_ep_set_configuration_handler(MockBluezMediaEndpoint1 *endpoint,
GDBusMethodInvocation *invocation, G_GNUC_UNUSED const char *transport,
G_GNUC_UNUSED GVariant *props, G_GNUC_UNUSED void *userdata) {
mock_bluez_media_endpoint1_complete_set_configuration(endpoint, invocation);
return TRUE;
}

int mock_bluez_device_media_endpoint_add(const char *endpoint_path,
const char *device_path, const char *uuid, uint32_t codec_id,
const void *capabilities, size_t capabilities_size) {

g_autoptr(MockBluezMediaEndpoint1) endpoint = mock_bluez_media_endpoint1_skeleton_new();
mock_bluez_media_endpoint1_set_uuid(endpoint, uuid);
mock_bluez_media_endpoint1_set_codec(endpoint, codec_id);
mock_bluez_media_endpoint1_set_capabilities(endpoint, g_variant_new_fixed_array(
G_VARIANT_TYPE_BYTE, capabilities, capabilities_size, sizeof(uint8_t)));
mock_bluez_media_endpoint1_set_device(endpoint, device_path);

g_signal_connect(endpoint, "handle-set-configuration",
G_CALLBACK(mock_bluez_media_ep_set_configuration_handler), NULL);

g_autoptr(GDBusObjectSkeleton) skeleton = g_dbus_object_skeleton_new(endpoint_path);
g_dbus_object_skeleton_add_interface(skeleton, G_DBUS_INTERFACE_SKELETON(endpoint));
g_dbus_object_manager_server_export(server, skeleton);

return 0;
}

static gboolean mock_bluez_media_transport_acquire_handler(MockBluezMediaTransport1 *transport,
GDBusMethodInvocation *invocation, G_GNUC_UNUSED void *userdata) {

Expand Down Expand Up @@ -333,8 +361,8 @@ static void mock_dbus_name_acquired(GDBusConnection *conn,
mock_bluez_profile_manager_add("/org/bluez");
mock_bluez_adapter_add(MOCK_BLUEZ_ADAPTER_PATH, MOCK_ADAPTER_ADDRESS);

mock_bluez_device_add(MOCK_BLUEZ_DEVICE_PATH_1, MOCK_BLUEZ_ADAPTER_PATH, MOCK_DEVICE_1);
mock_bluez_device_add(MOCK_BLUEZ_DEVICE_PATH_2, MOCK_BLUEZ_ADAPTER_PATH, MOCK_DEVICE_2);
mock_bluez_device_add(MOCK_BLUEZ_DEVICE_1_PATH, MOCK_BLUEZ_ADAPTER_PATH, MOCK_DEVICE_1);
mock_bluez_device_add(MOCK_BLUEZ_DEVICE_2_PATH, MOCK_BLUEZ_ADAPTER_PATH, MOCK_DEVICE_2);

g_dbus_object_manager_server_set_connection(server, conn);
mock_sem_signal(userdata);
Expand Down
Loading

0 comments on commit 13b0bf8

Please sign in to comment.