From 104c3232f2aa781918a9ba96ae24e86d3721f995 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 30 Jul 2021 18:37:13 +0200 Subject: [PATCH] Select A2DP codec configuration with CLI tool --- configure.ac | 2 +- doc/bluealsa-cli.1.rst | 9 +++- src/bluealsa-dbus.c | 39 +++++++-------- src/bluez.c | 32 +++---------- src/shared/hex.c | 59 +++++++++++++++++++++++ src/shared/hex.h | 24 ++++++++++ utils/Makefile.am | 3 ++ utils/a2dpconf.c | 86 ++++++++++++++++----------------- utils/cli/Makefile.am | 1 + utils/cli/cli.c | 106 ++++++++++++++++++++++++++++++----------- 10 files changed, 239 insertions(+), 122 deletions(-) create mode 100644 src/shared/hex.c create mode 100644 src/shared/hex.h diff --git a/configure.ac b/configure.ac index 81a50f030..75e9957a2 100644 --- a/configure.ac +++ b/configure.ac @@ -85,7 +85,7 @@ PKG_CHECK_MODULES([ALSA], [alsa]) PKG_CHECK_MODULES([BLUEZ], [bluez >= 5.0]) PKG_CHECK_MODULES([DBUS1], [dbus-1 >= 1.6]) PKG_CHECK_MODULES([GIO2], [gio-unix-2.0]) -PKG_CHECK_MODULES([GLIB2], [glib-2.0 >= 2.30]) +PKG_CHECK_MODULES([GLIB2], [glib-2.0 >= 2.32]) PKG_CHECK_MODULES([SBC], [sbc >= 1.2]) PKG_CHECK_MODULES([LIBBSD], [libbsd >= 0.8], diff --git a/doc/bluealsa-cli.1.rst b/doc/bluealsa-cli.1.rst index 60f56cf66..e9174f8da 100644 --- a/doc/bluealsa-cli.1.rst +++ b/doc/bluealsa-cli.1.rst @@ -6,7 +6,7 @@ bluealsa-cli a simple command line interface for the BlueALSA D-Bus API ---------------------------------------------------------- -:Date: February 2021 +:Date: July 2021 :Manual section: 1 :Manual group: General Commands Manual :Version: $VERSION$ @@ -61,13 +61,18 @@ info *PCM_PATH* The list of available codecs requires BlueZ SEP support (BlueZ >= 5.52) -codec *PCM_PATH* [*CODEC*] +codec *PCM_PATH* [*CODEC*] [*CONFIG*] If *CODEC* is given, change the codec to be used by the given PCM. This command will terminate the PCM if it is currently running. If *CODEC* is not given, print a list of additional codecs supported by the given PCM and the currently selected codec. + Optionally, for A2DP codecs, one can specify A2DP codec configuration which + should be selected. The *CONFIG* shall be given as a hexadecimal string. If + this parameter is omitted, BlueALSA will select default configuration based + on codec capabilities of connected Bluetooth device. + Selecting a codec and listing available codecs requires BlueZ SEP support (BlueZ >= 5.52). diff --git a/src/bluealsa-dbus.c b/src/bluealsa-dbus.c index d4c1f4424..609058fa4 100644 --- a/src/bluealsa-dbus.c +++ b/src/bluealsa-dbus.c @@ -176,15 +176,8 @@ static bool ba_variant_populate_sep(GVariantBuilder *props, const struct a2dp_se } g_variant_builder_init(props, G_VARIANT_TYPE("a{sv}")); - - GVariantBuilder vcaps; - g_variant_builder_init(&vcaps, G_VARIANT_TYPE("ay")); - - size_t i; - for (i = 0; i < size; i++) - g_variant_builder_add(&vcaps, "y", caps[i]); - - g_variant_builder_add(props, "{sv}", "Capabilities", g_variant_builder_end(&vcaps)); + g_variant_builder_add(props, "{sv}", "Capabilities", g_variant_new_fixed_array( + G_VARIANT_TYPE_BYTE, caps, size, sizeof(uint8_t))); switch (codec->codec_id) { case A2DP_CODEC_SBC: @@ -556,13 +549,13 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { GVariantIter *properties; GVariant *value = NULL; const char *errmsg = NULL; + const char *codec_name; const char *property; - const char *codec; - void *a2dp_configuration = NULL; + a2dp_t a2dp_configuration = {}; size_t a2dp_configuration_size = 0; - g_variant_get(params, "(sa{sv})", &codec, &properties); + g_variant_get(params, "(sa{sv})", &codec_name, &properties); while (g_variant_iter_next(properties, "{&sv}", &property, &value)) { if (strcmp(property, "Configuration") == 0 && @@ -571,8 +564,13 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { const void *data = g_variant_get_fixed_array(value, &a2dp_configuration_size, sizeof(char)); - g_free(a2dp_configuration); - a2dp_configuration = g_memdup(data, a2dp_configuration_size); + if (a2dp_configuration_size > sizeof(a2dp_configuration)) { + warn("Configuration blob size exceeded: %zu > %zu", + a2dp_configuration_size, sizeof(a2dp_configuration)); + a2dp_configuration_size = sizeof(a2dp_configuration); + } + + memcpy(&a2dp_configuration, data, a2dp_configuration_size); } @@ -588,7 +586,7 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { goto fail; } - uint16_t codec_id = ba_transport_codecs_a2dp_from_string(codec); + uint16_t codec_id = ba_transport_codecs_a2dp_from_string(codec_name); enum a2dp_dir dir = !t->a2dp.codec->dir; const GArray *seps = t->d->seps; struct a2dp_sep *sep = NULL; @@ -619,13 +617,13 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { goto fail; /* use codec configuration blob provided by user */ - if (a2dp_configuration != NULL) { - if (a2dp_check_configuration(codec, a2dp_configuration, + if (a2dp_configuration_size != 0) { + if (a2dp_check_configuration(codec, &a2dp_configuration, a2dp_configuration_size) != A2DP_CHECK_OK) { errmsg = "Invalid configuration blob"; goto fail; } - memcpy(sep->configuration, a2dp_configuration, sep->capabilities_size); + memcpy(sep->configuration, &a2dp_configuration, sep->capabilities_size); } if (ba_transport_select_codec_a2dp(t, sep) == -1) @@ -634,7 +632,7 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { } else { - uint16_t codec_id = ba_transport_codecs_hfp_from_string(codec); + uint16_t codec_id = ba_transport_codecs_hfp_from_string(codec_name); if (ba_transport_select_codec_sco(t, codec_id) == -1) goto fail; @@ -646,13 +644,12 @@ static void bluealsa_pcm_select_codec(GDBusMethodInvocation *inv) { fail: if (errmsg == NULL) errmsg = strerror(errno); - error("Couldn't select codec: %s: %s", codec, errmsg); + error("Couldn't select codec: %s: %s", codec_name, errmsg); g_dbus_method_invocation_return_error(inv, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "%s", errmsg); final: ba_transport_pcm_unref(pcm); - g_free(a2dp_configuration); g_variant_iter_free(properties); if (value != NULL) g_variant_unref(value); diff --git a/src/bluez.c b/src/bluez.c index 79a955385..5aea8bfe5 100644 --- a/src/bluez.c +++ b/src/bluez.c @@ -151,15 +151,9 @@ static void bluez_endpoint_select_configuration(GDBusMethodInvocation *inv) { if (a2dp_select_configuration(codec, &capabilities, size) == -1) goto fail; - GVariantBuilder caps; - size_t i; - - g_variant_builder_init(&caps, G_VARIANT_TYPE("ay")); - for (i = 0; i < size; i++) - g_variant_builder_add(&caps, "y", ((char *)&capabilities)[i]); - - g_dbus_method_invocation_return_value(inv, g_variant_new("(ay)", &caps)); - g_variant_builder_clear(&caps); + GVariant *rv[] = { + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, &capabilities, size, sizeof(uint8_t)) }; + g_dbus_method_invocation_return_value(inv, g_variant_new_tuple(rv, 1)); return; @@ -404,26 +398,20 @@ static int bluez_register_media_endpoint( const struct a2dp_codec *codec = dbus_obj->codec; GDBusMessage *msg = NULL, *rep = NULL; int ret = 0; - size_t i; debug("Registering media endpoint: %s", dbus_obj->path); msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path, BLUEZ_IFACE_MEDIA, "RegisterEndpoint"); - GVariantBuilder caps; GVariantBuilder properties; - - g_variant_builder_init(&caps, G_VARIANT_TYPE("ay")); g_variant_builder_init(&properties, G_VARIANT_TYPE("a{sv}")); - for (i = 0; i < codec->capabilities_size; i++) - g_variant_builder_add(&caps, "y", ((char *)&codec->capabilities)[i]); - g_variant_builder_add(&properties, "{sv}", "UUID", g_variant_new_string(uuid)); g_variant_builder_add(&properties, "{sv}", "DelayReporting", g_variant_new_boolean(TRUE)); g_variant_builder_add(&properties, "{sv}", "Codec", g_variant_new_byte(codec->codec_id)); - g_variant_builder_add(&properties, "{sv}", "Capabilities", g_variant_builder_end(&caps)); + g_variant_builder_add(&properties, "{sv}", "Capabilities", g_variant_new_fixed_array( + G_VARIANT_TYPE_BYTE, &codec->capabilities, codec->capabilities_size, sizeof(uint8_t))); g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", dbus_obj->path, &properties)); g_variant_builder_clear(&properties); @@ -1273,16 +1261,10 @@ bool bluez_a2dp_set_configuration( goto fail; } - GVariantBuilder caps; - g_variant_builder_init(&caps, G_VARIANT_TYPE("ay")); - - size_t i; - for (i = 0; i < sep->capabilities_size; i++) - g_variant_builder_add(&caps, "y", ((char *)sep->configuration)[i]); - GVariantBuilder props; g_variant_builder_init(&props, G_VARIANT_TYPE("a{sv}")); - g_variant_builder_add(&props, "{sv}", "Capabilities", g_variant_builder_end(&caps)); + g_variant_builder_add(&props, "{sv}", "Capabilities", g_variant_new_fixed_array( + G_VARIANT_TYPE_BYTE, sep->configuration, sep->capabilities_size, sizeof(uint8_t))); msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, sep->bluez_dbus_path, BLUEZ_IFACE_MEDIA_ENDPOINT, "SetConfiguration"); diff --git a/src/shared/hex.c b/src/shared/hex.c new file mode 100644 index 000000000..9138cf9e4 --- /dev/null +++ b/src/shared/hex.c @@ -0,0 +1,59 @@ +/* + * BlueALSA - hex.c + * Copyright (c) 2016-2021 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#include "shared/hex.h" + +#include +#include + +static const int hextable[255] = { + ['0'] = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + ['A'] = 10, 11, 12, 13, 14, 15, + ['a'] = 10, 11, 12, 13, 14, 15, +}; + +/** + * Encode a binary data into a hex string. + * + * @param bin A buffer with binary data. + * @param hex A buffer where null-terminated hexadecimal string will be + * stored. This buffer has to be big enough to store n*2 + 1 bytes of + * data. + * @param n The length of the binary buffer which shall be encoded. + * @return This function returns length of the hex string. */ +ssize_t bin2hex(const void *bin, char *hex, size_t n) { + for (size_t i = 0; i < n; i++) + sprintf(&hex[i * 2], "%.2x", ((unsigned char *)bin)[i]); + return n * 2; +} + +/** + * Decode a hex string into a binary data. + * + * @param hex A buffer with hexadecimal string. + * @param bin A buffer where decoded data will be stored. This buffer has to + * be big enough to store n/2 bytes of data. + * @param n The length of the string which shall be decoded. + * @return On success this function returns the size of the binary data. If + * an error has occurred, -1 is returned and errno is set to indicate the + * error. */ +ssize_t hex2bin(const char *hex, void *bin, size_t n) { + + if (n % 2 != 0) + return errno = EINVAL, -1; + + n /= 2; + for (size_t i = 0; i < n; i++) { + ((char *)bin)[i] = hextable[(int)hex[i * 2]] << 4; + ((char *)bin)[i] |= hextable[(int)hex[i * 2 + 1]]; + } + + return n; +} diff --git a/src/shared/hex.h b/src/shared/hex.h new file mode 100644 index 000000000..74d173844 --- /dev/null +++ b/src/shared/hex.h @@ -0,0 +1,24 @@ +/* + * BlueALSA - hex.h + * Copyright (c) 2016-2021 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#pragma once +#ifndef BLUEALSA_SHARED_HEX_H_ +#define BLUEALSA_SHARED_HEX_H_ + +#if HAVE_CONFIG_H +# include +#endif + +#include + +ssize_t bin2hex(const void *bin, char *hex, size_t n); +ssize_t hex2bin(const char *hex, void *bin, size_t n); + +#endif diff --git a/utils/Makefile.am b/utils/Makefile.am index 290c37ab1..f5256b432 100644 --- a/utils/Makefile.am +++ b/utils/Makefile.am @@ -7,6 +7,9 @@ bin_PROGRAMS = if ENABLE_A2DPCONF bin_PROGRAMS += a2dpconf +a2dpconf_SOURCES = \ + ../src/shared/hex.c \ + a2dpconf.c a2dpconf_CFLAGS = \ -I$(top_srcdir)/src a2dpconf_LDADD = \ diff --git a/utils/a2dpconf.c b/utils/a2dpconf.c index 6b7b910c3..8ceb68d34 100644 --- a/utils/a2dpconf.c +++ b/utils/a2dpconf.c @@ -23,12 +23,7 @@ #include "a2dp-codecs.h" #include "shared/defs.h" - -static const int hextable[255] = { - ['0'] = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - ['A'] = 10, 11, 12, 13, 14, 15, - ['a'] = 10, 11, 12, 13, 14, 15, -}; +#include "shared/hex.h" static const struct { uint16_t codec_id; @@ -93,19 +88,12 @@ static ssize_t get_codec_blob(const char *s, void *dest, size_t n) { return -1; } - len /= 2; - for (size_t i = 0; i < len; i++) { - ((char *)dest)[i] = hextable[(int)s[i * 2]] << 4; - ((char *)dest)[i] |= hextable[(int)s[i * 2 + 1]]; - } - - return len; + return hex2bin(s, dest, len); } static char *bintohex(const void *src, size_t n) { char *hex = calloc(1, n * 2 + 1); - for (size_t i = 0; i < n; i++) - sprintf(&hex[i * 2], "%.2x", ((unsigned char *)src)[i]); + bin2hex(src, hex, n); return hex; } @@ -478,6 +466,36 @@ static struct { { A2DP_CODEC_VENDOR_SAMSUNG_SC, -1, dump_vendor }, }; +int dump(const char *config, bool detect) { + + uint16_t codec_id = get_codec(config); + + ssize_t blob_size; + if ((blob_size = get_codec_blob(config, NULL, 0)) == -1) + return -1; + + void *blob = malloc(blob_size); + if (get_codec_blob(config, blob, blob_size) == -1) + return -1; + + for (size_t i = 0; i < ARRAYSIZE(dumps); i++) + if (dumps[i].codec_id == codec_id) { + dumps[i].dump(blob, blob_size); + return 0; + } + + if (detect) { + for (size_t i = 0; i < ARRAYSIZE(dumps); i++) + if (dumps[i].blob_size == (size_t)blob_size) + dumps[i].dump(blob, blob_size); + dump_vendor(blob, blob_size); + return 0; + } + + fprintf(stderr, "Couldn't detect codec type: %s\n", config); + return -1; +} + int main(int argc, char *argv[]) { int opt; @@ -489,6 +507,7 @@ int main(int argc, char *argv[]) { { 0, 0, 0, 0 }, }; + int rv = EXIT_SUCCESS; bool detect = false; while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1) @@ -496,7 +515,7 @@ int main(int argc, char *argv[]) { case 'h' /* --help */ : usage: printf("Usage:\n" - " %s [OPTION]... \n" + " %s [OPTION]... ...\n" "\nOptions:\n" " -h, --help\t\tprint this help and exit\n" " -V, --version\t\tprint version and exit\n" @@ -520,36 +539,13 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - if (argc - optind != 1) + if (argc - optind < 1) goto usage; - const char *codec = argv[optind]; - uint16_t codec_id = get_codec(codec); - - ssize_t blob_size; - if ((blob_size = get_codec_blob(codec, NULL, 0)) == -1) - return EXIT_FAILURE; - - void *blob = malloc(blob_size); - if (get_codec_blob(codec, blob, blob_size) == -1) - return EXIT_FAILURE; - - for (size_t i = 0; i < ARRAYSIZE(dumps); i++) - if (dumps[i].codec_id == codec_id) { - dumps[i].dump(blob, blob_size); - return EXIT_SUCCESS; - } - - if (detect) { - for (size_t i = 0; i < ARRAYSIZE(dumps); i++) - if (dumps[i].blob_size == (size_t)blob_size) - dumps[i].dump(blob, blob_size); - dump_vendor(blob, blob_size); - } - else { - fprintf(stderr, "Couldn't detect codec type: %s\n", codec); - return EXIT_FAILURE; - } + int i; + for (i = optind; i < argc; i++) + if (dump(argv[i], detect) == -1) + rv = EXIT_FAILURE; - return EXIT_SUCCESS; + return rv; } diff --git a/utils/cli/Makefile.am b/utils/cli/Makefile.am index 27d3ca2f6..0985358bb 100644 --- a/utils/cli/Makefile.am +++ b/utils/cli/Makefile.am @@ -7,6 +7,7 @@ bin_PROGRAMS = bluealsa-cli bluealsa_cli_SOURCES = \ ../../src/shared/dbus-client.c \ + ../../src/shared/hex.c \ ../../src/shared/log.c \ cli.c diff --git a/utils/cli/cli.c b/utils/cli/cli.c index dfecbc08c..1dd0ea069 100644 --- a/utils/cli/cli.c +++ b/utils/cli/cli.c @@ -19,12 +19,14 @@ #include #include #include +#include #include #include #include "shared/dbus-client.h" #include "shared/defs.h" +#include "shared/hex.h" #include "shared/log.h" /** @@ -138,7 +140,7 @@ static bool print_codecs(const char *path, DBusError *err) { DBusMessageIter iter; if (!dbus_message_iter_init(rep, &iter)) { - dbus_set_error(err, DBUS_ERROR_FAILED, "%s", strerror(ENOMEM)); + dbus_set_error(err, DBUS_ERROR_NO_MEMORY, NULL); goto fail; } @@ -348,7 +350,7 @@ static int cmd_info(int argc, char *argv[]) { static int cmd_codec(int argc, char *argv[]) { - if (argc < 2 || argc > 3) { + if (argc < 2 || argc > 4) { cmd_print_error("Invalid number of arguments"); return EXIT_FAILURE; } @@ -370,41 +372,77 @@ static int cmd_codec(int argc, char *argv[]) { DBusMessage *msg = NULL, *rep = NULL; const char *codec = argv[2]; + int result = EXIT_FAILURE; if ((msg = dbus_message_new_method_call(dbus_ctx.ba_service, path, BLUEALSA_INTERFACE_PCM, "SelectCodec")) == NULL) { - cmd_print_error("%s", strerror(ENOMEM)); - return EXIT_FAILURE; + dbus_set_error(&err, DBUS_ERROR_NO_MEMORY, NULL); + goto fail; } DBusMessageIter iter; dbus_message_iter_init_append(msg, &iter); if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &codec)) { - cmd_print_error("%s", strerror(ENOMEM)); - dbus_message_unref(msg); - return EXIT_FAILURE; + dbus_set_error(&err, DBUS_ERROR_NO_MEMORY, NULL); + goto fail; } DBusMessageIter props; if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &props)) { - cmd_print_error("%s", strerror(ENOMEM)); - dbus_message_unref(msg); - return EXIT_FAILURE; + dbus_set_error(&err, DBUS_ERROR_NO_MEMORY, NULL); + goto fail; + } + + if (argc == 4) { + + const char *property = "Configuration"; + const char *configuration = argv[3]; + ssize_t len = strlen(configuration); + uint8_t data[64]; + + len = MIN((size_t)len, sizeof(data) * 2); + if ((len = hex2bin(configuration, data, len)) == -1) { + dbus_set_error(&err, DBUS_ERROR_FAILED, "%s", strerror(errno)); + goto fail; + } + + DBusMessageIter dict; + DBusMessageIter config; + DBusMessageIter array; + const uint8_t *ptr = data; + + if (!dbus_message_iter_open_container(&props, DBUS_TYPE_DICT_ENTRY, NULL, &dict) || + !dbus_message_iter_append_basic(&dict, DBUS_TYPE_STRING, &property) || + !dbus_message_iter_open_container(&dict, DBUS_TYPE_VARIANT, "ay", &config) || + !dbus_message_iter_open_container(&config, DBUS_TYPE_ARRAY, "y", &array) || + !dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &ptr, len) || + !dbus_message_iter_close_container(&config, &array) || + !dbus_message_iter_close_container(&dict, &config) || + !dbus_message_iter_close_container(&props, &dict)) { + dbus_set_error(&err, DBUS_ERROR_NO_MEMORY, NULL); + goto fail; + } + } dbus_message_iter_close_container(&iter, &props); if ((rep = dbus_connection_send_with_reply_and_block(dbus_ctx.conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err)) == NULL) { - cmd_print_error("Couldn't select BlueALSA PCM Codec: %s", err.message); - dbus_message_unref(msg); - return EXIT_FAILURE; + goto fail; } - dbus_message_unref(rep); - dbus_message_unref(msg); - return EXIT_SUCCESS; + result = EXIT_SUCCESS; + +fail: + if (dbus_error_is_set(&err)) + cmd_print_error("Couldn't select BlueALSA PCM Codec: %s", err.message); + if (msg != NULL) + dbus_message_unref(msg); + if (rep != NULL) + dbus_message_unref(rep); + return result; } static int cmd_volume(int argc, char *argv[]) { @@ -668,20 +706,30 @@ static struct command { int (*func)(int argc, char *arg[]); const char *args; const char *help; + unsigned int name_len; + unsigned int args_len; } commands[] = { - { "list-services", cmd_list_services, "", "List all BlueALSA services" }, - { "list-pcms", cmd_list_pcms, "", "List all BlueALSA PCM paths" }, - { "status", cmd_status, "", "Show service runtime properties" }, - { "info", cmd_info, "", "Show PCM properties etc" }, - { "codec", cmd_codec, " []", "Change codec used by PCM" }, - { "volume", cmd_volume, " [] []", "Set audio volume" }, - { "mute", cmd_mute, " [y|n] [y|n]", "Mute/unmute audio" }, - { "soft-volume", cmd_softvol, " [y|n]", "Enable/disable SoftVolume property" }, - { "monitor", cmd_monitor, "", "Display PCMAdded & PCMRemoved signals" }, - { "open", cmd_open, "", "Transfer raw PCM via stdin or stdout" }, +#define CMD(name, f, args, help) { name, f, args, help, sizeof(name), sizeof(args) } + CMD("list-services", cmd_list_services, "", "List all BlueALSA services"), + CMD("list-pcms", cmd_list_pcms, "", "List all BlueALSA PCM paths"), + CMD("status", cmd_status, "", "Show service runtime properties"), + CMD("info", cmd_info, "", "Show PCM properties etc"), + CMD("codec", cmd_codec, " [] []", "Change codec used by PCM"), + CMD("volume", cmd_volume, " [] []", "Set audio volume"), + CMD("mute", cmd_mute, " [y|n] [y|n]", "Mute/unmute audio"), + CMD("soft-volume", cmd_softvol, " [y|n]", "Enable/disable SoftVolume property"), + CMD("monitor", cmd_monitor, "", "Display PCMAdded & PCMRemoved signals"), + CMD("open", cmd_open, "", "Transfer raw PCM via stdin or stdout"), }; static void usage(const char *progname) { + + unsigned int max_len = 0; + size_t i; + + for (i = 0; i < ARRAYSIZE(commands); i++) + max_len = MAX(max_len, commands[i].name_len + commands[i].args_len); + printf("%s - Utility to issue BlueALSA API commands\n", progname); printf("\nUsage:\n %s [options] [command-args]\n", progname); printf("\nOptions:\n"); @@ -690,9 +738,10 @@ static void usage(const char *progname) { printf(" -B, --dbus=NAME BlueALSA service name suffix\n"); printf(" -q, --quiet Do not print any error messages\n"); printf("\nCommands:\n"); - size_t i; for (i = 0; i < ARRAYSIZE(commands); i++) - printf(" %-14s%-27s%s\n", commands[i].name, commands[i].args, commands[i].help); + printf(" %s %-*s%s\n", commands[i].name, + max_len - commands[i].name_len, commands[i].args, + commands[i].help); printf("\nNotes:\n"); printf(" 1. must be a valid BlueALSA PCM path as returned by " "the list-pcms command.\n"); @@ -701,6 +750,7 @@ static void usage(const char *progname) { "attribute is printed.\n"); printf(" 3. The codec command requires BlueZ version >= 5.52 " "for SEP support.\n"); + } int main(int argc, char *argv[]) {