diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..821357c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Romain Vimont + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..84b2ac6 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# USBaudio + +This tool forwards audio from an Android device to the computer over USB. It +works on _Linux_ with _PulseAudio_. + +The purpose is to enable [audio forwarding][issue14] while mirroring with +[scrcpy]. However, it can be used independently, and does not require USB +debugging enabled. + +[issue14]: https://github.com/Genymobile/scrcpy/issues/14 +[scrcpy]: https://github.com/Genymobile/scrcpy + +## Build + +Install the following packages (on _Debian_): + + sudo apt install gcc git meson vlc libpulse-dev libusb-1.0-0-dev + +Then build: + + git clone https://github.com/rom1v/usbaudio + cd usbaudio + meson x --buildtype=release + cd x + ninja + +To install it: + + sudo ninja install + + +## Run + +Plug an Android device. + +If USB debugging is enabled, just execute: + +``` +usbaudio +``` + +You can specify a device by _serial_ or by _vendor id_ and _product id_: + + +```bash +# the serial can be found via "adb device" or "lsusb -v" +usbaudio -s 0123456789abcdef + +# the vid:pid is printed by "lsusb" +usbaudio -d 18d1:4ee2 +``` + +To stop playing, press Ctrl+C. + +To stop forwarding, unplug the device (and maybe restart your current audio +application). + +To only enable audio accessory without playing, use: + +```bash +usbaudio -n +``` diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..30b7447 --- /dev/null +++ b/meson.build @@ -0,0 +1,26 @@ +project('usbaudio', 'c', + version: '1.0', + default_options: 'c_std=c11') + +src = [ + 'src/main.c', + 'src/aoa.c', + 'src/pulse.c', +] + +dependencies = [ + dependency('libpulse'), + dependency('libusb-1.0'), +] + +src_dir = include_directories('src') + +# -Db_ndebug requires meson >= 0.45, do it manually to support older versions +conf = configuration_data() +conf.set('NDEBUG', get_option('buildtype') != 'debug') +configure_file(configuration: conf, output: 'config.h') + +executable('usbaudio', src, + dependencies: dependencies, + include_directories: src_dir, + install: true) diff --git a/src/aoa.c b/src/aoa.c new file mode 100644 index 0000000..0a5a6fd --- /dev/null +++ b/src/aoa.c @@ -0,0 +1,283 @@ +#define _GNU_SOURCE // for strdup() +#include "aoa.h" + +#include +#include + +#include "config.h" +#include "log.h" + +// +#define AOA_GET_PROTOCOL 51 +#define AOA_START_ACCESSORY 53 +#define AOA_SET_AUDIO_MODE 58 + +#define AUDIO_MODE_NO_AUDIO 0 +#define AUDIO_MODE_S16LSB_STEREO_44100HZ 1 + +#define DEFAULT_TIMEOUT 1000 + +typedef struct control_params { + uint8_t request_type; + uint8_t request; + uint16_t value; + uint16_t index; + unsigned char *data; + uint16_t length; + unsigned int timeout; +} control_params; + +static void +log_libusb_error(enum libusb_error errcode) { + LOGE("%s", libusb_strerror(errcode)); +} + +static bool +control_transfer(libusb_device_handle *handle, control_params *params) { + int r = libusb_control_transfer(handle, + params->request_type, + params->request, + params->value, + params->index, + params->data, + params->length, + params->timeout); + if (r < 0) { + log_libusb_error(r); + return false; + } + return true; +} + +static bool +get_serial(libusb_device *device, struct libusb_device_descriptor *desc, + char *data, int length) { + libusb_device_handle *handle; + int r = libusb_open(device, &handle); + if (r) { + LOGD("USB: cannot open device %04x:%04x (%s)", + desc->idVendor, desc->idProduct, libusb_strerror(r)); + return false; + } + + if (!desc->iSerialNumber) { + LOGD("USB: device %04x:%04x has no serial number available", + desc->idVendor, desc->idProduct); + libusb_close(handle); + return false; + } + + r = libusb_get_string_descriptor_ascii(handle, desc->iSerialNumber, + (unsigned char *) data, length); + if (r <= 0) { + LOGD("USB: cannot read serial of device %04x:%04x (%s)", + desc->idVendor, desc->idProduct, libusb_strerror(r)); + libusb_close(handle); + return false; + } + + data[length - 1] = '\0'; // just in case + + libusb_close(handle); + return true; +} + +static bool +has_adb(libusb_device *device, struct libusb_device_descriptor *desc) { +#define ADB_CLASS 0xff +#define ADB_SUBCLASS 0x42 +#define ADB_PROTOCOL 0x1 + for (unsigned i = 0; i < desc->bNumConfigurations; ++i) { + struct libusb_config_descriptor *config; + int r = libusb_get_config_descriptor(device, i, &config); + if (r) { + LOGE("Could not retrieve config descriptors"); + continue; + } + + for (unsigned j = 0; j < config->bNumInterfaces; ++j) { + const struct libusb_interface *intf = &config->interface[j]; + for (int k = 0; k < intf->num_altsetting; ++k) { + const struct libusb_interface_descriptor *d = + &intf->altsetting[k]; + if (d->bInterfaceClass == ADB_CLASS && + d->bInterfaceSubClass == ADB_SUBCLASS && + d->bInterfaceProtocol == ADB_PROTOCOL) { + // we found it! + libusb_free_config_descriptor(config); + return true; + } + } + } + + libusb_free_config_descriptor(config); + } + + return false; +} + +ssize_t +aoa_find_devices(const struct lookup *lookup, + struct usb_device *devices, size_t len) { + size_t nr = 0; // number of devices found + + libusb_device **list; + ssize_t cnt = libusb_get_device_list(NULL, &list); + if (cnt < 0) { + log_libusb_error(cnt); + return -1; + } + + for (ssize_t i = 0; i < cnt && nr < len; ++i) { + libusb_device *device = list[i]; + + struct usb_device *usb_device = &devices[nr]; + + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + + char serial[128]; + bool match = false; + switch (lookup->type) { + case LOOKUP_BY_ADB_INTERFACE: + match = has_adb(device, &desc); + break; + case LOOKUP_BY_SERIAL: { + bool ok = get_serial(device, &desc, serial, sizeof(serial)); + if (ok) { + match = !strcmp(lookup->serial, serial); + } + break; + } + case LOOKUP_BY_VID_PID: + match = lookup->vid == desc.idVendor && + lookup->pid == desc.idProduct; + break; + } + + if (match) { + // add the device to the result list + if (lookup->type != LOOKUP_BY_SERIAL) { + bool ok = get_serial(device, &desc, serial, sizeof(serial)); + if (!ok) { + LOGE("Could not read device serial"); + continue; + } + } + + usb_device->serial = strdup(serial); + usb_device->vid = desc.idVendor; + usb_device->pid = desc.idProduct; + usb_device->device = device; + libusb_ref_device(usb_device->device); + nr++; + } + } + + libusb_free_device_list(list, 1); + + return nr; +} + +void aoa_destroy_device(struct usb_device *usb_device) { + free(usb_device->serial); + libusb_unref_device(usb_device->device); +} + +static bool +aoa_get_protocol(libusb_device_handle *handle, uint16_t *version) { + unsigned char data[2]; + control_params params = { + .request_type = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR, + .request = AOA_GET_PROTOCOL, + .value = 0, + .index = 0, + .data = data, + .length = sizeof(data), + .timeout = DEFAULT_TIMEOUT + }; + if (control_transfer(handle, ¶ms)) { + // little endian + *version = (data[1] << 8) | data[0]; + return true; + } + return false; +} + +static bool +set_audio_mode(libusb_device_handle *handle, uint16_t mode) { + control_params params = { + .request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR, + .request = AOA_SET_AUDIO_MODE, + // + .value = mode, + .index = 0, // unused + .data = NULL, + .length = 0, + .timeout = DEFAULT_TIMEOUT + }; + return control_transfer(handle, ¶ms); +} + +static bool +start_accessory(libusb_device_handle *handle) { + control_params params = { + .request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR, + .request = AOA_START_ACCESSORY, + .value = 0, // unused + .index = 0, // unused + .data = NULL, + .length = 0, + .timeout = DEFAULT_TIMEOUT + }; + return control_transfer(handle, ¶ms); +} + +bool +aoa_init(void) { + return !libusb_init(NULL); +} + +void +aoa_exit(void) { + libusb_exit(NULL); +} + +bool +aoa_forward_audio(const struct usb_device *usb_device) { + libusb_device_handle *handle; + int r = libusb_open(usb_device->device, &handle); + if (r) { + log_libusb_error(r); + return false; + } + + uint16_t version; + if (!aoa_get_protocol(handle, &version)) { + LOGE("Could not get AOA protocol version"); + libusb_close(handle); + return false; + } + + LOGD("Device AOA version: %" PRIu16, version); + if (version < 2) { + LOGE("Device does not support AOA 2: %" PRIu16, version); + libusb_close(handle); + return false; + } + + if (!set_audio_mode(handle, AUDIO_MODE_S16LSB_STEREO_44100HZ)) { + LOGE("Could not set audio mode"); + libusb_close(handle); + return false; + } + + if (!start_accessory(handle)) { + LOGE("Could not start accessory"); + libusb_close(handle); + return false; + } + + libusb_close(handle); + return true; +} diff --git a/src/aoa.h b/src/aoa.h new file mode 100644 index 0000000..43f989a --- /dev/null +++ b/src/aoa.h @@ -0,0 +1,54 @@ +#ifndef AOA_H +#define AOA_H + +#include +#include +#include + +enum lookup_type { + // devices supporting adb + LOOKUP_BY_ADB_INTERFACE, + // devices having the provided serial + LOOKUP_BY_SERIAL, + // devices having the provided vid:pid + LOOKUP_BY_VID_PID, +}; + +struct lookup { + enum lookup_type type; + union { + const char *serial; + struct { + uint16_t vid; + uint16_t pid; + }; + }; +}; + +struct usb_device { + uint16_t vid; + uint16_t pid; + char *serial; + libusb_device *device; +}; + +bool +aoa_init(void); + +void +aoa_exit(void); + +ssize_t +aoa_find_devices(const struct lookup *lookup, + struct usb_device *devices, size_t len); + +bool +aoa_forward_audio(const struct usb_device *device); + +void +aoa_destroy_device(struct usb_device *device); + +// there is no function to disable forwarding, because it just does not work +// you need to unplug the device + +#endif diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..6a9bc5a --- /dev/null +++ b/src/log.h @@ -0,0 +1,17 @@ +#ifndef LOG_H +#define LOG_H + +#include + +#include "config.h" + +#ifndef NDEBUG +# define LOGD(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__) +#else +# define LOGD(fmt, ...) +#endif +#define LOGI(fmt, ...) printf("[INFO] " fmt "\n", ##__VA_ARGS__) +#define LOGW(fmt, ...) printf("[WARN] " fmt "\n", ##__VA_ARGS__) +#define LOGE(fmt, ...) fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__) + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d8bc291 --- /dev/null +++ b/src/main.c @@ -0,0 +1,288 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "aoa.h" +#include "log.h" +#include "pulse.h" + +#define DEFAULT_VLC_LIVE_CACHING 50 + +struct args { + bool help; + bool play; + const char *serial; + uint16_t vid; + uint16_t pid; + uint32_t live_caching; +}; + +static bool +parse_usb_id(const char *s, uint16_t *result) { + char *endptr; + if (*s == '\0') { + return false; + } + long value = strtol(s, &endptr, 16); + if (*endptr != '\0') { + return false; + } + if (value & ~0xffff) { + LOGE("Id out of range: %lx", value); + return false; + } + + *result = (uint16_t) value; + return true; +} + +static bool +parse_device(const char *s, uint16_t *vid, uint16_t *pid) { + const char *ptr = strchr(s, ':'); + if (!ptr) { + goto error; + } + size_t index = ptr - s; + size_t len = strlen(s); + if (index == 0 || index > 4 || index + 1 > len - 1 || index + 5 < len) { + // pid and vid must have between 1 and 4 chars + goto error; + } + + char id[5]; + + memcpy(id, s, index); + id[index] = '\0'; + if (!parse_usb_id(id, vid)) { + LOGE("Could not parse vid: %s", id); + goto error; + } + + memcpy(id, &s[index + 1], len - index - 1); + id[index] = '\0'; + if (!parse_usb_id(id, pid)) { + LOGE("Could not parse pid: %s", id); + goto error; + } + + return true; + +error: + LOGE("Invalid device format (expected vid:pid): %s", s); + return false; +} + +static bool +parse_live_caching(const char *s, uint32_t *result) { + char *endptr; + if (*s == '\0') { + return false; + } + long value = strtol(s, &endptr, 10); + if (*endptr != '\0') { + return false; + } + if (value & ~0xfffffffff) { + LOGE("Id out of range: %lx", value); + return false; + } + + *result = (uint32_t) value; + return true; +} + +static bool +parse_args(struct args *args, int argc, char *argv[]) { +#define OPT_LIVE_CACHING 1000 + static const struct option long_opts[] = { + {"device", required_argument, NULL, 'd'}, + {"help", no_argument, NULL, 'h'}, + {"live-caching", required_argument, NULL, OPT_LIVE_CACHING}, + {"no-play", no_argument, NULL, 'n'}, + {"serial", required_argument, NULL, 's'}, + }; + int c; + while ((c = getopt_long(argc, argv, "d:hns:", long_opts, NULL)) != -1) { + switch (c) { + case 'd': + if (!parse_device(optarg, &args->vid, &args->pid)) { + return false; + } + break; + case 'h': + args->help = true; + break; + case 'n': + args->play = false; + break; + case 's': + args->serial = optarg; + break; + case OPT_LIVE_CACHING: + if (!parse_live_caching(optarg, &args->live_caching)) { + return false; + } + break; + default: + // getopt prints the error message on stderr + return false; + } + } + + if (optind < argc) { + LOGE("Unexpected additional argument: %s", argv[optind]); + return false; + } + + return true; +} + +static void usage(const char *arg0) { + fprintf(stderr, + "Usage: %s [options]\n" + "\n" + "Options:\n" + "\n" + " -d, --device pid:vid\n" + " Lookup the USB device by pid:vid.\n" + "\n" + " -h, --help\n" + " Print this help.\n" + "\n" + " --live-caching ms\n" + " Forward the option to VLC. Default is %dms.\n" + "\n" + " -n, --no-play\n" + " Do not play the input source matching the device.\n" + "\n" + " -s, --serial serial\n" + " Lookup the USB device by serial.\n" + "\n", arg0, DEFAULT_VLC_LIVE_CACHING); +} + +static inline const char * +get_vlc_command(void) { + const char *vlc = getenv("VLC"); + if (!vlc) { + vlc = "vlc"; + } + return vlc; +} + +int main(int argc, char *argv[]) { + struct args args = { + .help = false, + .play = true, + .serial = NULL, + .vid = 0, + .pid = 0, + .live_caching = DEFAULT_VLC_LIVE_CACHING, + }; + + if (!parse_args(&args, argc, argv)) { + return 1; + } + + if (args.help) { + usage(argv[0]); + return 0; + } + + if (args.serial && (args.vid || args.pid)) { + LOGE("Could not provide device and serial simultaneously"); + return 1; + } + + if (!aoa_init()) { + LOGE("Could not initialize AOA"); + return 1; + } + + struct lookup lookup; + if (args.serial) { + lookup.type = LOOKUP_BY_SERIAL; + lookup.serial = args.serial; + } else if (args.vid || args.pid) { + lookup.type = LOOKUP_BY_VID_PID; + lookup.vid = args.vid; + lookup.pid = args.pid; + } else { + lookup.type = LOOKUP_BY_ADB_INTERFACE; + } + + struct usb_device devices[32]; + ssize_t ndevices = aoa_find_devices(&lookup, devices, 32); + if (ndevices < 0) { + LOGE("Could not get USB devices"); + return 1; + } + + if (ndevices == 0) { + LOGE("Could not find device"); + return 1; + } + + if (ndevices > 1) { + LOGE("Several devices found:"); + for (size_t i = 0; i < ndevices; ++i) { + struct usb_device *d = &devices[i]; + LOGE(" [%04x:%04x] %s", d->vid, d->pid, d->serial); + aoa_destroy_device(&devices[i]); + } + return 1; + } + + struct usb_device *device = &devices[0]; + + LOGI("Device: [%04x:%04x] %s", device->vid, device->pid, device->serial); + + if (!aoa_forward_audio(device)) { + LOGE("Could not forward audio"); + aoa_destroy_device(device); + return 1; + } + + LOGI("Audio forwarding enabled"); + + aoa_exit(); + + if (!args.play) { + // nothing more to do + aoa_destroy_device(device); + return 0; + } + + if (device->pid < 0x2D02 || device->pid > 0x2D05) { + // the AOA audio was already enabled, no need to wait + // + LOGI("Waiting for input source..."); + sleep(2); + } + + int nr = pulse_get_device_number(device->serial); + aoa_destroy_device(device); + if (nr < 0) { + LOGE("Could not find matching PulseAudio input source"); + return 1; + } + + char url[20]; + snprintf(url, sizeof(url), "pulse://%d", nr); + + LOGI("Playing %s", url); + + char caching[32]; + snprintf(caching, sizeof(caching), "--live-caching=%d", args.live_caching); + + const char *vlc = get_vlc_command(); + + // let's become VLC + execlp(vlc, vlc, "-Idummy", caching, "--play-and-exit", url, NULL); + + LOGE("Could not start VLC: %s", vlc); + return 1; +} diff --git a/src/pulse.c b/src/pulse.c new file mode 100644 index 0000000..6b83599 --- /dev/null +++ b/src/pulse.c @@ -0,0 +1,157 @@ +#include "pulse.h" + +#include +#include +#include +#include + +#include "log.h" + +#define DEVICE_NOT_FOUND_YET -1 +#define DEVICE_NOT_FOUND -2 + +struct pulse_device_data { + const char *req_serial; + size_t req_serial_len; + int index; +}; + +static void +pulse_state_cb(pa_context *ctx, void *userdata) { + pa_context_state_t *state = userdata; + *state = pa_context_get_state(ctx); +} + +static void +pulse_sourcelist_cb(pa_context *ctx, const pa_source_info *info, int eol, + void *userdata) { + struct pulse_device_data *device = userdata; + if (eol) { + if (device->index == DEVICE_NOT_FOUND_YET) { + device->index = DEVICE_NOT_FOUND; + } + return; + } + + // The PulseAudio serial is not exactly the same as the USB serial, + // it follows the pattern: "manufacturer_model_serial". + // To find a matching device, we check it ends with "_serial". + const char *serial = + pa_proplist_gets(info->proplist, PA_PROP_DEVICE_SERIAL); + if (!serial) { + return; + } + LOGD("%s ? %s", device->req_serial, serial); + size_t len = strlen(serial); + if (len < device->req_serial_len + 1) { // +1 for '_' + return; + } + if (serial[len - device->req_serial_len - 1] != '_') { + // it may not match "_serial" if there is no '_' + return; + } + if (!memcmp(device->req_serial, + &serial[len - device->req_serial_len], + device->req_serial_len)) { + // the serial matches + device->index = (int) info->index; + LOGI("Matching PulseAudio input source found: %d (%s:%s) %s", + device->index, + pa_proplist_gets(info->proplist, PA_PROP_DEVICE_VENDOR_ID), + pa_proplist_gets(info->proplist, PA_PROP_DEVICE_PRODUCT_ID), + serial); + } +} + +static bool +pulse_wait_ready(pa_context *ctx, pa_mainloop *ml) { + pa_context_state_t state = PA_CONTEXT_UNCONNECTED; + pa_context_set_state_callback(ctx, pulse_state_cb, &state); + + // state will be set during pa_mainloop_iterate() + bool ready = false; + for (;;) { + int r = pa_mainloop_iterate(ml, 1, NULL); + if (r <= 0) { + LOGE("Could not iterate on main loop"); + break; + } + + if (state == PA_CONTEXT_READY) { + ready = true; + break; + } + + if (state == PA_CONTEXT_FAILED || state == PA_CONTEXT_TERMINATED) { + LOGE("Connection to PulseAudio server terminated"); + break; + } + } + + pa_context_set_state_callback(ctx, NULL, NULL); + return ready; +} + +int +pulse_get_device_number(const char *serial) { + pa_mainloop *ml = pa_mainloop_new(); + if (!ml) { + LOGE("Could not create PulseAudio main loop"); + return -1; + } + + int ret = -1; + + pa_mainloop_api *mlapi = pa_mainloop_get_api(ml); + assert(mlapi); + + pa_context *ctx = pa_context_new(mlapi, "usbaudio"); + if (!ctx) { + LOGE("Could not create PulseAudio context"); + goto finally_ml_free; + } + + int r = pa_context_connect(ctx, NULL, 0, NULL); + if (r < 0) { + LOGE("Could not connect to PulseAudio server"); + goto finally_ctx_unref; + } + + bool ready = pulse_wait_ready(ctx, ml); + if (!ready) { + goto finally_ctx_disconnect; + } + + struct pulse_device_data device = { + .req_serial = serial, + .req_serial_len = strlen(serial), + .index = DEVICE_NOT_FOUND_YET, + }; + pa_operation *op = + pa_context_get_source_info_list(ctx, pulse_sourcelist_cb, &device); + do { + int r = pa_mainloop_iterate(ml, 1, NULL); + if (r <= 0) { + LOGE("Could not iterate on main loop"); + goto finally_ctx_disconnect; + } + } while (device.index == DEVICE_NOT_FOUND_YET); + if (device.index >= 0) { + // we don't need to receive further callbacks + pa_operation_cancel(op); + } + pa_operation_unref(op); + + if (device.index >= 0) { + ret = device.index; + } + +finally_ctx_disconnect: + pa_context_disconnect(ctx); +finally_ctx_unref: + pa_context_unref(ctx); +finally_ml_free: + pa_mainloop_free(ml); + + return ret; +} diff --git a/src/pulse.h b/src/pulse.h new file mode 100644 index 0000000..72e5202 --- /dev/null +++ b/src/pulse.h @@ -0,0 +1,8 @@ +#ifndef PULSE_H +#define PULSE_H + +// return -1 on error +int +pulse_get_device_number(const char *serial); + +#endif