Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rom1v committed Jun 20, 2019
0 parents commit 579b2f5
Show file tree
Hide file tree
Showing 9 changed files with 916 additions and 0 deletions.
21 changes: 21 additions & 0 deletions 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.
62 changes: 62 additions & 0 deletions 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
```
26 changes: 26 additions & 0 deletions 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)
283 changes: 283 additions & 0 deletions src/aoa.c
@@ -0,0 +1,283 @@
#define _GNU_SOURCE // for strdup()
#include "aoa.h"

#include <stdlib.h>
#include <string.h>

#include "config.h"
#include "log.h"

// <https://source.android.com/devices/accessories/aoa2>
#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, &params)) {
// 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,
// <https://source.android.com/devices/accessories/aoa2.html#audio-support>
.value = mode,
.index = 0, // unused
.data = NULL,
.length = 0,
.timeout = DEFAULT_TIMEOUT
};
return control_transfer(handle, &params);
}

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, &params);
}

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;
}

0 comments on commit 579b2f5

Please sign in to comment.