/* * BlueALSA - aplay.c * SPDX-FileCopyrightText: 2016-2025 BlueALSA developers * SPDX-License-Identifier: MIT */ #if HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shared/dbus-client.h" #include "shared/dbus-client-pcm.h" #include "shared/defs.h" #include "shared/ffb.h" #include "shared/log.h" #include "shared/nv.h" #include "alsa-mixer.h" #include "alsa-pcm.h" #include "dbus.h" #include "delay-report.h" #if WITH_LIBSAMPLERATE # include "resampler.h" #endif /* Many devices cannot synchronize A/V with very high audio latency. To keep * the overall latency below 400ms we choose default ALSA parameters such that * the ALSA latency for A2DP is below 200ms. For SCO we choose to prioritize * much lower latency over audio quality. */ #define DEFAULT_PERIOD_TIME_A2DP 50000 #define DEFAULT_PERIOD_TIME_SCO 20000 #define DEFAULT_PERIODS 4 enum volume_type { VOL_TYPE_AUTO, VOL_TYPE_MIXER, VOL_TYPE_SOFTWARE, VOL_TYPE_NONE, }; struct io_worker { pthread_t thread; bool thread_started; /* thread-safety for worker data access */ pthread_mutex_t mutex; /* used BlueALSA PCM device */ struct ba_pcm ba_pcm; /* file descriptor of PCM FIFO */ int ba_pcm_fd; /* file descriptor of PCM control */ int ba_pcm_ctrl_fd; /* opened playback PCM device */ struct alsa_pcm alsa_pcm; /* mixer for volume control */ struct alsa_mixer alsa_mixer; /* if true, playback is active */ atomic_bool active; /* human-readable BT address */ char addr[18]; }; static unsigned int verbose = 0; static bool list_bt_devices = false; static bool list_bt_pcms = false; static const char *pcm_device = "default"; static enum volume_type volume_type = VOL_TYPE_AUTO; static const char *mixer_device = "default"; static const char *mixer_elem_name = "Master"; static unsigned int mixer_elem_index = 0; static bool ba_profile_a2dp = true; static bool ba_addr_any = false; static bdaddr_t *ba_addrs = NULL; static size_t ba_addrs_count = 0; static unsigned int pcm_buffer_time = 0; static unsigned int pcm_period_time = 0; #if WITH_LIBSAMPLERATE static enum resampler_converter_type resampler_method = RESAMPLER_CONV_NONE; #endif /* local PCM muted state for software mute */ static bool pcm_muted = false; static int bluez_major = 5; static int bluez_minor = 85; static struct ba_dbus_ctx dbus_ctx; static char dbus_ba_service[32] = BLUEALSA_SERVICE; static struct ba_pcm *ba_pcms = NULL; static size_t ba_pcms_count = 0; static pthread_mutex_t single_playback_mutex = PTHREAD_MUTEX_INITIALIZER; static bool force_single_playback = false; static pthread_rwlock_t workers_lock = PTHREAD_RWLOCK_INITIALIZER; static struct io_worker *workers[16] = { NULL }; static size_t workers_size = ARRAYSIZE(workers); static int main_loop_quit_event_fd = -1; static void main_loop_stop(int sig) { eventfd_write(main_loop_quit_event_fd, sig); } static int parse_bt_addresses(char *argv[], size_t count) { ba_addrs_count = count; if ((ba_addrs = malloc(sizeof(*ba_addrs) * ba_addrs_count)) == NULL) return -1; for (size_t i = 0; i < ba_addrs_count; i++) { if (str2ba(argv[i], &ba_addrs[i]) != 0) return errno = EINVAL, -1; if (bacmp(&ba_addrs[i], BDADDR_ANY) == 0) ba_addr_any = true; } return 0; } static const char * ba_pcm_get_profile_name(const struct ba_pcm * pcm) { switch (pcm->transport) { case BA_PCM_TRANSPORT_A2DP_SOURCE: case BA_PCM_TRANSPORT_A2DP_SINK: return "A2DP"; case BA_PCM_TRANSPORT_ASHA_SOURCE: case BA_PCM_TRANSPORT_ASHA_SINK: return "ASHA"; case BA_PCM_TRANSPORT_HFP_AG: case BA_PCM_TRANSPORT_HFP_HF: case BA_PCM_TRANSPORT_HSP_AG: case BA_PCM_TRANSPORT_HSP_HS: return "SCO"; default: error("Unknown transport: %#x", pcm->transport); return "[...]"; } } static const char * ba_pcm_get_profile_tag(const struct ba_pcm * pcm) { switch (pcm->transport) { case BA_PCM_TRANSPORT_A2DP_SOURCE: case BA_PCM_TRANSPORT_A2DP_SINK: return "a2dp"; case BA_PCM_TRANSPORT_ASHA_SOURCE: case BA_PCM_TRANSPORT_ASHA_SINK: return "asha"; case BA_PCM_TRANSPORT_HFP_AG: case BA_PCM_TRANSPORT_HFP_HF: case BA_PCM_TRANSPORT_HSP_AG: case BA_PCM_TRANSPORT_HSP_HS: return "sco"; default: error("Unknown transport: %#x", pcm->transport); return "unknown"; } } static snd_pcm_format_t bluealsa_get_snd_pcm_format(const struct ba_pcm *pcm) { switch (pcm->format) { case 0x0108: return SND_PCM_FORMAT_U8; case 0x8210: return SND_PCM_FORMAT_S16_LE; case 0x8318: return SND_PCM_FORMAT_S24_3LE; case 0x8418: return SND_PCM_FORMAT_S24_LE; case 0x8420: return SND_PCM_FORMAT_S32_LE; default: error("Unknown PCM format: %#x", pcm->format); return SND_PCM_FORMAT_UNKNOWN; } } static void print_bt_device_list(void) { static const struct { const char * label; unsigned int mode; } section[2] = { { "**** List of PLAYBACK Bluetooth Devices ****", BA_PCM_MODE_SINK }, { "**** List of CAPTURE Bluetooth Devices ****", BA_PCM_MODE_SOURCE }, }; const char * tmp; size_t ii; for (size_t i = 0; i < ARRAYSIZE(section); i++) { printf("%s\n", section[i].label); for (ii = 0, tmp = ""; ii < ba_pcms_count; ii++) { struct ba_pcm * pcm = &ba_pcms[ii]; struct bluez_device dev = { 0 }; if (!(pcm->mode == section[i].mode)) continue; if (strcmp(pcm->device_path, tmp) != 0) { tmp = ba_pcms[ii].device_path; DBusError err = DBUS_ERROR_INIT; if (dbus_bluez_get_device(dbus_ctx.conn, pcm->device_path, &dev, &err) == -1) { warn("Couldn't get BlueZ device properties: %s", err.message); dbus_error_free(&err); } char bt_addr[18]; ba2str(&dev.bt_addr, bt_addr); printf("%s: %s [%s], %s%s\n", dev.hci_name, bt_addr, dev.name, dev.trusted ? "trusted " : "", dev.icon); } printf(" %s (%s): %s %d channel%s %d Hz\n", ba_pcm_get_profile_name(pcm), pcm->codec.name, snd_pcm_format_name(bluealsa_get_snd_pcm_format(pcm)), pcm->channels, pcm->channels != 1 ? "s" : "", pcm->rate); } } } static void print_bt_pcm_list(void) { DBusError err = DBUS_ERROR_INIT; struct bluez_device dev = { 0 }; const char * tmp = ""; for (size_t i = 0; i < ba_pcms_count; i++) { struct ba_pcm * pcm = &ba_pcms[i]; if (strcmp(pcm->device_path, tmp) != 0) { tmp = ba_pcms[i].device_path; if (dbus_bluez_get_device(dbus_ctx.conn, pcm->device_path, &dev, &err) == -1) { warn("Couldn't get BlueZ device properties: %s", err.message); dbus_error_free(&err); } } char bt_addr[18]; ba2str(&dev.bt_addr, bt_addr); printf( "bluealsa:DEV=%s,PROFILE=%s,SRV=%s\n" " %s, %s%s, %s\n" " %s (%s): %s %d channel%s %d Hz\n", bt_addr, ba_pcm_get_profile_tag(pcm), dbus_ba_service, dev.name, dev.trusted ? "trusted " : "", dev.icon, pcm->mode == BA_PCM_MODE_SINK ? "playback" : "capture", ba_pcm_get_profile_name(pcm), pcm->codec.name, snd_pcm_format_name(bluealsa_get_snd_pcm_format(pcm)), pcm->channels, pcm->channels != 1 ? "s" : "", pcm->rate); } } static struct ba_pcm *ba_pcm_add(const struct ba_pcm *pcm) { struct ba_pcm *tmp; if ((tmp = realloc(ba_pcms, (ba_pcms_count + 1) * sizeof(*ba_pcms))) == NULL) return NULL; ba_pcms = tmp; memcpy(&ba_pcms[ba_pcms_count], pcm, sizeof(*ba_pcms)); return &ba_pcms[ba_pcms_count++]; } static struct ba_pcm *ba_pcm_get(const char *path) { for (size_t i = 0; i < ba_pcms_count; i++) if (strcmp(ba_pcms[i].pcm_path, path) == 0) return &ba_pcms[i]; return NULL; } static void ba_pcm_remove(const char *path) { for (size_t i = 0; i < ba_pcms_count; i++) if (strcmp(ba_pcms[i].pcm_path, path) == 0) { memmove(&ba_pcms[i], &ba_pcms[i + 1], (ba_pcms_count - i - 1) * sizeof(*ba_pcms)); ba_pcms_count--; break; } } static struct io_worker *get_active_io_worker(void) { pthread_rwlock_rdlock(&workers_lock); struct io_worker *w = NULL; for (size_t i = 0; i < workers_size; i++) if (workers[i] != NULL && workers[i]->active) { w = workers[i]; break; } pthread_rwlock_unlock(&workers_lock); return w; } static int get_bluez_version_numbers(int *major, int *minor) { char buffer[64]; FILE *fp; fp = popen("/usr/libexec/bluetooth/bluetoothd -v", "r"); if (fp == NULL) { perror("Failed to run /usr/libexec/bluetooth/bluetoothd"); return -1; } if (fgets(buffer, sizeof(buffer), fp) != NULL) { if (sscanf(buffer, "%d.%d", major, minor) >= 2) { pclose(fp); return 0; } } pclose(fp); return -1; } static int pause_device_player(const struct ba_pcm *ba_pcm) { DBusMessage *msg = NULL, *rep = NULL; DBusError err = DBUS_ERROR_INIT; char path[160]; int ret = 0; if (bluez_major >= 5 && bluez_minor >= 80) snprintf(path, sizeof(path), "%s/avrcp/player0", ba_pcm->device_path); else snprintf(path, sizeof(path), "%s/player0", ba_pcm->device_path); msg = dbus_message_new_method_call("org.bluez", path, "org.bluez.MediaPlayer1", "Pause"); if ((rep = dbus_connection_send_with_reply_and_block(dbus_ctx.conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err)) == NULL) { warn("Couldn't pause player: %s", err.message); dbus_error_free(&err); goto fail; } debug("Requested playback pause"); goto final; fail: ret = -1; final: if (msg != NULL) dbus_message_unref(msg); if (rep != NULL) dbus_message_unref(rep); return ret; } /** * Update BlueALSA PCM volume according to ALSA mixer element. */ static int io_worker_mixer_volume_sync_ba_pcm( struct io_worker *worker, struct ba_pcm *ba_pcm) { unsigned int volume; /* If mixer element does not support playback switch, * use our global muted state as a default value. */ bool muted = pcm_muted; const int vmax = BA_PCM_VOLUME_MAX(ba_pcm); if (alsa_mixer_get_volume(&worker->alsa_mixer, vmax, &volume, &muted) != 0) return -1; for (size_t i = 0; i < ba_pcm->channels; i++) { ba_pcm->volume[i].muted = muted; ba_pcm->volume[i].volume = volume; } DBusError err = DBUS_ERROR_INIT; if (!ba_dbus_pcm_update(&dbus_ctx, ba_pcm, BLUEALSA_PCM_VOLUME, &err)) { error("Couldn't update BlueALSA source PCM: %s", err.message); dbus_error_free(&err); return -1; } return 0; } /** * Update ALSA mixer element according to BlueALSA PCM volume. */ static int io_worker_mixer_volume_sync_alsa_mixer( struct io_worker *worker, struct ba_pcm *ba_pcm) { /* Skip update in case of software volume. */ if (ba_pcm->soft_volume) return 0; int ret = 0; /* This function is called by the D-Bus signal handler, so we have to * make sure that we will not have any interference from the IO thread * trying to modify ALSA mixer at the same time. */ pthread_mutex_lock(&worker->mutex); if (!alsa_mixer_is_open(&worker->alsa_mixer)) goto final; /* User can connect BlueALSA PCM to mono, stereo or multi-channel output. * For mono input (audio from BlueALSA PCM), case case is simple: we are * changing all output channels at once. However, for stereo input it is * not possible to know how to control left/right volume unless there is * some kind of channel mapping. In order to simplify things, we will set * all channels to the average left-right volume. */ unsigned int volume_sum = 0, muted = 0; for (size_t i = 0; i < ba_pcm->channels; i++) { volume_sum += ba_pcm->volume[i].volume; muted |= ba_pcm->volume[i].muted; } /* keep local muted state up to date */ pcm_muted = muted; const unsigned int vmax = BA_PCM_VOLUME_MAX(ba_pcm); const unsigned int volume = volume_sum / ba_pcm->channels; ret = alsa_mixer_set_volume(&worker->alsa_mixer, vmax, volume, muted); final: pthread_mutex_unlock(&worker->mutex); return ret; } static void io_worker_mixer_event_callback(void *data) { struct io_worker *worker = data; io_worker_mixer_volume_sync_ba_pcm(worker, &worker->ba_pcm); } static snd_pcm_uframes_t io_worker_playback_delay( const struct io_worker *w, #if WITH_LIBSAMPLERATE const struct resampler *resampler, const ffb_t *resampler_buffer, #endif const ffb_t *read_buffer) { snd_pcm_uframes_t delay = 0; double rate_ratio = 1.0; unsigned int ba_pcm_buffered = 0; /* Get the delay due to BlueALSA PCM FIFO buffering. */ ioctl(w->ba_pcm_fd, FIONREAD, &ba_pcm_buffered); delay += ba_pcm_buffered / read_buffer->size / w->ba_pcm.channels; /* Get the delay accumulated in the read buffer. */ delay += ffb_len_out(read_buffer) / w->ba_pcm.channels; #if WITH_LIBSAMPLERATE if (resampler != NULL) { rate_ratio = resampler_current_rate_ratio(resampler); /* Get the delay accumulated in the resampler. */ delay += ffb_len_out(resampler_buffer) / w->ba_pcm.channels / rate_ratio; } #endif /* Get the ALSA device delay. In case when resampler is used, convert * ALSA device sample rate to the BlueALSA PCM sample rate. */ delay += w->alsa_pcm.delay / rate_ratio; return delay; } static void io_worker_routine_exit(struct io_worker *w) { pthread_mutex_lock(&w->mutex); if (w->ba_pcm_fd != -1) { close(w->ba_pcm_fd); w->ba_pcm_fd = -1; } if (w->ba_pcm_ctrl_fd != -1) { close(w->ba_pcm_ctrl_fd); w->ba_pcm_ctrl_fd = -1; } alsa_pcm_close(&w->alsa_pcm); alsa_mixer_close(&w->alsa_mixer); debug("Exiting IO worker %s", w->addr); pthread_mutex_unlock(&w->mutex); } static void *io_worker_routine(struct io_worker *w) { const snd_pcm_format_t pcm_format = bluealsa_get_snd_pcm_format(&w->ba_pcm); const ssize_t pcm_format_size = snd_pcm_format_size(pcm_format, 1); const size_t pcm_1s_samples = w->ba_pcm.rate * w->ba_pcm.channels; /* Buffer for audio frames read from the BlueALSA server. */ ffb_t read_buffer = { 0 }; /* Buffer from which audio frames are written to the ALSA PCM. If the * resampler is used then this is the resampler output buffer. Otherwise * it is the same as the read buffer. */ ffb_t *write_buffer = &read_buffer; /* Preferred format for the ALSA PCM. If not using the resampler then this * is the format of the incoming BlueALSA stream. */ snd_pcm_format_t format_1 = pcm_format; /* Alternative format that can be generated internally by the resampler. * This is only used if the resampler is enabled. */ snd_pcm_format_t format_2 = SND_PCM_FORMAT_UNKNOWN; #if WITH_LIBSAMPLERATE /* The resampler requires the native endian format for the input data. */ const snd_pcm_format_t resampler_pcm_format = resampler_native_endian_format(pcm_format); struct resampler resampler = { 0 }; ffb_t resampled_buffer = { 0 }; /* For detecting when the ALSA device has auto-started after reaching its * start threshold. */ bool alsa_pcm_started = false; bool use_resampler = false; #endif int pcm_flags = 0; /* Cancellation should be possible only in the carefully selected place * in order to prevent memory leaks and resources not being released. */ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); pthread_cleanup_push(PTHREAD_CLEANUP(io_worker_routine_exit), w); pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &read_buffer); #if WITH_LIBSAMPLERATE pthread_cleanup_push(PTHREAD_CLEANUP(resampler_free), &resampler); pthread_cleanup_push(PTHREAD_CLEANUP(ffb_free), &resampled_buffer); #endif /* Create a buffer big enough to hold enough PCM data for three periods. * This will be later be revised if necessary to match the actual ALSA * start threshold when the ALSA PCM is opened. */ const size_t nmemb = ((size_t)pcm_period_time * 3 / 1000) * (pcm_1s_samples / 1000); if (ffb_init(&read_buffer, nmemb, pcm_format_size) == -1) { error("Couldn't create PCM buffer: %s", strerror(errno)); goto fail; } DBusError err = DBUS_ERROR_INIT; /* initialize the PCM soft_volume setting */ if (volume_type != VOL_TYPE_AUTO) { bool softvol = (volume_type == VOL_TYPE_SOFTWARE); debug("Setting BlueALSA source PCM volume mode: %s: %s", w->ba_pcm.pcm_path, softvol ? "software" : "pass-through"); if (softvol != w->ba_pcm.soft_volume) { w->ba_pcm.soft_volume = softvol; if (!ba_dbus_pcm_update(&dbus_ctx, &w->ba_pcm, BLUEALSA_PCM_SOFT_VOLUME, &err)) { error("Couldn't set BlueALSA source PCM volume mode: %s", err.message); dbus_error_free(&err); goto fail; } } } debug("Opening BlueALSA source PCM: %s", w->ba_pcm.pcm_path); if (!ba_dbus_pcm_open(&dbus_ctx, w->ba_pcm.pcm_path, &w->ba_pcm_fd, &w->ba_pcm_ctrl_fd, &err)) { error("Couldn't open BlueALSA source PCM: %s", err.message); dbus_error_free(&err); goto fail; } #if WITH_LIBSAMPLERATE if (resampler_method != RESAMPLER_CONV_NONE) { if (!resampler_is_input_format_supported(pcm_format)) warn("Resampler not enabled: Unsupported input format: %s", snd_pcm_format_name(pcm_format)); else { use_resampler = true; /* Disable alsa-lib resampling because our internal resampler can * convert to a rate natively supported by the sound card. Initially * also disable alsa-lib format conversion to try to use a format * generated by the resampler. If that fails, then enable alsa-lib * format conversion and have the resampler output its preferred * FLOAT format. In this way the number of format conversions in the * processing chain is minimized. */ pcm_flags = SND_PCM_NO_AUTO_RESAMPLE | SND_PCM_NO_AUTO_FORMAT; /* The resampler uses FLOAT internally, so prefer that if the ALSA * device supports it, to avoid the need to convert. Otherwise, use * the resampler native endian integer format - input format. */ format_1 = resampler_preferred_output_format(); format_2 = resampler_pcm_format; } } #endif /* Track the lock state of the single playback mutex within this thread. */ bool single_playback_mutex_locked = false; /* Intervals in seconds between consecutive PCM open retry attempts. */ const unsigned int pcm_open_retry_intervals[] = { 1, 1, 2, 3, 5 }; size_t pcm_open_retry_pcm_samples = 0; size_t pcm_open_retries = 0; struct delay_report dr; delay_report_init(&dr, &dbus_ctx, &w->ba_pcm); size_t pause_retry_pcm_samples = pcm_1s_samples; size_t pause_retries = 0; int timeout = -1; debug("Starting IO loop"); for (;;) { if (single_playback_mutex_locked) { pthread_mutex_unlock(&single_playback_mutex); single_playback_mutex_locked = false; } struct pollfd fds[16] = { { main_loop_quit_event_fd, POLLIN, 0 }, { w->ba_pcm_fd, POLLIN, 0 }}; nfds_t nfds = 2; if (alsa_mixer_is_open(&w->alsa_mixer)) { nfds += alsa_mixer_poll_descriptors_count(&w->alsa_mixer); if (nfds <= ARRAYSIZE(fds)) alsa_mixer_poll_descriptors(&w->alsa_mixer, fds + 2, nfds - 2); else { error("Poll FD array size exceeded: %zu > %zu", nfds, ARRAYSIZE(fds)); goto fail; } } /* Reading from the FIFO won't block unless there is an open connection * on the writing side. However, the server does not open PCM FIFO until * a transport is created. With the A2DP, the transport is created when * some clients (BT device) requests audio transfer. */ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); int poll_rv = poll(fds, nfds, timeout); pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); pthread_mutex_lock(&w->mutex); /* Check the PCM running status on every iteration. */ bool ba_pcm_running = w->ba_pcm.running; pthread_mutex_unlock(&w->mutex); if (poll_rv == -1) { if (errno == EINTR) continue; error("IO loop poll error: %s", strerror(errno)); goto fail; } if (poll_rv == 0 && ba_pcm_running && w->active && ffb_blen_out(&read_buffer) == 0 && !alsa_pcm_is_running(&w->alsa_pcm)) { /* The BT device is in the running state, but is not sending audio * frames. As there is no work for the ALSA device to do we simply * wait for more audio to arrive from the server. */ timeout = -1; continue; } if (fds[0].revents & POLLIN) break; if (alsa_mixer_is_open(&w->alsa_mixer)) alsa_mixer_handle_events(&w->alsa_mixer); size_t read_samples = 0; if (fds[1].revents & POLLIN) { /* If the read buffer is full then we have an overrun. We must * discard audio frames in order to continue reading fresh data * from the server. */ if (ffb_blen_in(&read_buffer) == 0) { unsigned int buffered = 0; ioctl(w->ba_pcm_fd, FIONREAD, &buffered); const size_t discard_bytes = MIN(buffered, ffb_blen_out(&read_buffer)); const size_t discard_samples = discard_bytes / pcm_format_size; ffb_shift(&read_buffer, discard_samples); if (alsa_pcm_is_open(&w->alsa_pcm)) warn("Dropping PCM frames: %zu", discard_samples / w->ba_pcm.channels); } ssize_t ret; if ((ret = read(w->ba_pcm_fd, read_buffer.tail, ffb_blen_in(&read_buffer))) == -1) { if (errno == EINTR) continue; error("BlueALSA source PCM read error: %s", strerror(errno)); goto fail; } read_samples = ret / pcm_format_size; if (ret % pcm_format_size != 0) warn("Invalid read from BlueALSA source PCM: %zd %% %zd != 0", ret, pcm_format_size); #if WITH_LIBSAMPLERATE if (use_resampler) resampler_convert_to_native_endian_format(read_buffer.tail, read_samples, pcm_format); #endif ffb_seek(&read_buffer, read_samples); } else if (fds[1].revents & POLLHUP) { /* Source PCM FIFO has been terminated on the writing side. */ debug("BlueALSA source PCM disconnected: %s", w->ba_pcm.pcm_path); ba_pcm_running = false; break; } else if (fds[1].revents) { error("Unexpected BlueALSA source PCM poll event: %#x", fds[1].revents); } /* If current worker is not active and the single playback mode was * enabled, we have to check if there is any other active worker. */ if (!w->active) { /* Before checking active worker, we need to lock the single playback * mutex. It is required to lock it, because the active state is changed * in the worker thread after opening the PCM device, so we have to * synchronize all threads at this point. */ pthread_mutex_lock(&single_playback_mutex); single_playback_mutex_locked = true; if (get_active_io_worker() != NULL) { /* In order not to flood BT connection with AVRCP packets, * we are going to send pause command every 0.5 second. */ if (pause_retries < 5 && (pause_retry_pcm_samples += read_samples) > pcm_1s_samples / 2) { if (pause_device_player(&w->ba_pcm) == -1) /* pause command does not work, stop further requests */ pause_retries = 5; pause_retry_pcm_samples = 0; pause_retries++; timeout = 100; } continue; } } if (!alsa_pcm_is_open(&w->alsa_pcm)) { if (pcm_open_retries > 0) { /* After PCM open failure wait some time before retry. This can not be * done with a sleep() call, because we have to drain PCM FIFO, so it * will not have any stale data. */ unsigned int interval = pcm_open_retries > ARRAYSIZE(pcm_open_retry_intervals) ? pcm_open_retry_intervals[ARRAYSIZE(pcm_open_retry_intervals) - 1] : pcm_open_retry_intervals[pcm_open_retries - 1]; if ((pcm_open_retry_pcm_samples += read_samples) <= interval * pcm_1s_samples) continue; } debug("Opening ALSA playback PCM: name=%s channels=%u rate%s=%u", pcm_device, w->ba_pcm.channels, pcm_flags & SND_PCM_NO_AUTO_RESAMPLE ? "~" : "", w->ba_pcm.rate); char *tmp = NULL; int res = alsa_pcm_open(&w->alsa_pcm, pcm_device, format_1, format_2, w->ba_pcm.channels, w->ba_pcm.rate, pcm_buffer_time, pcm_period_time, pcm_flags, &tmp); switch (res) { case 0: break; case -EINVAL: #if WITH_LIBSAMPLERATE /* If the PCM failed to open because the sound card does not * natively support either the float or integer formats of the * resampler, then try again but this time with alsa-lib format * conversion enabled. */ if (use_resampler && w->alsa_pcm.format == SND_PCM_FORMAT_UNKNOWN) { free(tmp); if (alsa_pcm_open(&w->alsa_pcm, pcm_device, format_1, format_2, w->ba_pcm.channels, w->ba_pcm.rate, pcm_buffer_time, pcm_period_time, pcm_flags & ~SND_PCM_NO_AUTO_FORMAT, &tmp) == 0) { break; } } #endif /* fall-through */ default: warn("Couldn't open ALSA playback PCM: %s", tmp); pcm_open_retry_pcm_samples = 0; pcm_open_retries++; free(tmp); continue; } /* Resize the read buffer to ensure it is not less than the * ALSA start threshold. This is to ensure that the PCM re-starts * quickly after an overrun. */ if (w->alsa_pcm.start_threshold > read_buffer.nmemb / w->ba_pcm.channels) ffb_init(&read_buffer, w->alsa_pcm.start_threshold * w->ba_pcm.channels, read_buffer.size); #if WITH_LIBSAMPLERATE if (use_resampler) { /* Initialize the resampler which will try to keep the playback delay * within configured limits. The lower and upper limits are set to * the ALSA start threshold and the start threshold plus the period * size respectively. The resampler will adapt the rate to keep the * delay within these limits. */ if ((res = resampler_init( &resampler, resampler_method, w->ba_pcm.channels, resampler_pcm_format, w->ba_pcm.rate, w->alsa_pcm.format, w->alsa_pcm.rate, w->alsa_pcm.start_threshold, w->alsa_pcm.start_threshold + w->alsa_pcm.period_frames)) == -1) { error("Couldn't initialize resampler: %s", strerror(errno)); goto fail; } #if DEBUG if (verbose >= 4) debug("PCM sample rate conversion: %u Hz -> %#.2f Hz", w->ba_pcm.rate, w->ba_pcm.rate * resampler_current_rate_ratio(&resampler)); #endif /* The resampler output buffer is sized to accommodate the * result of resampling a full read_buffer, plus a little extra * to allow for positive adaptive resampling adjustment. */ size_t buffer_size = read_buffer.nmemb * w->alsa_pcm.rate / w->ba_pcm.rate; buffer_size = (buffer_size * 110) / 100; ffb_init(&resampled_buffer, buffer_size, snd_pcm_format_size(w->alsa_pcm.format, 1)); write_buffer = &resampled_buffer; } #endif /* Skip mixer setup in case of software volume. */ if (mixer_device != NULL && !w->ba_pcm.soft_volume) { pthread_mutex_lock(&w->mutex); debug("Opening ALSA mixer: name=%s elem=%s index=%u", mixer_device, mixer_elem_name, mixer_elem_index); if (alsa_mixer_open(&w->alsa_mixer, mixer_device, mixer_elem_name, mixer_elem_index, &tmp) == 0) io_worker_mixer_volume_sync_ba_pcm(w, &w->ba_pcm); else { warn("Couldn't open ALSA mixer: %s", tmp); free(tmp); } pthread_mutex_unlock(&w->mutex); } /* Reset retry counters. */ pcm_open_retry_pcm_samples = 0; pcm_open_retries = 0; /* Reset moving delay window buffer. */ delay_report_reset(&dr); if (verbose >= 2) { info("Used configuration for %s:\n" " BlueALSA PCM format: %s\n" " BlueALSA PCM sample rate: %u Hz\n" " BlueALSA PCM channels: %u\n" " ALSA PCM buffer time: %u us (%zu bytes)\n" " ALSA PCM period time: %u us (%zu bytes)\n" " ALSA PCM format: %s\n" " ALSA PCM sample rate: %u Hz\n" " ALSA PCM channels: %u\n" " ALSA mixer volume mapping: %s", w->addr, snd_pcm_format_name(pcm_format), w->ba_pcm.rate, w->ba_pcm.channels, w->alsa_pcm.buffer_time, alsa_pcm_frames_to_bytes(&w->alsa_pcm, w->alsa_pcm.buffer_frames), w->alsa_pcm.period_time, alsa_pcm_frames_to_bytes(&w->alsa_pcm, w->alsa_pcm.period_frames), snd_pcm_format_name(w->alsa_pcm.format), w->alsa_pcm.rate, w->alsa_pcm.channels, w->alsa_mixer.mixer ? (w->alsa_mixer.has_db_scale ? "dB scale" : "linear") : "none"); } if (verbose >= 3) alsa_pcm_dump(&w->alsa_pcm, stderr); } /* Mark device as active. */ w->active = true; /* Current worker was marked as active, so we can safely * release the single playback mutex if it was locked. */ if (single_playback_mutex_locked) { pthread_mutex_unlock(&single_playback_mutex); single_playback_mutex_locked = false; } if (!w->alsa_mixer.has_mute_switch && pcm_muted) { snd_pcm_format_t format = w->alsa_pcm.format; #if WITH_LIBSAMPLERATE if (use_resampler) format = resampler_pcm_format; #endif snd_pcm_format_set_silence(format, read_buffer.data, ffb_len_out(&read_buffer)); } #if WITH_LIBSAMPLERATE if (use_resampler && resampler_process(&resampler, &read_buffer, write_buffer) != 0) goto close_alsa; #endif if (alsa_pcm_write(&w->alsa_pcm, write_buffer, !ba_pcm_running) < 0) goto close_alsa; if (!ba_pcm_running) goto device_inactive; /* Set the poll() timeout such that this thread is always woken before * an ALSA underrun can occur. */ if (alsa_pcm_is_running(&w->alsa_pcm)) { timeout = 1000 * w->alsa_pcm.hw_avail / w->alsa_pcm.rate; /* poll() timeouts may be late because of the kernel scheduler and * workload, and there may be additional processing delays before * we can write to the ALSA PCM again. So we allow for this by setting * the timeout value 5ms before the underrun deadline. */ if ((timeout -= 5) < 0) timeout = 0; } const snd_pcm_uframes_t delay_frames = io_worker_playback_delay(w, #if WITH_LIBSAMPLERATE use_resampler ? &resampler : NULL, &resampled_buffer, #endif &read_buffer); if (!delay_report_update(&dr, delay_frames, &err)) { error("Couldn't update BlueALSA PCM client delay: %s", err.message); dbus_error_free(&err); goto fail; } #if WITH_LIBSAMPLERATE if (use_resampler) { bool rate_changed = false; if (w->alsa_pcm.underrun) { debug("Resetting resampler"); resampler_reset(&resampler); rate_changed = true; } if (!alsa_pcm_is_running(&w->alsa_pcm)) alsa_pcm_started = false; else { if (!alsa_pcm_started) { /* The ALSA start threshold has been reached, so reset the * resampler to initialize adaptive resampling. */ resampler_reset(&resampler); alsa_pcm_started = true; } rate_changed = resampler_update_rate_ratio(&resampler, read_samples / w->ba_pcm.channels, dr.avg_value); } if (verbose >= 4 && rate_changed) debug("PCM sample rate conversion: %u Hz -> %#.2f Hz", w->ba_pcm.rate, w->ba_pcm.rate * resampler_current_rate_ratio(&resampler)); } #endif continue; device_inactive: debug("BT device marked as inactive: %s", w->addr); pause_retry_pcm_samples = pcm_1s_samples; pause_retries = 0; timeout = -1; close_alsa: ffb_rewind(&read_buffer); #if WITH_LIBSAMPLERATE if (use_resampler) { ffb_rewind(&resampled_buffer); resampler_free(&resampler); } #endif pthread_mutex_lock(&w->mutex); alsa_pcm_close(&w->alsa_pcm); alsa_mixer_close(&w->alsa_mixer); pthread_mutex_unlock(&w->mutex); w->active = !force_single_playback; } fail: #if WITH_LIBSAMPLERATE pthread_cleanup_pop(1); pthread_cleanup_pop(1); #endif pthread_cleanup_pop(1); pthread_cleanup_pop(1); return NULL; } static bool pcm_hw_params_equal( const struct ba_pcm *ba_pcm_1, const struct ba_pcm *ba_pcm_2) { if (ba_pcm_1->format != ba_pcm_2->format) return false; if (ba_pcm_1->channels != ba_pcm_2->channels) return false; if (ba_pcm_1->rate != ba_pcm_2->rate) return false; return true; } static void io_worker_stop(struct io_worker *w) { if (w->thread_started) { pthread_cancel(w->thread); pthread_join(w->thread, NULL); w->thread_started = false; } } /** * Stop the IO worker thread and free its resources. */ static void io_worker_destroy(struct io_worker *w) { io_worker_stop(w); pthread_mutex_destroy(&w->mutex); free(w); } static struct io_worker *supervise_io_worker_start(const struct ba_pcm *ba_pcm) { struct io_worker *worker = NULL; char addr[sizeof(worker->addr)]; ssize_t worker_slot = -1; for (size_t i = 0; i < workers_size; i++) { if (workers[i] == NULL) { if (worker_slot == -1) worker_slot = i; } else if (strcmp(workers[i]->ba_pcm.pcm_path, ba_pcm->pcm_path) == 0) { /* If the codec has changed after the device connected, then the * audio format may have changed. If it has, the worker thread * needs to be restarted. Otherwise, update the running state. */ if (!pcm_hw_params_equal(&workers[i]->ba_pcm, ba_pcm)) { io_worker_stop(workers[i]); worker = workers[i]; worker_slot = i; break; } else { pthread_mutex_lock(&workers[i]->mutex); workers[i]->ba_pcm.running = ba_pcm->running; pthread_mutex_unlock(&workers[i]->mutex); return workers[i]; } } } if (worker == NULL) { /* Human-readable BT address for early error reporting. */ ba2str(&ba_pcm->addr, addr); if (worker_slot == -1) { error("Couldn't start IO worker %s: %s", addr, "No empty slots"); return NULL; } if ((worker = malloc(sizeof(struct io_worker))) == NULL) { error("Couldn't start IO worker %s: %s", addr, strerror(errno)); return NULL; } worker->thread_started = false; pthread_mutex_init(&worker->mutex, NULL); strcpy(worker->addr, addr); worker->ba_pcm_fd = -1; worker->ba_pcm_ctrl_fd = -1; } /* Synchronize access to the worker data, which we are about * to modify, with other IO worker threads. */ pthread_rwlock_wrlock(&workers_lock); memcpy(&worker->ba_pcm, ba_pcm, sizeof(worker->ba_pcm)); alsa_pcm_init(&worker->alsa_pcm); alsa_mixer_init(&worker->alsa_mixer, io_worker_mixer_event_callback, worker); worker->active = !force_single_playback; debug("Starting IO worker %s", worker->addr); if ((errno = pthread_create(&worker->thread, NULL, PTHREAD_FUNC(io_worker_routine), worker)) == 0) worker->thread_started = true; else { error("Couldn't start IO worker %s: %s", worker->addr, strerror(errno)); io_worker_destroy(worker); worker = NULL; } workers[worker_slot] = worker; pthread_rwlock_unlock(&workers_lock); return worker; } static struct io_worker *supervise_io_worker_stop(const struct ba_pcm *ba_pcm) { pthread_rwlock_wrlock(&workers_lock); for (size_t i = 0; i < workers_size; i++) if (workers[i] && strcmp(workers[i]->ba_pcm.pcm_path, ba_pcm->pcm_path) == 0) { io_worker_destroy(workers[i]); workers[i] = NULL; break; } pthread_rwlock_unlock(&workers_lock); return NULL; } static struct io_worker *supervise_io_worker(const struct ba_pcm *ba_pcm) { if (ba_pcm == NULL) return NULL; if (ba_pcm->mode != BA_PCM_MODE_SOURCE) goto stop; if ((ba_profile_a2dp && !(ba_pcm->transport & BA_PCM_TRANSPORT_MASK_A2DP)) || (!ba_profile_a2dp && !(ba_pcm->transport & BA_PCM_TRANSPORT_MASK_SCO))) goto stop; /* check whether SCO has selected codec */ if (ba_pcm->transport & BA_PCM_TRANSPORT_MASK_SCO && ba_pcm->rate == 0) { debug("Skipping SCO with codec not selected"); goto stop; } if (ba_addr_any) goto start; for (size_t i = 0; i < ba_addrs_count; i++) if (bacmp(&ba_addrs[i], &ba_pcm->addr) == 0) goto start; stop: return supervise_io_worker_stop(ba_pcm); start: return supervise_io_worker_start(ba_pcm); } static DBusHandlerResult dbus_signal_handler(DBusConnection *conn, DBusMessage *message, void *data) { (void)conn; (void)data; if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_SIGNAL) return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; const char *path = dbus_message_get_path(message); const char *interface = dbus_message_get_interface(message); const char *signal = dbus_message_get_member(message); DBusMessageIter iter; struct io_worker *worker; if (strcmp(interface, DBUS_INTERFACE_OBJECT_MANAGER) == 0) { if (strcmp(signal, "InterfacesAdded") == 0) { if (!dbus_message_iter_init(message, &iter)) goto fail; struct ba_pcm pcm; DBusError err = DBUS_ERROR_INIT; if (!dbus_message_iter_get_ba_pcm(&iter, &err, &pcm)) { error("Couldn't add new BlueALSA PCM: %s", err.message); dbus_error_free(&err); goto fail; } if (pcm.transport == BA_PCM_TRANSPORT_NONE) goto fail; if (ba_pcm_add(&pcm) == NULL) { error("Couldn't add new BlueALSA PCM: %s", strerror(errno)); goto fail; } supervise_io_worker(&pcm); return DBUS_HANDLER_RESULT_HANDLED; } if (strcmp(signal, "InterfacesRemoved") == 0) { if (!dbus_message_iter_init(message, &iter) || dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) { error("Couldn't remove BlueALSA PCM: %s", "Invalid signal signature"); goto fail; } dbus_message_iter_get_basic(&iter, &path); struct ba_pcm *pcm; if ((pcm = ba_pcm_get(path)) == NULL) goto fail; supervise_io_worker_stop(pcm); ba_pcm_remove(path); return DBUS_HANDLER_RESULT_HANDLED; } } if (strcmp(interface, DBUS_INTERFACE_PROPERTIES) == 0) { if (dbus_message_iter_init(message, &iter) && dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) { const char *prop_interface; dbus_message_iter_get_basic(&iter, &prop_interface); if (strcmp(prop_interface, "org.bluez.MediaPlayer1") == 0) { dbus_message_iter_next(&iter); if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY) { DBusMessageIter dict_iter, entry_iter, variant_iter; dbus_message_iter_recurse(&iter, &dict_iter); const char *status = NULL; while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) { dbus_message_iter_recurse(&dict_iter, &entry_iter); const char *key; dbus_message_iter_get_basic(&entry_iter, &key); dbus_message_iter_next(&entry_iter); dbus_message_iter_recurse(&entry_iter, &variant_iter); if (strcmp(key, "Status") == 0 && dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_STRING) { dbus_message_iter_get_basic(&variant_iter, &status); break; } dbus_message_iter_next(&dict_iter); } if (status != NULL && strcmp(status, "playing") == 0) { const char *signal_path = dbus_message_get_path(message); const char *suffix = (bluez_major >= 5 && bluez_minor >= 80) ? "/avrcp/player0" : "/player0"; size_t path_len = strlen(signal_path); size_t suffix_len = strlen(suffix); if (path_len > suffix_len && strcmp(signal_path + path_len - suffix_len, suffix) == 0) { const size_t dev_len = path_len - suffix_len; char device_path[256]; if (dev_len > 0 && dev_len < sizeof(device_path)) { memcpy(device_path, signal_path, dev_len); device_path[dev_len] = '\0'; pthread_rwlock_rdlock(&workers_lock); for (size_t i = 0; i < workers_size; i++) { if (workers[i] == NULL) continue; pthread_mutex_lock(&workers[i]->mutex); const bool is_running = workers[i]->ba_pcm.running; const bool is_self = strcmp( workers[i]->ba_pcm.device_path, device_path) == 0; struct ba_pcm pcm_copy = workers[i]->ba_pcm; pthread_mutex_unlock(&workers[i]->mutex); if (is_running && !is_self) pause_device_player(&pcm_copy); } pthread_rwlock_unlock(&workers_lock); } } } } return DBUS_HANDLER_RESULT_HANDLED; } } struct ba_pcm *pcm; if ((pcm = ba_pcm_get(path)) == NULL) goto fail; if (!dbus_message_iter_init(message, &iter) || dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { error("Couldn't update BlueALSA PCM: %s", "Invalid signal signature"); goto fail; } dbus_message_iter_get_basic(&iter, &interface); dbus_message_iter_next(&iter); if (!dbus_message_iter_get_ba_pcm_props(&iter, NULL, pcm)) goto fail; if ((worker = supervise_io_worker(pcm)) != NULL) io_worker_mixer_volume_sync_alsa_mixer(worker, pcm); return DBUS_HANDLER_RESULT_HANDLED; } fail: return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } int main(int argc, char *argv[]) { int opt; const char *opts = "hVSvlLB:D:M:"; const struct option longopts[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "syslog", no_argument, NULL, 'S' }, { "loglevel", required_argument, NULL, 9 }, { "verbose", no_argument, NULL, 'v' }, { "list-devices", no_argument, NULL, 'l' }, { "list-pcms", no_argument, NULL, 'L' }, { "dbus", required_argument, NULL, 'B' }, { "pcm", required_argument, NULL, 'D' }, { "pcm-buffer-time", required_argument, NULL, 3 }, { "pcm-period-time", required_argument, NULL, 4 }, { "volume", required_argument, NULL, '8' }, { "mixer-device", required_argument, NULL, 'M' }, { "mixer-control", required_argument, NULL, 6 }, { "mixer-index", required_argument, NULL, 7 }, { "profile-a2dp", no_argument, NULL, 1 }, { "profile-sco", no_argument, NULL, 2 }, #if WITH_LIBSAMPLERATE { "resampler", required_argument, NULL, 10}, #endif { "single-audio", no_argument, NULL, 5 }, { 0, 0, 0, 0 }, }; static const nv_entry_t nv_log_levels[] = { { "error", .v.i = LOG_ERR }, { "warning", .v.i = LOG_WARNING }, { "info", .v.i = LOG_INFO }, { "debug", .v.i = LOG_DEBUG }, { 0 }, }; static const nv_entry_t nv_volume_types[] = { { "auto", .v.u = VOL_TYPE_AUTO }, { "mixer", .v.u = VOL_TYPE_MIXER }, { "software", .v.u = VOL_TYPE_SOFTWARE }, { "none", .v.u = VOL_TYPE_NONE }, { 0 }, }; #if WITH_LIBSAMPLERATE static const nv_entry_t nv_resampler_methods[] = { { "sinc-best", .v.u = RESAMPLER_CONV_SINC_BEST_QUALITY }, { "sinc-medium", .v.u = RESAMPLER_CONV_SINC_MEDIUM_QUALITY }, { "sinc-fastest", .v.u = RESAMPLER_CONV_SINC_FASTEST }, { "linear", .v.u = RESAMPLER_CONV_LINEAR }, { "zero-order-hold", .v.u = RESAMPLER_CONV_ZERO_ORDER_HOLD }, { "none", .v.u = RESAMPLER_CONV_NONE }, { 0 }, }; #endif bool syslog = false; /* Check if syslog forwarding has been enabled. This check has to be * done before anything else, so we can log early stage warnings and * errors. */ opterr = 0; while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1) switch (opt) { case 'h' /* --help */ : printf("Usage:\n" " %s [OPTION]... [BT-ADDR]...\n" "\nOptions:\n" " -h, --help\t\t\tprint this help and exit\n" " -V, --version\t\t\tprint version and exit\n" " -S, --syslog\t\t\tsend logs to the system logger\n" " --loglevel=LEVEL\t\tset logging level; default: %s\n" " -v, --verbose\t\t\tincrease output verbosity\n" " -l, --list-devices\t\tlist available BT audio devices\n" " -L, --list-pcms\t\tlist available BT audio PCMs\n" " -B, --dbus=NAME\t\tBlueALSA D-Bus service name suffix\n" " -D, --pcm=NAME\t\tplayback PCM device to use; default: %s\n" " --pcm-buffer-time=SEC\tplayback PCM buffer time in us; default: %u\n" " --pcm-period-time=SEC\tplayback PCM period time in us; default: %u\n" " --volume=TYPE\t\tset volume control type; default: %s\n" " -M, --mixer-device=NAME\tmixer device to use; default: %s\n" " --mixer-control=NAME\tmixer control name; default: %s\n" " --mixer-index=NUM\t\tmixer element index; default: %u\n" " --profile-a2dp\t\tuse A2DP profile (default)\n" " --profile-sco\t\tuse SCO profile\n" #if WITH_LIBSAMPLERATE " --resampler=METHOD\tresample conversion method; default: %s\n" #endif " --single-audio\t\tenable single audio mode\n" "%s" "\nNote:\n" "If one wants to receive audio from more than one Bluetooth device, it is\n" "possible to specify more than one MAC address. By specifying any/empty MAC\n" "address (00:00:00:00:00:00), one will allow connections from any Bluetooth\n" "device. Without given explicit MAC address any/empty MAC is assumed.\n", argv[0], nv_name_from_int(nv_log_levels, log_level), pcm_device, pcm_buffer_time, pcm_period_time, nv_name_from_uint(nv_volume_types, volume_type), mixer_device, mixer_elem_name, mixer_elem_index, #if WITH_LIBSAMPLERATE nv_name_from_uint(nv_resampler_methods, resampler_method), #endif ""); return EXIT_SUCCESS; case 'V' /* --version */ : printf("%s\n", PACKAGE_VERSION); return EXIT_SUCCESS; case 'S' /* --syslog */ : syslog = true; break; case 'v' /* --verbose */ : verbose++; break; } log_open(basename(argv[0]), syslog); dbus_threads_init_default(); /* parse options */ optind = 0; opterr = 1; while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1) switch (opt) { case 'h' /* --help */ : case 'V' /* --version */ : case 'S' /* --syslog */ : case 'v' /* --verbose */ : break; case 9 /* --loglevel=LEVEL */ : { const nv_entry_t * entry; if ((entry = nv_lookup_entry(nv_log_levels, optarg)) == NULL) { error("Invalid loglevel {%s}: %s", nv_join_names(nv_log_levels), optarg); return EXIT_FAILURE; } log_level = entry->v.i; break; } case 'l' /* --list-devices */ : list_bt_devices = true; break; case 'L' /* --list-pcms */ : list_bt_pcms = true; break; case 'B' /* --dbus=NAME */ : snprintf(dbus_ba_service, sizeof(dbus_ba_service), BLUEALSA_SERVICE ".%s", optarg); if (!dbus_validate_bus_name(dbus_ba_service, NULL)) { error("Invalid BlueALSA D-Bus service name: %s", dbus_ba_service); return EXIT_FAILURE; } break; case 'D' /* --pcm=NAME */ : pcm_device = optarg; break; case 3 /* --pcm-buffer-time=SEC */ : pcm_buffer_time = atoi(optarg); break; case 4 /* --pcm-period-time=SEC */ : pcm_period_time = atoi(optarg); break; case '8' /* --volume */ : { const nv_entry_t * entry; if ((entry = nv_lookup_entry(nv_volume_types, optarg)) == NULL) { error("Invalid volume control type {%s}: %s", nv_join_names(nv_volume_types), optarg); return EXIT_FAILURE; } volume_type = entry->v.u; break; } case 'M' /* --mixer-device=NAME */ : mixer_device = optarg; break; case 6 /* --mixer-control=NAME */ : mixer_elem_name = optarg; break; case 7 /* --mixer-index=NUM */ : mixer_elem_index = atoi(optarg); break; case 1 /* --profile-a2dp */ : ba_profile_a2dp = true; break; case 2 /* --profile-sco */ : ba_profile_a2dp = false; break; case 5 /* --single-audio */ : force_single_playback = true; break; #if WITH_LIBSAMPLERATE case 10 /* --resampler */ : { const nv_entry_t * entry; if ((entry = nv_lookup_entry(nv_resampler_methods, optarg)) == NULL) { error("Invalid resampler method {%s}: %s", nv_join_names(nv_resampler_methods), optarg); return EXIT_FAILURE; } resampler_method = entry->v.u; break; } #endif default: fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]); return EXIT_FAILURE; } get_bluez_version_numbers(&bluez_major, &bluez_minor); info("BlueZ version: %d.%d", bluez_major, bluez_minor); if ((main_loop_quit_event_fd = eventfd(0, EFD_CLOEXEC)) == -1) { error("Couldn't create quit event: %s", strerror(errno)); return EXIT_FAILURE; } DBusError err = DBUS_ERROR_INIT; if (!ba_dbus_connection_ctx_init(&dbus_ctx, dbus_ba_service, &err)) { error("Couldn't initialize D-Bus context: %s", err.message); return EXIT_FAILURE; } if (list_bt_devices || list_bt_pcms) { if (!ba_dbus_pcm_get_all(&dbus_ctx, &ba_pcms, &ba_pcms_count, &err)) { warn("Couldn't get BlueALSA PCM list: %s", err.message); return EXIT_FAILURE; } if (list_bt_pcms) print_bt_pcm_list(); if (list_bt_devices) print_bt_device_list(); return EXIT_SUCCESS; } if (optind == argc) ba_addr_any = true; else if (parse_bt_addresses(&argv[optind], argc - optind) == -1) { error("Couldn't parse BT addresses: %s", strerror(errno)); return EXIT_FAILURE; } if (volume_type == VOL_TYPE_NONE || volume_type == VOL_TYPE_SOFTWARE) mixer_device = NULL; if (pcm_buffer_time == 0) { if (pcm_period_time == 0) pcm_period_time = ba_profile_a2dp ? DEFAULT_PERIOD_TIME_A2DP : DEFAULT_PERIOD_TIME_SCO; pcm_buffer_time = pcm_period_time * DEFAULT_PERIODS; } else if (pcm_period_time == 0) { pcm_period_time = pcm_buffer_time / DEFAULT_PERIODS; } if (verbose >= 1) { char *ba_str = malloc(19 * ba_addrs_count + 1); char *tmp = ba_str; for (size_t i = 0; i < ba_addrs_count; i++, tmp += 19) ba2str(&ba_addrs[i], stpcpy(tmp, ", ")); const char *mixer_device_str = "(not used)"; char mixer_element_str[128] = "(not used)"; if (mixer_device != NULL) { mixer_device_str = mixer_device; snprintf(mixer_element_str, sizeof(mixer_element_str), "'%s',%u", mixer_elem_name, mixer_elem_index); } info("Selected configuration:\n" " BlueALSA service: %s\n" " ALSA PCM device: %s\n" " ALSA PCM buffer time: %u us\n" " ALSA PCM period time: %u us\n" " ALSA mixer device: %s\n" " ALSA mixer element: %s\n" #if WITH_LIBSAMPLERATE " Resampler method: %s\n" #endif " Volume control type: %s\n" " Bluetooth device(s): %s\n" " Profile: %s", dbus_ba_service, pcm_device, pcm_buffer_time, pcm_period_time, mixer_device_str, mixer_element_str, #if WITH_LIBSAMPLERATE nv_name_from_uint(nv_resampler_methods, resampler_method), #endif nv_name_from_uint(nv_volume_types, volume_type), ba_addr_any ? "ANY" : &ba_str[2], ba_profile_a2dp ? "A2DP" : "SCO"); free(ba_str); } ba_dbus_connection_signal_match_add(&dbus_ctx, dbus_ba_service, NULL, DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesAdded", "path_namespace='/org/bluealsa'"); ba_dbus_connection_signal_match_add(&dbus_ctx, dbus_ba_service, NULL, DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesRemoved", "path_namespace='/org/bluealsa'"); ba_dbus_connection_signal_match_add(&dbus_ctx, dbus_ba_service, NULL, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", "arg0='"BLUEALSA_INTERFACE_PCM"'"); ba_dbus_connection_signal_match_add(&dbus_ctx, "org.bluez", NULL, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", "arg0='org.bluez.MediaPlayer1'"); if (!dbus_connection_add_filter(dbus_ctx.conn, dbus_signal_handler, NULL, NULL)) { error("Couldn't add D-Bus filter: %s", err.message); return EXIT_FAILURE; } if (!ba_dbus_pcm_get_all(&dbus_ctx, &ba_pcms, &ba_pcms_count, &err)) warn("Couldn't get BlueALSA PCM list: %s", err.message); for (size_t i = 0; i < ba_pcms_count; i++) supervise_io_worker(&ba_pcms[i]); struct sigaction sigact = { .sa_handler = main_loop_stop, .sa_flags = SA_RESETHAND }; /* Call to these handlers restores the default action, so on the * second call the program will be forcefully terminated. */ sigaction(SIGTERM, &sigact, NULL); sigaction(SIGINT, &sigact, NULL); debug("Starting main loop"); for (;;) { struct pollfd fds[10] = { { main_loop_quit_event_fd, POLLIN, 0 } }; nfds_t nfds = ARRAYSIZE(fds) - 1; if (!ba_dbus_connection_poll_fds(&dbus_ctx, &fds[1], &nfds)) { error("Couldn't get D-Bus connection file descriptors"); return EXIT_FAILURE; } if (poll(fds, nfds + 1, -1) == -1 && errno == EINTR) continue; if (fds[0].revents & POLLIN) break; if (ba_dbus_connection_poll_dispatch(&dbus_ctx, &fds[1], nfds)) while (dbus_connection_dispatch(dbus_ctx.conn) == DBUS_DISPATCH_DATA_REMAINS) continue; } for (size_t i = 0; i < workers_size; i++) if (workers[i] != NULL) io_worker_stop(workers[i]); /* When all workers are stopped, we can safely free the resources * in a lockless manner without risking any race conditions. */ for (size_t i = 0; i < workers_size; i++) if (workers[i] != NULL) io_worker_destroy(workers[i]); ba_dbus_connection_ctx_free(&dbus_ctx); return EXIT_SUCCESS; }