| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2004-2006 Marcel Holtmann <marcel@holtmann.org> |
| * |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdio.h> |
| #include <errno.h> |
| #include <malloc.h> |
| #include <signal.h> |
| #include <sys/ioctl.h> |
| #include <sys/socket.h> |
| |
| #include <bluetooth/bluetooth.h> |
| #include <bluetooth/rfcomm.h> |
| |
| #include <alsa/asoundlib.h> |
| #include <alsa/pcm_external.h> |
| |
| #include "sbc.h" |
| |
| //#define DBG(fmt, arg...) printf("DEBUG: %s: " fmt "\n" , __FUNCTION__ , ## arg) |
| #define DBG(D...) |
| |
| static void a2dp_init(void) __attribute__ ((constructor)); |
| static void a2dp_exit(void) __attribute__ ((destructor)); |
| |
| typedef struct snd_pcm_a2dp { |
| snd_pcm_ioplug_t io; |
| int refcnt; |
| int timeout; |
| unsigned long state; |
| bdaddr_t src; |
| bdaddr_t dst; |
| int sk; |
| sbc_t sbc; |
| snd_pcm_sframes_t num; |
| unsigned char buf[1024]; |
| unsigned int len; |
| unsigned int frame_bytes; |
| } snd_pcm_a2dp_t; |
| |
| static void inline a2dp_get(snd_pcm_a2dp_t *a2dp) |
| { |
| a2dp->refcnt++; |
| a2dp->timeout = 0; |
| } |
| |
| static void inline a2dp_put(snd_pcm_a2dp_t *a2dp) |
| { |
| a2dp->refcnt--; |
| |
| if (a2dp->refcnt <= 0) |
| a2dp->timeout = 2; |
| } |
| |
| static int a2dp_start(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->len = 0; |
| |
| return 0; |
| } |
| |
| static int a2dp_stop(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->len = 0; |
| |
| return 0; |
| } |
| |
| static snd_pcm_sframes_t a2dp_pointer(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| return a2dp->num; |
| } |
| |
| static snd_pcm_sframes_t a2dp_transfer(snd_pcm_ioplug_t *io, |
| const snd_pcm_channel_area_t *areas, |
| snd_pcm_uframes_t offset, snd_pcm_uframes_t size) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| unsigned char *buf; |
| int err, len; |
| |
| buf = (unsigned char *) areas->addr + (areas->first + areas->step * offset) / 8; |
| |
| len = sbc_encode(&a2dp->sbc, buf, size * a2dp->frame_bytes); |
| if (len <= 0) |
| return len; |
| |
| if (a2dp->len + a2dp->sbc.len > sizeof(a2dp->buf)) { |
| err = write(a2dp->sk, a2dp->buf, a2dp->len); |
| a2dp->len = 0; |
| } |
| |
| memcpy(a2dp->buf + a2dp->len, a2dp->sbc.data, a2dp->sbc.len); |
| a2dp->len += a2dp->sbc.len; |
| |
| if (a2dp->state == BT_CONNECTED) |
| a2dp->num += len / a2dp->frame_bytes; |
| |
| return len / a2dp->frame_bytes; |
| } |
| |
| static int a2dp_close(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->len = 0; |
| |
| a2dp_put(a2dp); |
| |
| return 0; |
| } |
| |
| static int a2dp_params(snd_pcm_ioplug_t *io, snd_pcm_hw_params_t *params) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| unsigned int period_bytes; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->frame_bytes = (snd_pcm_format_physical_width(io->format) * io->channels) / 8; |
| |
| period_bytes = io->period_size * a2dp->frame_bytes; |
| |
| DBG("format %s rate %d channels %d", snd_pcm_format_name(io->format), |
| io->rate, io->channels); |
| |
| DBG("frame_bytes %d period_bytes %d period_size %ld buffer_size %ld", |
| a2dp->frame_bytes, period_bytes, io->period_size, io->buffer_size); |
| |
| return 0; |
| } |
| |
| static int a2dp_prepare(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->len = 0; |
| |
| a2dp->num = 0; |
| |
| a2dp->sbc.rate = io->rate; |
| a2dp->sbc.channels = io->channels; |
| |
| return 0; |
| } |
| |
| static int a2dp_drain(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| DBG("a2dp %p", a2dp); |
| |
| a2dp->len = 0; |
| |
| return 0; |
| } |
| |
| static int a2dp_descriptors_count(snd_pcm_ioplug_t *io) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| if (a2dp->state == BT_CLOSED) |
| return 0; |
| |
| return 1; |
| } |
| |
| static int a2dp_descriptors(snd_pcm_ioplug_t *io, struct pollfd *pfds, unsigned int space) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| if (a2dp->state == BT_CLOSED) |
| return 0; |
| |
| if (space < 1) { |
| SNDERR("Can't fill in descriptors"); |
| return 0; |
| } |
| |
| pfds[0].fd = a2dp->sk; |
| pfds[0].events = POLLOUT; |
| |
| return 1; |
| } |
| |
| static int a2dp_poll(snd_pcm_ioplug_t *io, struct pollfd *pfds, |
| unsigned int nfds, unsigned short *revents) |
| { |
| snd_pcm_a2dp_t *a2dp = io->private_data; |
| |
| *revents = pfds[0].revents; |
| |
| if (a2dp->state == BT_CLOSED) |
| return 0; |
| |
| if (pfds[0].revents & POLLHUP) { |
| a2dp->state = BT_CLOSED; |
| snd_pcm_ioplug_reinit_status(&a2dp->io); |
| } |
| |
| return 0; |
| } |
| |
| static snd_pcm_ioplug_callback_t a2dp_callback = { |
| .start = a2dp_start, |
| .stop = a2dp_stop, |
| .pointer = a2dp_pointer, |
| .transfer = a2dp_transfer, |
| .close = a2dp_close, |
| .hw_params = a2dp_params, |
| .prepare = a2dp_prepare, |
| .drain = a2dp_drain, |
| .poll_descriptors_count = a2dp_descriptors_count, |
| .poll_descriptors = a2dp_descriptors, |
| .poll_revents = a2dp_poll, |
| }; |
| |
| static int a2dp_connect(snd_pcm_a2dp_t *a2dp) |
| { |
| struct sockaddr_rc addr; |
| socklen_t len; |
| int sk; |
| |
| DBG("a2dp %p", a2dp); |
| |
| sk = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); |
| if (sk < 0) |
| return -errno; |
| |
| memset(&addr, 0, sizeof(addr)); |
| addr.rc_family = AF_BLUETOOTH; |
| bacpy(&addr.rc_bdaddr, &a2dp->src); |
| |
| if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { |
| close(sk); |
| return -errno; |
| } |
| |
| memset(&addr, 0, sizeof(addr)); |
| addr.rc_family = AF_BLUETOOTH; |
| bacpy(&addr.rc_bdaddr, &a2dp->dst); |
| addr.rc_channel = 1; |
| |
| if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { |
| close(sk); |
| return -errno; |
| } |
| |
| memset(&addr, 0, sizeof(addr)); |
| len = sizeof(addr); |
| |
| if (getsockname(sk, (struct sockaddr *) &addr, &len) < 0) { |
| close(sk); |
| return -errno; |
| } |
| |
| bacpy(&a2dp->src, &addr.rc_bdaddr); |
| |
| fcntl(sk, F_SETFL, fcntl(sk, F_GETFL) | O_NONBLOCK); |
| |
| a2dp->sk = sk; |
| |
| return 0; |
| } |
| |
| static int a2dp_constraint(snd_pcm_a2dp_t *a2dp) |
| { |
| snd_pcm_ioplug_t *io = &a2dp->io; |
| snd_pcm_access_t access_list[] = { |
| SND_PCM_ACCESS_RW_INTERLEAVED, |
| SND_PCM_ACCESS_MMAP_INTERLEAVED, |
| }; |
| unsigned int format[2], channel[2], rate[2]; |
| int err; |
| |
| DBG("a2dp %p", a2dp); |
| |
| err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS, 2, access_list); |
| if (err < 0) |
| return err; |
| |
| format[0] = SND_PCM_FORMAT_S16_LE; |
| |
| err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_FORMAT, 1, format); |
| if (err < 0) |
| return err; |
| |
| channel[0] = 1; |
| channel[1] = 2; |
| |
| err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_CHANNELS, 2, channel); |
| if (err < 0) |
| return err; |
| |
| rate[0] = 44100; |
| rate[1] = 48000; |
| |
| err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_RATE, 2, rate); |
| if (err < 0) |
| return err; |
| |
| err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIOD_BYTES, 8192, 8192); |
| if (err < 0) |
| return err; |
| |
| err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIODS, 2, 2); |
| if (err < 0) |
| return err; |
| |
| return 0; |
| } |
| |
| #define MAX_CONNECTIONS 10 |
| |
| static snd_pcm_a2dp_t *connections[MAX_CONNECTIONS]; |
| |
| static snd_timer_t *timer = NULL; |
| |
| static volatile sig_atomic_t __locked = 0; |
| |
| static inline void a2dp_lock(void) |
| { |
| while (__locked) |
| usleep(100); |
| |
| __locked = 1; |
| } |
| |
| static inline void a2dp_unlock(void) |
| { |
| __locked = 0; |
| } |
| |
| static inline snd_pcm_a2dp_t *a2dp_alloc(void) |
| { |
| snd_pcm_a2dp_t *a2dp; |
| |
| a2dp = malloc(sizeof(*a2dp)); |
| if (!a2dp) |
| return NULL; |
| |
| memset(a2dp, 0, sizeof(*a2dp)); |
| |
| a2dp->refcnt = 1; |
| |
| a2dp->state = BT_OPEN; |
| |
| sbc_init(&a2dp->sbc, SBC_NULL); |
| |
| return a2dp; |
| } |
| |
| static inline void a2dp_free(snd_pcm_a2dp_t *a2dp) |
| { |
| if (a2dp->sk > fileno(stderr)) |
| close(a2dp->sk); |
| |
| sbc_finish(&a2dp->sbc); |
| |
| free(a2dp); |
| } |
| |
| static void a2dp_timer(snd_async_handler_t *async) |
| { |
| snd_timer_t *handle = snd_async_handler_get_timer(async); |
| snd_timer_read_t tr; |
| int i, ticks = 0; |
| |
| while (snd_timer_read(handle, &tr, sizeof(tr)) == sizeof(tr)) |
| ticks += tr.ticks; |
| |
| a2dp_lock(); |
| |
| for (i = 0; i < MAX_CONNECTIONS; i++) { |
| snd_pcm_a2dp_t *a2dp = connections[i]; |
| |
| if (a2dp && a2dp->refcnt <= 0) { |
| a2dp->timeout = ((a2dp->timeout * 1000) - ticks) / 1000; |
| if (a2dp->timeout <= 0) { |
| connections[i] = NULL; |
| a2dp_free(a2dp); |
| } |
| } |
| } |
| |
| a2dp_unlock(); |
| } |
| |
| static void a2dp_init(void) |
| { |
| snd_async_handler_t *async; |
| snd_timer_info_t *info; |
| snd_timer_params_t *params; |
| long resolution; |
| char timername[64]; |
| int err, i; |
| |
| a2dp_lock(); |
| |
| for (i = 0; i < MAX_CONNECTIONS; i++) |
| connections[i] = NULL; |
| |
| a2dp_unlock(); |
| |
| snd_timer_info_alloca(&info); |
| snd_timer_params_alloca(¶ms); |
| |
| sprintf(timername, "hw:CLASS=%i,SCLASS=%i,CARD=%i,DEV=%i,SUBDEV=%i", |
| SND_TIMER_CLASS_GLOBAL, SND_TIMER_CLASS_NONE, 0, |
| SND_TIMER_GLOBAL_SYSTEM, 0); |
| |
| err = snd_timer_open(&timer, timername, SND_TIMER_OPEN_NONBLOCK); |
| if (err < 0) { |
| SNDERR("Can't open global timer"); |
| return; |
| } |
| |
| err = snd_timer_info(timer, info); |
| if (err < 0) { |
| SNDERR("Can't get global timer info"); |
| return; |
| } |
| |
| snd_timer_params_set_auto_start(params, 1); |
| |
| resolution = snd_timer_info_get_resolution(info); |
| snd_timer_params_set_ticks(params, 1000000000 / resolution); |
| if (snd_timer_params_get_ticks(params) < 1) |
| snd_timer_params_set_ticks(params, 1); |
| |
| err = snd_timer_params(timer, params); |
| if (err < 0) { |
| SNDERR("Can't set global timer parameters"); |
| snd_timer_close(timer); |
| return; |
| } |
| |
| err = snd_async_add_timer_handler(&async, timer, a2dp_timer, NULL); |
| if (err < 0) { |
| SNDERR("Can't create global async callback"); |
| snd_timer_close(timer); |
| return; |
| } |
| |
| err = snd_timer_start(timer); |
| } |
| |
| static void a2dp_exit(void) |
| { |
| int err, i; |
| |
| err = snd_timer_stop(timer); |
| |
| err = snd_timer_close(timer); |
| |
| a2dp_lock(); |
| |
| for (i = 0; i < MAX_CONNECTIONS; i++) { |
| snd_pcm_a2dp_t *a2dp = connections[i]; |
| |
| if (a2dp) { |
| connections[i] = NULL; |
| a2dp_free(a2dp); |
| } |
| } |
| |
| a2dp_unlock(); |
| } |
| |
| SND_PCM_PLUGIN_DEFINE_FUNC(a2dp) |
| { |
| snd_pcm_a2dp_t *a2dp = NULL; |
| snd_config_iterator_t i, next; |
| bdaddr_t src, dst; |
| int err, n, pos = -1; |
| |
| DBG("name %s mode %d", name, mode); |
| |
| bacpy(&src, BDADDR_ANY); |
| bacpy(&dst, BDADDR_ANY); |
| |
| snd_config_for_each(i, next, conf) { |
| snd_config_t *n = snd_config_iterator_entry(i); |
| const char *id, *addr; |
| |
| if (snd_config_get_id(n, &id) < 0) |
| continue; |
| |
| if (!strcmp(id, "comment") || !strcmp(id, "type")) |
| continue; |
| |
| if (!strcmp(id, "bdaddr") || !strcmp(id, "dst")) { |
| if (snd_config_get_string(n, &addr) < 0) { |
| SNDERR("Invalid type for %s", id); |
| return -EINVAL; |
| } |
| str2ba(addr, &dst); |
| continue; |
| } |
| |
| if (!strcmp(id, "local") || !strcmp(id, "src")) { |
| if (snd_config_get_string(n, &addr) < 0) { |
| SNDERR("Invalid type for %s", id); |
| return -EINVAL; |
| } |
| str2ba(addr, &src); |
| continue; |
| } |
| |
| SNDERR("Unknown field %s", id); |
| return -EINVAL; |
| } |
| |
| a2dp_lock(); |
| |
| for (n = 0; n < MAX_CONNECTIONS; n++) { |
| if (connections[n]) { |
| if (!bacmp(&connections[n]->dst, &dst) && |
| (!bacmp(&connections[n]->src, &src) || |
| !bacmp(&src, BDADDR_ANY))) { |
| a2dp = connections[n]; |
| a2dp_get(a2dp); |
| break; |
| } |
| } else if (pos < 0) |
| pos = n; |
| } |
| |
| if (!a2dp) { |
| if (pos < 0) { |
| SNDERR("Too many connections"); |
| return -ENOMEM; |
| } |
| |
| a2dp = a2dp_alloc(); |
| if (!a2dp) { |
| SNDERR("Can't allocate"); |
| return -ENOMEM; |
| } |
| |
| connections[pos] = a2dp; |
| |
| a2dp->state = BT_CONNECT; |
| |
| bacpy(&a2dp->src, &src); |
| bacpy(&a2dp->dst, &dst); |
| } |
| |
| a2dp_unlock(); |
| |
| if (a2dp->state != BT_CONNECTED) { |
| err = a2dp_connect(a2dp); |
| if (err < 0) { |
| SNDERR("Can't connect"); |
| goto error; |
| } |
| |
| a2dp->state = BT_CONNECTED; |
| } |
| |
| a2dp->io.version = SND_PCM_IOPLUG_VERSION; |
| a2dp->io.name = "Bluetooth Advanced Audio Distribution"; |
| a2dp->io.mmap_rw = 0; |
| a2dp->io.callback = &a2dp_callback; |
| a2dp->io.private_data = a2dp; |
| |
| err = snd_pcm_ioplug_create(&a2dp->io, name, stream, mode); |
| if (err < 0) |
| goto error; |
| |
| err = a2dp_constraint(a2dp); |
| if (err < 0) { |
| snd_pcm_ioplug_delete(&a2dp->io); |
| goto error; |
| } |
| |
| *pcmp = a2dp->io.pcm; |
| return 0; |
| |
| error: |
| a2dp_put(a2dp); |
| |
| return err; |
| } |
| |
| SND_PCM_PLUGIN_SYMBOL(a2dp); |